Custom DeskClient Actions using Client Scripts

In addition to extending the Sophora server, scripts can also be used to extend the Sophora DeskClient.

Table of Contents

These extensions appear as actions in the toolbar and in the document menu or are run in the background when the user executes some document actions.

Custom client actions can do any of the following things:

  • Read and modify the Sophora document of the active editor.
  • Show a message box to the user.
  • Search for documents in the Sophora Master Server and show the result list in the search view.
  • Full access to the Sophora Master Server using the Client API.
  • Abort the current user operation.

When a client action is selected in the toolbar or the document menu, the corresponding script is executed in the DeskClient. The script will have access to the document currently active in the DeskClient and will have the permissions of the current user.

When a document action is executed (like publish, save or delete) the corresponding scripts are executed in the DeskClient. The script will have access to the document and will have the permissions of the current user. The script should not assume that an editor is currently open. But if an editor is open it can be updated.

Creating a Client Script

To create a new client script, open the Administration view and select Client Scripts > New: Client Script. A client script is a Sophora system document with the following fields:

NameDescription
NameThis name will be shown in the list of client scripts in the Administration view
Script typeThe type of the script. See the table below.
OrderDetermines the order of the scripts in toolbar and context menus. Valid values are positive and negative integers. An empty field will be evaluated as '0'. Scripts with equal values will be sorted alphabetically by their name.
Publishing scripts will be displayed left/above (negative) or right/below (positive) of the publish button.
Document sourcesSpecifies from where the script gets the documents it works on. See table below. If empty, the script will have no toolbar button and no menu entry.
Key bindingA keyboard shortcut for running the script.
Menu textThis name will be shown in the list of client scripts in the menu Document > Client Scripts. If empty, the script will have no menu entry.
TooltipA tooltip shown when hovering over the toolbar icon of the script.
Triggered bySelect the document actions for which the script should be run. The script is only run if the node type also matches. Must be filled when no document source is selected, or else the script will never run.
Document typesSelect the document types for which the script can be run. If the node type of the active document does not match any of the selected document types, the client script action will not be shown in the document menu and on the toolbar.
ScriptThe Groovy source code of the script. See examples below.
Active on read-onlyWhether the script can be run on a document that has been opened in a read-only editor.
Toolbar iconAn icon for the script. The icon is shown in the document menu and in the toolbar.
The following script types are available:
NameDescription
DefaultA script that can be run independent from document state. Depending on 'document sources' a script button appears in the toolbar, document menu and context menus.
PublishA publishing script. This is almost like the "Default" type, but the script button will appear near the regular "Publish" button and is only enabled if the document is not in published state.
The following document sources are available:

A script can support only one or both sources. If a script is only a background script triggered on document actions like 'publish' or 'save' no document source has to be selected. In this case, the script will not appear in a UI menu.

NameDescription
Active editorAn editor-dependent script. The script button will appear in a toolbar section that is dependent on the current editor. The script will also appear in the document menu if a menu text is set.
Current selectionA script that works on the currently selected documents, e.g. in the search view. The script will appear in the context menu of the view and in the document menu if a menu text is set.
Full search resultA script that works on the current search in the search view. The script button will appear in the main menu: "Document" > "Search Result". Note that only documents of the document types for which the script can be run are given to the script at runtime, e.g. if the script is configured for stories only, but the user performed a search for stories and images, the script will run only on the story documents of the search result. Users must have the system permission for mass operations to perform such scripts.
Note: A client script must be published to be active.

Writing the Script Code

The source code of scripts has to be written in Groovy. In code you have access to some variables and methods to communicate with the Sophora DeskClient and Sophora Server.

Further information on how to develop scripts using an IDE can be found here (Section "Developing Scripts").

Variables and Methods Available to Scripts

NameTypeDescription
sophoraClientISophoraClientAccess to the full Sophora Client API.
context.getDocument()INodeThe active document. If a script is run from a context menu for a selection of multiple documents, the first document is returned.
context.getDocuments()List<INode>The document of the active editor, the selected documents of the focused view if no editor is active or the proper documents of the users' current search.
context.getSelectedDocumentSummaries()List<IContent>The current selected documents as document summaries. May return null if an editor is currently focused.
context.getSelectedComponentsAndBoxes()List<Long>The child node IDs of the selected components and boxes of the active editor.
context.isFromEditor(IContent document)booleanTrue if the given document is opened in an editor.
context.openInEditor(UUID uuid)voidOpen and/or focus the document with the given uuid in an editor. See example.
context.documentIsDirty()booleanTrue if the active document has unsaved changes. Returns false if the editor is not currently focused.
context.documentIsDirty(INode document)booleanTrue if the given document has unsaved changes.
context.isUpdatable(INode document)booleanTrue if the given document is opened in an editor and the editor is editable. Returns false if the document is not opened in an editor or if the editor is read-only.
context.updateDocument(INode document)voidIf the script changes the active document, the editor must be updated using this method. The method will do nothing if the document is not updatable (see context.isUpdatable(INode document))
context.showDocumentListToUser(String title, List uuids)voidShows a list of documents in the search view. The given title text will be displayed above the result list in the search view. The list parameter must be a list of Sophora document uuids.
context.getTrigger()com.subshell.sophora.client.clientscript.ClientScriptTriggerAllows the script to detect which action triggered its execution:
DIRECT, DOCUMENT_ACTION_CLONE, DOCUMENT_ACTION_CREATE, DOCUMENT_ACTION_DELETE, DOCUMENT_ACTION_OFFLINE, DOCUMENT_ACTION_POST_MERGE, DOCUMENT_ACTION_PUBLISH, DOCUMENT_ACTION_READ, DOCUMENT_ACTION_RELEASE, DOCUMENT_ACTION_RESTORE, DOCUMENT_ACTION_SAVE,
FIELD_ACTION_PRE_UPLOAD, FIELD_ACTION_UPLOAD_FINISHED, FIELD_ACTION_URL_TO_COPYTEXT_DROPPED
context.abortOperation()voidTells the DeskClient to cancel the current operation which triggered the script execution after the script finished.
context.
saveDocument(String idStem, INode node)
releaseDocument(UUID documentUuid)
publishDocument(UUID uuid)
setOffline(UUID uuid)
deleteDocument(UUID uuid)
restoreDocument(UUID documentUuid, UUID structureNodeUuid)

INode
void
void
void
void
void
These methods call their equivalent in ISophoraClient. But before the action is executed, client scripts will be run and are able to abort the operation.

Use these methods in favor of the client methods when you want to modify a document in your script.
context.
saveDocument(String idStem, INode node, boolean force)
releaseDocument(UUID documentUuid, boolean force)
publishDocument(UUID uuid, boolean force)
setOffline(UUID uuid, boolean force)
deleteDocument(UUID uuid, boolean force)

INode
void
void
void
void
The same as above, but with the option to suppress confirmation dialogs if the last parameter is true.
cloneDocument(UUID uuid, UUID structureNodeUuid)UUIDCalls cloneDocument(UUID uuid, UUID structureNodeUuid, boolean isEditorialClone) with isEditorialClone = true.
cloneDocument(UUID uuid, UUID structureNodeUuid, boolean isEditorialClone)UUIDCalls its equivalent in ISophoraClient. But before the action is executed, client scripts with a clone trigger will be run and are able to abort the operation.
The isEditorialClone parameter specifies whether an editorial or technical clone will be created. Further information can be found here.
The returned UUID is the UUID of the clone. It may also be null if the document could not be cloned (e.g. editor was dirty and user declined the save).

Use this method in favor of the client method when you want to clone a document in your script.
context.merge(ISophoraDocument targetDocument, INode sourceNode, IMergeDialogResult mergeDialogResult)booleanAdopts the information specified by the merge dialog result from the source node into the target document and triggers all scripts that listen to the post merge trigger.
context.getMergeDialogResult()Optional<IMergeDialogResult>Returns a merge dialog result object. Note that the result is optional and may not be present if the script has not been triggered by a post merge event.
context.showBusyWhile(String taskName, Runnable runnable)voidRuns the given runnable while a blocking modal dialog displays the given task name to the user.

There is a fluent API for creating dialogs in client scripts. It is accessed by context.dialog(). The following table shows all calls possible to such an dialog. There's also an example.

Methods for dialogs (prefix with context.dialog())
NameTypeDescription
withTitle(String message)IClientScriptDialogOptionally sets a title for the dialog.
showMessage(String message)voidShows a message box with the given text and an OK button.
confirmMessage(String message)voidShows a message box with the given text and OK/Cancel buttons. If the user hits cancel the script and operation will be aborted.
askQuestion(String question)booleanShows a question dialog to the user with yes/no/cancel buttons. If the user hits cancel the script and operation will be aborted.
selectDocuments(String message, List<UUID> uuids)List<UUID>Shows a dialog with a list of documents from which the user can choose any. By default all documents are selected. If the user hits cancel the script and operation will be aborted.
selectDocuments(String message, List<UUID> uuids, List<UUID> preselectedUuids)List<UUID>Shows a dialog with a list of documents from which the user can choose any. The parameter preselectedUuids set which documents should be selected by default. If it is null or empty no document will be selected.
selectDocuments(String message, List<UUID> uuids, List<UUID> preselectedUuids, boolean orderBySophoraID)List<UUID>The same as above, but with the option to order the documents by Sophora ID. Otherwise, the documents will be shown in the order they are provided.
create()IFieldsDialogBuilderContextCreate a custom dialog. Following calls will configure what will be displayed in that dialog.
IFieldsDialogBuilderContext.addLabel(String label)IFieldsDialogBuilderContextAdds the given Text to the dialog.
IFieldsDialogBuilderContext.addField(String id, String label, IDialogField field)IFieldsDialogBuilderContextAdds an input field to the dialog. The first parameter is an id to access the value which the user inputs. The seconds parameter is a label which will be displayed and the last parameter defines the type of input field.
IFieldsDialogBuilderContext.open()IDialogResultDisplays the configured dialog to the user with OK/Cancel buttons. The result allows access to values of input fields.
createMerge()IMergeDialogBuilderContextCreates a new dialog builder context to fill the contents of a merge dialog.
IMergeDialogBuilderContext.setOriginalData(ISophoraDocument originalData)IMergeDialogBuilderContextSets the original data for the merge dialog.
IMergeDialogBuilderContext.setNewData(INode newData)IMergeDialogBuilderContextSets the new data that can be merged.
IMergeDialogBuilderContext.setDataLabels(String originalDataLabel, String newDataLabel)IMergeDialogBuilderContextSets the labels for the headings of the 'original data' and 'new data' sides of the dialog.
IMergeDialogBuilderContext.setPropertiesAndChildNodes(Set<String> propertyNames, List<String> childNodeNames)IMergeDialogBuilderContextSets the names of properties and child nodes that can be merged. If no properties and child node names have been set, all properties and child nodes can be merged instead.
IMergeDialogBuilderContext.open()IMergeDialogResultOpens the merge dialog and returns the result.
Note: Some dialogs have a cancel button. If the user clicks 'Cancel', the script and the current operation (when triggered by an action) will be stopped. This is done by throwing an com.subshell.sophora.client.clientscript.CancelScriptException. If your script allocates some resources before the dialog is shown and needs to cleanup, you should surround your dialog call with a try/finally block.
Dialog fields
NameTypeDescription
text()
text(String text)
TextFieldA text input field with an optional default value.
checkbox()
checkbox(boolean checked)
CheckboxFieldA checkbox for boolean values with an optional default value.
date()
date(Calendar date)
DateFieldA date field with an optional default value.
select(UUID selectValuesDocumentUUID)SelectValueFieldA dropdown field which offers the values of the given select values document.
file(FileMode fileMode)
file(Path path, FileMode fileMode)
FilePickerFieldA file/directory path field with an optional default path.
structure(IFilteredStructure filteredStructure, boolean multiSelection, boolean showSitesOnly)

structure(UUID initialSelectedStructureNodeUuid, IFilteredStructure filteredStructure, boolean multiSelection, boolean showSitesOnly)

structure(List<UUID> initialSelectedStructureNodeUuids, IFilteredStructure filteredStructure, boolean multiSelection, boolean showSitesOnly)
StructureNodePickerFieldA structure node picker field with an optional pre-selected structure node (when single selection) or a list of pre-checked structure nodes (when multi selection).
Parameters for dialog fields
NameApplicable FieldtypeDescription
required()allMarks the field as required. This field must be filled to successfully close the dialog.
multi()TextField
SelectValueField
Puts the field into multi-line mode.
charCounter()TextFieldAdds a character counter to the fields label
maxChars(int)TextFieldSets the maximum number of characters the field will accept.

Default Imports

These classes are imported for all client scripts:

  • java.io.*
  • java.lang.*
  • java.math.BigDecimal
  • java.math.BigInteger
  • java.net.*
  • java.nio.file.*
  • java.util.*
  • groovy.lang.*
  • groovy.util.*
  • com.subshell.sophora.api.*
  • com.subshell.sophora.api.content.*
  • com.subshell.sophora.api.content.value.*
  • com.subshell.sophora.api.exceptions.*
  • com.subshell.sophora.api.nodetype.*
  • com.subshell.sophora.api.search.*
  • com.subshell.sophora.api.structure.*
  • com.subshell.sophora.client.*
  • com.subshell.sophora.client.clientscript.*
  • com.subshell.sophora.client.clientscript.field.*

Get the Right Document(s)

Client scripts can be run in the background (triggered by a document action), in the context of an open editor or having documents selected outside of an editor, for example in the search view. Therefore scripts have to be configured from where they get the documents they work on: the active editor, the current selection, both or none.

If your script should run ...

  • ... only on the document of the active editor, you can use context.getDocument() to get it like before version 1.53.
  • ... only on the selected documents outside of an editor, you can use context.getSelectedDocumentSummaries() to get them. Note that only summaries of the documents are returned and you have to get the full documents and locks by client calls if you want to modify and save them.
  • ... on both document sources (depending on the focused view/editor), you can use the methods from above or just context.getDocuments() to get the focused document(s). The method context.isFromEditor(IContent document) helps you to get the information whether the given document is opened in the focused editor.
  • ... in background triggered by actions like 'publish' or 'save', you do not have to specify a document source but you can use the above methods to get the document(s) the action has been triggered on.

The small script below works for all cases from above and demonstrates the methods context.getDocuments() and context.isFromEditor(IContent document) by just printing the focused documents in a dialog. This will show you which document(s) will be handled in which case.

List<INode> docs = context.getDocuments()
if (!docs.isEmpty()) {
    List<String> ids = docs.inject(new ArrayList()) {
         result, doc -> result.add(doc.getString('sophora:id') + " (is from editor: " + context.isFromEditor(doc) + ")"); result
    }
    context.dialog().showMessage("Documents:\n" + ids.join('\n'))
} else {
    context.dialog().showMessage("No documents")
}

Example 1: Edit document of active editor

The following script shows the content of the property "sophora-content:topline" of the document in the active editor to the user, then it sets the property to a new value:

def doc = context.getDocument()
def name = doc.getString('sophora-content:topline')
context.dialog().showMessage("The topline is '" + name + "'.")
doc.setString('sophora-content:topline', 'Breaking News')
context.updateDocument(doc)

Example 2: Searching for other documents

The next script searches for all documents with 'something' in the repository and shows the result in the search view (the Query object has to be an instance of com.subshell.sophora.api.search.IQuery):

def q = new TextQuery("something")
def params = new SearchParameters()
params.pageSize = Integer.MAX_VALUE
def searchResult = sophoraClient.findDocumentUuids(q, params)
if (searchResult) {
	context.showDocumentListToUser("Matching documents", searchResult.UUIDs)
} else {
	context.dialog().showMessage("No matching documents found.")
}

Example 3: Script triggered by document action

The following script copies the value of the property "sophora-content:topline" to the property "sophora-content:title". It saves the changed document, if the script was called before the document is published.

import static com.subshell.sophora.client.clientscript.ClientScriptTrigger.*
def doc = context.getDocument()
if (!doc) {
	doc = context.getDocuments().get(0)
}
def topline = doc.getString("sophora-content:topline")
if (topline) {
	doc.setString("sophora-content:title", topline)
} else {
	doc.removeProperty("sophora-content:title")
}
context.updateDocument(doc)
if (context.getTrigger() == DOCUMENT_ACTION_PUBLISH) {
	context.saveDocument(null, doc)
}

Example 4: Use a dialog to get input from user

The following script asks the user to input a date, a label and confirm the rules. Therefore a dialog with input fields will be build. Note that the type of field value retrieved from the IDialogResult depends on the input field type.

def dialogResult = context.dialog().withTitle("Example Script").create()
	.addLabel("Please choose the date and describe the action.")
	.addField("date", "Date of event", date())
	.addField("action", "Action text", text("Once upon a time ...").charCounter().maxChars(118).required())
    .addField("attachment", "Attachment", file(FileMode.FILE))
	.addField("location", "Location", structure(context.getDocument().getStructureNodeUuid(), sophoraClient.getFilteredStructureFactory().getReadableStructure(), false, false))
	.addField("confirm", "Acknowledge rules", checkbox())
	.open()
// same getter but type depends on field
Calendar date = dialogResult.get("date")
String action = dialogResult.get("action")
Path attachment = dialogResult.get("attachment")
boolean confirmed = dialogResult.get("confirm")
// do something with the values

Example 5: Use a merge dialog to merge a document

The following script shows how to open a merge dialog and process the user's input after the dialog has been closed i.e. merge a document based on the results of the dialog.

This can be useful if you want to offer the possibility of merging the content of a standard document into others.

Open the merge dialog
ISophoraDocument targetDocument = context.document
ISophoraDocument mergeNode = sophoraClient.getDocumentByUuid(UUID.fromString("8b549962-1514-42b8-952c-1b4ce241da0e"))
IMergeDialogResult dialogResult = context.dialog().createMerge()
	.setOriginalData(targetDocument)
	.setNewData(mergeNode)
	.setPropertiesAndChildNodes(["sophora-content:title"] as Set,
		["sophora-content:copytext", "sophora-content-nt:storyref"])
	.open()
Perform the actual merge based on the dialog's result and trigger post merge scripts.
if (!dialogResult.abort) {
	boolean successful = context.merge(targetDocument, mergeNode, dialogResult)
	if (successful) {
		sophoraClient.saveDocument(null, targetDocument)
	}
}

Example 6: Use a script to open a document in an editor

The following script opens or focuses an editor with the given uuid. A dialog is shown with a specific reason if a document with the uuid could not be found.

def uuid = UUID.fromString("50fc8c22-56d1-4155-885a-19fb0790e6f1")
try {
 	context.openInEditor(uuid)
} catch (SophoraException e) {
	context.dialog().withTitle("Error: Could not open the document") 
 		.create()
		.addLabel("Could not open the document in an editor, reason:")
		.addLabel(e.getMessage())
		.open()
}