Skip to content

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

ShapePurposeOwns
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.
GroupedEntityWorkspaceTabbed 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 GoRouter listener (set up in didChangeDependencies, torn down in dispose) that fans onRouteChanged(GoRouterState) to mixins.
  • A _disposers list that capability mixins register teardown closures with — the base walks it once at dispose.
  • A QueryCacheService.invalidations subscription that converts InvalidationEvents into typed MutationEvents and dispatches to the workspace's onAfterMutation callback.
  • dispatchHook / dispatchAsyncHook helpers 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).

MixinResponsibility
WithRouterAwarenessURL ↔ state sync — observes GoRouterState and exposes the current segments to the workspace.
WithListControllerOwns the EntityListController<T> lifecycle, search debounce, and pagination.
WithDetailTracks URL-driven panel intent, loads detail data, owns the dock controller, and fires entity-load callbacks.
WithDraftIsolationExposes isCurrentInDraft, a computed value derived from the selected detail entity's draft identity.
WithBatchActionsWires multi-select, batch action menus, and selection clearing.
UrlBindingHelper 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 and EntityActions.header.
  • EntityBodyPart<T> — the selected CollectionLayout<T> rendered inside the workspace.
  • EntityDetailPart — the detail dock chrome that wraps the panel widget 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 of GroupedEntityTab (id, label, icon, lazy build, optional badgeBuilder, 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.

HookFires when
onEntitySelectedRow selection changes (id-only).
onPanelModeChangedDetail panel mode flips (split / stacked / dock).
onBeforeEntityLoadDetail fetch starts.
onAfterEntityLoadDetail fetch completes successfully.
onBeforeMutationEditor about to save / delete.
onAfterMutationA 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):

  1. Extend WorkspaceTemplate<T> and add the typed callback fields you want to expose.
  2. Implement the State by extending WorkspaceTemplateState<S, T> and mixing in the capabilities you need (typically WithRouterAwareness<S, T> plus your own private mixins).
  3. Override buildCallbackContext() to return the read-only WorkspaceContext<T> snapshot passed to every callback.
  4. Override build to compose the parts. Use cdx_panes primitives 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 EntityConfiguration declares for a workspace to consume.
  • Data Flow — the cache invalidation events that drive onAfterMutation.