These extensions appear as actions in the editor toolbar and in the document context 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 Primary Server and show the result list in the search view.
- Full access to the Sophora Primary Server using the Client API.
- Abort the current user operation.
When a client action is selected in the editor toolbar or the document context 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:
Name | Description |
---|---|
Name | This name will be shown in the list of client scripts in the Administration view |
Script type | The type of the script. See the table below. |
Order | Determines 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 sources | Specifies from where the script gets the documents it works on and where it is accessible. See table below. If empty, the script will have no toolbar button and no menu entry. |
Key binding | A keyboard shortcut for running the script. |
Menu text | This name will be shown in the list of client scripts in the context menu of selected documents or the tools menu (see Document sources). If empty, the script will not be visible (can be used with triggered scripts, see below). |
Tooltip | A tooltip shown when hovering over the toolbar icon of the script. |
Triggered by | Select 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 types | Select 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 context menu of a document and on the editor toolbar. For the document source "Without document" this is ignored. |
Script | The Groovy source code of the script. See examples below. |
Active on read-only | Whether the script can be run on a document that has been opened in a read-only editor. |
Toolbar icon | An icon for the script. The icon is shown in the document context menu and in the editor toolbar. |
The following script types are available:
Name | Description |
---|---|
Default | A script that can be run independent from document state. Depending on 'document sources' a script button appears in the editor toolbar and context menus. |
Publish | A 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 one to all of the 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.
Name | Description |
---|---|
Active editor | An editor-dependent script. The script button will appear in the toolbar of the current editor if a menu text is set. |
Current selection | A script that works on the currently selected documents, e.g. in the search view. The script will appear in the context menu of documents under "Document Actions" if a menu text is set. |
Full search result | A script that works on the current search in the search view. The script button will appear in the view 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. |
Without document | The script runs without any document. The script will appear in the top toolbar below "Tools". If a document is selected or an editor is open that document will be ignored. |
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.
Variables and Methods Available to Scripts
Name | Type | Description |
---|---|---|
sophoraClient | ISophoraClient | Access to the full Sophora Client API. |
context.getDocument() | INode | The active document. Is null if the script is run without document. If the 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. Returns an empty list if the script is run without document. |
context.getSelectedDocumentSummaries() | List<IContent> | The current selected documents as document summaries. May be empty 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) | boolean | True if the given document is opened in an editor. |
context.openInEditor(UUID uuid) | void | Open and/or focus the document with the given uuid in an editor. See example. |
context.documentIsDirty() | boolean | True if the active document has unsaved changes. Returns false if the editor is not currently focused. |
context.documentIsDirty(INode document) | boolean | True if the given document has unsaved changes. |
context.isUpdatable(INode document) | boolean | True 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) | void | If 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) | void | Shows 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.getTextFromClipboard () | String | If present, returns the clipboard text, otherwise returns an empty string. |
context.getTrigger() | com.subshell.sophora.client.clientscript.ClientScriptTrigger | Allows 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, OPEN_DOCUMENT_FROM_CLIPBOARD |
context.abortOperation() | void | Tells 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 . |
context. publishDocument(UUID uuid, VersionParameters versionParameters) publishDocument(UUID uuid, VersionParameters versionParameters, boolean force) | void void | The same as above, but with version parameters as additional metadata for the publication process. Read the documentation of the document lifecyle for more information about version parameters and their usage. |
context.cloneDocument(UUID uuid, UUID structureNodeUuid) | UUID | Calls cloneDocument(UUID uuid, UUID structureNodeUuid, boolean isEditorialClone) with isEditorialClone = true . |
context.cloneDocument(UUID uuid, UUID structureNodeUuid, boolean isEditorialClone) | UUID | Calls 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) | boolean | Adopts 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) | void | Runs the given runnable while a blocking modal dialog displays the given task name to the user. |
context.doWithProgress(String taskName, Collection<T> collection, Function<T, String> labelProvider, Consumer<T> consumer, boolean cancelable) | void | Executes mass operations by applying the given consumer function to each element of the given collection. A blocking modal dialog displays the given taskName and a progress bar. If the "labelProvider" function is given, a label will be displayed for each element during the progress (may be null). See example. (Since DeskClient 4.1.5) |
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.
Name | Type | Description |
---|---|---|
withTitle(String message) | IClientScriptDialog | Optionally sets a title for the dialog. |
withConfirmLabel(String label) | IClientScriptDialog | Optionally sets the label for the OK or YES button. |
withDeclineLabel(String label) | IClientScriptDialog | Optionally sets the label for the NO button. |
withCancelLabel(String label) | IClientScriptDialog | Optionally sets the label for the CANCEL button. |
showMessage(String message) | void | Shows a message box with the given text and an OK button. |
confirmMessage(String message) | void | Shows 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) | boolean | Shows 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() | IFieldsDialogBuilderContext | Create a custom dialog. Following calls will configure what will be displayed in that dialog. |
IFieldsDialogBuilderContext.addLabel(String label) | IFieldsDialogBuilderContext | Adds the given Text to the dialog. |
IFieldsDialogBuilderContext.addField(String id, String label, IDialogField field) | IFieldsDialogBuilderContext | Adds 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() | IDialogResult | Displays the configured dialog to the user with OK/Cancel buttons. The result allows access to values of input fields. |
createMerge() | IMergeDialogBuilderContext | Creates a new dialog builder context to fill the contents of a merge dialog. |
IMergeDialogBuilderContext.setOriginalData(ISophoraDocument originalData) | IMergeDialogBuilderContext | Sets the original data for the merge dialog. |
IMergeDialogBuilderContext.setNewData(INode newData) | IMergeDialogBuilderContext | Sets the new data that can be merged. |
IMergeDialogBuilderContext.setDataLabels(String originalDataLabel, String newDataLabel) | IMergeDialogBuilderContext | Sets the labels for the headings of the 'original data' and 'new data' sides of the dialog. |
IMergeDialogBuilderContext.setPropertiesAndChildNodes(Set<String> propertyNames, List<String> childNodeNames) | IMergeDialogBuilderContext | Sets 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() | IMergeDialogResult | Opens the merge dialog and returns the result. |
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.Name | Type | Description |
---|---|---|
text() text(String text) | TextField | A text input field with an optional default value. |
checkbox() checkbox(boolean checked) | CheckboxField | A checkbox for boolean values with an optional default value. |
date() date(Calendar date) | DateField | A date field with an optional default value. |
select(UUID selectValuesDocumentUUID) select(UUID selectValuesDocumentUUID, String... selectedValues) select(List<SelectValue> selectValues) select(List<SelectValue> selectValues, String... selectedValues) | SelectValueField | A dropdown field which offers the values of the given select values document or list of select values. Optionally you can pass a number of pre-selected values. SelectValue.listOf(String... keysAndLabels) can be used to a create a list of select value objects to be used with select(List<SelectValue> selectValues). |
file(FileMode fileMode) file(Path path, FileMode fileMode) | FilePickerField | A 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) | StructureNodePickerField | A 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). |
Name | Applicable Fieldtype | Description |
---|---|---|
required() | all | Marks 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() | TextField | Adds a character counter to the fields label |
maxChars(int) | TextField | Sets 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.retrievalresult.*
- 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 methodcontext.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")
}
// this check is unnecessary if the script is only configured for "Triggered by: Publish"
if (context.getTrigger() == DOCUMENT_ACTION_PUBLISH) {
context.saveDocument(null, doc)
} else {
context.updateDocument(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.
import static com.subshell.sophora.client.clientscript.field.DialogFields.*
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()
}
Example 7: Use a script to execute mass operations, e.g. to set multiple documents offline
The following script sets the given documents offline while showing a progress dialog to the user.
context.doWithProgress("Set everything offline", documents, {document -> document.getSophoraId()}, {document -> document.setOffline()}, true)
Example 8: Open a document for a custom URL
If a user tries to open a document from an URL in the clipboard and the URL cannot be resolved by Sophora a script can be triggered. In such a script you can open a document editor for the URL.
Create a client script with the field "Triggered by" set to "After "Open document by URL in clipboard" couldn't resolve the URL". To access the URL in the script call context.getTextFromClipboard()
. Afterwards process the URL and determine which document it refers to. You could for example do a query for a part of the URL by using the sophoraClient
.
Afterwards the script must open the editor itself by calling context.openInEditor(UUID)
. If no document was found a feedback message should be displayed to the user that no document could be found as the DeskClient will not display it if scripts exist.
String url = context.getTextFromClipboard()
// resolve the document from the text as you need
String sophoraId = url.split('/').last()
Optional<ISophoraDocument> document = client.getDocumentBySophoraIdIfExists(sophoraId)
// open document or show message if none found
document.ifPresentOrElse(
{ doc -> context.openInEditor(doc.getUUID())},
{ context.dialog().showMessage("Es konnte kein Dokument ermittelt werden zu der URL '${url}'")})