diff --git a/16/umbraco-cms/SUMMARY.md b/16/umbraco-cms/SUMMARY.md index df1cd51c554..474c079fd4e 100644 --- a/16/umbraco-cms/SUMMARY.md +++ b/16/umbraco-cms/SUMMARY.md @@ -156,8 +156,10 @@ * [Section View](customizing/extending-overview/extension-types/sections/section-view.md) * [Workspaces](customizing/extending-overview/extension-types/workspaces/README.md) * [Workspace Actions](customizing/extending-overview/extension-types/workspaces/workspace-editor-actions.md) + * [Workspace Action Menu Items](customizing/extending-overview/extension-types/workspaces/workspace-action-menu-items.md) * [Workspace Context](customizing/extending-overview/extension-types/workspaces/workspace-context.md) * [Workspace Views](customizing/extending-overview/extension-types/workspaces/workspace-views.md) + * [Workspace Footer Apps](customizing/extending-overview/extension-types/workspaces/workspace-footer-apps.md) * [Menu](customizing/extending-overview/extension-types/menu.md) * [Header Apps](customizing/extending-overview/extension-types/header-apps.md) * [Icons](customizing/extending-overview/extension-types/icons.md) diff --git a/16/umbraco-cms/customizing/extending-overview/extension-types/workspaces/README.md b/16/umbraco-cms/customizing/extending-overview/extension-types/workspaces/README.md index 5581b2339ac..0d45592e0ba 100644 --- a/16/umbraco-cms/customizing/extending-overview/extension-types/workspaces/README.md +++ b/16/umbraco-cms/customizing/extending-overview/extension-types/workspaces/README.md @@ -1,6 +1,67 @@ --- description: >- - An overview of the available extension types related to workspaces. + Learn about workspace extension types that provide shared functionality and communication within workspace environments. --- # Extension Types: Workspaces + +Workspace extensions provide functionality that operates within specific workspace environments, such as document editing, media management, or member editing. These extensions can communicate through shared workspace contexts to create cohesive, integrated functionality. + +## Available Extension Types + +Workspace extensions include several types that work together through shared state management: + +### Core Extensions +- **[Workspace Context](workspace-context.md)** - Provides shared state management and communication between workspace extensions +- **[Workspace](workspace.md)** - Defines the main workspace environment and routing + +### User Interface Extensions +- **[Workspace Views](workspace-views.md)** - Tab-based content areas for organizing different aspects of entity editing +- **[Workspace Footer Apps](workspace-footer-apps.md)** - Persistent status information and contextual data in the footer area + +### Action Extensions +- **[Workspace Actions](workspace-editor-actions.md)** - Primary action buttons that appear in the workspace footer +- **[Workspace Action Menu Items](workspace-action-menu-items.md)** - Dropdown menu items that extend workspace actions with additional functionality + +## Integration Patterns + +Workspace extensions communicate through shared contexts using these patterns: + +1. **Workspace Context** manages centralized state using observables that automatically notify subscribers of changes +2. **Workspace Actions** consume the context to modify state when users interact with buttons or menu items +3. **Workspace Views** observe context state to automatically update their UI when data changes +4. **Footer Apps** monitor context state to display real-time status information + +### Communication Flow + +``` +Workspace Context (State Management) + ↕️ +Workspace Actions (State Modification) + ↕️ +Workspace Views (State Display) + ↕️ +Footer Apps (Status Monitoring) +``` + +## Getting Started + +To create a complete workspace extension system: + +1. **Start with a [Workspace Context](workspace-context.md)** to provide shared state management +2. **Add [Workspace Actions](workspace-editor-actions.md)** for primary user interactions +3. **Create [Workspace Views](workspace-views.md)** for dedicated editing interfaces +4. **Include [Footer Apps](workspace-footer-apps.md)** for persistent status information +5. **Extend actions with [Menu Items](workspace-action-menu-items.md)** for additional functionality + +{% hint style="info" %} +All workspace extensions are automatically scoped to their workspace instance, ensuring that extensions in different workspaces cannot interfere with each other. +{% endhint %} + +## Example Implementation + +For a complete working example that demonstrates all workspace extension types working together, see: + +{% content-ref url="../../../../../examples/workspace-context-counter/" %} +[Complete Integration Example](../../../../../examples/workspace-context-counter/) +{% endcontent-ref %} \ No newline at end of file diff --git a/16/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-action-menu-item.md b/16/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-action-menu-item.md deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/16/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-action-menu-items.md b/16/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-action-menu-items.md new file mode 100644 index 00000000000..c682b8eb227 --- /dev/null +++ b/16/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-action-menu-items.md @@ -0,0 +1,151 @@ +--- +description: >- + Learn how to create workspace action menu items that extend workspace actions with additional functionality. +--- + +# Workspace Action Menu Item + +Workspace Action Menu Items extend existing workspace actions by adding dropdown menu options. They provide secondary functionality that relates to the primary action without cluttering the workspace footer. + +## Purpose + +Action Menu Items provide: +- **Secondary actions** that extend primary workspace actions +- **Grouped functionality** under a single action button +- **Progressive disclosure** for related operations +- **Context integration** with workspace state + +{% hint style="info" %} +Action Menu Items are associated with specific Workspace Actions using the `forWorkspaceActions` property. +{% endhint %} + +## Manifest + +{% code caption="manifest.ts" %} +```typescript +{ + type: 'workspaceActionMenuItem', + kind: 'default', + alias: 'example.workspaceActionMenuItem.resetCounter', + name: 'Reset Counter Menu Item', + api: () => import('./reset-counter-menu-item.action.js'), + forWorkspaceActions: 'example.workspaceAction.incrementor', + weight: 100, + meta: { + label: 'Reset Counter', + icon: 'icon-refresh', + }, +} +``` +{% endcode %} + +### Key Properties +- **`forWorkspaceActions`** - Specifies which workspace action this extends +- **`weight`** - Controls ordering within the dropdown menu +- **`meta.label`** - Text displayed in dropdown +- **`meta.icon`** - Icon displayed alongside label + +## Implementation + +Create a workspace action menu item by extending `UmbWorkspaceActionMenuItemBase` and implementing the `execute` method. This provides the functionality that runs when users select the menu item: + +{% code caption="reset-counter-menu-item.action.ts" %} +```typescript +import { EXAMPLE_COUNTER_CONTEXT } from './counter-workspace-context.js'; +import { UmbWorkspaceActionMenuItemBase } from '@umbraco-cms/backoffice/workspace'; +import type { UmbWorkspaceActionMenuItem } from '@umbraco-cms/backoffice/workspace'; + +export class ExampleResetCounterMenuItemAction extends UmbWorkspaceActionMenuItemBase implements UmbWorkspaceActionMenuItem { + override async execute() { + const context = await this.getContext(EXAMPLE_COUNTER_CONTEXT); + if (!context) { + throw new Error('Could not get the counter context'); + } + + context.reset(); + } +} + +export const api = ExampleResetCounterMenuItemAction; +``` +{% endcode %} + +## Action Relationship + +Menu items create dropdown menus for their associated actions: + +### Primary Action +```typescript +// The main action that appears as a button +{ + type: 'workspaceAction', + alias: 'example.workspaceAction.save', + meta: { label: 'Save' }, +} +``` + +### Menu Item Extensions +```typescript +// Multiple menu items can extend the same action +{ + type: 'workspaceActionMenuItem', + alias: 'example.menuItem.saveAndClose', + forWorkspaceActions: 'example.workspaceAction.save', + meta: { label: 'Save and Close' }, +} + +{ + type: 'workspaceActionMenuItem', + alias: 'example.menuItem.saveAsDraft', + forWorkspaceActions: 'example.workspaceAction.save', + meta: { label: 'Save as Draft' }, +} +``` + +## Use Cases + +### Alternative Operations +Provide different ways to perform related actions: +```typescript +// Primary: Quick save +// Menu: Save and close, Save as template, Save and publish +``` + +### Destructive Actions +Keep dangerous operations in dropdown menus: +```typescript +// Primary: Edit +// Menu: Delete, Archive, Reset +``` + +### Context-Specific Options +Add options that depend on entity state: +```typescript +override async execute() { + const entityContext = await this.getContext(ENTITY_CONTEXT); + + if (entityContext.isDraft()) { + await entityContext.saveAsDraft(); + } else { + await entityContext.saveAndPublish(); + } +} +``` + +## Best Practices + +### Related Functionality +Only group actions that are logically related to the primary action. + +### Clear Labels +Use descriptive labels that clearly indicate the menu item's purpose. + +### Appropriate Ordering +Use weight to order menu items by importance or frequency of use. + +### Context Dependencies +Always check context availability before performing operations. + +{% content-ref url="../../../../../examples/workspace-context-counter/" %} +[Complete Integration Example](../../../../../examples/workspace-context-counter/) +{% endcontent-ref %} \ No newline at end of file diff --git a/16/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-context.md b/16/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-context.md index 2000323ca5c..8c9a22775ab 100644 --- a/16/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-context.md +++ b/16/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-context.md @@ -1,135 +1,198 @@ --- -description: Establish an extension to communicate across the application. +description: >- + Learn how to create workspace contexts that provide shared state management and communication between workspace extensions. --- # Workspace Context -The general Workspace Context is a container for the data of a workspace. It is a wrapper around the data of the entity that the workspace is working on. It is responsible for loading and saving the data to the server. Workspace Contexts are used to bring additional context alongside the default context of a workspace. +Workspace Contexts serve as the central communication hub for workspace extensions, providing shared state management within workspace boundaries. They enable different workspace components to interact through a common data layer. -* A workspace context knows about its entity type (for example content, media, member, etc.) and holds its unique string (for example: key). -* Most workspace contexts hold a draft state of its entity data. It is a copy of the entity data that can be modified at runtime and sent to the server to be saved. +## Purpose -You can add additional Workspace Contexts using the `workspaceContext` Extension Type, which allows you to inject custom logic into your own Workspace or another one. +Workspace Contexts provide: +- **Shared state** scoped to a specific workspace instance +- **Communication layer** between workspace extensions +- **Entity lifecycle management** for workspace data +- **Context isolation** ensuring workspace independence -## Example of Workspace Context +{% hint style="info" %} +Workspace Contexts are automatically scoped to their workspace - extensions in different workspaces cannot access each other's contexts. +{% endhint %} -The API will be initiated with the same host as the default Workspace Context. +## Manifest +{% code caption="manifest.ts" %} ```typescript { - type: 'workspaceContext', - alias: 'My.WorkspaceContext.Counter', - name: 'My Counter Context', - api: 'my-workspace-counter.context.js', - conditions: [ - { - alias: 'Umb.Condition.WorkspaceAlias', - match: 'Umb.Workspace.Document', - } - ] + type: 'workspaceContext', + name: 'Example Counter Workspace Context', + alias: 'example.workspaceContext.counter', + api: () => import('./counter-workspace-context.js'), + conditions: [ + { + alias: UMB_WORKSPACE_CONDITION_ALIAS, + match: 'Umb.Workspace.Document', + }, + ], } ``` +{% endcode %} -The code for such an API file might look like this: +## Implementation +Create a workspace context by extending `UmbContextBase` and providing a unique context token. Add this to your project to enable shared state management between workspace extensions: + +{% code caption="counter-workspace-context.ts" %} ```typescript -import { - UmbController, - UmbControllerHost, -} from "@umbraco-cms/backoffice/controller-api"; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; -import { UmbContextToken } from "@umbraco-cms/backoffice/context-api"; -import { UmbNumberState } from "@umbraco-cms/backoffice/observable-api"; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbNumberState } from '@umbraco-cms/backoffice/observable-api'; -export class MyContextApi extends UmbContextBase { - #counter = new UmbNumberState(0); - readonly counter = this.#counter.asObservable(); +export class WorkspaceContextCounterElement extends UmbContextBase { + #counter = new UmbNumberState(0); + readonly counter = this.#counter.asObservable(); - constructor(host: UmbControllerHost) { - super(host, My_COUNTER_CONTEXT); - } + constructor(host: UmbControllerHost) { + super(host, EXAMPLE_COUNTER_CONTEXT); + } - increment() { - this.#counter.next(this.#counter.value + 1); - } + increment() { + this.#counter.setValue(this.#counter.value + 1); + } + + reset() { + this.#counter.setValue(0); + } } -// Important to export as api for the Extension Registry to pick up the class: -export const api = MyContextCounterApi; +export const api = WorkspaceContextCounterElement; + +export const EXAMPLE_COUNTER_CONTEXT = new UmbContextToken( + 'UmbWorkspaceContext', + 'example.workspaceContext.counter', +); ``` +{% endcode %} + +## Context Token Pattern -A Context Token for a Workspace Context Extension should look like this: +Always use `'UmbWorkspaceContext'` as the first parameter in your context token to ensure proper workspace scoping and isolation: ```typescript -export const My_COUNTER_CONTEXT = new UmbContextToken( - "UmbWorkspaceContext", - "My.WorkspaceContext.Counter" +export const MY_WORKSPACE_CONTEXT = new UmbContextToken( + 'UmbWorkspaceContext', // Ensures workspace scoping + 'my.extension.alias', // Must match manifest alias ); ``` -It is recommended to use `UmbWorkspaceContext` as the Context Alias for your Context Token. This will ensure that the requester only retrieves this Context if it's present at their nearest Workspace Context. Use the Extension Manifest Alias as the API Alias for your Context Token to ensure its unique. For more information, see the [Context API](../../../foundation/context-api/) article. +## Workspace Lifecycle -## Use the Workspace Context +### Initialization +- Created when workspace loads +- Available to all extensions within that workspace +- Destroyed when workspace closes -The following example declares an element that will be present in the same workspace for which the workspace context is provided. In this particular example, it is a workspace view that will be registered to the `Umb.Workspace.Document` workspace. +### Scoping +- Context instances are isolated per workspace +- Extensions can only access contexts from their own workspace +- Context requests automatically scope to nearest workspace + +### Conditions +Workspace contexts only initialize when their conditions match: ```typescript -import { My_COUNTER_CONTEXT } from './example-workspace-context.js'; -import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import { css, html, customElement, state, LitElement } from '@umbraco-cms/backoffice/external/lit'; -import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api'; +conditions: [ + { + alias: UMB_WORKSPACE_CONDITION_ALIAS, + match: 'Umb.Workspace.Document', // Only available in document workspaces + }, +], +``` -@customElement('example-counter-workspace-view') -export class ExampleCounterWorkspaceView extends UmbElementMixin(LitElement) { - - #counterContext?: typeof My_COUNTER_CONTEXT.TYPE; +## Entity Data Patterns - @state() - private count = 0; +### Draft State Management +```typescript +export class EntityWorkspaceContext extends UmbContextBase { + #entity = new UmbObjectState(null); + #isDirty = new UmbBooleanState(false); + + readonly entity = this.#entity.asObservable(); + readonly isDirty = this.#isDirty.asObservable(); + + updateEntity(changes: Partial) { + const current = this.#entity.getValue(); + if (current) { + this.#entity.setValue({ ...current, ...changes }); + this.#isDirty.setValue(true); + } + } +} +``` - constructor() { - super(); - this.consumeContext(My_COUNTER_CONTEXT, (context) => { - this.#counterContext = context; - this.observe(this.#counterContext.counter, (count) => { - this.count = count; - }); +### Server Integration +```typescript +export class ServerEntityContext extends UmbContextBase { + #repository = inject(MyEntityRepository); + + async save() { + const entity = this.#entity.getValue(); + const saved = await this.#repository.save(entity); + this.#entity.setValue(saved); + this.#isDirty.setValue(false); } +} +``` + +## Extension Communication - #onClick() { - this.#counterContext?.increment(); +### In Workspace Actions +```typescript +export class MyWorkspaceAction extends UmbWorkspaceActionBase { + override async execute() { + const context = await this.getContext(MY_WORKSPACE_CONTEXT); + context.performAction(); } +} +``` - override render() { - return html` - Current count value: ${this.count} - - `; +### In Workspace Views +```typescript +export class MyWorkspaceView extends UmbElementMixin(LitElement) { + constructor() { + super(); + this.consumeContext(MY_WORKSPACE_CONTEXT, (context) => { + this.observe(context.data, (data) => this.requestUpdate()); + }); } } +``` -// Important to export as 'element' otherwise the Extension Registry cannot pick up the class. -export const element = ExampleCounterWorkspaceView +## Best Practices + +### State Encapsulation +```typescript +// ✅ Private state with public observables +#data = new UmbObjectState(initialData); +readonly data = this.#data.asObservable(); + +// ❌ Direct state exposure +data = new UmbObjectState(initialData); ``` -Manifest to register this: - -
{
-    type: 'workspaceView',
-    name: 'Example Counter Workspace View',
-    alias: 'example.workspaceView.counter',
-    element: () => import('./example-workspace-view.js'),
-    weight: 900,
-    meta: {
-        label: 'Counter',
-        pathname: 'counter',
-        icon: 'icon-lab',
-    },
-    conditions: [
-        {
-	    alias: UMB_WORKSPACE_CONDITION_ALIAS,
-            match: 'Umb.Workspace.Document',
-        },
-    ],
-}
-
+### Context Token Consistency +```typescript +// ✅ Use workspace scoping +new UmbContextToken('UmbWorkspaceContext', 'my.alias'); + +// ❌ Generic context (not workspace-scoped) +new UmbContextToken('MyContext', 'my.alias'); +``` + +### Conditional Availability +Only provide contexts when they're meaningful for the workspace type. + +{% content-ref url="../../../../../examples/workspace-context-counter/" %} +[Complete Integration Example](../../../../../examples/workspace-context-counter/) +{% endcontent-ref %} \ No newline at end of file diff --git a/16/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-editor-actions.md b/16/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-editor-actions.md index 2eb896ea50a..f51bb71566f 100644 --- a/16/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-editor-actions.md +++ b/16/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-editor-actions.md @@ -1,55 +1,246 @@ +--- +description: >- + Learn how to create workspace actions that provide primary user interactions within workspace environments. +--- + # Workspace Actions -{% hint style="warning" %} -This page is a work in progress and may undergo further revisions, updates, or amendments. The information contained herein is subject to change without notice. -{% endhint %} +Workspace Actions appear as buttons in the workspace footer, providing primary interaction points for workspace operations. They integrate directly with workspace contexts and can be extended with dropdown menu items. + +## Purpose + +Workspace Actions provide: +- **Primary interactions** prominently displayed in workspace footer +- **Context integration** with direct access to workspace state +- **Extensibility** through action menu items +- **Conditional availability** based on workspace state or type + +## Manifest + +{% code caption="manifest.ts" %} +```typescript +{ + type: 'workspaceAction', + kind: 'default', + name: 'Example Count Incrementor Workspace Action', + alias: 'example.workspaceAction.incrementor', + weight: 1000, + api: () => import('./incrementor-workspace-action.js'), + meta: { + label: 'Increment', + look: 'primary', + color: 'danger', + }, + conditions: [ + { + alias: UMB_WORKSPACE_CONDITION_ALIAS, + match: 'Umb.Workspace.Document', + }, + ], +} +``` +{% endcode %} + +### Key Properties +- **`weight`** - Controls action ordering (higher appears first) +- **`meta.look`** - Button style: `'primary'`, `'secondary'`, `'outline'` +- **`meta.color`** - Color theme: `'default'`, `'positive'`, `'warning'`, `'danger'` +- **`conditions`** - Determines workspace availability + +## Implementation + +Create a workspace action by extending `UmbWorkspaceActionBase` and implementing the `execute` method. This provides the functionality that runs when users click the action button: + +{% code caption="incrementor-workspace-action.ts" %} +```typescript +import { EXAMPLE_COUNTER_CONTEXT } from './counter-workspace-context.js'; +import { UmbWorkspaceActionBase, type UmbWorkspaceAction } from '@umbraco-cms/backoffice/workspace'; + +export class ExampleIncrementorWorkspaceAction extends UmbWorkspaceActionBase implements UmbWorkspaceAction { + override async execute() { + const context = await this.getContext(EXAMPLE_COUNTER_CONTEXT); + if (!context) { + throw new Error('Could not get the counter context'); + } + context.increment(); + } +} + +export const api = ExampleIncrementorWorkspaceAction; +``` +{% endcode %} + +## Workspace Integration + +### Context Access +Actions automatically have access to their workspace's contexts: + +```typescript +// Context is scoped to the current workspace +const context = await this.getContext(MY_WORKSPACE_CONTEXT); +``` + +### Execution Lifecycle +- Actions execute when clicked +- Can be async for complex operations +- Have access to workspace state during execution +- Can modify workspace contexts + +### Conditional Execution +Check workspace state before performing actions: + +```typescript +override async execute() { + const entityContext = await this.getContext(ENTITY_CONTEXT); + + if (!entityContext.canPerformAction()) { + return; // Silently skip if not available + } + + await entityContext.performAction(); +} +``` -Workspace actions are a set of functionalities or operations that can be performed within a workspace. These actions involve creating documents within the workspace, organizing and categorizing documents, publishing content and so on. +## Action Menu Integration -Workspace action relates to a workspace alias (Umb.Workspace.Document) and has Access to the workspace context. +Actions can be extended with dropdown menu items using `forWorkspaceActions`: -

Workspace Actions

+{% code caption="action-with-menu.ts" %} +```typescript +// Primary Action +{ + type: 'workspaceAction', + alias: 'example.action.save', + api: () => import('./save-action.js'), + meta: { label: 'Save' }, +} -**JavaScript Manifest example** +// Menu Item Extension +{ + type: 'workspaceActionMenuItem', + alias: 'example.menuItem.saveAndClose', + api: () => import('./save-close-action.js'), + forWorkspaceActions: 'example.action.save', // Extends the save action + meta: { label: 'Save and Close' }, +} +``` +{% endcode %} -
import { extensionRegistry } from '@umbraco-cms/extension-registry';
-import { MyWorkspaceAction } from './my-workspace-action';
+## Action Events
 
-const manifest = {
- type: 'workspaceAction',
- alias: 'My.WorkspaceAction',
- name: 'My Workspace Action',
- api: MyWorkspaceAction,
- meta: {
-  label: 'My Action',
- },
- conditions: [
-  {
-   alias: 'Umb.Condition.WorkspaceAlias',
-   match: 'My.Workspace',
-  },
- ],
-};
+Workspace actions dispatch a generic `action-executed` event when they complete:
 
-extensionRegistry.register(manifest);
-
+```typescript +// Event is automatically dispatched by the action UI element +export class UmbActionExecutedEvent extends Event { + constructor() { + super('action-executed', { bubbles: true, composed: true, cancelable: false }); + } +} +``` -## The Workspace Action Class +### Event Characteristics +- **Generic signal** - No action-specific data included +- **Always dispatched** - Fires on both success and failure +- **DOM bubbling** - Event bubbles up through the workspace +- **No payload** - Contains no information about the action or results -As part of the Extension Manifest you can attach a class that will be instantiated as part of the action. It will have access to the host element and the Workspace Context. When the action is clicked the `execute` method on the API class will be run. When the action is completed, an event on the host element will be dispatched to notify any surrounding elements. +### Listening for Action Events +Components can listen for action completion: -```ts -import { UmbWorkspaceActionBase } from '@umbraco-cms/backoffice/workspace'; +```typescript +// In a workspace component +this.addEventListener('action-executed', (event) => { + // Action completed (success or failure) + // Refresh UI, close modals, etc. +}); +``` + +### When to Use Events +- **UI cleanup** - Close dropdowns, modals after action execution +- **General refresh** - Update displays when any action completes +- **State synchronization** - Trigger broad UI updates + +{% hint style="info" %} +For action-specific communication, use workspace contexts rather than events. Events provide only generic completion signals. +{% endhint %} -export class MyWorkspaceAction extends UmbWorkspaceActionBase { - execute() { - this.workspaceContext.myAction(this.selection); - } +## Common Patterns + +### Entity Operations +```typescript +export class SaveAction extends UmbWorkspaceActionBase { + override async execute() { + const workspace = await this.getContext(DOCUMENT_WORKSPACE_CONTEXT); + await workspace.save(); + } } ``` -**Default Element** +### State-Dependent Actions +```typescript +export class PublishAction extends UmbWorkspaceActionBase { + override async execute() { + const workspace = await this.getContext(DOCUMENT_WORKSPACE_CONTEXT); + + if (workspace.hasValidationErrors()) { + // Action doesn't execute if invalid + return; + } + + await workspace.saveAndPublish(); + } +} +``` + +### Multi-Step Operations +```typescript +export class ComplexAction extends UmbWorkspaceActionBase { + override async execute() { + const workspace = await this.getContext(MY_WORKSPACE_CONTEXT); + + workspace.setStatus('processing'); + await workspace.validate(); + await workspace.process(); + await workspace.save(); + workspace.setStatus('complete'); + } +} +``` + +## Best Practices +### Action Availability +Only show actions when they're meaningful: ```typescript -interface UmbWorkspaceActionElement {} +conditions: [ + { + alias: UMB_WORKSPACE_CONDITION_ALIAS, + match: 'Umb.Workspace.Document', + }, + { + alias: 'My.Condition.EntityState', + match: 'draft', // Only show for draft entities + }, +], ``` + +### Visual Hierarchy +Use appropriate styling for action importance: +```typescript +// Primary action (most important) +meta: { look: 'primary', color: 'positive' } + +// Secondary action +meta: { look: 'secondary' } + +// Destructive action +meta: { look: 'primary', color: 'danger' } +``` + +### Context Dependencies +Always check context availability before performing operations. + +{% content-ref url="../../../../../examples/workspace-context-counter/" %} +[Complete Integration Example](../../../../../examples/workspace-context-counter/) +{% endcontent-ref %} \ No newline at end of file diff --git a/16/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-footer-app.md b/16/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-footer-app.md deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/16/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-footer-apps.md b/16/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-footer-apps.md new file mode 100644 index 00000000000..edd77d84806 --- /dev/null +++ b/16/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-footer-apps.md @@ -0,0 +1,221 @@ +--- +description: >- + Learn how to create workspace footer apps that provide persistent status information and contextual data in workspace environments. +--- + +# Workspace Footer App + +Workspace Footer Apps provide persistent status information and contextual data in the workspace footer area. They offer a non-intrusive way to display important information that remains visible while users work with workspace content. + +## Purpose + +Footer Apps provide: +- **Persistent status** information visible while editing +- **Contextual data** related to the current entity +- **Non-intrusive monitoring** without taking main workspace space +- **Real-time updates** through workspace context integration + +{% hint style="info" %} +Footer apps appear in the bottom area of workspaces and are ideal for status indicators, counters, and contextual information. +{% endhint %} + +## Manifest + +{% code caption="manifest.ts" %} +```typescript +{ + type: 'workspaceFooterApp', + alias: 'example.workspaceFooterApp.counterStatus', + name: 'Counter Status Footer App', + element: () => import('./counter-status-footer-app.element.js'), + weight: 900, + conditions: [ + { + alias: UMB_WORKSPACE_CONDITION_ALIAS, + match: 'Umb.Workspace.Document', + }, + ], +} +``` +{% endcode %} + +### Key Properties +- **`element`** - Points to the Lit element implementation +- **`weight`** - Controls positioning within footer area +- **`conditions`** - Determines workspace availability + +## Implementation + +Implement your workspace footer app as a Lit element that extends `UmbElementMixin`. This provides access to workspace contexts and reactive state management: + +{% code caption="counter-status-footer-app.element.ts" %} +```typescript +import { customElement, html, state, LitElement } from '@umbraco-cms/backoffice/external/lit'; +import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api'; +import { EXAMPLE_COUNTER_CONTEXT } from './counter-workspace-context.js'; + +@customElement('example-counter-status-footer-app') +export class ExampleCounterStatusFooterAppElement extends UmbElementMixin(LitElement) { + @state() + private _counter = 0; + + constructor() { + super(); + this.#observeCounter(); + } + + async #observeCounter() { + const context = await this.getContext(EXAMPLE_COUNTER_CONTEXT); + if (!context) return; + + this.observe(context.counter, (counter: number) => { + this._counter = counter; + }); + } + + override render() { + return html`Counter: ${this._counter}`; + } +} + +export default ExampleCounterStatusFooterAppElement; + +declare global { + interface HTMLElementTagNameMap { + 'example-counter-status-footer-app': ExampleCounterStatusFooterAppElement; + } +} +``` +{% endcode %} + +## Footer App Lifecycle + +### Initialization +- Footer apps initialize when workspace loads +- Context consumption happens during construction +- Apps persist for the workspace lifetime + +### Updates +- Use `observe()` for reactive updates from workspace contexts +- Apps update automatically when observed state changes +- Efficient rendering keeps footer responsive + +## Common Patterns + +### Status Indicators +```typescript +@customElement('entity-status-footer-app') +export class EntityStatusFooterApp extends UmbElementMixin(LitElement) { + @state() + private status = 'loading'; + + constructor() { + super(); + this.consumeContext(ENTITY_CONTEXT, (context) => { + this.observe(context.status, (status) => { + this.status = status; + }); + }); + } + + override render() { + return html` +
+ + ${this.status} +
+ `; + } + + #getStatusIcon() { + switch (this.status) { + case 'saved': return 'check'; + case 'draft': return 'edit'; + case 'error': return 'alert'; + default: return 'hourglass'; + } + } +} +``` + +### Live Counters +```typescript +@customElement('word-count-footer-app') +export class WordCountFooterApp extends UmbElementMixin(LitElement) { + @state() + private wordCount = 0; + + constructor() { + super(); + this.consumeContext(CONTENT_CONTEXT, (context) => { + this.observe(context.content, (content) => { + this.wordCount = this.#countWords(content); + }); + }); + } + + #countWords(content: string): number { + return content.trim().split(/\s+/).filter(word => word.length > 0).length; + } + + override render() { + return html`${this.wordCount} words`; + } +} +``` + +### Validation Summary +```typescript +@customElement('validation-footer-app') +export class ValidationFooterApp extends UmbElementMixin(LitElement) { + @state() + private errorCount = 0; + + @state() + private warningCount = 0; + + constructor() { + super(); + this.consumeContext(VALIDATION_CONTEXT, (context) => { + this.observe(context.errors, (errors) => { + this.errorCount = errors.filter(e => e.severity === 'error').length; + this.warningCount = errors.filter(e => e.severity === 'warning').length; + }); + }); + } + + override render() { + if (this.errorCount === 0 && this.warningCount === 0) { + return html`✓ Valid`; + } + + return html` +
+ ${this.errorCount > 0 ? html`${this.errorCount} errors` : ''} + ${this.warningCount > 0 ? html`${this.warningCount} warnings` : ''} +
+ `; + } +} +``` + +## Best Practices + +### Performance +Keep footer apps lightweight for responsive workspace interaction. + +### Information Density +Display only essential information - footer space is limited. + +### Context Dependencies +Always check context availability before accessing properties. + +### Responsive Design +Ensure footer apps work across different workspace sizes. + +### Visual Consistency +Use Umbraco's design system for consistent styling. + +{% content-ref url="../../../../../examples/workspace-context-counter/" %} +[Complete Integration Example](../../../../../examples/workspace-context-counter/) +{% endcontent-ref %} \ No newline at end of file diff --git a/16/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-views.md b/16/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-views.md index 7f46a46edf0..829ba31d0fe 100644 --- a/16/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-views.md +++ b/16/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-views.md @@ -1,100 +1,275 @@ --- -description: Append a view to any Workspace +description: >- + Learn how to create workspace views that provide tab-based content areas for organizing different aspects of entity editing. --- # Workspace Views -{% hint style="warning" %} -This page is a work in progress and may undergo further revisions, updates, or amendments. The information contained herein is subject to change without notice. -{% endhint %} +Workspace Views provide tab-based content areas within workspaces, allowing you to organize different aspects of entity editing into focused interfaces. They appear as tabs alongside the default content editing interface. {% hint style="info" %} -Workspace Views was previously called Content Apps. +Workspace Views were previously called Content Apps in earlier versions of Umbraco. {% endhint %} -Workspace Views are customizable companion **tabs** with the ability to take place in any workspace. +## Purpose + +Workspace Views provide: +- **Tab-based organization** for different editing aspects +- **Contextual interfaces** related to the current entity +- **Workspace integration** with access to workspace contexts +- **Custom functionality** specific to entity types

Workspace Views

-**In Content Section** +## Manifest + +{% code caption="manifest.ts" %} +```typescript +{ + type: 'workspaceView', + name: 'Example Counter Workspace View', + alias: 'example.workspaceView.counter', + element: () => import('./counter-workspace-view.js'), + weight: 900, + meta: { + label: 'Counter', + pathname: 'counter', + icon: 'icon-lab', + }, + conditions: [ + { + alias: UMB_WORKSPACE_CONDITION_ALIAS, + match: 'Umb.Workspace.Document', + }, + ], +} +``` +{% endcode %} + +### Key Properties +- **`weight`** - Tab ordering (higher weight appears first) +- **`meta.label`** - Text displayed on the tab +- **`meta.pathname`** - URL segment for the view +- **`meta.icon`** - Icon displayed on the tab +- **`conditions`** - Determines workspace availability -With Workspace Views, editors can switch from editing 'Content' to accessing contextual information related to the item they are editing. +## Implementation -The default workspace view is **'Info'** - displaying Links, History and Status of the current content item. +Implement your workspace view as a Lit element that extends `UmbElementMixin`. This creates a tab-based interface that users can navigate to within the workspace: -## Example of a Workspace View +{% code caption="counter-workspace-view.ts" %} +```typescript +import { EXAMPLE_COUNTER_CONTEXT } from './counter-workspace-context.js'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { css, html, customElement, state, LitElement } from '@umbraco-cms/backoffice/external/lit'; +import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api'; -1. Follow the [Vite Package Setup](../../../development-flow/vite-package-setup.md) by creating a new project folder called "`workspaceview`" in `App_Plugins`. -2. Create a manifest file named `umbraco-package.json` at the root of the `workspaceview` folder. Here we define and configure our workspace view. -3. Add the following code to `umbraco-package.json`: +@customElement('example-counter-workspace-view') +export class ExampleCounterWorkspaceView extends UmbElementMixin(LitElement) { + #counterContext?: typeof EXAMPLE_COUNTER_CONTEXT.TYPE; -{% code title="umbraco-package.json" lineNumbers="true" %} -```json -{ - "$schema": "../../umbraco-package-schema.json", - "name": "My workspace", - "version": "0.1.0", - "extensions": [ - { - "type": "workspaceView", - "alias": "My.WorkspaceView", - "name": "My Workspace View", - "element": "/App_Plugins/workspaceview/dist/workspaceview.js", - "meta": { - "label": "My Workspace View", - "pathname": "/my-workspace-view", - "icon": "icon-add" - }, - "conditions": [ - { - "alias": "Umb.Condition.WorkspaceAlias", - "match": "Umb.Workspace.Document" - } - ] - } - ] + @state() + private count = 0; + + constructor() { + super(); + this.consumeContext(EXAMPLE_COUNTER_CONTEXT, (instance) => { + this.#counterContext = instance; + this.#observeCounter(); + }); + } + + #observeCounter(): void { + if (!this.#counterContext) return; + this.observe(this.#counterContext.counter, (count) => { + this.count = count; + }); + } + + override render() { + return html` + +

Counter Example

+

Current count value: ${this.count}

+

This workspace view consumes the Counter Context and displays the current count.

+
+ `; + } + + static override styles = [ + UmbTextStyles, + css` + :host { + display: block; + padding: var(--uui-size-layout-1); + } + `, + ]; +} + +export default ExampleCounterWorkspaceView; + +declare global { + interface HTMLElementTagNameMap { + 'example-counter-workspace-view': ExampleCounterWorkspaceView; + } } ``` {% endcode %} -4. Add the following code to the existing `my-element.ts` from the `src`folder: +## View Lifecycle + +### Initialization +- Views initialize when their tab becomes active +- Context consumption happens during construction +- Views have access to workspace-scoped contexts + +### Tab Navigation +- Views are lazy-loaded when first accessed +- Navigation updates the workspace URL with view pathname +- Views remain in memory while workspace is open + +### Context Integration +Views can consume multiple workspace contexts: -{% code title="my-element.ts" lineNumbers="true" %} ```typescript -import { LitElement, html, customElement, css } from "@umbraco-cms/backoffice/external/lit"; -import { UmbElementMixin } from "@umbraco-cms/backoffice/element-api"; - -@customElement('my-workspaceview') -export default class MyWorspaceViewElement extends UmbElementMixin(LitElement) { - - render() { - return html` - - Welcome to my newly created workspace view. - - ` - } - - static styles = css` - uui-box { - margin: 20px; - } - ` +constructor() { + super(); + + // Consume multiple contexts + this.consumeContext(ENTITY_CONTEXT, (context) => { + this.observe(context.entity, (entity) => this.requestUpdate()); + }); + + this.consumeContext(VALIDATION_CONTEXT, (context) => { + this.observe(context.errors, (errors) => this.requestUpdate()); + }); } +``` -declare global { - interface HTMLElementTagNameMap { - 'my-workspaceview': MyWorspaceViewElement - } +## Common Patterns + +### Entity Information View +```typescript +@customElement('entity-info-view') +export class EntityInfoView extends UmbElementMixin(LitElement) { + #entityContext?: EntityWorkspaceContext; + + constructor() { + super(); + this.consumeContext(ENTITY_CONTEXT, (context) => { + this.#entityContext = context; + }); + } + + override render() { + const entity = this.#entityContext?.getCurrentEntity(); + + return html` + +
+
Name
+
${entity?.name}
+
Created
+
${entity?.createDate}
+
+
+ `; + } } +``` + +### Interactive Configuration View +```typescript +@customElement('config-view') +export class ConfigView extends UmbElementMixin(LitElement) { + #configContext?: ConfigWorkspaceContext; + + #handleConfigChange(property: string, value: any) { + this.#configContext?.updateConfig(property, value); + } + override render() { + return html` + + this.#handleConfigChange('enabled', e.target.checked)}> + Enable Feature + + + `; + } +} ``` -{% endcode %} -In the `workspaceview` folder run `npm run build` and then run the project. Then in the content section of the Backoffice you will see our new Workspace View: +### Analytics Dashboard View +```typescript +@customElement('analytics-view') +export class AnalyticsView extends UmbElementMixin(LitElement) { + @state() + private analytics?: AnalyticsData; -

Workspace View Example

+ constructor() { + super(); + this.#loadAnalytics(); + } -{% hint style="info" %} -To see the Workspace View that we have created in the content section, first you will need to have some content created. -{% endhint %} + async #loadAnalytics() { + const entityContext = await this.getContext(ENTITY_CONTEXT); + const entityId = entityContext.getEntityId(); + + const analyticsService = await this.getContext(ANALYTICS_SERVICE); + this.analytics = await analyticsService.getAnalytics(entityId); + } + + override render() { + if (!this.analytics) { + return html``; + } + + return html` + +
+
+ ${this.analytics.pageViews} + Page Views +
+
+
+ `; + } +} +``` + +## Best Practices + +### View Organization +- Use descriptive tab labels that clearly indicate the view purpose +- Order views by importance using weight property +- Group related functionality into single views rather than many small tabs + +### Context Usage +- Consume contexts in constructor for immediate availability +- Use `observe()` for reactive updates when context state changes +- Check context availability before accessing properties + +### Performance +- Keep views lightweight for fast tab switching +- Load expensive data only when view becomes active +- Use loading states for async operations + +### Conditional Availability +Only show views when relevant: +```typescript +conditions: [ + { + alias: UMB_WORKSPACE_CONDITION_ALIAS, + match: 'Umb.Workspace.Document', + }, + { + alias: 'My.Condition.EntityType', + match: 'blogPost', // Only show for blog posts + }, +], +``` \ No newline at end of file diff --git a/16/umbraco-cms/customizing/workspaces.md b/16/umbraco-cms/customizing/workspaces.md index 9606400ec88..e27b00c1681 100644 --- a/16/umbraco-cms/customizing/workspaces.md +++ b/16/umbraco-cms/customizing/workspaces.md @@ -4,12 +4,17 @@ This page is a work in progress and may undergo further revisions, updates, or amendments. The information contained herein is subject to change without notice. {% endhint %} -A Workspace is the editor for a specific entity type. It can either be a view of data or a complex editor with multiple views. +Workspaces provide dedicated editing environments for specific entity types in Umbraco. They create isolated areas where users can edit content, media, members, or other entities with specialized interfaces and functionality. -* A workspace is based on an entity type (for example content, media, member, etc.) and a unique string (ex: key). -* Most workspaces hold a draft state of an entity. It is a copy of the entity data that can be modified at runtime and sent to the server to be saved. -* A workspace can be a single view or consist of multiple views. -* A workspace should host a workspace context, with which anything within can communicate. +## Key Concepts + +**Entity-Based Structure**: Each workspace is designed for a specific entity type (content, media, member, etc.) and is identified by a unique string (such as a key or ID). + +**Draft State Management**: Workspaces maintain a draft copy of entity data that can be modified without affecting the published version until explicitly saved. + +**Flexible Interface**: Workspaces can range from simple single-view interfaces to complex multi-tabbed editors with specialized functionality. + +**Shared Communication**: Workspaces host workspace contexts that enable all extensions within the workspace to communicate and share state.

Workspace

@@ -17,8 +22,29 @@ A Workspace is the editor for a specific entity type. It can either be a view of interface UmbWorkspaceElement {} ``` -## [Workspace Context](extending-overview/extension-types/workspaces/workspace-context.md) +## Extension Types + +Workspaces support several extension types that work together to create comprehensive editing experiences. These extensions communicate through shared workspace contexts to provide integrated functionality: + +### [Workspace Context](extending-overview/extension-types/workspaces/workspace-context.md) +The foundation extension that provides shared state management and communication between all workspace extensions. Start here when building workspace functionality. + +### [Workspace Views](extending-overview/extension-types/workspaces/workspace-views.md) +Create tab-based content areas within workspaces for organizing different aspects of entity editing. These appear as tabs in the main workspace area. + +### [Workspace Actions](extending-overview/extension-types/workspaces/workspace-editor-actions.md) +Add primary action buttons to workspace footers for user interactions like save, publish, or custom operations. + +### [Workspace Action Menu Items](extending-overview/extension-types/workspaces/workspace-action-menu-items.md) +Extend workspace actions with dropdown menu items to provide additional functionality without cluttering the footer. + +### [Workspace Footer Apps](extending-overview/extension-types/workspaces/workspace-footer-apps.md) +Display persistent status information and contextual data in the workspace footer area for always-visible information. + +## Complete Integration Example -## [Workspace Views](extending-overview/extension-types/workspaces/workspace-views.md) +{% content-ref url="../../examples/workspace-context-counter/" %} +[Workspace Extensions Complete Example](../../examples/workspace-context-counter/) +{% endcontent-ref %} -## [Workspace Actions](extending-overview/extension-types/workspaces/workspace-editor-actions.md) +The workspace-context-counter example demonstrates all 5 workspace extension types working together as an integrated system, showcasing how they communicate through shared workspace context to create cohesive functionality.