diff --git a/16/umbraco-cms/customizing/property-editors/property-actions.md b/16/umbraco-cms/customizing/property-editors/property-actions.md index e628e2f2514..710100d733e 100644 --- a/16/umbraco-cms/customizing/property-editors/property-actions.md +++ b/16/umbraco-cms/customizing/property-editors/property-actions.md @@ -4,10 +4,6 @@ description: Guide on how to implement Property Actions for Property Editors in # Property 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 %} - Property Actions are a built-in feature of Umbraco that allows you to add extra functionality to a Property Editor. Think of them as small, secondary actions that you can attach to a property without modifying the editor itself. Property Actions appear as a small button next to the property label, which expands to show the available actions. diff --git a/16/umbraco-cms/customizing/workspaces.md b/16/umbraco-cms/customizing/workspaces.md index 1e679db0c72..0a3e30a2802 100644 --- a/16/umbraco-cms/customizing/workspaces.md +++ b/16/umbraco-cms/customizing/workspaces.md @@ -1,9 +1,5 @@ # Workspaces -{% 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 %} - 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. ## Key Concepts diff --git a/16/umbraco-cms/reference/api-documentation.md b/16/umbraco-cms/reference/api-documentation.md index 410b7ebb2f8..68bdbdf5c89 100644 --- a/16/umbraco-cms/reference/api-documentation.md +++ b/16/umbraco-cms/reference/api-documentation.md @@ -10,13 +10,13 @@ A library of API Reference documentation is auto-generated from the comments wit C# API references for the Umbraco Core, Infrastructure, Extensions and Web libraries. -### [Umbraco.Cms.Core](https://apidocs.umbraco.com/v15/csharp/api/Umbraco.Cms.Core.html) +### [Umbraco.Cms.Core](https://apidocs.umbraco.com/v16/csharp/api/Umbraco.Cms.Core.html) -### [Umbraco.Cms.Infrastructure](https://apidocs.umbraco.com/v15/csharp/api/Umbraco.Cms.Infrastructure.html) +### [Umbraco.Cms.Infrastructure](https://apidocs.umbraco.com/v16/csharp/api/Umbraco.Cms.Infrastructure.html) -### [Umbraco.Cms.Web](https://apidocs.umbraco.com/v15/csharp/api/Umbraco.Cms.Web.Common.html) +### [Umbraco.Cms.Web](https://apidocs.umbraco.com/v16/csharp/api/Umbraco.Cms.Web.Common.html) -### [Umbraco.Extensions](https://apidocs.umbraco.com/v15/csharp/api/Umbraco.Extensions.html) +### [Umbraco.Extensions](https://apidocs.umbraco.com/v16/csharp/api/Umbraco.Extensions.html) {% hint style="info" %} Opens a documentation browser that is different from the documentation section you're viewing now. diff --git a/16/umbraco-forms/developer/ajaxforms.md b/16/umbraco-forms/developer/ajaxforms.md index 2223d869eb9..d6cccd7def0 100644 --- a/16/umbraco-forms/developer/ajaxforms.md +++ b/16/umbraco-forms/developer/ajaxforms.md @@ -599,8 +599,9 @@ With [expanded output](https://docs.umbraco.com/umbraco-cms/reference/content-de } ``` -## Dynamic form injection -For dynamic form injection on a page, such as in a modal dialog, there's a specific JavaScript event and API method. This allows reinitializing Umbraco Forms for the new content. +## Dynamic Form Injection + +For dynamic Form injection on a page, such as in a modal dialog, there's a specific JavaScript event and API method. This allows reinitializing Umbraco Forms for the new content. ```javascript // Execute a reinitialize on dynamic injections diff --git a/17/umbraco-cms/.gitbook.yaml b/17/umbraco-cms/.gitbook.yaml index 41d7d74474c..ed43a66b57c 100644 --- a/17/umbraco-cms/.gitbook.yaml +++ b/17/umbraco-cms/.gitbook.yaml @@ -146,3 +146,6 @@ redirects: extending/backoffice-setup/extension-types: customizing/extending-overview/extension-types/README.md customizing/extending-overview/extension-registry/extension-registry: customizing/extending-overview/extension-registry/register-extensions.md fundamentals/backoffice/property-editors/built-in-umbraco-property-editors/date: fundamentals/backoffice/property-editors/built-in-umbraco-property-editors/date-time-editor/README.md + customizing/extending-overview/extension-types/workspaces/workspace-action-menu-item: customizing/extending-overview/extension-types/workspaces/workspace-action-menu-items.md + customizing/extending-overview/extension-types/workspaces/workspace-footer-app: customizing/extending-overview/extension-types/workspaces/workspace-footer-apps.md + \ No newline at end of file diff --git a/17/umbraco-cms/.gitbook/assets/DeliveryAPIContentIndex.png b/17/umbraco-cms/.gitbook/assets/DeliveryAPIContentIndex.png new file mode 100644 index 00000000000..ead9b5e058f Binary files /dev/null and b/17/umbraco-cms/.gitbook/assets/DeliveryAPIContentIndex.png differ diff --git a/17/umbraco-cms/.gitbook/assets/DeliveryAPIContentIndexRebuild.png b/17/umbraco-cms/.gitbook/assets/DeliveryAPIContentIndexRebuild.png new file mode 100644 index 00000000000..71641282dee Binary files /dev/null and b/17/umbraco-cms/.gitbook/assets/DeliveryAPIContentIndexRebuild.png differ diff --git a/17/umbraco-cms/.gitbook/assets/property-actions-blocklist.png b/17/umbraco-cms/.gitbook/assets/property-actions-blocklist.png new file mode 100644 index 00000000000..15da7316fc8 Binary files /dev/null and b/17/umbraco-cms/.gitbook/assets/property-actions-blocklist.png differ diff --git a/17/umbraco-cms/SUMMARY.md b/17/umbraco-cms/SUMMARY.md index 5acbe99f23a..f43de56b210 100644 --- a/17/umbraco-cms/SUMMARY.md +++ b/17/umbraco-cms/SUMMARY.md @@ -188,7 +188,9 @@ * [Trees](customizing/extending-overview/extension-types/tree.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 Footer Apps](customizing/extending-overview/extension-types/workspaces/workspace-footer-apps.md) * [Workspace Views](customizing/extending-overview/extension-types/workspaces/workspace-views.md) * [Extension Kind](customizing/extending-overview/extension-kind.md) * [Extension Conditions](customizing/extending-overview/extension-conditions.md) diff --git a/17/umbraco-cms/customizing/extending-overview/extension-types/dashboard.md b/17/umbraco-cms/customizing/extending-overview/extension-types/dashboard.md index 8ce78688c34..f7e3cd50a38 100644 --- a/17/umbraco-cms/customizing/extending-overview/extension-types/dashboard.md +++ b/17/umbraco-cms/customizing/extending-overview/extension-types/dashboard.md @@ -39,6 +39,78 @@ Even though these dashboards are useful, you might want to create your own custo You can try and [create a custom dashboard](../../../tutorials/creating-a-custom-dashboard/) as a way on getting started on this topic. +### Hiding or adding conditional rules to existing dashboards + +You might need to hide or add conditions to existing dashboards. Common use cases include hiding the Getting Started dashboard or restricting the Redirect Management dashboard to Admin users only. + +To do this, you will first need to [create an extension](../../../tutorials/creating-your-first-extension.md). For this example, which implements dashboard customizations, a descriptive name like `DashboardCustomization` works well: + +{% code title="~/App_Plugins/DashboardCustomization/umbraco-package.json" lineNumbers="true" %} +```json +{ + "name": "DashboardCustomization", + "version": "1.0.0", + "extensions": [ + { + "type": "backofficeEntryPoint", + "alias": "DashboardCustomization.EntryPoint", + "name": "Dashboard Customization Entry Point", + "js": "/App_Plugins/DashboardCustomization/dashboards-setup.js" + } + ] +} +``` +{% endcode %} + +Use the `onInit` function to configure the dashboard removal and customization. The optional `onUnload` function can clean up any customizations when the extension is disposed: + +{% code title="~/App_Plugins/DashboardCustomization/dashboards-setup.js" lineNumbers="true" %} +```js +export const onInit = (host, extensionRegistry) => { + // Remove Getting Started dashboard for all users + extensionRegistry.exclude('Umb.Dashboard.UmbracoNews'); + + // Restrict Redirect Management dashboard to Admin users only + extensionRegistry.appendCondition('Umb.Dashboard.RedirectManagement', { + alias: 'Umb.Condition.CurrentUser.IsAdmin' + }); +} + +/** + * Optional: Perform cleanup when the extension is unloaded + */ +export const onUnload = (host, extensionRegistry) => { + // Note: In most cases, cleanup is not necessary as the extension registry + // handles this automatically when the backoffice reloads +} +``` +{% endcode %} + +#### Restricting to specific user groups + +To allow multiple user groups (for example, Admin or a custom group), use the `Umb.Condition.CurrentUser.GroupId` condition with the `oneOf` parameter: + +{% code title="~/App_Plugins/DashboardCustomization/dashboards-setup.js" lineNumbers="true" %} +```js +export const onInit = (host, extensionRegistry) => { + extensionRegistry.appendCondition('Umb.Dashboard.RedirectManagement', { + alias: 'Umb.Condition.CurrentUser.GroupId', + oneOf: [ + 'CUSTOM-GROUP-GUID-1-HERE', + 'CUSTOM-GROUP-GUID-2-HERE' + ] + }); +}; +``` +{% endcode %} + +You can find user group GUIDs in the **Users > User Groups** section of the backoffice. + +Read more about the available conditions: + +{% content-ref url="condition.md" %} +[condition.md](condition.md) +{% endcontent-ref %} ## Registering your Dashboard This section dives into the Dashboard Extension Manifest, shows how to register one, and append additional details. diff --git a/17/umbraco-cms/customizing/extending-overview/extension-types/workspaces/README.md b/17/umbraco-cms/customizing/extending-overview/extension-types/workspaces/README.md index 5581b2339ac..3eb5a10f7dc 100644 --- a/17/umbraco-cms/customizing/extending-overview/extension-types/workspaces/README.md +++ b/17/umbraco-cms/customizing/extending-overview/extension-types/workspaces/README.md @@ -1,6 +1,53 @@ --- description: >- - An overview of the available extension types related to workspaces. + Learn about workspace extension types that provide shared functionality and enable communication within workspace environments. --- # Extension Types: Workspaces + +Workspace extensions are specialized components that enhance Umbraco's editing environments for documents, media, and members. They share state through workspace contexts. This enables coordinated functionality like synchronized actions, real-time status updates, and seamless data flow across the editing interface. + +## Available Extension Types + +Workspace extensions can be grouped into these types: + +### Core Extensions + +- **[Workspace Context](workspace-context.md)** - Provides shared state management and communication between workspace extensions +- **[Workspace](../../../workspaces.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](workspace-context.md)** manages centralized state using observables that automatically notify subscribers of changes +2. **[Workspace Actions](workspace-editor-actions.md)** consume the context to modify state when users interact with buttons or menu items +3. **[Workspace Action Menu Items](workspace-action-menu-items.md)** add additional options for workspace actions +4. **[Workspace Views](workspace-views.md)** observe context state to automatically update their UI when data changes +5. **[Footer Apps](workspace-footer-apps.md)** 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) +``` + +{% hint style="info" %} +All workspace extensions are automatically scoped to their workspace instance, ensuring that extensions in different workspaces do not interfere with each other. +{% endhint %} diff --git a/17/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-action-menu-item.md b/17/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-action-menu-item.md deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/17/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-action-menu-items.md b/17/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-action-menu-items.md new file mode 100644 index 00000000000..6c83776811f --- /dev/null +++ b/17/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-action-menu-items.md @@ -0,0 +1,92 @@ +--- +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. + +## 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 +- **`kind`** - Specifies which type of element should be shown (if no `element` is provided). The `default` option refers to the ``, which supports a label and an href +- **`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 a user interacts with 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 display a dropdown menu 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' }, +} +``` \ No newline at end of file diff --git a/17/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-context.md b/17/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-context.md index 2000323ca5c..e7efdcad4e0 100644 --- a/17/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-context.md +++ b/17/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-context.md @@ -1,135 +1,205 @@ --- -description: Establish an extension to communicate across the application. +description: >- + Learn how to create workspace contexts that manage shared state and enable communication between extensions in a workspace. --- # 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: -## Example of Workspace Context +- **Shared state** scoped to a specific workspace instance +- **Communication layer** between extensions in the workspace +- **Entity lifecycle management** for workspace data +- **Context isolation** ensures workspace independence -The API will be initiated with the same host as the default Workspace Context. +{% hint style="info" %} +Workspace Contexts are automatically scoped to their workspace. Extensions in different workspaces cannot access each other's contexts. +{% endhint %} +## 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 %} + +## Implementation -The code for such an API file might look like this: +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 WorkspaceContextCounterElement extends UmbContextBase { + #counter = new UmbNumberState(0); + readonly counter = this.#counter.asObservable(); -export class MyContextApi extends UmbContextBase { - #counter = new UmbNumberState(0); - readonly counter = this.#counter.asObservable(); + constructor(host: UmbControllerHost) { + super(host, EXAMPLE_COUNTER_CONTEXT); + } - constructor(host: UmbControllerHost) { - super(host, My_COUNTER_CONTEXT); - } + increment() { + this.#counter.setValue(this.#counter.value + 1); + } - increment() { - this.#counter.next(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 + +### Initialization + +- Created when workspace loads +- Available to all extensions within that workspace +- Destroyed when workspace closes + +### Scoping -## Use the Workspace Context +- Context instances are isolated per workspace +- Extensions can only access contexts from their own workspace +- Context requests automatically scope to the nearest workspace -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. +### 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 - constructor() { - super(); - this.consumeContext(My_COUNTER_CONTEXT, (context) => { - this.#counterContext = context; - this.observe(this.#counterContext.counter, (count) => { - this.count = count; - }); +```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); + } } +} +``` - #onClick() { - this.#counterContext?.increment(); - } +### Server Integration - override render() { - return html` - Current count value: ${this.count} - - `; +```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 -// Important to export as 'element' otherwise the Extension Registry cannot pick up the class. -export const element = ExampleCounterWorkspaceView +### In Workspace Actions + +```typescript +export class MyWorkspaceAction extends UmbWorkspaceActionBase { + override async execute() { + const context = await this.getContext(MY_WORKSPACE_CONTEXT); + context.performAction(); + } +} ``` -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',
-        },
-    ],
+### 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());
+		});
+	}
 }
-
+``` + +## 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); +``` + +### 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 are meaningful for the workspace type. diff --git a/17/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-editor-actions.md b/17/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-editor-actions.md index 2eb896ea50a..92a2318c0d2 100644 --- a/17/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-editor-actions.md +++ b/17/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-editor-actions.md @@ -1,55 +1,256 @@ +--- +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 a user clicks 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(); + } +} -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. +export const api = ExampleIncrementorWorkspaceAction; +``` +{% endcode %} + +## Workspace Integration + +### Context Access -Workspace action relates to a workspace alias (Umb.Workspace.Document) and has Access to the workspace context. +Actions automatically have access to their workspace's contexts: -

Workspace Actions

+```typescript +// Context is scoped to the current workspace +const context = await this.getContext(MY_WORKSPACE_CONTEXT); +``` -**JavaScript Manifest example** +### Execution Lifecycle -
import { extensionRegistry } from '@umbraco-cms/extension-registry';
-import { MyWorkspaceAction } from './my-workspace-action';
+- Actions execute when clicked
+- Can be async for complex operations
+- Have access to workspace state during execution
+- Can modify workspace contexts
 
-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',
-  },
- ],
-};
+### Conditional Execution
 
-extensionRegistry.register(manifest);
-
+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(); +} +``` -## The Workspace Action Class +## Action Menu Integration -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. +Actions can be extended with dropdown menu items using `forWorkspaceActions`: -```ts -import { UmbWorkspaceActionBase } from '@umbraco-cms/backoffice/workspace'; +{% code caption="action-with-menu.ts" %} +```typescript +// Primary Action +{ + type: 'workspaceAction', + alias: 'example.action.save', + api: () => import('./save-action.js'), + meta: { label: 'Save' }, +} -export class MyWorkspaceAction extends UmbWorkspaceActionBase { - execute() { - this.workspaceContext.myAction(this.selection); - } +// 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 %} + +## Action Events -**Default Element** +Workspace actions dispatch a generic `action-executed` event when they complete: ```typescript -interface UmbWorkspaceActionElement {} +// Event is automatically dispatched by the action UI element +export class UmbActionExecutedEvent extends Event { + constructor() { + super('action-executed', { bubbles: true, composed: true, cancelable: false }); + } +} ``` + +### 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 + +### Listening for Action Events + +Components can listen for action completion: + +```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 %} + +## Common Patterns + +### Entity Operations + +```typescript +export class SaveAction extends UmbWorkspaceActionBase { + override async execute() { + const workspace = await this.getContext(DOCUMENT_WORKSPACE_CONTEXT); + await workspace.save(); + } +} +``` + +### 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 are meaningful: +```typescript +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 that the context is available before performing operations. diff --git a/17/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-footer-app.md b/17/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-footer-app.md deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/17/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-footer-apps.md b/17/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-footer-apps.md new file mode 100644 index 00000000000..849404b5aa7 --- /dev/null +++ b/17/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-footer-apps.md @@ -0,0 +1,217 @@ +--- +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 up the main workspace space +- **Real-time updates** through workspace context integration + +{% hint style="info" %} +Footer apps appear at the bottom of workspaces and are ideal for displaying 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 the 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 that the context is available before accessing its properties. + +### Responsive Design +Ensure footer apps work across different workspace sizes. + +### Visual Consistency +Use Umbraco's design system for consistent styling. diff --git a/17/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-views.md b/17/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-views.md index 9c21aacd57c..da55deddea3 100644 --- a/17/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-views.md +++ b/17/umbraco-cms/customizing/extending-overview/extension-types/workspaces/workspace-views.md @@ -1,100 +1,287 @@ --- -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. - -

Workspace Views

+## Purpose -**In Content Section** +Workspace Views provide: -With Workspace Views, editors can switch from editing 'Content' to accessing contextual information related to the item they are editing. +- **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 -The default workspace view is **'Info'** - displaying Links, History and Status of the current content item. - -## Example of a Workspace View +

Workspace Views

-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`: +## Manifest -{% code title="umbraco-package.json" lineNumbers="true" %} -```json +{% code caption="manifest.ts" %} +```typescript { - "$schema": "../../umbraco-package-schema.json", - "name": "My workspace", - "version": "0.1.0", - "extensions": [ + 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: [ { - "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" - } - ] - } - ] + alias: UMB_WORKSPACE_CONDITION_ALIAS, + match: 'Umb.Workspace.Document', + }, + ], } ``` {% endcode %} -4. Add the following code to the existing `my-element.ts` from the `src`folder: +### 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 + +## Implementation + +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: -{% code title="my-element.ts" lineNumbers="true" %} +{% code caption="counter-workspace-view.ts" %} ```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 MyWorkspaceViewElement extends UmbElementMixin(LitElement) { - - render() { - return html` - - Welcome to my newly created workspace view. - - ` +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'; + +@customElement('example-counter-workspace-view') +export class ExampleCounterWorkspaceView extends UmbElementMixin(LitElement) { + #counterContext?: typeof EXAMPLE_COUNTER_CONTEXT.TYPE; + + @state() + private count = 0; + + constructor() { + super(); + this.consumeContext(EXAMPLE_COUNTER_CONTEXT, (instance) => { + this.#counterContext = instance; + this.#observeCounter(); + }); } - static styles = css` - uui-box { - margin: 20px; - } - ` + #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 { - 'my-workspaceview': MyWorkspaceViewElement + 'example-counter-workspace-view': ExampleCounterWorkspaceView; } } - ``` {% 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: +## View Lifecycle -

Workspace View Example

+### Initialization -{% 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 %} +- 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 the workspace is open + +### Context Integration + +Views can consume multiple workspace contexts: + +```typescript +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()); + }); +} +``` + +## 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 + + + `; + } +} +``` + +### Analytics Dashboard View + +```typescript +@customElement('analytics-view') +export class AnalyticsView extends UmbElementMixin(LitElement) { + @state() + private analytics?: AnalyticsData; + + constructor() { + super(); + this.#loadAnalytics(); + } + + 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 indicate the view's purpose +- Order views by importance using the `weight` property +- Group related functionality into a single view rather than many small tabs + +### Context Usage + +- Consume contexts in the 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 + }, +], +``` diff --git a/17/umbraco-cms/customizing/property-editors/property-actions.md b/17/umbraco-cms/customizing/property-editors/property-actions.md index e40d6b560b6..710100d733e 100644 --- a/17/umbraco-cms/customizing/property-editors/property-actions.md +++ b/17/umbraco-cms/customizing/property-editors/property-actions.md @@ -4,108 +4,62 @@ description: Guide on how to implement Property Actions for Property Editors in # Property 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 %} - -Property Actions are a built-in feature that provide a generic place for secondary functionality for property editors. - -Property Actions appear as a small button next to the label of the property, which expands to show the available actions. They are defined and implemented in the Property Editor, making it open as to what a Property Action is. - -## Data Structure of Property Actions - -Property Actions are an array of objects defining each action. An action is defined by the following properties: - -```js -{ - labelKey: 'clipboard_labelForRemoveAllEntries', - labelTokens: [], - icon: 'trash', - method: removeAllEntries, - isDisabled: true -} -``` - -We use `labelKey` and `labelTokens` to retrieve a localized string that is displayed as the Actions label. [See localization for more info.](../../extending/language-files/) - -`isDisabled` is used to disable an Action, which change the visual appearance and prevents interaction. Use this option when an action wouldn't provide any change. In the example above, the action `remove all entries` would not have any impact if there is no entries. - -## Implementation - -The implementation of Property Actions varies depending on whether your Property Editor is implemented with a Controller or as a Component. - -### Controller Implementation +Property Actions are a built-in feature of Umbraco that allows you to add extra functionality to a Property Editor. Think of them as small, secondary actions that you can attach to a property without modifying the editor itself. -When your Property Editor is implemented with a Controller, use the following approach for the Property Action: +Property Actions appear as a small button next to the property label, which expands to show the available actions. -```js -angular.module("umbraco").controller("My.MarkdownEditorController", function ($scope) { +## Property Actions in the UI -function myActionExecutionMethod() { - alert('My Custom Property Action Clicked'); - // Disable the action so it can not be re-run - // You may have custom logic to enable or disable the action - // Based on number of items selected etc... - myAction.isDisabled = true; -}; +
+ +

Property action in Block List

+
-var myAction = { - labelKey: 'general_labelForMyAction', - labelTokens: [], - icon: 'action', - method: myActionExecutionMethod, - isDisabled: false -} +## Registering a Property Action -var propertyActions = [ - myAction -]; +{% hint style="info" %} +Before creating a Property Action, make sure you are familiar with the [Extension Registry in Umbraco](https://docs.umbraco.com/umbraco-cms/customizing/extending-overview/extension-registry/extension-registry). +{% endhint %} -this.$onInit = function () { - if ($scope.umbProperty) { - $scope.umbProperty.setPropertyActions(propertyActions); +Here is how you can register a new Property Action: +``` +import { extensionRegistry } from '@umbraco-cms/extension-registry'; +import { MyEntityAction } from './my-property-action.api'; +const manifest = + { + type: 'propertyAction', + kind: 'default', + alias: 'My.propertyAction', + name: 'My Property Action ', + forPropertyEditorUis: ["my-property-editor"], // Target specific property editors + api: () => import('./my-property-action.api.js'), + weight: 10, // Order if multiple actions exist + meta: { + icon: 'icon-add', // Icon to display in the UI + label: 'My property action', // Label shown to editors } -}; - + }; -}); +extensionRegistry.register(manifest); ``` +### Creating the Property Action Class -### Component Implementation +Every Property Action needs a class that defines what happens when the action is executed. +You can extend the `UmbPropertyActionBase` class for this. -Follow this guide if your Property Editor is implemented as a Component. The Component must be configured to retrieve an optional reference to `umbProperty`. The requirement must be optional because property-editors are implemented in scenarios where it's not presented. - -See the following example: - -```js -angular.module('umbraco').component('myPropertyEditor', { - controller: MyController, - controllerAs: 'vm', - require: { - umbProperty: '?^umbProperty' - } - … -}); ``` - -See the following example for implementation of Property Actions in a Component, notice the difference is that we are parsing actions to `this.umbProperty.setPropertyActions(...)`. - -```js -var myAction = { - labelKey: 'general_labelForMyAction', - labelTokens: [], - icon: 'action', - method: myActionExecutionMethod, - isDisabled: false +import { UmbPropertyActionBase } from '@umbraco-cms/backoffice/property-action'; +import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property'; + +export class MyPropertyAction extends UmbPropertyActionBase { + // The execute method is called when the user triggers the action. + async execute() { + // Retrieve the property’s current state, + const propertyContext = await this.getContext(UMB_PROPERTY_CONTEXT); + + // Here it's possible to modify the property or perform other actions. In this case, setting a value. + propertyContext.setValue("Default text here"); + } } - -var propertyActions = [ - myAction -]; - -this.$onInit = function () { - if (this.umbProperty) { - this.umbProperty.setPropertyActions(propertyActions); - } -}; -``` +export { MyPropertyAction as api }; +``` \ No newline at end of file diff --git a/17/umbraco-cms/customizing/workspaces.md b/17/umbraco-cms/customizing/workspaces.md index 9606400ec88..39100651a0e 100644 --- a/17/umbraco-cms/customizing/workspaces.md +++ b/17/umbraco-cms/customizing/workspaces.md @@ -1,15 +1,16 @@ # Workspaces -{% 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 %} +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 the editor for a specific entity type. It can either be a view of data or a complex editor with multiple views. +## Key Concepts -* 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. +**Entity-Based Structure**: Each workspace is designed for a specific entity type (content, media, member, etc.). It 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 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 +18,26 @@ 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 different 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 Views](extending-overview/extension-types/workspaces/workspace-views.md) +### [Workspace Footer Apps](extending-overview/extension-types/workspaces/workspace-footer-apps.md) -## [Workspace Actions](extending-overview/extension-types/workspaces/workspace-editor-actions.md) +Display persistent status information and contextual data in the workspace footer area for always-visible information. \ No newline at end of file diff --git a/17/umbraco-cms/fundamentals/backoffice/property-editors/built-in-umbraco-property-editors/rich-text-editor/style-menu.md b/17/umbraco-cms/fundamentals/backoffice/property-editors/built-in-umbraco-property-editors/rich-text-editor/style-menu.md index f17178b4861..e1ec6a6d51b 100644 --- a/17/umbraco-cms/fundamentals/backoffice/property-editors/built-in-umbraco-property-editors/rich-text-editor/style-menu.md +++ b/17/umbraco-cms/fundamentals/backoffice/property-editors/built-in-umbraco-property-editors/rich-text-editor/style-menu.md @@ -5,7 +5,7 @@ A Style Select Menu is a configurable extension that adds a cascading menu to th ![Rich Text Editor cascading style menu](images/rte-tiptap-stylemenu.png) {% hint style="info" %} -Any custom stylesheets associated with the Rich Text Editor will not auto generate a style select menu in the toolbar. +Any custom stylesheets associated with the Rich Text Editor will not auto-generate a style select menu in the toolbar. See the [Creating a Style Select Menu](#creating-a-style-select-menu) section below. {% endhint %} ## Adding Style Select to Rich Text Editor diff --git a/17/umbraco-cms/reference/api-documentation.md b/17/umbraco-cms/reference/api-documentation.md index 410b7ebb2f8..80fc9146695 100644 --- a/17/umbraco-cms/reference/api-documentation.md +++ b/17/umbraco-cms/reference/api-documentation.md @@ -10,13 +10,13 @@ A library of API Reference documentation is auto-generated from the comments wit C# API references for the Umbraco Core, Infrastructure, Extensions and Web libraries. -### [Umbraco.Cms.Core](https://apidocs.umbraco.com/v15/csharp/api/Umbraco.Cms.Core.html) +### [Umbraco.Cms.Core](https://apidocs.umbraco.com/v17/csharp/api/Umbraco.Cms.Core.html) -### [Umbraco.Cms.Infrastructure](https://apidocs.umbraco.com/v15/csharp/api/Umbraco.Cms.Infrastructure.html) +### [Umbraco.Cms.Infrastructure](https://apidocs.umbraco.com/v17/csharp/api/Umbraco.Cms.Infrastructure.html) -### [Umbraco.Cms.Web](https://apidocs.umbraco.com/v15/csharp/api/Umbraco.Cms.Web.Common.html) +### [Umbraco.Cms.Web](https://apidocs.umbraco.com/v17/csharp/api/Umbraco.Cms.Web.Common.html) -### [Umbraco.Extensions](https://apidocs.umbraco.com/v15/csharp/api/Umbraco.Extensions.html) +### [Umbraco.Extensions](https://apidocs.umbraco.com/v17/csharp/api/Umbraco.Extensions.html) {% hint style="info" %} Opens a documentation browser that is different from the documentation section you're viewing now. diff --git a/17/umbraco-cms/reference/content-delivery-api/README.md b/17/umbraco-cms/reference/content-delivery-api/README.md index 66809fb59a2..e545717967c 100644 --- a/17/umbraco-cms/reference/content-delivery-api/README.md +++ b/17/umbraco-cms/reference/content-delivery-api/README.md @@ -59,11 +59,13 @@ Once the Content Delivery API is enabled, the next step is to rebuild the Delive 1. Access the Umbraco Backoffice. 2. Navigate to the **Settings** section. 3. Open the **Examine Management** dashboard. -4. Scroll down to find the **Tools**. +4. Click the **DeliveryAPIContentIndex**. -

Use the "Rebuild index" button under Tools on the Examine Management dashboard in the Settings section.

+

Click the DeliveryAPIContentIndex on the Examine Management dashboard in the Settings section.

-5. Use the **Rebuild index** button. +5. Scroll down and click the **Rebuild index** button. + +

Use the "Rebuild index" button in the DeliveryAPIContentIndex under Tools on the Examine Management dashboard in the Settings section.

Once the index is rebuilt, the API can serve the latest content from the multiple-items endpoint. @@ -239,7 +241,7 @@ Preview: true Api-Key: my-api-key ``` -Is the API key not applied using the `Api-Key` request header, the unpublished content will not be included in the JSON response. +If the API key is not applied using the `Api-Key` request header, the unpublished content will not be included in the JSON response. diff --git a/17/umbraco-cms/reference/searching/examine/indexing.md b/17/umbraco-cms/reference/searching/examine/indexing.md index d2602f85767..bd2eea25ed7 100644 --- a/17/umbraco-cms/reference/searching/examine/indexing.md +++ b/17/umbraco-cms/reference/searching/examine/indexing.md @@ -259,7 +259,6 @@ public class ProductIndexValueSetBuilder : IValueSetBuilder [UmbracoExamineFieldNames.NodeNameFieldName] = content.Name!, ["name"] = content.Name!, // add the fields you want in the index - ["nodeName"] = content.Name!, ["id"] = content.Id, }; diff --git a/17/umbraco-cms/reference/security/two-factor-authentication.md b/17/umbraco-cms/reference/security/two-factor-authentication.md index bc8e437faac..aab07d6193b 100644 --- a/17/umbraco-cms/reference/security/two-factor-authentication.md +++ b/17/umbraco-cms/reference/security/two-factor-authentication.md @@ -100,7 +100,7 @@ public class UmbracoAppAuthenticator : ITwoFactorProvider /// The required data to setup the authenticator app public Task GetSetupDataAsync(Guid userOrMemberKey, string secret) { - var member = _memberService.GetByKey(userOrMemberKey); + var member = _memberService.GetById(userOrMemberKey); var applicationName = "testingOn15"; var twoFactorAuthenticator = new TwoFactorAuthenticator(); @@ -173,47 +173,61 @@ If you already have a members-only page with the edit profile options, you can s ```csharp @using Umbraco.Cms.Core.Services; @using Umbraco.Cms.Web.Website.Controllers; +@using Umbraco.Cms.Core.Models; @using Umbraco.Cms.Web.Website.Models; @using My.Website; -@inject MemberModelBuilderFactory memberModelBuilderFactory -@inject ITwoFactorLoginService twoFactorLoginService +@inject MemberModelBuilderFactory MemberModelBuilderFactory +@inject IMemberTwoFactorLoginService MemberTwoFactorLoginService @{ // Build a profile model to edit - var profileModel = await memberModelBuilderFactory - .CreateProfileModel() - .BuildForCurrentMemberAsync(); + var profileModel = await MemberModelBuilderFactory + .CreateProfileModel() + .BuildForCurrentMemberAsync(); + + List? providerNameList = null; + if (profileModel != null) + { + var providerNamesAttempt = await MemberTwoFactorLoginService.GetProviderNamesAsync(profileModel.Key); + + if (providerNamesAttempt.Success) + { + providerNameList = providerNamesAttempt.Result.ToList(); + } + } // Show all two factor providers - var providerNames = twoFactorLoginService.GetAllProviderNames(); - if (providerNames.Any()) + if (providerNameList != null && providerNameList.Any()) {
- foreach (var providerName in providerNames) + foreach (var provider in providerNameList) { - var setupData = await twoFactorLoginService.GetSetupInfoAsync(profileModel.Key, providerName); + var setupData = await MemberTwoFactorLoginService.GetSetupInfoAsync(profileModel.Key, provider.ProviderName); - // If the `setupData` is `null` for the specified `providerName` it means the provider is already set up. - // In this case, a button to disable the authentication is shown. - if (setupData is null) + // If the `setupData.Success` is `true` for the specified `providerName` it means the provider is not set up. + if (setupData.Success) { - @using (Html.BeginUmbracoForm(nameof(UmbTwoFactorLoginController.Disable))) + if (setupData.Result is QrCodeSetupData qrCodeSetupData) { - - + @using (Html.BeginUmbracoForm(nameof(UmbTwoFactorLoginController.ValidateAndSaveSetup))) + { +

Setup @provider.ProviderName

+ +

Scan the code above with your authenticator app
and enter the resulting code here to validate:

+ + + + + } } } - // If `setupData` is not `null` the type is checked and the UI for how to set up the App Authenticator is shown. - else if(setupData is QrCodeSetupData qrCodeSetupData) + // If `setupData.Success` is `false` the provider is already setup. + // In this case, a button to disable the authentication is shown. + else { - @using (Html.BeginUmbracoForm(nameof(UmbTwoFactorLoginController.ValidateAndSaveSetup))) + @using (Html.BeginUmbracoForm(nameof(UmbTwoFactorLoginController.Disable))) { -

Setup @providerName

- -

Scan the code above with your authenticator app
and enter the resulting code here to validate:

- - - - + + } } } diff --git a/17/umbraco-commerce/SUMMARY.md b/17/umbraco-commerce/SUMMARY.md index 04384d67cde..8026d915ed9 100644 --- a/17/umbraco-commerce/SUMMARY.md +++ b/17/umbraco-commerce/SUMMARY.md @@ -75,6 +75,7 @@ * [List of notification events](key-concepts/events/list-of-notification-events.md) * [Fluent API](key-concepts/fluent-api.md) * [Order Calculation State](key-concepts/order-calculation-state.md) +* [Order Number Generators](key-concepts/order-number-generators.md) * [Payment Forms](key-concepts/payment-forms.md) * [Payment Providers](key-concepts/payment-providers.md) * [Pipelines](key-concepts/pipelines.md) diff --git a/17/umbraco-commerce/how-to-guides/order-number-customization.md b/17/umbraco-commerce/how-to-guides/order-number-customization.md index 21e8931b988..af284566618 100644 --- a/17/umbraco-commerce/how-to-guides/order-number-customization.md +++ b/17/umbraco-commerce/how-to-guides/order-number-customization.md @@ -4,46 +4,122 @@ description: Learn how to customize the default order number generated in Umbrac # Order Number Customization -In Umbraco Commerce, the default order number generation can be customized by implementing the `IOrderNumberGenerator` interface. This interface defines two methods: `GenerateCartNumber(Guid storeId)` and `GenerateOrderNumber(Guid storeId)`, which you can override to create a custom numbering system.​ +Umbraco Commerce provides flexible options for customizing order numbers to meet your business requirements. This guide covers different approaches, from template-based customization to implementing fully custom generators. + +## Built-in Generators + +Before implementing a custom solution, understand that Umbraco Commerce includes two built-in order number generators: + +- **CompactSortableOrderNumberGenerator** (Recommended) - Produces compact, time-sortable identifiers with high scalability and multi-node support +- **DateHashOrderNumberGenerator** (Legacy) - Date-based format maintained for backward compatibility + +For detailed information about these generators and how they work, see the [Order Number Generators](../key-concepts/order-number-generators.md) key concepts documentation. + +## Before Creating a Custom Generator + +Consider these alternatives before implementing a custom generator: + +### 1. Using Store Templates + +You can customize order numbers through store-level templates. Templates allow you to add prefixes, suffixes, or formatting without writing any code: + +```csharp +// Configure templates via the Store entity +await store.SetCartNumberTemplateAsync("CART-{0}"); +await store.SetOrderNumberTemplateAsync("ORDER-{0}"); +``` + +**Examples:** +- Template: `"ORDER-{0}"` + Generated: `"22345-67ABC"` = Final: `"ORDER-22345-67ABC"` +- Template: `"SO-{0}-2025"` + Generated: `"12345"` = Final: `"SO-12345-2025"` + +This approach works with any generator and requires no custom code. + +### 2. Using CompactSortableOrderNumberGenerator + +The `CompactSortableOrderNumberGenerator` handles most common requirements: +- Compact format (10-11 characters) +- Time-sortable +- Multi-node safe +- High-volume capable (1,024 orders/sec per node) + +If your store was upgraded from an earlier version and is using the legacy generator, you can explicitly switch to the recommended generator: + +```csharp +public class MyComposer : IComposer +{ + public void Compose(IUmbracoBuilder builder) + { + builder.Services.AddUnique(); + } +} +``` ## Implementing a Custom Order Number Generator -To create a custom order number generator, define a class that implements the `IOrderNumberGenerator` interface, for example, `CustomOrderNumberGenerator.cs`: +If the built-in generators don't meet your needs, you can create a custom implementation by implementing the `IOrderNumberGenerator` interface. + +### Creating the Custom Generator + +Define a class that implements the `IOrderNumberGenerator` interface: {% code title="CustomOrderNumberGenerator.cs" %} ```csharp +using System; +using System.Threading; +using System.Threading.Tasks; using Umbraco.Commerce.Core.Generators; +using Umbraco.Commerce.Core.Services; public class CustomOrderNumberGenerator : IOrderNumberGenerator { - public string GenerateCartNumber(Guid storeId) + private readonly IStoreService _storeService; + + public CustomOrderNumberGenerator(IStoreService storeService) { - // Implement custom logic for cart numbers + _storeService = storeService; } - public string GenerateOrderNumber(Guid storeId) + public async Task GenerateCartNumberAsync(Guid storeId, CancellationToken cancellationToken = default) { - // Implement custom logic for order numbers + var store = await _storeService.GetStoreAsync(storeId); + + // Implement your custom logic for cart numbers + var cartNumber = $"{DateTime.UtcNow:yyyyMMdd}-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}"; + + // Apply store template if configured + return string.Format(store.CartNumberTemplate ?? "{0}", cartNumber); + } + + public async Task GenerateOrderNumberAsync(Guid storeId, CancellationToken cancellationToken = default) + { + var store = await _storeService.GetStoreAsync(storeId); + + // Implement your custom logic for order numbers + var orderNumber = $"{DateTime.UtcNow:yyyyMMdd}-{Random.Shared.Next(1000, 9999)}"; + + // Apply store template if configured + return string.Format(store.OrderNumberTemplate ?? "{0}", orderNumber); } } ``` {% endcode %} -## Registering the Custom Implementation +### Registering the Custom Implementation After creating your custom generator, register it in `Program.cs` to replace the default implementation: {% code title="Program.cs" %} ```csharp -builder.Services.AddUnique(); +builder.Services.AddUnique(); ``` {% endcode %} -The `AddUnique` method ensures that your custom generator replaces the default `IOrderNumberGenerator`. For more details on dependency injection, see the [Dependency Injection](dependency-injection.md) article. +The `AddUnique` method ensures that your custom generator replaces the default `IOrderNumberGenerator`, overriding both the automatic selection system and the built-in generators. For more details on dependency injection, see the [Dependency Injection](dependency-injection.md) article. ## Important Considerations @@ -54,3 +130,8 @@ Before implementing a custom order number generator, be aware of the following: - **Accounting Considerations:** Umbraco Commerce is not designed as an accounting platform. If strict sequential numbering is required for accounting purposes, it is recommended to integrate with a dedicated accounting system to handle such requirements. By understanding these factors, you can implement a custom order number generator that aligns with your specific requirements while maintaining optimal performance and compliance. + +## Related Documentation + +- [Order Number Generators](../key-concepts/order-number-generators.md) - Detailed documentation about the built-in generators +- [Dependency Injection](dependency-injection.md) - Learn more about registering services in Umbraco Commerce diff --git a/17/umbraco-commerce/key-concepts/discount-rules-and-rewards.md b/17/umbraco-commerce/key-concepts/discount-rules-and-rewards.md index 8d58bf60b1f..d93fdc6c71a 100644 --- a/17/umbraco-commerce/key-concepts/discount-rules-and-rewards.md +++ b/17/umbraco-commerce/key-concepts/discount-rules-and-rewards.md @@ -131,19 +131,19 @@ public class TieredPercentageRewardProvider : DiscountRewardProviderBase GenerateCartNumberAsync(Guid storeId, CancellationToken cancellationToken = default); + Task GenerateOrderNumberAsync(Guid storeId, CancellationToken cancellationToken = default); +} +``` + +The interface provides two key methods: + +- **GenerateCartNumberAsync** - Generates a unique number for shopping carts (orders that haven't been finalized) +- **GenerateOrderNumberAsync** - Generates a unique number for finalized orders + +Both methods are store-scoped, allowing different stores in a multi-tenant environment to have independent number sequences. + +## Built-in Generators + +Umbraco Commerce includes two built-in order number generators: + +### CompactSortableOrderNumberGenerator (Recommended) + +The `CompactSortableOrderNumberGenerator` is the recommended generator introduced in Umbraco Commerce 16.4. It produces compact, time-sortable identifiers with high scalability characteristics. + +**Format:** 10-character Base32 encoded ID with hyphen formatting + +Where: +- `timeComponent` - 7 Base32 characters encoding seconds since January 1, 2025 (supports ~1089 years) +- `nodeId` - 1 Base32 character representing the server/node ID (0-31) +- `sequence` - 2 Base32 characters encoding a per-second sequence number (0-1023) + +**Example:** `22345-67ABC` (hyphen inserted at position 5 for readability) + +**Characteristics:** +- **Compact** - Only 10 characters (11 with hyphen formatting) +- **Time-sortable** - Lexicographic ordering matches chronological ordering +- **Scalable** - Supports up to 1,024 orders per second per node (gracefully waits for the next second if capacity is exceeded rather than failing) +- **Multi-node safe** - Node ID prevents collisions in clustered environments +- **Visual variety** - Character rotation based on time reduces visual repetition + +**Base32 Alphabet:** Uses Crockford-like Base32 (`23456789ABCDEFGHJKLMNPQRSTUVWXYZ`) which excludes ambiguous characters (0, 1, I, O). + +**Number Template Support:** +Cart and order numbers can be formatted using the store's `CartNumberTemplate` and `OrderNumberTemplate` settings (defaults: `"CART-{0}"` and `"ORDER-{0}"`). + +### DateHashOrderNumberGenerator (Legacy) + +The `DateHashOrderNumberGenerator` is the legacy generator from earlier versions of Umbraco Commerce. It creates order numbers based on the current date and time, combined with a random string component. + +{% hint style="info" %} +This generator is maintained for backward compatibility with existing stores, but will eventually be deprecated. New implementations should use `CompactSortableOrderNumberGenerator`. +{% endhint %} + +**Format:** `{dayCount:00000}-{timeCount:000000}-{randomString}` + +Where: +- `dayCount` - Number of days since January 1, 2020 (5 digits) +- `timeCount` - Number of seconds elapsed in the current day (6 digits for carts, 5 for orders) +- `randomString` - 5 random characters from the set `BCDFGHJKLMNPQRSTVWXYZ3456789` + +**Example:** `02103-45678-H4K9P` + +**Characteristics:** +- Human-readable with embedded date information +- Random component reduces predictability +- No sequential ordering within the same second +- Collision risk increases if multiple orders are placed simultaneously +- Not suitable for high-volume or multi-node scenarios + +## How Generators Are Chosen + +Since Umbraco Commerce 16.4, the platform automatically selects the appropriate generator based on your store's state: + +- **New installations (16.4+)** - Uses `CompactSortableOrderNumberGenerator` for all new stores +- **Upgrades with existing orders** - Continues using `DateHashOrderNumberGenerator` for stores that already have orders, ensuring consistent number formats +- **Upgrades without orders** - Switches to `CompactSortableOrderNumberGenerator` for stores with no existing orders + +Prior to version 16.4, all installations used `DateHashOrderNumberGenerator` by default. + +This automatic selection ensures backward compatibility while enabling improved functionality for new stores. + +## Explicitly Choosing a Generator + +If you want to explicitly choose which generator to use, you can override the registration in your application startup. + +### Using CompactSortableOrderNumberGenerator (Recommended) + +To explicitly use the compact sortable generator (for example, when migrating an existing store to the new format): + +```csharp +public class MyComposer : IComposer +{ + public void Compose(IUmbracoBuilder builder) + { + builder.Services.AddUnique(); + } +} +``` + +### Using DateHashOrderNumberGenerator (Legacy Only) + +Only use this if you need to maintain the legacy format on a system that was upgraded: + +```csharp +public class MyComposer : IComposer +{ + public void Compose(IUmbracoBuilder builder) + { + builder.Services.AddUnique(); + } +} +``` + +{% hint style="warning" %} +Changing the order number generator on an existing store will result in different number formats for new orders. Ensure your business processes and integrations can handle mixed formats before making changes. +{% endhint %} + +## Creating a Custom Generator + +You can create custom order number generators by implementing the `IOrderNumberGenerator` interface: + +```csharp +public class CustomOrderNumberGenerator : IOrderNumberGenerator +{ + private readonly IStoreService _storeService; + + public CustomOrderNumberGenerator(IStoreService storeService) + { + _storeService = storeService; + } + + public async Task GenerateCartNumberAsync(Guid storeId, CancellationToken cancellationToken = default) + { + var store = await _storeService.GetStoreAsync(storeId); + + // Your custom cart number generation logic + var cartNumber = Guid.NewGuid().ToString("N")[..8].ToUpperInvariant(); + + // Apply store template if configured + return string.Format(store.CartNumberTemplate ?? "{0}", cartNumber); + } + + public async Task GenerateOrderNumberAsync(Guid storeId, CancellationToken cancellationToken = default) + { + var store = await _storeService.GetStoreAsync(storeId); + + // Your custom order number generation logic + var orderNumber = DateTime.UtcNow.ToString("yyyyMMdd") + "-" + + Random.Shared.Next(1000, 9999); + + // Apply store template if configured + return string.Format(store.OrderNumberTemplate ?? "{0}", orderNumber); + } +} +``` + +### Registering a Custom Generator + +Register your custom generator during application startup to replace the default implementation: + +```csharp +public class MyComposer : IComposer +{ + public void Compose(IUmbracoBuilder builder) + { + builder.Services.AddUnique(); + } +} +``` + +### Important Considerations for Custom Generators + +When implementing a custom generator, ensure: + +1. **Uniqueness** - Generated numbers must be globally unique across all time +2. **Consistency** - The same store should produce numbers in a consistent format +3. **Thread-safety** - Handle concurrent calls safely, especially for sequential numbering +4. **Template support** - Apply the store's `CartNumberTemplate` and `OrderNumberTemplate` settings +5. **Store isolation** - Consider supporting store-specific sequences in multi-store scenarios +6. **Scalability** - Handle high-volume scenarios if your store expects significant traffic +7. **Cluster-awareness** - In multi-node environments, ensure numbers don't collide across nodes +8. **Readability** - Balance uniqueness with human readability for customer communication + +{% hint style="warning" %} + Order numbers may have gaps if customers cancel or modify orders during the checkout process. Umbraco Commerce is not designed as an accounting platform. If you require strict sequential numbering for accounting purposes, integration with a dedicated accounting system is recommended. Additionally, sequential numbering can impact performance due to potential database access requirements. +{% endhint %} + +## Configuration + +Order number templates are configured at the store level and provide a way to add prefixes or suffixes to generated numbers: + +```csharp +// Set via the Store entity +await store.SetCartNumberTemplateAsync("CART-{0}"); +await store.SetOrderNumberTemplateAsync("ORDER-{0}"); +``` + +The `{0}` placeholder is replaced with the generated number from the active generator. + +**Examples:** +- Template: `"ORDER-{0}"` + Generated: `"22345-67ABC"` = Final: `"ORDER-22345-67ABC"` +- Template: `"{0}"` + Generated: `"02103-45678-H4K9P"` = Final: `"02103-45678-H4K9P"` +- Template: `"SO-{0}-2025"` + Generated: `"12345"` = Final: `"SO-12345-2025"` + +Templates are applied regardless of which generator is active, providing consistent branding across your order numbers. diff --git a/17/umbraco-engage/.gitbook/assets/External-profile-data-tab-v16.png b/17/umbraco-engage/.gitbook/assets/External-profile-data-tab-v16.png index 913f27849fa..f6390a1f002 100644 Binary files a/17/umbraco-engage/.gitbook/assets/External-profile-data-tab-v16.png and b/17/umbraco-engage/.gitbook/assets/External-profile-data-tab-v16.png differ diff --git a/17/umbraco-engage/.gitbook/assets/engage-tutorials-personalized-segments-cockpit-formatted-v16.png b/17/umbraco-engage/.gitbook/assets/engage-tutorials-personalized-segments-cockpit-formatted-v16.png new file mode 100644 index 00000000000..e9764cd72ec Binary files /dev/null and b/17/umbraco-engage/.gitbook/assets/engage-tutorials-personalized-segments-cockpit-formatted-v16.png differ diff --git a/17/umbraco-engage/.gitbook/assets/engage-tutorials-personalized-segments-cockpit-v16.png b/17/umbraco-engage/.gitbook/assets/engage-tutorials-personalized-segments-cockpit-v16.png new file mode 100644 index 00000000000..3af58e6c896 Binary files /dev/null and b/17/umbraco-engage/.gitbook/assets/engage-tutorials-personalized-segments-cockpit-v16.png differ diff --git a/17/umbraco-engage/.gitbook/assets/engage-tutorials-personalized-segments-v16.png b/17/umbraco-engage/.gitbook/assets/engage-tutorials-personalized-segments-v16.png new file mode 100644 index 00000000000..f57d72ac160 Binary files /dev/null and b/17/umbraco-engage/.gitbook/assets/engage-tutorials-personalized-segments-v16.png differ diff --git a/17/umbraco-engage/developers/personalization/implement-your-own-segment-parameters.md b/17/umbraco-engage/developers/personalization/implement-your-own-segment-parameters.md index c7f46a81033..ebb3380545b 100644 --- a/17/umbraco-engage/developers/personalization/implement-your-own-segment-parameters.md +++ b/17/umbraco-engage/developers/personalization/implement-your-own-segment-parameters.md @@ -1,61 +1,53 @@ --- description: >- - Umbraco Engage has different built-in segment parameters to build segments, - such as "Customer Journey" and "Time of Day". -hidden: true + Discover how to create and manage custom segments. --- # Implement your own segment parameters -You may want to build segments with custom rules not included in Umbraco Engage by default. You can add your custom segment parameters to Umbraco Engage. +Umbraco Engage comes with built-in parameters to build a segment, such as "Customer Journey" and "Time of Day". +However, segments can also be built with custom rules that are not included in Engage by default by adding custom segment parameters. -In the following guide, we will show how this is done. There are three steps: +The following guide explains how to achieve this. It is aimed at developers. +There are three steps, two are mandatory, and the last one is optional: -1. [C# Definition](#1-c-definition) -2. [AngularJS Definition](#2-angularjs-definition) -3. [[Optional] Cockpit Visualization](#3-optional-cockpit-visualization) +1. C# definition +2. Web component definition +3. Cockpit visualization (optional) -This guide will use code samples to add a "**Day of week**" segment parameter where you can select a single day of the week. If a pageview happens on that day the segment parameter will be satisfied. +This guide uses code samples to add a "Day of week" segment parameter where you can select a single day of the week. If a pageview happens on that day, the segment parameter is satisfied. -You can download the following code files to your project to add the parameter directly to your solution. +## 1. C# definition -{% file src="../../.gitbook/assets/day-of-the-week-segment-parameter.zip" %} +Your custom segment parameter needs to be defined in C# for Engage to use it. +In code, a segment parameter is referred to as a "segment rule". -## 1. C# Definition +A segment rule is not much more than this: -Your custom segment parameter must be defined in C# for the Umbraco Engage to use it.\ -In code, we refer to a segment parameter as a **segment rule**. - -A segment rule is: - -* A unique rule identifier, e.g. `DayOfWeek`. -* A configuration object, e.g. `{ dayOfWeek: 3 }`. - * This is optional, but most rules will have some sort of configuration that the user can alter in the Segment Builder. In our example, the user can configure the specific day of the week. -* A method that specifies whether the rule is satisfied by the current page view. +* A unique rule identifier, e.g. "DayOfWeek". +* A configuration object, e.g. "{ dayOfWeek: "Monday" }" + * This is optional, but most rules will have some sort of configuration that the user can alter in the Segment Builder. In our example, the user can configure the specific day of the week. +* A method that specifies whether the rule is satisfied by the current pageview. You will have to implement the following interfaces for a new custom parameter: - -* `Umbraco.Engage.Business.Personalization.Segments.Rules.ISegmentRule` - * You can extend the existing `BaseSegmentRule` to simplify the implementation. - * It is important to implement the `bool IsSatisfied(IPersonalizationProfile context)` method. -* `Umbraco.Engage.Business.Personalization.Segments.Rules.ISegmentRuleFactory` - * Register your implementation of the segment rule factory with `Lifetime.Transient` in a composer. - -For the "**Day of week**" example, the code looks like this: - -```csharp -// Define the segment rule +* `Umbraco.Engage.Infrastructure.Personalization.Segments.ISegmentRule` + * You can extend the existing `BaseSegmentRule` to simplify the implementation. + * The most important part to implement is the `bool IsSatisfied(IPersonalizationProfile context)` method. +* `Umbraco.Engage.Infrastructure.Personalization.Segments.Rules.ISegmentRuleFactory` + * Register your implementation of the segment rule factory with `Lifetime.Transient` in a composer. +For the "Day of week" example, the code looks like this: + +```c# public class DayOfWeekSegmentRule : BaseSegmentRule { public DayOfWeekSegmentRuleConfig TypedConfig { get; } - + public override SegmentRuleValidationMode ValidationMode => SegmentRuleValidationMode.Once; - public DayOfWeekSegmentRule(long id, long segmentId, string type, string config, - bool isNegation, DateTime created, DateTime? updated, DayOfWeekSegmentRuleConfig typedConfig) + public DayOfWeekSegmentRule(long id, Guid key, long segmentId, string type, string config, bool isNegation, DateTime created, DateTime? updated, DayOfWeekSegmentRuleConfig typedConfig) : base(id, segmentId, type, config, isNegation, created, updated) => TypedConfig = typedConfig; - + public override bool IsSatisfied(IPersonalizationProfile context) => context.Pageview.Timestamp.DayOfWeek == TypedConfig.DayOfWeek; } @@ -63,242 +55,228 @@ public class DayOfWeekSegmentRule : BaseSegmentRule And the factory which is used to create an instance of this rule: -```csharp +```c# +//The segment rule factory needs to be registered so Engage can use it. +[RegisterService(ServiceLifetime.Transient)] public class DayOfWeekSegmentRuleFactory : ISegmentRuleFactory { public string RuleType { get; } = "DayOfWeek"; - public ISegmentRule CreateRule(string config, bool isNegation, long id, - long segmentId, DateTime created, DateTime? updated) + public ISegmentRule CreateRule(string config, bool isNegation, long id, Guid key, long segmentId, DateTime created, DateTime? updated) { var typedConfig = JsonConvert.DeserializeObject(config); - return new DayOfWeekSegmentRule(id, segmentId, RuleType, config, isNegation, - created, updated, typedConfig); + return new DayOfWeekSegmentRule(id, key, segmentId, RuleType, config, isNegation, created, updated, typedConfig); } } ``` -We are using the class `DayOfWeekSegmentRuleConfig` as a representation of the configuration of the rule, which is not strictly necessary but makes it easier. The configuration is stored as a string in the database or IntelliSense support in code. The stored configuration is parsed into this class: +The class `DayOfWeekSegmentRuleConfig` is used to represent the rule configuration. This is not strictly necessary, but it makes it easier. +The configuration is stored as a string in the database. In code, Intellisense is enabled to parse the stored configuration to this class: -{% code overflow="wrap" %} -```csharp +```c# +//Generating config schema on client side. +[GenerateEngageSchema] public class DayOfWeekSegmentRuleConfig { public DayOfWeek DayOfWeek { get; set; } } ``` -{% endcode %} - -The segment rule factory needs to be registered so Umbraco Engage can use it.\ -The code below registers the factory in a new composer, you can use an existing composer for this if you like: - -```csharp -public class DayOfWeekSegmentRuleComposer : IUserComposer -{ - public void Compose(Composition composition) - { - composition.Register(Lifetime.Transient); - } -} -``` - -In the above example, we have shown how you can define custom segment parameters using C#. Next we look into enabling and configuring our segment in the Umbraco Engage segment builder. - -## 2. AngularJS Definition - -We implemented the business logic for the segment parameter in the previous step, however, the parameter cannot be added to your backoffice segments yet. In this step, we will add some JavaScript and HTML to enable you to add and configure your segments in the Umbraco Engage segment builder. - -This step will show concrete code samples that belong to our demo parameter "**Day of week**". - -You need to create a folder in the _App\_Plugins_ folder of your project that will hold the new files. - -For this example name it "`day-of-week`". The folder and content look like this: - -* `App_Plugins\day-of-week` - * `package.manifest` - * Instructs Umbraco to load your JavaScript files - * `segment-rule-day-of-week-display.html` - * View for displaying the configuration of your segment parameter - * `segment-rule-day-of-week-display.js` - * AngularJS component for displaying your segment parameter - * `segment-rule-day-of-week-editor.html` - * View for editing the configuration of your segment parameter - * `segment-rule-day-of-week-editor.js` - * AngularJS component for editing the configuration of your segment parameter - * `segment-rule-day-of-week.js` - * Define your segment parameter and register it in the segment rule repository of Umbraco Engage - -You can name the files, however, make sure to reference the JS files in your `package.manifest` properly. - -The contents for each of the files are below: - -* `segment-rule-day-of-week.js` - -In this file, you define the segment parameter and register it in the repository of Umbraco Engage. - -```javascript -// If you have your own custom module, use this: -// angular.module("myCustomModule", ["Engage"]); -// angular.module("umbraco").requires.push("myCustomModule"); -// angular.module("myCustomModule").run([ ... ]) - -angular.module("umbraco").run([ - "umsSegmentRuleRepository", - function (ruleRepo) { - var rule = { - name: "Day of week", // Friendly name - type: "DayOfWeek", // Rule type / identifier - - iconUrl: "/path/to/icon.png", - // You can also reuse existing Engage icons by specifying - //the "icon" property rather than the "iconUrl" property. - // Use either one or the other, not both. - // icon: "icon-browser", - - order: 4, // Position in segment builder - - // Default config is passed in to your editor when a user adds - // the rule to the segment - defaultConfig: { - dayOfWeek: null - }, - - // If you need any data in your editor, specify it here - data: { - days: { - 0: "Sunday", - 1: "Monday", - 2: "Tuesday", - 3: "Wednesday", - 4: "Thursday", - 5: "Friday", - 6: "Saturday", - } - }, - - // Specify the names of the display and editor components here. - // These will be dynamically rendered in our segment builder and in - // some other places. - components: { - display: "segment-rule-day-of-week-display", - editor: "segment-rule-day-of-week-editor", - }, - - init: function() { - // Optional. Use this in case you need to fetch some data - // for your segment parameter. - // For example, the built-in "Browser" segment parameter will fetch - // the list of possible browsers here and will update - // the "data" property. The "thisArg" of this function is set to - // the rule definition object, i.e. if you use "this.data" in - // this callback you can manipulate the data object of this rule. - } - }; - - ruleRepo.addRule(rule); - } -]); -``` - -* `segment-rule-day-of-week-editor.html` -This file contains the view of your parameter editor. Our example editor is a ` - - - +```text +npm run generate:api ``` -* `segment-rule-day-of-week-editor.js` +**segment-rule-base.ts** -This registers the editor component in the Umbraco Engage module so we can use it.\ -It should not be necessary to update this file other than update the component name and `templateUrl`. - -```javascript -// If you have your own custom module, use that name instead of "umbraco" here. -angular.module("umbraco").component("segmentRuleDayOfWeekEditor", { - templateUrl: "/App_Plugins/day-of-week/segment-rule-day-of-week-editor.html", - bindings: { - rule: "<", - config: "<", - save: "&", - }, -}); -``` +```typescript +enum RuleDirection { + INCLUDE = "include", + EXCLUDE = "exclude", +} -* `segment-rule-day-of-week-display.html` +export interface UeSegmentRuleParameterConfig { + isNegation: boolean; + config: ValueType; +} -This is the view file used for the visual representation of the segment parameter.\ -We want to display the picked day to the user: +export class UeSegmentRuleBaseElement extends UmbLitElement { + abstract renderReadOnly(); + abstract renderEditor(); + value: UeSegmentRuleParameterConfig; + initialized: Promise; + + @property({ type: Boolean, attribute: true, reflect: true }) + readonly?: boolean; + + updateParameterValue(value: any, key: keyof ValueType) { + if (!this.value?.config) return; + + const config = { ...(this.value.config ?? {}), ...{ [key]: value } }; + this.pending = assignToFrozenObject(this.value, { config }); + this.renderReadOnly(); + } + + render() { + return this.readonly ? this.#renderReadOnly() : this.#renderEditor(); + } + + #renderReadOnly() { + return html` +
+ + ${this.renderReadOnly()} +
+ `; + } + + #renderEditor() { + return html` +
+

${this.manifest?.meta.name}

+ +
+
${this.renderEditor()}
+ `; + } +} -```html - - - ``` -The chosen day of the week is stored as an integer (0-6) in `$ctrl.config.dayOfWeek`, but in the display component shows the actual day (for example. `Monday`). Our rule definition defines the mapping in its `data.days` property so we convert it using that and display the name of the day. +**segment-rule-day-of-week.ts** + +```typescript +export interface UeSegmentRuleDayOfWeekConfig + extends DayOfWeekSegmentRuleConfigModel {} + +const elementName = "ue-segment-rule-day-of-week"; + +@customElement(elementName) +export class UeSegmentRuleDayOfWeekElement extends UeSegmentRuleBaseElement { + @state() + private _options: Array = []; + + connectedCallback(): void { + super.connectedCallback(); + this._options = makeArray( + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday" + ).map((x, i) => ({ + value: x, + name: x, + selected: this.value?.config.dayOfWeek === x || i === 0, + })); + } + + renderReadOnly() { + return html`Day of week: ${this.value?.config.dayOfWeek}`; + } + + renderEditor() { + return html` + +
+ this.onSelectChange(e)} + > +
+
+ `; + } + + onSelectChange(e: UUISelectEvent) { + if (!this.value) return; + + const selectedValue = e.target.value as string; + + this.updateParameterValue(selectedValue, "dayOfWeek"); + } +} -* `segment-rule-day-of-week-display.js` +export { UeSegmentRuleDayOfWeekElement as api }; -In this file, we register the display component. +declare global { + interface HTMLElementTagNameMap { + [elementName]: UeSegmentRuleDayOfWeekElement; + } +} -```csharp -// If you have a custom module, use that name instead of "umbraco" -here.angular.module("umbraco").component("segmentRuleDayOfWeekDisplay", -{ - templateUrl: "/App_Plugins/day-of-week/segment-rule-day-of-week-display.html", - bindings: { - config: "<", - rule: "<", - },}); ``` -* `package.manifest` - -To make sure Umbraco loads your JS files we specify them here: +**index.ts** -```javascript -{ -"javascript": [ - "~/App_Plugins/day-of-week/segment-rule-day-of-week.js", - "~/App_Plugins/day-of-week/segment-rule-day-of-week-display.js", - "~/App_Plugins/day-of-week/segment-rule-day-of-week-editor.js" -]} +```text +export { UeSegmentRuleDayOfWeekElement } from "./segment-rule-day-of-week.js"; +export { UeSegmentRuleBaseElement } from "./segment-rule-base.js"; ``` -If all goes well you will see your custom parameter editor show up in the segment builder: +**manifest.ts** -![Day of week segment parameter](../../.gitbook/assets/engage-personalization-developer1.png) +```json +{ + type: ENGAGE_SEGMENT_RULE_EXTENSION_TYPE, + name: 'Engage Day of Week Segment Rule', + alias: 'Engage.Segment.Rule.DayOfWeek', + elementName: 'ue-segment-rule-day-of-week', + weight: 100, + meta: { + name: 'Day of week', + icon: 'icon-calendar', + type: 'DayOfWeek', + config: { dayOfWeek: 'Sunday' }, + }, +} +``` -## 3. \[Optional] Cockpit Visualization +That's it. If all went well you will see your custom parameter editor show up in the segment builder: -The new segment parameter will show up automatically in the [Cockpit](../../getting-started/for-marketers-and-editors/cockpit.md) that is part of our package. The cockpit is a live view of Umbraco Engage data for the current visitor. +
Day of week Segment.

Day of week Segment.

-This includes active segments of the current visitor, and therefore your new segment parameter can also show up in the cockpit. By default, it will display the **raw configuration of the parameter** as stored in the database ("`{ dayOfWeek: 3 }`" in our example). +## 3. Cockpit visualization (optional) -If you hover over it you will see the rule identifier `DayOfWeek` rather than a friendly name. +The new segment parameter will show up automatically in the Cockpit that is part of our package. The cockpit is a live view of Engage data for the current visitor. This includes active segments of the current visitor, and therefore your new segment parameter can also show up in the cockpit. -![Raw display of DayOfWeek](../../.gitbook/assets/engage-personalization-day-of-week-raw.png) +By default, it will display the raw configuration of the parameter as stored in the database ("{ dayOfWeek: Thursday }" in our example). If you hover over it, you will see the rule identifier "DayOfWeek" rather than a friendly name. -If you want to change this to be more readable you can implement the `Umbraco.Engage.Web.Cockpit.Segments.ICockpitSegmentRuleFactory` interface. +
-For the `DayOfWeek` demo parameter, this is the implementation: +If you would like to change this to be a bit more readable, you can implement the `Engage.Web.Cockpit.Segments.ICockpitSegmentRuleFactory` interface. For the `DayOfWeek` demo parameter, this is the implementation: -```csharp +```c# +//Registering this factory. +[RegisterService(ServiceLifetime.Transient)] public class DayOfWeekCockpitSegmentRuleFactory : ICockpitSegmentRuleFactory { - public bool TryCreate(ISegmentRule segmentRule, bool isSatisfied, out CockpitSegmentRule cockpitSegmentRule) + public DayOfWeekCockpitSegmentRuleFactory() { } + + public bool TryCreate(ISegmentRule segmentRule, bool isSatisfied, out CockpitSegmentRule? cockpitSegmentRule) { cockpitSegmentRule = null; if (segmentRule is DayOfWeekSegmentRule dayOfWeekRule) @@ -319,16 +297,8 @@ public class DayOfWeekCockpitSegmentRuleFactory : ICockpitSegmentRuleFactory } ``` -So we transform the JSON into a human-readable representation and we configure an icon to show up in the cockpit. Make sure to register this class in a composer (you can reuse the composer from the first step): - -```csharp -composition.Register(Lifetime.Transient); -``` - -After it has been registered, Umbraco Engage will use the additional information to properly render your segment parameter in the cockpit as well. +The JSON is transformed into a human-readable representation, and an icon is configured to appear in the cockpit. Make sure to register this class in a composer (you can reuse the composer from step 1): -{% hint style="info" %} -The "**DayOfWeek** test" string is the name of the segment. This segment happens to have only 1 parameter which is the DayOfWeek parameter. -{% endhint %} +Engage will then use the additional information to render your segment parameter correctly in the cockpit. The "DayOfWeek test" string is the name of the segment. This segment happens to have only 1 parameter which is the DayOfWeek parameter. -![DayOfWeek formatted](../../.gitbook/assets/engage-personalization-day-of-week-formatted.png) +
diff --git a/17/umbraco-engage/developers/profiling/external-profile-data.md b/17/umbraco-engage/developers/profiling/external-profile-data.md index b1c60d0a5c1..76d7d03c16b 100644 --- a/17/umbraco-engage/developers/profiling/external-profile-data.md +++ b/17/umbraco-engage/developers/profiling/external-profile-data.md @@ -14,7 +14,7 @@ If you want to use external data in a custom segment you have to write the data ## Visualization -It is possible to visualize this external data alongside the Umbraco Engage profile in the backoffice by providing a custom `AngularJS` component for this purpose. +It is possible to visualize this external data alongside the Umbraco Engage profile in the backoffice by providing a custom Web component for this purpose. When this component is registered a new tab will be rendered in the Profiles section when viewing profile details. This will render the custom component that was provided and get passed the Umbraco Engage visitor ID. @@ -22,7 +22,7 @@ When this component is registered a new tab will be rendered in the Profiles sec ### Register custom components -To render this External Profile Tab with a custom component, create your component and register it with Umbraco Engage. The following code demonstrates both steps. Add the below code in a TypeScript file: +To render this External Data Demo tab with a custom component, create your component and register it with Umbraco Engage. The following code demonstrates both steps. Add the below code in a TypeScript file: ```typescript export class EngageProfileInsightElement extends UmbLitElement { @@ -38,8 +38,15 @@ export class EngageProfileInsightElement extends UmbLitElement { } render() { return html` -

This is a custom external profile data element

-

Current profile id: ${this.#profileId}

`; + + + + + + + +
This is a custom profile insight element
Current profile id: ${this.#profileId}
+ `; } } export { EngageProfileInsightElement as element } @@ -52,7 +59,7 @@ Then, load your component using a `manifest.ts` file. The extension type must be { "type": "engageExternalDataComponent", "alias": "EngageDemo.ExternalProfileData", - "name": "External Profile Data Demo", + "name": "External Data Demo", "weight": 100, "js": "/path/to/my-javascript.js" } diff --git a/17/umbraco-forms/developer/ajaxforms.md b/17/umbraco-forms/developer/ajaxforms.md index ba1e678dab6..7bb2810dcf6 100644 --- a/17/umbraco-forms/developer/ajaxforms.md +++ b/17/umbraco-forms/developer/ajaxforms.md @@ -544,7 +544,7 @@ Examples demonstrating how to handle a file upload and use reCAPTCHA fields are ## Working with the CMS Content Delivery API -The [Content Delivery API](https://docs.umbraco.com/umbraco-cms/v/12.latest/reference/content-delivery-api) provides headless capabilities within Umbraco by allowing you to retrieve content in JSON format. +The [Content Delivery API](https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api) provides headless capabilities within Umbraco by allowing you to retrieve content in JSON format. When retrieving content that contains an Umbraco Forms form picker, the output by default will consist of the ID of the selected form: @@ -570,7 +570,7 @@ When retrieving content that contains an Umbraco Forms form picker, the output b } ``` -With [expanded output](https://docs.umbraco.com/umbraco-cms/v/12.latest/reference/content-delivery-api#output-expansion) for the property, the full details of the form will be available. The structure and content of the form representation will be exactly the same as that provided by the Forms API itself. +With [expanded output](https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api#output-expansion) for the property, the full details of the form will be available. The structure and content of the form representation will be exactly the same as that provided by the Forms API itself. ```json { @@ -598,3 +598,17 @@ With [expanded output](https://docs.umbraco.com/umbraco-cms/v/12.latest/referenc } } ``` + +## Dynamic Form injection + +For dynamic Form injection on a page, such as in a modal dialog, there's a specific JavaScript event and API method. This allows reinitializing Umbraco Forms for the new content. + +```javascript +// Execute a reinitialize on dynamic injections +const reinitializeEvent = new Event('umbracoFormsReinitialize'); +document.dispatchEvent(reinitializeEvent); + +// Render a specific form via the API +const injectedForm = document.getElementById('injected-umbraco-form'); +window.UmbracoForms.reinitialize(injectedForm); +``` diff --git a/17/umbraco-forms/developer/email-templates.md b/17/umbraco-forms/developer/email-templates.md index 6e175528457..b1ad7e8d47f 100644 --- a/17/umbraco-forms/developer/email-templates.md +++ b/17/umbraco-forms/developer/email-templates.md @@ -24,222 +24,240 @@ Below is an example of an email template from the `~/Views/Partials/Forms/Emails @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage @{ - //This is an example email template where you can use Razor Views to send HTML emails + //This is an example email template where you can use Razor Views to send HTML emails - //You can use Umbraco.TypedContent & Umbraco.TypedMedia etc to use Images & content from your site - //directly in your email templates too + //You can use Umbraco.Content & Umbraco.Media etc to use Images & content from your site + //directly in your email templates too - //Strongly Typed - //@Model.GetValue("aliasFormField") - //@foreach (var color in Model.GetValues("checkboxField")){} + //Strongly Typed + //@Model.GetValue("aliasFormField") + //@foreach (var color in Model.GetValues("checkboxField")){} - //Dynamics - //@Model.DynamicFields.aliasFormField - //@foreach(var color in Model.DynamicFields.checkboxField - //Images need to be absolute - so fetching domain to prefix with images - var siteDomain = Context.Request.Scheme + "://" + Context.Request.Host; - var assetUrl = siteDomain + "/App_plugins/UmbracoForms/Assets/Email-Example"; + //Images need to be absolute - so fetching domain to prefix with images + var siteDomain = Context.Request.Scheme + "://" + Context.Request.Host; + var assetUrl = siteDomain + "/App_Plugins/UmbracoForms/assets/Email-Example"; } - - - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - -
- - - - - -
- - Logo - -
- -
- - - - - -
-

Umbraco Forms

-
- -
- - - - - - - - - - - - - - - - - - - - - - -
- This is an example email template from Umbraco Forms Razor based email templates. You can build forms using any HTML markup you wish. -
- - CodeGarden16 Attendees - -
-

Form Results

-
- - @foreach (var field in Model.Fields) - { -

@field.Name

- - switch (field.FieldType) - { - case "FieldType.FileUpload.cshtml": -

@field.GetValue()

- break; - - case "FieldType.DatePicker.cshtml": - DateTime dt; - var fieldValue = field.GetValue(); - var dateValid = DateTime.TryParse(fieldValue != null ? fieldValue.ToString() : string.Empty, out dt); - var dateStr = dateValid ? dt.ToString("f") : ""; -

@dateStr

- break; - - case "FieldType.CheckBoxList.cshtml": -

- @foreach (var color in field.GetValues()) - { - @color
- } -

- break; - default: -

@field.GetValue()

- break; - } - } - -
- -
- - - - - - -
-

Need more help?

-

Find our documentation here

-
- -
+ + + + + + + + + + + +
+ + + + + +
+

Submission for @Model.FormName

+
+ +
+ + + + @if (Model.HeaderHtml is not null) + { + + + + } + + + @if (Model.BodyHtml is not null) + { + + + + } + + + + + + + + + + + + + @if (Model.FooterHtml is not null) + { + + + + } +
+ @Model.HeaderHtml +
+ @Model.BodyHtml +
+

Form Results

+
+ + @{ + string[] ignoreFields = new string[] + { + "FieldType.Recaptcha2.cshtml", + "FieldType.Recaptcha3.cshtml", + "FieldType.RichText.cshtml", + "FieldType.Text.cshtml" + }; + } + + @foreach (var field in Model.Fields.Where(x => ignoreFields.Contains(x.FieldType) == false)) + { +

@field.Name

+ +

+ @switch (field.FieldType) + { + case "FieldType.FileUpload.cshtml": + var uploadCount = 0; + foreach (var fileUploadValue in field.GetValues()) + { + if (fileUploadValue != null && !string.IsNullOrEmpty(fileUploadValue.ToString())) + { + uploadCount++; + } + } + + if (uploadCount > 0) + { + @uploadCount file@(uploadCount == 1 ? string.Empty : "s") uploaded + } + + break; + + case "FieldType.DatePicker.cshtml": + var datePickerValue = field.GetValue(); + if (datePickerValue != null && !string.IsNullOrEmpty(datePickerValue.ToString())) + { + DateTime dt; + var dateValid = DateTime.TryParse(datePickerValue != null ? datePickerValue.ToString() : string.Empty, out dt); + var dateStr = dateValid ? dt.ToString("f") : ""; + @dateStr + } + break; + + default: + var values = field.GetValues(); + if (values != null) + { + foreach (var value in values) + { + if (value != null) + { + @(value is string strValue ? strValue.ApplyPrevalueCaptions(field.Id, Model.PrevalueMaps) : value) + +
+ } + } + } + break; + } +

+ } + +
+ @Model.FooterHtml +
+ +
``` diff --git a/17/umbraco-ui-builder/SUMMARY.md b/17/umbraco-ui-builder/SUMMARY.md index 574945dc299..1e72027ae09 100644 --- a/17/umbraco-ui-builder/SUMMARY.md +++ b/17/umbraco-ui-builder/SUMMARY.md @@ -45,6 +45,7 @@ * [Retrieve Child Collections](collections/retrieve-child-collections.md) * [Related Collections](collections/related-collections.md) * [Entity Identifier Converters](collections/entity-identifier-converters.md) +* [Localization](collections/localization.md) ## Searching diff --git a/17/umbraco-ui-builder/collections/localization.md b/17/umbraco-ui-builder/collections/localization.md new file mode 100644 index 00000000000..2d745d7f295 --- /dev/null +++ b/17/umbraco-ui-builder/collections/localization.md @@ -0,0 +1,71 @@ +--- +description: Using the available context to handle localization for an UI Builder collection +--- + +# Localization + +The localization context enables developers to use multilingual collection names and descriptions in fluent configurations. It also supports translations for actions, context apps, dashboards, sections, and trees. + +To enable localization, prefix the input string with the `#` character. + +Upon character identification in the fluent configuration, the localization context will attempt to lookup a matching localized string using two services available. If no matching record is found, it will default to the provided string value. + +## Localization Services + +The localization context uses two abstractions to provide localization options. + +The first uses the Umbraco translations dictionary to retrieve a value based on a provided key. + +The second uses the CMS `ILocalizedTextService` to retrieve a value based on area and alias. These values are supplied in the collection's fluent configuration, separated by an underscore `_` from the localization resources. + +## Example + +### Localizing a Collection + +For a `Students` collection, use the following fluent configuration: + +```csharp +treeConfig.AddCollection(x => x.Id, "#CollectionStudents", "#CollectionStudents", "A list of students", "icon-umb-members", "icon-umb-members", collectionConfig => +{ + ... +}); +``` + +![collection_translation](../images/collection_translation.png) + +Alternatively, you can use the lowercase version: + +```csharp +treeConfig.AddCollection(x => x.Id, "#collection_students", "#collection_students", "A list of students", "icon-umb-members", "icon-umb-members", collectionConfig => +{ + ... +}); +``` + +Define the translation in your localization dictionary file: +``` +import type { UmbLocalizationDictionary } from "@umbraco-cms/backoffice/localization-api"; + +export default { + collection: { + students: "Studerende" + } + ... +} +``` + +![collection_name](../images/collection_name.png) + +### Localizing a Section +For a custom section, use the following configuration: + +```csharp +.AddSection("#UmbracoTraining", sectionConfig => +{ + ... +} +``` + +![section_name](../images/section_name.png) + + diff --git a/17/umbraco-ui-builder/images/collection_name.png b/17/umbraco-ui-builder/images/collection_name.png new file mode 100644 index 00000000000..9727ff50228 Binary files /dev/null and b/17/umbraco-ui-builder/images/collection_name.png differ diff --git a/17/umbraco-ui-builder/images/collection_translation.png b/17/umbraco-ui-builder/images/collection_translation.png new file mode 100644 index 00000000000..3821c3ae286 Binary files /dev/null and b/17/umbraco-ui-builder/images/collection_translation.png differ diff --git a/17/umbraco-ui-builder/images/section_name.png b/17/umbraco-ui-builder/images/section_name.png new file mode 100644 index 00000000000..6c0d8158993 Binary files /dev/null and b/17/umbraco-ui-builder/images/section_name.png differ