Studio Developer Manual / Version 2406.1
Table Of ContentsIn the previous section, the basics of the Service Agent API were covered. This section covers the built-in services and related utility that are ready to use in custom apps. In addition, the services that are already offered by the Content App and the Workflow App are covered.
Framework Services
Several service types and their descriptors are already defined in the
Studio Apps framework. The following services
are defined in the @coremedia/studio-client.app-context-models
module
and can be used as top-level imports.
- StudioAppService: This service represents a Studio App
itself. For each app with a proper manifest according to the guidelines from Section
Section 9.36.4, “App Manifest and Apps Menu Entries”, a
StudioAppService
is automatically registered where the most important feature is the app's descriptor with properties of typeStudioAppServiceProps
. They mirror the contents of the manifest and provide this information at run-time. - RouterService: This service allows to set the sub-URL/path of an app.
As described in Section Section 9.36.4, “App Manifest and Apps Menu Entries”, setting up a
RouterService
is mandatory for each app that offers URL shortcuts in its manifest. Otherwise theRunAppEntry#run()
methods returned fromstudioAppsContext._.observeRunAppSections()
do not work. For theRouterService
only the interface is provided by the framework. A sub-path is set via the methodRouterService#setPath()
. How this path is actually manifested in the app is up to the app's implementation. For example, the My-Edited-Contents App uses aHashRouter
and theuseNavigate()
hook is used for the implementation of theRouterService#setPath()
. - AutoLogoutService: In the CoreMedia Studio,
the user is automatically logged out after a certain period of inactivity. For a smooth
user experience it is important that all apps log out jointly. For this purpose,
a custom app needs to register its own
AutoLogoutService
. Note that the service does not do the actual log-out itself. Instead, it is responsible for tracking user inactivity and communicate this with the otherAutoLogoutService
s of other apps. So typically, the service is embedded in some form of a wider login context. For example, in the My-Edited-Contents App, the service is set up as follows:import { AutoLogoutService, createAutoLogoutService, createAutoLogoutServiceDescriptor } from "@coremedia/studio-client.app-context-models"; import InputActivityTracker from "@coremedia/studio-client.app-context-models/activitytracker/InputActivityTracker"; import FetchActivityTracker from "@coremedia/studio-client.app-context-models/activitytracker/FetchActivityTracker"; ... async #setupAutoLogoutService(): Promise<void> { this.#autoLogoutService = createAutoLogoutService({ autoLogoutDelay: 1800000, activityTrackers: [ new InputActivityTracker(document.body), new FetchActivityTracker(), ], serviceAgent: getServiceAgent(), }); getServiceAgent().registerService( this.#autoLogoutService, createAutoLogoutServiceDescriptor() ); this.#autoLogoutSubscription = this.#autoLogoutService .observe_status() .subscribe(async (status) => { if (status === "loggedOut" || status === "autoLoggedOut") { await this.#doLogout(); await this.tearDownAutoLogoutService(); } }); }
The service is created with a delay of 30 minutes and two activity trackers, then registered with the Service Agent and finally an observer for the service's status is set up. If the status switches to "loggedOut" or "autoLoggedOut", then the actual logout is performed viadoLogout()
which is not covered here and is not part of theAutoLogoutService
. In addition, when the user explicitly logs out in the current app for example via a button thenAutoLogoutService#forceLogout()
must be called to trigger the logout.Just as all apps should log out jointly, logging into one app should also reload the other apps that are currently opened in a browser window but do not have a valid session. The above mentioned
StudioAppService
offers a method for that. In the My-Edited-Contents App, this code is executed upon login:const appServices = getServiceAgent().getServices( createStudioAppServiceDesc(), {state: "running"} ).filter(handler => handler.descriptor.provider !== getServiceAgent().getOrigin()); appServices.forEach( async (appServiceHandler) => (await appServiceHandler.fetch()).reloadWithoutSession() );
Studio Apps Utilities
As already mentioned in Section Section 9.36.3, “Accessing the Studio Apps Context”,
the studioAppsContext
utility is also available as a top-level import
of the @coremedia/studio-client.app-context-models
module. It provides the
following utility functions:
- initAppServices(): This method has already been covered in Section Section 9.36.4, “App Manifest and Apps Menu Entries”.
- observeRunSections(): This method has already been covered in Section Section 9.36.4, “App Manifest and Apps Menu Entries”.
- getShortcutRunnerRegistry(): This method has already been covered in Section Section 9.36.4, “App Manifest and Apps Menu Entries”.
- runApp(): This method takes a
ServiceDescriptorWithProps<StudioAppService, StudioAppServiceProps>
argument to run the app that this descriptor denotes. If the app is already running in a browser window, it is brought into the foreground. - getMyAppDescriptor(): Returns the
ServiceDescriptorWithProps<StudioAppService, StudioAppServiceProps>
of the current app. It mirrors the contents of the app's manifest. - focusMe(): Focuses the current app and brings its browser window / tab into the foreground.
This method is typically called inside service methods where an action is triggered that must be
immediately visible to the user, for example
ContentFormService#openContentForm()
. - getAppStartupParameters(): Sometimes an app is not just run in a browser window but
also with additional parameters. Currently, this is only the case when the app is run via a
service runner for one of the app's
cmServices
from their manifest, see Section Section 9.36.4, “App Manifest and Apps Menu Entries”. The startup parameters are then available via this method. In this case, it provides the app with the information which service was initially requested to trigger the app's start-up. An example to use this method is given in Section Section 9.36.6, “Multi-Instance Apps”. - setWindowOpenHandler(): Apps are opened in browser windows / tabs.
This method allows to override the handler that is called when an app is run or
brought into the foreground. In the future this might be a lever to allow for
more advanced PWA features. For now, it is mainly to deal with browser security
restrictions. A popup blocker might prevent the focussing of an app's window / tab.
This goes unchecked by the default window handler of the Studio Apps
framework. But the module
@coremedia/studio-client.app-context-models
provides a handler that display a warning for that. It is currently used by all built-in apps and also by the My-Edited-Contents App.import { openOrFocusApp, studioAppsContext } from "@coremedia/studio-client.app-context-models"; studioAppsContext._.setWindowOpenHandler(openOrFocusApp);
- addWindowValidityObservable(): This is another tool to deal with
browser security restrictions. For app windows to be able to interact properly
with each other (especially to focus each other), they need to be in the same
so-called "window group" of the browser. This is only the case, if all apps were
started beginning with one app and further apps are always opened from one of the
already running ones. Once a browser window is opened isolated from this chain
and an app run in it, this app is not connected to the other apps. This method allows
to add an
Observable
to track whether the current app window is a connected one. By default, no suchObservable
is in place but the module@coremedia/studio-client.app-context-models
provides one. It is currently used by all built-in apps and also for the My-Edited-Contents App.import { observeWindowStudioAppsConnection, studioAppsContext } from "@coremedia/studio-client.app-context-models"; studioAppsContext._.addWindowValidityObservable( observeWindowStudioAppsConnection() );
In addition, the My-Edited-Contents App also reacts if the window connection cannot be established.studioAppsContext._.observeWindowValidity().subscribe((valid) => { if (!valid) { alert("This browser tab has no connection to the other Studio browser tabs. Please close it and continue in another Studio browser tab."); } });
Data Transfer Services
The module @coremedia/studio-client.interaction-services-api
contains some base services
for data transfers between apps.
- DragDropService: The built-in CoreMedia Studio Apps, namely
the Content App and the Workflow App, offer to drag content items
between apps and thus between browser windows. For this, HTML5 drag/drop is used
(also cf. Section Section 9.14, “HTML5 Drag And Drop”). In order
to allow such drag operations to result in a drop in a custom app, it is sufficient to
just register a "drop" event handler. However, the
DragDropService
also offers information about the dragged items during the drag operation and not only once the drop happens. The service is only running during a drag operation and unregistered once this is finished.The property
DragDropService#dataTransferItems
mirrors the drag data of an HTML5 drag / drop event (DragEvent.dataTransfer.items
). No matter whether the service data is used or the data from the drop event, they have the same structure. For example, for a typical drag operation of content items, the drag data looks like this:{ "cm-studio-rest/uri-list": "[\"content/3350\",\"content/3356\",\"content/3370\",\"content/3360\"]", "cm-content/uuid-list": "[\"14e56f5c-170a-43d6-8381-a9230f202040\",\"ca06b1ca-6d62-4040-a72b-3ff15b1f9dc8\",\"b5b54648-268b-41cb-91cd-1e1c805ae172\",\"66c1b819-1142-4480-9faa-0ed4d50a370c\"]", "cm-member/uuid-list": "[]", }
Here it is visible that different flavors / types of drag data exist. The first two types are just different representations of the same content items. But it is visible that also member items (users and groups) might be dragged. The currently supported types are enumerated in
DataTransferTypes
of the module@coremedia/studio-client.interaction-services-api
.The following shows the drop handler of the My-Edited-Contents App to receive content items via a drag drop operation from the Content App.
onDrop={event => { const uriListData = event.dataTransfer.getData(DataTransferTypes.CM_STUDIO_REST_URI_LIST); if (!uriListData) { event.preventDefault(); return; } const parsedUriList = JSON.parse(uriListData); if (!parsedUriList || !Array.isArray(parsedUriList)) { event.preventDefault(); return; } const contentUriRestTemplate = new URITemplate(ContentImpl.REST_RESOURCE_URI_TEMPLATE); const contents = parsedUriList.filter((uri) => contentUriRestTemplate.matches(uri)).map(beanFactory._.getRemoteBean); contents && contents.length > 0 && session._.getConnection().getCapListRepository() .getEditedContents().addItems(contents); }}
While custom apps can receive drops of content items from the built-in apps, there currently exists no further support to set up a drag operation from a custom app back to the built-in apps. However, it is possible if HTML5 drag / drop is used and the data is set up according to the above structure.
- Clipboard: Similar to drag / drop, the clipboard allows to transfer content or member
items between apps. The
Clipboard
of the module@coremedia/studio-client.interaction-services-api
implements the Clipboard Web API and offers methods for reading and writing clipboard data. To access the clipboard, the global constantclipboard
from@coremedia/studio-client.interaction-services-api
can be used. For it to be initialized, the app needs an import of@coremedia/studio-client.interaction-services-impl/init
somewhere.The clipboard reads and writes data in the form of
ClipboardItem
s. These items support different flavors / types, analogous to the drag / drop data from above. For example, the My-Edited-Contents App has a global key handler to paste from the clipboard.useEffect(() => { const onPaste = async (ev: KeyboardEvent) => { if (ev.key === "v" && (ev.ctrlKey || ev.metaKey)) { const clipboardItems = await clipboard._.read(); const clipboardItem = clipboardItems.find((clipboardItem) => clipboardItem.types.includes(DataTransferTypes.CM_STUDIO_REST_URI_LIST)); if (!clipboardItem) { return; } const restUrisItem = await clipboardItem .getType(DataTransferTypes.CM_STUDIO_REST_URI_LIST); const restUrisItemString = restUrisItem === "string" ? restUrisItem : await (restUrisItem as Blob).text(); try { const restUris: Array<string> = JSON.parse(restUrisItemString); if (restUris) { const contents = restUris.map(beanFactory._ .getRemoteBean).filter((bean) => is(bean, Content)); contents && contents.length > 0 && session._.getConnection().getCapListRepository() .getEditedContents().addItems(contents); } } catch (e) { // ignore } } }; window.addEventListener("keyup", onPaste, false); return () => { window.removeEventListener("keyup", onPaste, false); }; }, []);
The My-Edited-Contents App also has a similar global key handler to copy into the clipboard, using the selection of the edited contents list.
const [selection, setSelection] = useState<GridSelectionModel>([]); ... useEffect(() => { if (!selection || selection.length === 0) { return; } const onCopy = async (ev: KeyboardEvent) => { if (ev.key === "c" && (ev.ctrlKey || ev.metaKey)) { try { const data: Record<string, any> = {}; const remoteBeans = selection .map(selected => beanFactory._.getRemoteBean(selected.toString())) .filter(Boolean); if (!remoteBeans.every(RemoteBeanUtil.isAccessible)) { return; } const restUriBlob = new Blob( [ JSON.stringify(remoteBeans.map((remoteBean) => remoteBean.getUriPath())), ], {type: DataTransferTypes.CM_STUDIO_REST_URI_LIST}, ); data[restUriBlob.type] = restUriBlob; await clipboard._.write([new ClipboardItemImpl(data)]); } catch (e) { // ignore } } }; window.addEventListener("keyup", onCopy, false); return () => { window.removeEventListener("keyup", onCopy, false); }; }, [selection]);
Feature Services Of Content And Workflow App
The Content App and the Workflow App offer
some feature services out of the box. All of them are also listed under the
cmServices
property of the app manifests, so there exist automatic service runners
for them that launch the apps if needed, cf. Section Section 9.36.4, “App Manifest and Apps Menu Entries”.
- ContentFormService: This service and its descriptor
factory are exported by the module
@coremedia/studio-client.content-service-api
. The service is offered by the Content App and allows to open content items in form tabs, track which content items are currently opened and which content item is the active one. - ProjectFormService: This service and its descriptor
factory are exported by the module
@coremedia/studio-client.project-services-api
. The service is offered by the Content App and allows to open project items in form tabs, track which project items are currently opened and which project item is the active one. - CollectionViewService: This service and its descriptor
factory are exported by the module
@coremedia/studio-client.collection-view-services-api
. The service is offered by the Content App and allows to display content items in the collection view (library) and to open the collection view in a specific content search state. - WorkflowFormService: This service and its descriptor
factory are exported by the module
@coremedia/studio-client.workflow-services-api
. The service is offered by the Workflow App and allows to open workflow objects (processes and tasks) in forms, track which workflow objects are currently opened and which workflow object is the active one. In the case of the Workflow App, there is at most one workflow object opened but the API was intentionally kept similar to the above-mentioned form service APIs.