Workspaces
A workspace is the runtime surface that hosts an entity inside an application. It owns the URL ↔ selection ↔ panel coordination, the list controller, the mutation event dispatch, and the route-aware machinery that keeps detail tabs, drafts, and breadcrumbs in sync. Workspaces are the smallest unit of UI you compose at the route level — everything else (the header, body, detail dock, tabs) is wired through the workspace's descriptor.
The framework ships three shapes; new shapes compose from the same shared template and capability mixins.
The shapes
| Shape | Purpose | Owns |
|---|---|---|
EntityWorkspace<T> | Standard list-with-detail. The default for any multi-instance entity. | EntityListController<T>, cdx_panes.WorkspaceController, header / body / detail parts. |
SingletonEntityWorkspace<T> | Single-instance entities (settings, security configuration, module config). No list, no row selection. | The active route's content as a navigationShell. |
GroupedEntityWorkspace | Tabbed shell hosting child workspaces. The Inbox is the canonical example. | A list of GroupedEntityTabs, each producing its own child workspace. |
All three extend WorkspaceTemplate<T> and inherit:
- A single
GoRouterlistener (set up indidChangeDependencies, torn down indispose) that fansonRouteChanged(GoRouterState)to mixins. - A
_disposerslist that capability mixins register teardown closures with — the base walks it once atdispose. - A
QueryCacheService.invalidationssubscription that convertsInvalidationEvents into typedMutationEvents and dispatches to the workspace'sonAfterMutationcallback. dispatchHook/dispatchAsyncHookhelpers for typed callbacks.
Capability mixins
Workspaces compose by mixin. Each mixin handles one capability and participates in the lifecycle by overriding onTemplateInit, onTemplateDispose, and onRouteChanged (always with super. calls so the chain stays intact).
| Mixin | Responsibility |
|---|---|
WithRouterAwareness | URL ↔ state sync — observes GoRouterState and exposes the current segments to the workspace. |
WithListController | Owns the EntityListController<T> lifecycle, search debounce, and pagination. |
WithDetail | Tracks URL-driven panel intent, loads detail data, owns the dock controller, and fires entity-load callbacks. |
WithDraftIsolation | Exposes isCurrentInDraft, a computed value derived from the selected detail entity's draft identity. |
WithBatchActions | Wires multi-select, batch action menus, and selection clearing. |
UrlBinding | Helper for two-way URL ↔ value bindings (filters, panel mode, tab id). |
A new shape only mixes in the capabilities it needs. SingletonEntityWorkspace deliberately omits WithListController and WithDetail because there is no list and no row selection.
Parts
EntityWorkspaceConfig<T> composes the three visual parts of an EntityWorkspace:
EntityHeaderPart— title strip, action buttons, view-mode toggle. Built from the entity's metadata andEntityActions.header.EntityBodyPart<T>— the selectedCollectionLayout<T>rendered inside the workspace.EntityDetailPart— the detail dock chrome that wraps thepanelwidget supplied by the route tree.
SingletonEntityWorkspaceConfig<T> carries only EntityHeaderPart — singletons have no body / detail split. GroupedEntityWorkspaceConfig is shaped around tabs:
basePath— the URL prefix the active tab maps under (/<basePath>/<tab.id>).tabs— a list ofGroupedEntityTab(id, label, icon, lazybuild, optionalbadgeBuilder, optional permission filter).scrollableTabs,headerActions, etc.
Tabs render through an IndexedStack so switching from tab A to B and back preserves A's scroll, sort, filter, and selection state by construction.
Lifecycle hooks
Both EntityWorkspace and SingletonEntityWorkspace accept a small set of typed callbacks. They fire from the workspace base class through dispatchHook, so they are always safe to set/clear without manual wiring.
| Hook | Fires when |
|---|---|
onEntitySelected | Row selection changes (id-only). |
onPanelModeChanged | Detail panel mode flips (split / stacked / dock). |
onBeforeEntityLoad | Detail fetch starts. |
onAfterEntityLoad | Detail fetch completes successfully. |
onBeforeMutation | Editor about to save / delete. |
onAfterMutation | A MutationEvent is dispatched after QueryCacheService invalidation. |
MutationEvent is a sealed hierarchy: EntityCreated, EntityUpdated (with optional changedFields), EntityDeleted, EntityRestored, EntityDraftSaved, EntityDraftSubmitted, EntityDraftDiscarded. Use exhaustive switch expressions when reacting.
URL is the source of truth
Workspaces never duplicate state the URL already carries. The current selected id, panel mode, active tab, drafts toggle, and filter state all live in the URL (or in derived EntityFilterState). Mixins read them on every onRouteChanged and project them into the workspace's observables. This keeps deep-links, hot-reload, and browser back/forward all behaving predictably.
When mounting a workspace at a non-canonical URL (e.g. an Inbox tab at /lms/inbox/usage instead of /lms/activities), pass an explicit route: NavigationPathBuilder so descendant row taps and breadcrumb links stay inside the host shell. Cache keys, API calls, and entity identity are unaffected — only the URLs produced for in-app navigation differ.
Custom shapes
To build a new shape (e.g. a calendar workspace, a kanban workspace):
- Extend
WorkspaceTemplate<T>and add the typed callback fields you want to expose. - Implement the
Stateby extendingWorkspaceTemplateState<S, T>and mixing in the capabilities you need (typicallyWithRouterAwareness<S, T>plus your own private mixins). - Override
buildCallbackContext()to return the read-onlyWorkspaceContext<T>snapshot passed to every callback. - Override
buildto compose the parts. Usecdx_panesprimitives directly for split / dock layouts; render the body inside any other container that fits.
Workspaces are deliberately self-sufficient via mixins. EntityShellLayout and MasterDetailScaffold still exist for the standard route-builder path, but new workspace shapes should compose WorkspaceTemplate, cdx_panes, and the capability mixins directly. New shapes inherit the same router listener, mutation dispatcher, and disposer audit trail as the built-in shapes.
Next steps
- Layouts — how layout slots compose with each workspace shape.
- Configuration — what an
EntityConfigurationdeclares for a workspace to consume. - Data Flow — the cache invalidation events that drive
onAfterMutation.