Moving a Folder to Inactive?

The scenario: HR wants to have a folder dedicated to employee files. Each employee has their own folder containing the files. Once the employee is no longer employed we would like to move their folder to an “Inactive” folder.

With our current workflow process the files have been successfully moved to a new folder in a new location, but the original folder is still there. Is there any way to delete the original folder, move over the original folder in its entirety, or is there a better suggested process for this?

Thanks.

You can do this in ECM UI by drag/drop the folder into another folder - but there is no way to do this via the workflow steps that I can find, but if you can (are willing) write an API/REST call, there is an API service for ECM called MoveFolder.
Reference for https://cloudbeta.docstar.com/EclipseServer/AstriaV2/Folder.svc/restssl/MoveFolder

EDIT - See solution code below

2 Likes

I didn’t realize they would expose a folder move through the API but not the workflows. While it seems strange to me to do it that way I’ve done enough work with REST that I think this is the best solution I have been able to consider.

Thank you very much!

1 Like

When you get it working, please come back and post. Our small ECM community is always looking for ways to get things done :slight_smile:

I finally got it finished up and ran last night. It worked for us, but I can’t promise it will suit someone else’s needs exactly. Either way, here’s the layout for what I created.

To reiterate, the goal was for a folder to be moved if an employee was considered inactive. HR would come into DocStar and run a workflow on the employee’s documents to set them to inactive and change permissions on their documents. Optimally, the workflow would have moved the folder afterwards but we couldn’t find a way until @MikeGross’s alternate suggestion. Using PowerShell and calling the API we will run it nightly using Power Automate to move any employee folders that still need moved, as it does not need to be done immediately, but can be triggered manually if required.

What our folder structure looks like:

Before:

Root Folder (Personnel)
	Employee Type 1
		Austin
			Documents (with "zInactiveEmployee" flag set on all documents ran from workflow)
		Boston
			Documents
	Employee Type 2
		Costin
	Employee Type 3
		Flossin
			Documents (with "zInactiveEmployee" flag set on all documents ran from workflow)
		Josslyn
	Inactive Employees
		Employee Type 1
		Employee Type 2
		Employee Type 3

After:

Root Folder (Personnel)
	Employee Type 1
		Boston
			Documents
	Employee Type 2
		Costin
	Employee Type 3
		Josslyn
	Inactive Employees
		Employee Type 1
			Austin
				Documents
		Employee Type 2
		Employee Type 3
			Flossin
				Documents

We first call their API (HostingV2/User.svc/rest/LogIn) for a token and pass that through to our calls.

function LogAccountIn($user, $password)
{
    $body = @"
    {
	    `"Password`":`"$($password)`",
	    `"ProxyLogin`": false,
	    `"ProxyOnBehalfOf`": `"00000000-0000-0000-0000-000000000000`",
	    `"Username`":`"$($user)`"
    }
"@

    $response = Invoke-RestMethod 'http://$($rootURL)/HostingV2/User.svc/rest/LogIn' -Method 'POST' -Headers $headers -Body $body
    $response = $response | ConvertTo-Json | ConvertFrom-Json
    return $response.Result.Token
}

Next we make another call (AstriaV2/Search.svc/rest/Search) and use that for subsequent searching through our “directory”.

function SearchByFolderId($folderID)
{
    $body = @"
    {
      `"MaxRows`": `"25`",
      `"IncludeFolders`": true,
      `"IncludeInboxes`": false,
      `"IncludeDocuments`": true,
      `"IncludePackages`": true,
      `"ContentTypeId`": null,
      `"ContentTypeTitle`": `"`",
      `"InboxId`": null,
      `"FolderId`": `"$($folderID)`",
      `"ContainerName`": `"REPLACE`", #root folder name. I hard-coded, could be dynamic if necessary
      `"Start`": 0,
      `"TextCriteria`": `"`",
      `"FieldedCriteria`": [
        {
          `"Concatenation`": 1,
          `"GroupConcatenation`": 1
        }
      ],
      `"SortBy`": `"title`",
      `"SortOrder`": `"desc`",
      `"refreshSearch`": false,
      `"DocumentRetrieveLimit`": null,
      `"PredefinedSearch`": 0,
      `"IncludeSubFolders`": true,
      `"IncludedFolderIds`": [],
      `"IncludeColumn`": false,
      `"Columns`": null,
      `"LegacyTextParser`": true,
      `"Name`": null,
      `"PassThroughPaging`": false,
      `"SaveAsRecent`": false,
      `"WholeStringMatch`": false
    }
"@

    $response = Invoke-RestMethod 'http://$($rootURL)/AstriaV2/Search.svc/rest/Search' -Method 'POST' -Headers $headers -Body $body
    $results = ($response | ConvertTo-Json -Depth 8 | ConvertFrom-Json).Result.Results

    return $results
}

For each one that matches our requirements, we move the folder (AstriaV2/Folder.svc/rest/MoveFolder) to the “Inactive Employees”/* folder.

function MoveFolder($employeeFolderId, $inactiveFolderId)
{
    $body = @"
    {
	    `"Id`":`"$($employeeFolderId)`",
	    `"NewRootId`":`"$($inactiveFolderId)`",
	    `"Title`":`"`"
    }
"@

    $result = Invoke-RestMethod 'http://$($rootURL)/AstriaV2/Folder.svc/rest/MoveFolder' -Method 'POST' -Headers $headers -Body $body
}

Additionally, we call (AstriaV2/Folder.svc/rest/GetSecurityInformation) replace the SecurityClassID and update it with (AstriaV2/Folder.svc/rest/SetSecurityInformation).

function GetSecurityInformation($folder)
{
    $body = @"
    `"$($folder)`"
"@

    $response = Invoke-RestMethod 'http://$($rootURL)/AstriaV2/Folder.svc/rest/GetSecurityInformation' -Method 'POST' -Headers $headers -Body $body
    $results = ($response | ConvertTo-Json -Depth 8 | ConvertFrom-Json).Result.

    return $results
}

function SetSecurityInformation($folder, $site)
{
    $securityResult = GetSecurityInformation -folder $folder

    $securityClassID = switch ($site)
    {
        #REPLACE comment with values hard-coded with securityClassIDs
        # 'ThisSite' { "REPLACE GUID" }
    }

    $securityResult.SecurityClassId = $securityClassID
    $body = $securityResult| ConvertTo-Json

    $response = Invoke-RestMethod 'http://$($rootURL)/AstriaV2/Folder.svc/rest/SetSecurityInformation' -Method 'POST' -Headers $headers -Body $body
}

The calling code:

###REPLACE VALUES
$user = ""                                 #Docstar Username
$password = ""                             #Docstar Password
$rootURL = ""                              #URL Path to DocStar
$rootFolder = ""                           #guid of base directory folder, in our instance: Personnel
$inactiveFolderName = "Inactive Employees" #change to name of inactive folder
$inactiveFlag = "zInactiveEmployee"        #name of flag being detected to know if the folder needs to be moved
###REPLACE VALUES

$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$headers.Add("Content-Type", "application/json")
$token = LogAccountIn -user $user -password $password
$headers.Add("ds-token", $token)


$results = SearchByFolderId -folderID $rootFolder

$rootSubfolders = $results | Where-Object {$_.Type -eq 1024 -and ($_.DynamicFields.Key -eq "DfFolderId" -and $_.DynamicFields.Value -eq $rootFolder) -and $_.Title -ne $inactiveFolderName} | ForEach-Object {
    [PSCustomObject]@{
        ID = $_.ID
        Title = $_.Title
    }

}

$inactiveEmployeesFolderID = ($results | Where-Object {$_.Title -eq $inactiveFolderName} | Select-Object Id).Id
$inactiveEmployeesSubFolders = $results | Where-Object {$_.Type -eq 1024 -and ($_.DynamicFields.Key -eq "DfFolderId" -and $_.DynamicFields.Value -eq $inactiveEmployeesFolderID)} | ForEach-Object {
    [PSCustomObject]@{
        ID = $_.ID
        Title = $_.Title
    }

}

$employeeFolders = foreach($i in $rootSubfolders)
{
     $results | Where-Object {$_.Type -eq 1024 -and ($_.DynamicFields.Key -eq "DfFolderId" -and $_.DynamicFields.Value -eq $i.ID)}| ForEach-Object {
        [PSCustomObject]@{
            ParentID = ($_.DynamicFields | Where-Object {$_.Key -eq "DfFolderId"} | Select-Object Value).Value
            ID = $_.Id
        }

    }
}

$oldToNewFolderID = foreach($i in $inactiveEmployeesSubFolders)
{
    foreach($j in $rootSubfolders)
    {
        if ($i.Title -eq $j.Title)
        {
                [PSCustomObject]@{
                    OldID = $j.ID
                    NewID = $i.ID
                    Title = $i.Title
                }
        }
    }
}

$foldersToMove = $results | Where-Object {$_.DynamicFields.Key -eq $inactiveFlag -and $_.DynamicFields.Value -eq $true} | ForEach-Object {
    [PSCustomObject]@{
        ID = ($_.DynamicFields | Where-Object {$_.Key -eq "DfFolderId"} | Select-Object Value).Value
    }
}

foreach ($i in (($foldersToMove).ID | Sort-Object | Get-Unique))
{
    $employeeFolderParentID = ($employeeFolders | Where-Object {$_.ID -eq $i} | Select-Object $_.ParentID).ParentID

    if ($employeeFolderParentID -eq $null)
    {
        continue
    }

    #Write-Output "Folder to move - $($i)"

    $inactiveFolderID = ($oldToNewFolderID | Where-Object {$_.OldID -eq $employeeFolderParentID} | Select-Object $_.NewID).NewID
    #$site = ($oldToNewFolderID | Where-Object {$_.OldID -eq $employeeFolderParentID} | Select-Object $_.Title).Title
    #$site = $site.Split(" ")[0]

    #Write-Output "Moving folder $($i) from $($employeeFolderParentID) to $($inactiveFolderId)"
    MoveFolder -employeeFolderId $i -inactiveFolderId $inactiveFolderID
    SetSecurityInformation -folder $i #-site $site
}

“$Site” has been commented out as it might not suit someone else’s needs, but we are controlling our folders by site as well, such as “$Site Hourly” requiring additional logic on where to move the inactive employee’s folder.

I’m sure the code may look clunky to some, but it’s the solution I came up with. There are a few “REPLACE” statements in the code for things that need changed if you plan on using any part of this. Additionally, the Write-Outputs at the end are optional but help when diagnosing what is happening.

1 Like

To be clear, the calling code is a PS script, but where are the functions code stored? I’m no PS guru - I’m seeing how PS is calling “LogInAccount” but not where it is calling it from.

I made a couple mistakes sanitizing it for posting, so those will be fixed soon if anyone notices anything egregious.

To your point, maybe I shouldn’t have called it “Calling code”, naming isn’t my strong suit. Every code block (the ones that start with “function”) after the first two are also included in the script, making one big script. There are different ways to do it in PowerShell but I kept it basic.

1 Like

Would it be easier to have an Employee folder structure

In workflow, use remove from all folders to change the user from one folder to another. Next action have an Add to Folder task.
The workflow may need to not end to use this functionality, but may be simpler and allow for the user record to move from Folder to Folder.

Company\Inactive<InsertName>
Company\Active<InsertName>

It would be much easier if done that way. In fact, that’s what was originally being done through workflows, but we found that the original folder stayed in the “Active” path which confused things having an employee in both active and inactive.

Additionally, I came into the project a bit late but they had already decided to bucket it out by both Site and employee type. Specific permissions are set based on those types so we would have had to split it out in some way.

1 Like

I know I’m late to the game, but folders are just a human construct in ECM. There are no folders, just a single document property with a path in it if I remember correctly. I would imagine that one could have a document properties for site, employee type, and active. From there, one should be able to build views to get what you want. The world is messy and doesn’t always fit into one folder, so many Document Management systems did away with them so a single document could appear in several views. Just some thoughts…

Nothing To See Here GIF

3 Likes

I won’t pretend to understand everything ECM has to offer (in fact I look at it for a day, finish my project, and forget about it for a few months), and you are definitely right that leveraging the ability to add custom properties could have probably made this project more simple. One of my hang-ups on not utilizing the “folder” structure was the sensitive nature of the HR documents. Feeling like they were bucketed out in the correct areas with their specific permissions set felt better than having them just out there, even with their properties set correctly. Plus, even if the tech world is trying to move on from folders, it doesn’t mean everyone else has. The extra effort I needed to go through was worth it to keep it basic for my users.

All that to say I do appreciate the advice and I will try to make an effort to make future projects more DMS-y per your suggestions.

1 Like

One more suggestion then, as for security, use Document Types and the very robust security that ECM has to offer in Security Groups. I’ve never applied Security Groups to folders in ECM, and maybe it’s possible but I’ve usually used Document Types to control workflows, retention, and security. For the future…

2 Likes

Brother?

Joe Dirt Brother GIF

2 Likes

the matrix there is no spoon GIF

This just trigger another thought - There are two tasks in the ECM workflow - “Remove from (All) folder(s)” and “Add to Folder”. We use these in pairs all the time to move things around. If you already had a workflow, this should be and easy modification.

Folders are just as @Mark_Wonsil said - a built in system level meta data field that contains a list of folder paths. Easily editable - and probably easily editable in SQL directly if you want to take that path.

The other thing is the WAIT task. Your workflow can process an employee file and then WAIT for a termination flag on the Employee record and at that point it could move the folders using the above task pair. Not sure if this would work for you, but maybe it’s an idea.

HOLD ON - Data Monitors!!!
You can create a data monitor and use a “Wait for Data” task. The inline Help is limited, but looks like a Datalink that take an input value from the Document Meta Data in the workflow queue (like the Employee #) could return a True/False type value that would cause the Workflow task to re-evaluate and continue to the Task pair to relocate the folder.

I’m kind of excited to try this for something now :slight_smile:

2 Likes