diff --git a/.vscode/settings.json b/.vscode/settings.json index 6d4441fffc..662f47d2d0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,8 @@ { - "cSpell.words": ["unprovide"], + "cSpell.words": [ + "unprovide", + "Unproviding" + ], "eslint.useFlatConfig": true, "eslint.workingDirectories": [ "./src/Umbraco.Web.UI.Client/", diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index d3e356ad7b..3c831031ac 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -31,9 +31,9 @@ "./collection": "./dist-cms/packages/core/collection/index.js", "./components": "./dist-cms/packages/core/components/index.js", "./const": "./dist-cms/packages/core/const/index.js", + "./content-picker": "./dist-cms/packages/property-editors/content-picker/index.js", "./content-type": "./dist-cms/packages/content/content-type/index.js", "./content": "./dist-cms/packages/content/content/index.js", - "./content-picker": "./dist-cms/packages/property-editors/content-picker/index.js", "./culture": "./dist-cms/packages/core/culture/index.js", "./current-user": "./dist-cms/packages/user/current-user/index.js", "./dashboard": "./dist-cms/packages/core/dashboard/index.js", @@ -47,8 +47,8 @@ "./entity-action": "./dist-cms/packages/core/entity-action/index.js", "./entity-bulk-action": "./dist-cms/packages/core/entity-bulk-action/index.js", "./entity-create-option-action": "./dist-cms/packages/core/entity-create-option-action/index.js", - "./entity": "./dist-cms/packages/core/entity/index.js", "./entity-item": "./dist-cms/packages/core/entity-item/index.js", + "./entity": "./dist-cms/packages/core/entity/index.js", "./event": "./dist-cms/packages/core/event/index.js", "./extension-registry": "./dist-cms/packages/core/extension-registry/index.js", "./health-check": "./dist-cms/packages/health-check/index.js", @@ -68,9 +68,9 @@ "./media-type": "./dist-cms/packages/media/media-types/index.js", "./media": "./dist-cms/packages/media/media/index.js", "./member-group": "./dist-cms/packages/members/member-group/index.js", + "./member-public-access": "./dist-cms/packages/members/member-public-access/index.js", "./member-type": "./dist-cms/packages/members/member-type/index.js", "./member": "./dist-cms/packages/members/member/index.js", - "./member-public-access": "./dist-cms/packages/members/member-public-access/index.js", "./menu": "./dist-cms/packages/core/menu/index.js", "./modal": "./dist-cms/packages/core/modal/index.js", "./models": "./dist-cms/packages/core/models/index.js", @@ -96,9 +96,10 @@ "./search": "./dist-cms/packages/search/index.js", "./section": "./dist-cms/packages/core/section/index.js", "./segment": "./dist-cms/packages/segment/index.js", - "./server": "./dist-cms/packages/core/server/index.js", "./server-file-system": "./dist-cms/packages/core/server-file-system/index.js", + "./server": "./dist-cms/packages/core/server/index.js", "./settings": "./dist-cms/packages/settings/index.js", + "./shortcut": "./dist-cms/packages/core/shortcut/index.js", "./sorter": "./dist-cms/packages/core/sorter/index.js", "./static-file": "./dist-cms/packages/static-file/index.js", "./store": "./dist-cms/packages/core/store/index.js", diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts index 7a17520579..bbe8d7b98e 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts @@ -22,6 +22,7 @@ import { filter, first, firstValueFrom } from '@umbraco-cms/backoffice/external/ import { hasOwnOpener, redirectToStoredPath } from '@umbraco-cms/backoffice/utils'; import { UmbApiInterceptorController } from '@umbraco-cms/backoffice/resources'; import { umbHttpClient } from '@umbraco-cms/backoffice/http-client'; +import { UmbViewContext } from '@umbraco-cms/backoffice/view'; import './app-logo.element.js'; import './app-oauth.element.js'; @@ -159,6 +160,8 @@ export class UmbAppElement extends UmbLitElement { new UmbContextDebugController(this); new UmbNetworkConnectionStatusManager(this); + + new UmbViewContext(this, null); } override connectedCallback(): void { diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/content-type-workspace-context-base.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/content-type-workspace-context-base.ts index 9f72381411..3f46cbe18f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/content-type-workspace-context-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/content-type-workspace-context-base.ts @@ -7,7 +7,6 @@ import { UmbRequestReloadChildrenOfEntityEvent, UmbRequestReloadStructureForEntityEvent, } from '@umbraco-cms/backoffice/entity-action'; -import { UmbViewContext } from '@umbraco-cms/backoffice/view'; import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; import type { Observable } from '@umbraco-cms/backoffice/observable-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; @@ -53,8 +52,6 @@ export abstract class UmbContentTypeWorkspaceContextBase< public readonly structure: UmbContentTypeStructureManager; - public readonly view = new UmbViewContext(this, null); - constructor(host: UmbControllerHost, args: UmbContentTypeWorkspaceContextArgs) { super(host, args); diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts index ee60251f62..2714250e54 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts @@ -33,7 +33,6 @@ import { } from '@umbraco-cms/backoffice/property'; import { UmbSegmentCollectionRepository } from '@umbraco-cms/backoffice/segment'; import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; -import { UmbViewContext } from '@umbraco-cms/backoffice/view'; import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; import { UMB_VALIDATION_CONTEXT, @@ -145,9 +144,6 @@ export abstract class UmbContentDetailWorkspaceContextBase< readonly collection: UmbContentCollectionManager; - /* View */ - readonly view = new UmbViewContext(this, null); - /* Variant Options */ // TODO: Optimize this so it uses either a App Language Context? [NL] #languageRepository = new UmbLanguageCollectionRepository(this); diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/views/edit/content-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/views/edit/content-editor.element.ts index 731389f7d0..85e79ba0fa 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/views/edit/content-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/views/edit/content-editor.element.ts @@ -7,7 +7,7 @@ import { } from '@umbraco-cms/backoffice/content-type'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import { UMB_VIEW_CONTEXT, UmbViewContext } from '@umbraco-cms/backoffice/view'; +import { UMB_VIEW_CONTEXT, UmbViewController } from '@umbraco-cms/backoffice/view'; import type { PageComponent, UmbRoute, @@ -31,7 +31,7 @@ export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements @state() private _hasRootProperties = false; */ - #viewContext?: UmbViewContext; + #viewContext?: typeof UMB_VIEW_CONTEXT.TYPE; @state() private _hasRootGroups = false; @@ -51,7 +51,7 @@ export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements @state() private _hintMap: Map = new Map(); - #tabViewContexts: Array = []; + #tabViewContexts: Array = []; #structureManager?: UmbContentTypeStructureManager; @@ -150,7 +150,7 @@ export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements #createViewContext(viewAlias: string | null, tabName: string) { if (!this.#tabViewContexts.find((context) => context.viewAlias === viewAlias)) { - const view = new UmbViewContext(this, viewAlias); + const view = new UmbViewController(this, viewAlias); this.#tabViewContexts.push(view); if (viewAlias === null) { @@ -176,7 +176,7 @@ export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements } } - #currentProvidedView?: UmbViewContext; + #currentProvidedView?: UmbViewController; #provideViewContext(viewAlias: string | null, component: PageComponent) { const view = this.#tabViewContexts.find((context) => context.viewAlias === viewAlias); @@ -188,6 +188,11 @@ export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements throw new Error(`View context with alias ${viewAlias} not found`); } this.#currentProvidedView = view; + // ViewAlias null is only for the root tab, therefor we can implement this hack. + if (viewAlias === null) { + // Specific hack for the Generic tab to only show its name if there are other tabs. + view.setBrowserTitle(this._tabs && this._tabs?.length > 0 ? '#general_generic' : undefined); + } view.provideAt(component as any); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/property-type/workspace/property-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/content/property-type/workspace/property-type-workspace.context.ts index bd4e7ed78b..086ac32e49 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/property-type/workspace/property-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/property-type/workspace/property-type-workspace.context.ts @@ -6,6 +6,7 @@ import type { UmbInvariantDatasetWorkspaceContext, UmbRoutableWorkspaceContext, ManifestWorkspace, + UmbNamableWorkspaceContext, } from '@umbraco-cms/backoffice/workspace'; import { UmbSubmittableWorkspaceContextBase, @@ -26,7 +27,7 @@ type PropertyTypeDataModel = UmbPropertyTypeScaffoldModel; export class UmbPropertyTypeWorkspaceContext extends UmbSubmittableWorkspaceContextBase - implements UmbInvariantDatasetWorkspaceContext, UmbRoutableWorkspaceContext + implements UmbInvariantDatasetWorkspaceContext, UmbRoutableWorkspaceContext, UmbNamableWorkspaceContext { // Just for context token safety: public readonly IS_PROPERTY_TYPE_WORKSPACE_CONTEXT = true; @@ -62,11 +63,22 @@ export class UmbPropertyTypeWorkspaceContext this.validationContext = new UmbValidationContext(this); this.addValidationContext(this.validationContext); - this.observe(this.unique, (unique) => { - if (unique) { - this.validationContext.setDataPath(UmbDataPathPropertyTypeQuery({ id: unique })); - } - }); + this.observe( + this.unique, + (unique) => { + if (unique) { + this.validationContext.setDataPath(UmbDataPathPropertyTypeQuery({ id: unique })); + } + }, + null, + ); + this.observe( + this.name, + (name) => { + this.view.setBrowserTitle(name); + }, + null, + ); this.#init = this.consumeContext(UMB_CONTENT_TYPE_WORKSPACE_CONTEXT, (context) => { this.#contentTypeContext = context; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/component/modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/component/modal.element.ts index 67f35e0f7f..e9067ba9cb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/component/modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/component/modal.element.ts @@ -55,6 +55,7 @@ export class UmbModalElement extends UmbLitElement { } this.#modalContext.addEventListener('umb:destroy', this.#onContextDestroy); + this.#modalContext.view.provideAt(this); this.element = await this.#createContainerElement(); // Makes sure that the modal triggers the reject of the context promise when it is closed by pressing escape. diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/context/modal.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/context/modal.context.ts index b27388d406..870b849d60 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/context/modal.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/context/modal.context.ts @@ -2,10 +2,11 @@ import { UmbModalToken } from '../token/modal-token.js'; import type { UmbModalConfig, UmbModalType } from '../types.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { UUIModalElement, UUIModalSidebarSize } from '@umbraco-cms/backoffice/external/uui'; -import { umbDeepMerge } from '@umbraco-cms/backoffice/utils'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import { umbDeepMerge } from '@umbraco-cms/backoffice/utils'; import { UmbId } from '@umbraco-cms/backoffice/id'; import { UmbObjectState, UmbStringState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbViewController } from '@umbraco-cms/backoffice/view'; import { UMB_ROUTE_CONTEXT } from '@umbraco-cms/backoffice/router'; import type { ElementLoaderProperty } from '@umbraco-cms/backoffice/extension-api'; import type { IRouterSlot } from '@umbraco-cms/backoffice/router'; @@ -61,6 +62,8 @@ export class UmbModalContext< #size = new UmbStringState('small'); public readonly size = this.#size.asObservable(); + public readonly view; + constructor( host: UmbControllerHost, modalAlias: string | UmbModalToken, @@ -71,6 +74,9 @@ export class UmbModalContext< this.router = args.router ?? null; this.alias = modalAlias; + this.view = new UmbViewController(this, modalAlias.toString()); + + let title: string | undefined = undefined; let size = 'small'; if (this.alias instanceof UmbModalToken) { @@ -78,8 +84,11 @@ export class UmbModalContext< size = this.alias.getDefaultModal()?.size ?? size; this.element = this.alias.getDefaultModal()?.element || this.element; this.backdropBackground = this.alias.getDefaultModal()?.backdropBackground || this.backdropBackground; + title = this.alias.getDefaultModal()?.title ?? undefined; } + this.view.setBrowserTitle(title); + this.type = args.modal?.type || this.type; size = args.modal?.size ?? size; this.element = args.modal?.element || this.element; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/types.ts index 3fe7068550..bd3d4a397c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/types.ts @@ -35,4 +35,9 @@ export interface UmbModalConfig { * Set the background property of the modal backdrop */ backdropBackground?: string; + + /** + * Set the title of the modal, this is used as Browser Title + */ + title?: string; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-items.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-items.manager.ts index fc9f7fd124..6e3121f680 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-items.manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-items.manager.ts @@ -1,4 +1,5 @@ import type { UmbItemRepository } from './item/index.js'; +import type { UmbRepositoryItemsStatus } from './types.js'; import { UmbDeprecation } from '@umbraco-cms/backoffice/utils'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; @@ -7,7 +8,6 @@ import { UmbExtensionApiInitializer } from '@umbraco-cms/backoffice/extension-ap import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; import { UmbEntityUpdatedEvent } from '@umbraco-cms/backoffice/entity-action'; -import type { UmbRepositoryItemsStatus } from './types.js'; const ObserveRepositoryAlias = Symbol(); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/shortcut/context/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/shortcut/context/index.ts new file mode 100644 index 0000000000..54d4d1fd8a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/shortcut/context/index.ts @@ -0,0 +1,3 @@ +export * from './shortcut.context-token.js'; +export * from './shortcut.context.js'; +export * from './shortcut.controller.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/shortcut/context/shortcut.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/shortcut/context/shortcut.context-token.ts new file mode 100644 index 0000000000..c682a7d1b4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/shortcut/context/shortcut.context-token.ts @@ -0,0 +1,4 @@ +import type { UmbShortcutController } from './shortcut.controller.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export const UMB_SHORTCUT_CONTEXT = new UmbContextToken('UmbShortcutContext'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/shortcut/context/shortcut.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/shortcut/context/shortcut.context.ts new file mode 100644 index 0000000000..fd3144d627 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/shortcut/context/shortcut.context.ts @@ -0,0 +1,10 @@ +import { UMB_SHORTCUT_CONTEXT } from './shortcut.context-token.js'; +import { UmbShortcutController } from './shortcut.controller.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; + +export class UmbShortcutContext extends UmbShortcutController { + constructor(host: UmbControllerHost) { + super(host); + this.provideContext(UMB_SHORTCUT_CONTEXT, this as unknown as UmbShortcutContext); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/shortcut/context/shortcut.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/shortcut/context/shortcut.controller.ts new file mode 100644 index 0000000000..8e79808ae0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/shortcut/context/shortcut.controller.ts @@ -0,0 +1,189 @@ +import type { UmbShortcut } from '../types.js'; +import { UMB_SHORTCUT_CONTEXT } from './shortcut.context-token.js'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbControllerBase, type UmbClassInterface } from '@umbraco-cms/backoffice/class-api'; +import type { UmbContextProviderController } from '@umbraco-cms/backoffice/context-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { UmbPartialSome } from '@umbraco-cms/backoffice/utils'; + +type IncomingShortcutType = UmbPartialSome; + +const IsMac = navigator.userAgent ? /Mac/i.test(navigator.userAgent) : navigator.platform.toUpperCase().includes('MAC'); + +export class UmbShortcutController extends UmbControllerBase { + // + #inUnprovidingState = false; + + #parent?: UmbShortcutController; + + readonly #shortcuts = new UmbArrayState([], (x) => x.unique); + public readonly all = this.#shortcuts.asObservable(); + + constructor(host: UmbControllerHost) { + super(host); + + this.#shortcuts.sortBy((a, b) => (b.weight || 0) - (a.weight || 0)); + } + + #providerCtrl?: UmbContextProviderController; + #currentProvideHost?: UmbClassInterface; + /** + * Provide this validation context to a specific controller host. + * This can be used to Host a validation context in a Workspace, but provide it on a certain scope, like a specific Workspace View. + * @param {UmbClassInterface} controllerHost - The controller host to provide this validation context to. + */ + provideAt(controllerHost: UmbClassInterface): void { + if (this.#currentProvideHost === controllerHost) return; + + this.unprovide(); + + this.#currentProvideHost = controllerHost; + this.#providerCtrl = controllerHost.provideContext(UMB_SHORTCUT_CONTEXT, this as any); + } + + unprovide(): void { + if (this.#providerCtrl) { + // We need to set this in Unprovide state, so this context can be provided again later. + this.#inUnprovidingState = true; + this.#providerCtrl.destroy(); + this.#providerCtrl = undefined; + this.#inUnprovidingState = false; + this.#currentProvideHost = undefined; + } + } + + inherit(): void { + this.consumeContext(UMB_SHORTCUT_CONTEXT, (parent) => { + this.inheritFrom(parent); + }).skipHost(); + // Notice skipHost ^^, this is because we do not want it to consume it self, as this would be a match for this consumption, instead we will look at the parent and above. [NL] + } + + inheritFrom(parent: UmbShortcutController | undefined): void { + if (this.#parent === parent) return; + this.#parent = parent; + } + + initiateChange() { + this.#shortcuts.mute(); + } + finishChange() { + this.#shortcuts.unmute(); + } + + /** + * Add a new hint + * @param {IncomingShortcutType} shortcut - The hint to add + * @returns {UmbShortcut['unique']} Unique value of the hint + */ + addOne(shortcut: IncomingShortcutType): string | symbol { + const newShortcut = { ...shortcut } as unknown as UmbShortcut; + newShortcut.unique ??= Symbol(); + newShortcut.weight ??= 0; + newShortcut.modifier ??= false; + newShortcut.shift ??= false; + newShortcut.alt ??= false; + this.#shortcuts.appendOne(newShortcut); + return shortcut.unique!; + } + + /** + * Add multiple rules + * @param {IncomingShortcutType[]} shortcuts - Array of hints to add + */ + add(shortcuts: IncomingShortcutType[]) { + this.#shortcuts.mute(); + shortcuts.forEach((hint) => this.addOne(hint)); + this.#shortcuts.unmute(); + } + + /** + * Remove a hint + * @param {UmbShortcut['unique']} unique Unique value of the hint to remove + */ + removeOne(unique: UmbShortcut['unique']) { + this.#shortcuts.removeOne(unique); + } + + /** + * Remove multiple hints + * @param {UmbShortcut['unique'][]} uniques Array of unique values to remove + */ + remove(uniques: UmbShortcut['unique'][]) { + this.#shortcuts.remove(uniques); + } + + /** + * Check if a hint exists + * @param {UmbShortcut['unique']} unique Unique value of the hint to check + * @returns {boolean} True if the hint exists, false otherwise + */ + has(unique: UmbShortcut['unique']): boolean { + return this.#shortcuts.getHasOne(unique); + } + + /** + * Get all hints + * @returns {UmbShortcut[]} Array of hints + */ + getAll(): UmbShortcut[] { + return this.#shortcuts.getValue(); + } + + /** + * Get all hints + * @param key + * @param modifier + * @param shift + * @param alt + * @returns {UmbShortcut[]} Array of hints + */ + findShortcut(key: string, modifier: boolean, shift: boolean = false, alt: boolean = false): UmbShortcut | undefined { + const shortcuts = this.#shortcuts.getValue(); + for (const s of shortcuts) { + if (s.key.toLowerCase() === key.toLowerCase() && s.modifier === modifier && s.shift === shift && s.alt === alt) { + return s; + } + } + + return undefined; + } + + /** + * Clear all hints + */ + clear(): void { + this.#shortcuts.setValue([]); + } + + activate() { + window.addEventListener('keydown', this.#onKeyDown); + } + + deactivate() { + window.removeEventListener('keydown', this.#onKeyDown); + } + + #onKeyDown = (e: KeyboardEvent) => { + const keyDown = e.key.toLowerCase(); + const modifierDown = IsMac ? e.metaKey : e.ctrlKey; + + const shortcut = this.findShortcut(keyDown, modifierDown, e.shiftKey, e.altKey); + if (shortcut) { + e.preventDefault(); + shortcut.action(); + } + }; + + override destroy(): void { + super.destroy(); + if (this.#inUnprovidingState === true) { + // TODO: What is it i'm doing here, check if it actually makes sense, if so add a comment on why [NL] + return; + } + this.unprovide(); + this.#parent = undefined; + + this.#shortcuts.destroy(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/shortcut/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/shortcut/index.ts new file mode 100644 index 0000000000..66e7bbbc85 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/shortcut/index.ts @@ -0,0 +1,2 @@ +export * from './context/index.js'; +export type * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/shortcut/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/shortcut/types.ts new file mode 100644 index 0000000000..a645cdb6a7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/shortcut/types.ts @@ -0,0 +1,12 @@ +export interface UmbShortcut { + unique: string | symbol; + key: string; + modifier: boolean; + shift: boolean; + alt: boolean; + label?: string; + weight?: number; + action: () => void | Promise; + // TODO: Consider implementing a global option, to make a shortcut be available despite children setting up their own inheritance scopes. [NL] + // TODO: Addition thought, also a bit dangerous cause how do you know the interest of the children. [NL] +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/view/context/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/view/context/index.ts index 3fff04337f..49147ec12b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/view/context/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/view/context/index.ts @@ -1,2 +1,3 @@ +export * from './view.controller.js'; export * from './view.context.js'; export * from './view.context-token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/view/context/view.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/view/context/view.controller.ts index 0375dcdfb4..087ba4f583 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/view/context/view.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/view/context/view.controller.ts @@ -1,10 +1,6 @@ +import { UmbShortcutController } from '../../shortcut/context/shortcut.controller.js'; import { UMB_VIEW_CONTEXT } from './view.context-token.js'; -import { - UmbBooleanState, - UmbClassState, - UmbStringState, - mergeObservables, -} from '@umbraco-cms/backoffice/observable-api'; +import { UmbClassState, UmbStringState, mergeObservables } from '@umbraco-cms/backoffice/observable-api'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; import { UmbHintController } from '@umbraco-cms/backoffice/hint'; import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api'; @@ -14,12 +10,7 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { UmbVariantHint } from '@umbraco-cms/backoffice/hint'; import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; -const ObserveParentActiveCtrlAlias = Symbol(); - /** - * - * TODO: - * Include Shortcuts * * The View Context handles the aspects of three Features: * Browser Titles — Provide a title for this view and it will be set or joint with parent views depending on the inheritance setting. @@ -28,6 +19,8 @@ const ObserveParentActiveCtrlAlias = Symbol(); * */ export class UmbViewController extends UmbControllerBase { + // + static #ActiveView?: UmbViewController; // #attached = false; #providerCtrl?: UmbContextProviderController; @@ -37,13 +30,34 @@ export class UmbViewController extends UmbControllerBase { // State used to know if the context can be auto activated when attached. #autoActivate = true; - #active = new UmbBooleanState(false); - public readonly active = this.#active.asObservable(); + #active = false; get isActive() { - return this.#active.getValue(); + return this.#active; } - #hasActiveChild = false; - #inherit?: boolean; + #setActive() { + this.#active = true; + if (this.#inherit) { + // Secure the parent in the inheritance chain is active. + this.#parentView?._internal_activate(); + } else { + // This is for a single, or top level of the inheritance chain, so we can disable the previous active view. + if (UmbViewController.#ActiveView && UmbViewController.#ActiveView !== this) { + UmbViewController.#ActiveView._internal_deactivate(); + UmbViewController.#ActiveView = undefined; + } + UmbViewController.#ActiveView = this; + } + } + #removeActive() { + this.#active = false; + if (!this.#inherit) { + if (UmbViewController.#ActiveView === this) { + UmbViewController.#ActiveView = undefined; + } + } + } + + #inherit = false; #explicitInheritance?: boolean; #parentView?: UmbViewController; #title?: string; @@ -55,9 +69,11 @@ export class UmbViewController extends UmbControllerBase { #variantId = new UmbClassState(undefined); protected readonly variantId = this.#variantId.asObservable(); - public hints; + public readonly hints; - readonly firstHintOfVariant; + public readonly shortcuts = new UmbShortcutController(this); + + public readonly firstHintOfVariant; constructor(host: UmbControllerHost, viewAlias: string | null) { super(host); @@ -79,24 +95,25 @@ export class UmbViewController extends UmbControllerBase { this.#consumeParentCtrl = this.consumeContext(UMB_VIEW_CONTEXT, (parentView) => { // In case of explicit inheritance we do not want to overview the parent view. if (this.#explicitInheritance) return; - if (this.isActive && !this.#hasActiveChild) { - // If we were active we will react as if we got deactivated and then activated again below if state allows. [NL] - this.#propagateActivation(); - } - this.#active.setValue(false); if (parentView) { - this.#parentView = parentView; - } - if (this.#inherit) { - this.#inheritFromParent(); + this.#setParentView(parentView); } // only activate if we had an incoming parentView, cause if not we are in a disassembling state. [NL] if (parentView && this.#attached && this.#autoActivate) { - this._internal_activate(); + this._internal_requestActivate(); } }).skipHost(); } + #setParentView(view: UmbViewController | undefined) { + if (this.#parentView === view) return; + this.#parentView = view; + + if (this.#inherit) { + this.#inheritFromParent(); + } + } + public setVariantId(variantId: UmbVariantId | undefined): void { this.#variantId.setValue(variantId); this.hints.updateScaffold({ variantId: variantId }); @@ -105,7 +122,6 @@ export class UmbViewController extends UmbControllerBase { public setBrowserTitle(title: string | undefined): void { if (this.#title === title) return; this.#title = title; - // TODO: This check should be if its the most child being active, but again think about how the parents in the active chain should work. this.#computeTitle(); this.#updateTitle(); } @@ -119,9 +135,10 @@ export class UmbViewController extends UmbControllerBase { this.#currentProvideHost = controllerHost; this.#providerCtrl = controllerHost.provideContext(UMB_VIEW_CONTEXT, this); this.hints.provideAt(controllerHost); + this.shortcuts.provideAt(controllerHost); - if (this.#attached && this.#autoActivate) { - this._internal_activate(); + if (this.#attached) { + this._internal_requestActivate(); } } @@ -131,30 +148,41 @@ export class UmbViewController extends UmbControllerBase { this.#providerCtrl = undefined; } this.hints.unprovide(); + this.shortcuts.unprovide(); this._internal_deactivate(); + this.#requestActivateParent(); } override hostConnected(): void { const wasActive = this.isActive; + const wasAttached = this.#attached; this.#attached = true; super.hostConnected(); + if (!wasAttached) { + this.#parentView?._internal_addChild(this); + } // Check that we have a providerController, otherwise this is not provided. [NL] if (this.#autoActivate && !wasActive) { - this._internal_activate(); + this._internal_requestActivate(); } } override hostDisconnected(): void { const wasAttached = this.#attached; - const wasActive = this.isActive; this.#attached = false; - this.#active.setValue(false); - super.hostDisconnected(); - if (wasAttached === true && wasActive) { - // Check that we have a providerController, otherwise this is not provided. [NL] - this.#propagateActivation(); + if (wasAttached) { + this.#parentView?._internal_removeChild(this); } + + this._internal_deactivate(); + super.hostDisconnected(); + this.#autoActivate = true; + this.#requestActivateParent(); + } + + public isInheriting() { + return this.#inherit; } public inherit() { @@ -166,21 +194,7 @@ export class UmbViewController extends UmbControllerBase { this.#explicitInheritance = true; this.#consumeParentCtrl?.destroy(); this.#consumeParentCtrl = undefined; - this.#parentView = context; - // Notice because we cannot break the inheritance, we do not need to stop this observation in any of the logic. [NL] - this.observe( - this.#parentView?.active, - (isActive) => { - if (isActive) { - this._internal_activate(); - } else { - this._internal_deactivate(); - } - }, - ObserveParentActiveCtrlAlias, - ); - this.#inheritFromParent(); - this.#propagateActivation(); + this.#setParentView(context); } #inheritFromParent(): void { @@ -205,19 +219,10 @@ export class UmbViewController extends UmbControllerBase { this.hints.inheritFrom(this.#parentView?.hints); } - #propagateActivation() { - if (!this.#parentView) return; - if (this.#inherit) { - if (this.isActive) { - this.#parentView._internal_childActivated(); - } else { - this.#parentView._internal_childDeactivated(); - } - } else { - if (this.isActive) { - this.#parentView._internal_deactivate(); - } else { - this.#parentView._internal_activate(); + #requestActivateParent() { + if (!this.#inherit) { + if (this.#parentView) { + this.#parentView._internal_requestActivate(); } } } @@ -227,58 +232,52 @@ export class UmbViewController extends UmbControllerBase { * Notify that a view context has been activated. */ // eslint-disable-next-line @typescript-eslint/naming-convention - public _internal_activate() { + public _internal_requestActivate(): boolean { if (!this.#providerCtrl) { // If we are not provided we should not be activated. [NL] - return; + return false; } + // TODO: Check this one: We do not want a parent to auto activate if a child is having the activation. [NL], well maybe it not that bad because of the asking of the children... this.#autoActivate = true; if (this.isActive) { - return; + return true; } // If not attached then propagate the activation to the parent. [NL] if (this.#attached === false) { if (!this.#parentView) { throw new Error('Cannot activate a view that is not attached to the DOM.'); } - this.#propagateActivation(); } else { - this.#active.setValue(true); - this.#propagateActivation(); - this.#updateTitle(); - // TODO: Start shortcuts. [NL] - } - } - - /** - * @internal - * Notify that a child has been activated. - */ - // eslint-disable-next-line @typescript-eslint/naming-convention - public _internal_childActivated() { - if (this.#hasActiveChild) return; - this.#hasActiveChild = true; - this._internal_activate(); - } - - /** - * @internal - * Notify that a child is no longer activated. - */ - // eslint-disable-next-line @typescript-eslint/naming-convention - public _internal_childDeactivated() { - this.#hasActiveChild = false; - if (this.#attached === false) { - if (this.#parentView) { - return; - } else { - throw new Error('Cannot re-activate(_childDeactivated) a view that is not attached to the DOM.'); + // Check if any of the children likes to be activated instead: + // A reverse loop ensures latest added child gets first chance to activate. This may matter in some future issue-scenario, I will say it could be that it is not the right way to determine if multiple children wants to be active. [NL] + let i = this.#children.length; + while (i--) { + const child = this.#children[i]; + if (child._internal_requestActivate()) { + // If we have an active child we should not update the title. + return true; + } + } + // if not then check your self: + if (this.#autoActivate && this.#attached) { + this._internal_activate(); + return true; } } - if (this.#autoActivate) { - this._internal_activate(); - } else { - this.#propagateActivation(); + return false; + } + + /** + * @internal + * Notify that a view context has been activated. + */ + // eslint-disable-next-line @typescript-eslint/naming-convention + public _internal_activate() { + if (this.#attached) { + this.#autoActivate = true; + this.#setActive(); + this.#updateTitle(); + this.shortcuts.activate(); } } @@ -289,16 +288,21 @@ export class UmbViewController extends UmbControllerBase { */ // eslint-disable-next-line @typescript-eslint/naming-convention public _internal_deactivate() { - this.#autoActivate = false; if (!this.isActive) return; - this.#active.setValue(false); - // TODO: Stop shortcuts. [NL] - // Deactivate parents: - this.#propagateActivation(); + this.#autoActivate = false; + + // Deactive children: + this.#children.forEach((child) => { + if (child.isInheriting()) { + child._internal_deactivate(); + } + }); + this.shortcuts.deactivate(); + this.#removeActive(); } #updateTitle() { - if (!this.#active || this.#hasActiveChild) { + if (!this.#active || this.#hasActiveChildren()) { return; } const localTitle = this.getComputedTitle(); @@ -320,9 +324,32 @@ export class UmbViewController extends UmbControllerBase { return this.#computedTitle.getValue(); } + #children: UmbViewController[] = []; + // eslint-disable-next-line @typescript-eslint/naming-convention + public _internal_addChild(child: UmbViewController) { + this.#children.push(child); + if (this.isActive) { + child._internal_activate(); + } + } + // eslint-disable-next-line @typescript-eslint/naming-convention + public _internal_removeChild(child: UmbViewController) { + const index = this.#children.indexOf(child); + if (index !== -1) { + this.#children.splice(index, 1); + } + // update title? + if (this.#active && !this.#hasActiveChildren()) { + this.#updateTitle(); + } + } + #hasActiveChildren() { + return this.#children.some((child) => child.isActive); + } + override destroy(): void { this.#inherit = false; - this.#active.setValue(false); + this.#removeActive(); this.#autoActivate = false; (this as any).provideAt = undefined; this.unprovide(); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/vite.config.ts b/src/Umbraco.Web.UI.Client/src/packages/core/vite.config.ts index 35aa800b95..0cc97b1c69 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/vite.config.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/vite.config.ts @@ -26,18 +26,18 @@ export default defineConfig({ 'entity-action/index': './entity-action/index.ts', 'entity-bulk-action/index': './entity-bulk-action/index.ts', 'entity-create-option-action/index': './entity-create-option-action/index.ts', - 'entity/index': './entity/index.ts', 'entity-item/index': './entity-item/index.ts', + 'entity/index': './entity/index.ts', 'entry-point': 'entry-point.ts', 'event/index': './event/index.ts', 'extension-registry/index': './extension-registry/index.ts', - 'http-client/index': './http-client/index.ts', 'hint/index': './hint/index.ts', + 'http-client/index': './http-client/index.ts', 'icon-registry/index': './icon-registry/index.ts', 'id/index': './id/index.ts', + 'interaction-memory/index': './interaction-memory/index.ts', 'lit-element/index': './lit-element/index.ts', 'localization/index': './localization/index.ts', - 'interaction-memory/index': './interaction-memory/index.ts', 'menu/index': './menu/index.ts', 'modal/index': './modal/index.ts', 'models/index': './models/index.ts', @@ -53,8 +53,9 @@ export default defineConfig({ 'resources/index': './resources/index.ts', 'router/index': './router/index.ts', 'section/index': './section/index.ts', - 'server/index': './server/index.ts', 'server-file-system/index': './server-file-system/index.ts', + 'server/index': './server/index.ts', + 'shortcut/index': './shortcut/index.ts', 'sorter/index': './sorter/index.ts', 'store/index': './store/index.ts', 'style/index': './style/index.ts', diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/entity-named-detail-workspace-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/entity-named-detail-workspace-base.ts index 4d14510056..28d055df58 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/entity-named-detail-workspace-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/entity-named-detail-workspace-base.ts @@ -2,7 +2,6 @@ import type { UmbNamableWorkspaceContext } from '../types.js'; import { UmbNameWriteGuardManager } from '../namable/index.js'; import { UmbEntityDetailWorkspaceContextBase } from './entity-detail-workspace-base.js'; import type { UmbEntityDetailWorkspaceContextArgs, UmbEntityDetailWorkspaceContextCreateArgs } from './types.js'; -import { UmbViewContext } from '@umbraco-cms/backoffice/view'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { UmbDetailRepository } from '@umbraco-cms/backoffice/repository'; import type { UmbNamedEntityModel } from '@umbraco-cms/backoffice/entity'; @@ -24,8 +23,6 @@ export abstract class UmbEntityNamedDetailWorkspaceContextBase< public readonly nameWriteGuard = new UmbNameWriteGuardManager(this); - public readonly view = new UmbViewContext(this, null); - constructor(host: UmbControllerHost, args: UmbEntityDetailWorkspaceContextArgs) { super(host, args); this.nameWriteGuard.fallbackToPermitted(); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/kinds/default/default-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/kinds/default/default-workspace.context.ts index 00a1906991..0f2bb13877 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/kinds/default/default-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/kinds/default/default-workspace.context.ts @@ -1,22 +1,26 @@ import { UMB_WORKSPACE_CONTEXT } from '../../workspace.context-token.js'; import type { UmbWorkspaceContext } from '../../workspace-context.interface.js'; -import type { ManifestWorkspace } from '../../extensions/types.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; import { UmbEntityContext, type UmbEntityUnique } from '@umbraco-cms/backoffice/entity'; +import { UmbViewContext } from '@umbraco-cms/backoffice/view'; +import type { ManifestWorkspaceDefaultKind } from './types.js'; export class UmbDefaultWorkspaceContext extends UmbContextBase implements UmbWorkspaceContext { public workspaceAlias!: string; #entityContext = new UmbEntityContext(this); + public readonly view = new UmbViewContext(this, null); + constructor(host: UmbControllerHost) { super(host, UMB_WORKSPACE_CONTEXT.toString()); } - set manifest(manifest: ManifestWorkspace) { + set manifest(manifest: ManifestWorkspaceDefaultKind) { this.workspaceAlias = manifest.alias; this.setEntityType(manifest.meta.entityType); + this.view.setBrowserTitle(manifest.meta.headline); } setUnique(unique: UmbEntityUnique): void { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/submittable/submittable-workspace-context-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/submittable/submittable-workspace-context-base.ts index 445cead0f0..d62ec59335 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/submittable/submittable-workspace-context-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/submittable/submittable-workspace-context-base.ts @@ -8,6 +8,7 @@ import type { UmbModalContext } from '@umbraco-cms/backoffice/modal'; import { UMB_MODAL_CONTEXT } from '@umbraco-cms/backoffice/modal'; import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; import type { UmbValidationController } from '@umbraco-cms/backoffice/validation'; +import { UmbViewContext } from '@umbraco-cms/backoffice/view'; export abstract class UmbSubmittableWorkspaceContextBase extends UmbContextBase @@ -20,6 +21,8 @@ export abstract class UmbSubmittableWorkspaceContextBase #validationContexts: Array = []; + public readonly view = new UmbViewContext(this, null); + /** * Appends a validation context to the workspace. * @param context @@ -54,6 +57,13 @@ export abstract class UmbSubmittableWorkspaceContextBase this.consumeContext(UMB_MODAL_CONTEXT, (context) => { (this.modalContext as UmbModalContext | undefined) = context; }); + + this.view.shortcuts.addOne({ + key: 's', + modifier: true, + action: () => this.requestSubmit(), + label: '#general_submit', + }); } protected resetState() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/save-modal/document-save-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/save-modal/document-save-modal.element.ts index 6eebcb0c70..3990df78f9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/save-modal/document-save-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/save-modal/document-save-modal.element.ts @@ -1,9 +1,10 @@ import type { UmbDocumentVariantOptionModel } from '../../types.js'; import type { UmbDocumentSaveModalData, UmbDocumentSaveModalValue } from './document-save-modal.token.js'; import { css, customElement, html, state } from '@umbraco-cms/backoffice/external/lit'; +import { umbFocus } from '@umbraco-cms/backoffice/lit-element'; import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; -import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { UmbSelectionManager } from '@umbraco-cms/backoffice/utils'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import '../shared/document-variant-language-picker.element.js'; @@ -56,21 +57,23 @@ export class UmbDocumentSaveModalElement extends UmbModalBaseElement< } override render() { - return html` - - -
- - -
-
`; + return html` + + +
+ + +
+
+ `; } static override styles = [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/modal/document-publish-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/modal/document-publish-modal.element.ts index 755a366b4f..84923fd865 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/modal/document-publish-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/modal/document-publish-modal.element.ts @@ -2,9 +2,10 @@ import { UmbDocumentVariantState, type UmbDocumentVariantOptionModel } from '../ import { isNotPublishedMandatory } from '../../utils.js'; import type { UmbDocumentPublishModalData, UmbDocumentPublishModalValue } from './document-publish-modal.token.js'; import { css, customElement, html, state, when } from '@umbraco-cms/backoffice/external/lit'; +import { umbFocus } from '@umbraco-cms/backoffice/lit-element'; import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; -import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { UmbSelectionManager } from '@umbraco-cms/backoffice/utils'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import '../../../modals/shared/document-variant-language-picker.element.js'; @@ -103,33 +104,34 @@ export class UmbDocumentPublishModalElement extends UmbModalBaseElement< override render() { const headline = this.data?.headline ?? this.localize.term('content_publishModalTitle'); - return html` -

- -

+ return html` + +

- ${when( - !this._isInvariant, - () => - html` `, - )} + ${when( + !this._isInvariant, + () => + html``, + )} -
- - -
-
`; +
+ + +
+
+ `; } static override styles = [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/workspace-context/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/workspace-context/constants.ts index fe7ae40fca..dfafaf0c4a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/workspace-context/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/workspace-context/constants.ts @@ -1 +1,3 @@ export * from './document-publishing.workspace-context.token.js'; + +export const UMB_DOCUMENT_PUBLISHING_SHORTCUT_UNIQUE = 'umb-document-publishing-shortcut'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/workspace-context/document-publishing.workspace-context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/workspace-context/document-publishing.workspace-context.ts index 0bd43d3764..0173b8e100 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/workspace-context/document-publishing.workspace-context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/workspace-context/document-publishing.workspace-context.ts @@ -11,21 +11,22 @@ import { UMB_DOCUMENT_PUBLISH_WITH_DESCENDANTS_MODAL } from '../publish-with-des import { UMB_DOCUMENT_PUBLISH_MODAL } from '../publish/constants.js'; import { UmbUnpublishDocumentEntityAction } from '../unpublish/index.js'; import { UMB_DOCUMENT_PUBLISHING_WORKSPACE_CONTEXT } from './document-publishing.workspace-context.token.js'; -import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; +import { UMB_DOCUMENT_PUBLISHING_SHORTCUT_UNIQUE } from './constants.js'; +import { firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs'; +import { observeMultiple } from '@umbraco-cms/backoffice/observable-api'; import { umbOpenModal } from '@umbraco-cms/backoffice/modal'; +import { DocumentVariantStateModel } from '@umbraco-cms/backoffice/external/backend-api'; +import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; +import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api'; import { UmbRequestReloadChildrenOfEntityEvent, UmbRequestReloadStructureForEntityEvent, } from '@umbraco-cms/backoffice/entity-action'; -import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; +import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification'; -import { firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs'; -import { observeMultiple } from '@umbraco-cms/backoffice/observable-api'; -import { DocumentVariantStateModel } from '@umbraco-cms/backoffice/external/backend-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { UmbEntityUnique } from '@umbraco-cms/backoffice/entity'; -import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api'; export class UmbDocumentPublishingWorkspaceContext extends UmbContextBase { /** @@ -48,7 +49,18 @@ export class UmbDocumentPublishingWorkspaceContext extends UmbContextBase { this.#init = Promise.all([ this.consumeContext(UMB_DOCUMENT_WORKSPACE_CONTEXT, async (context) => { + if (this.#documentWorkspaceContext) { + // remove shortcut: + this.#documentWorkspaceContext.view.shortcuts.removeOne(UMB_DOCUMENT_PUBLISHING_SHORTCUT_UNIQUE); + } this.#documentWorkspaceContext = context; + this.#documentWorkspaceContext?.view.shortcuts.addOne({ + unique: UMB_DOCUMENT_PUBLISHING_SHORTCUT_UNIQUE, + label: this.#localize.term('content_saveAndPublishShortcut'), + key: 'p', + modifier: true, + action: () => this.saveAndPublish(), + }); this.#initPendingChanges(); }) .asPromise({ preventTimeout: true }) diff --git a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/logviewer-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/logviewer-workspace.context.ts index e765508dd2..2f6f8b380b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/logviewer-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/logviewer-workspace.context.ts @@ -15,6 +15,7 @@ import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; import { query } from '@umbraco-cms/backoffice/router'; import type { UmbWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; +import { UmbViewContext } from '@umbraco-cms/backoffice/view'; export type UmbPoolingInterval = 0 | 2000 | 5000 | 10000 | 20000 | 30000; export interface UmbPoolingConfig { @@ -31,6 +32,8 @@ export class UmbLogViewerWorkspaceContext extends UmbContextBase implements UmbW public readonly workspaceAlias: string = 'Umb.Workspace.LogViewer'; #repository: UmbLogViewerRepository; + public readonly view = new UmbViewContext(this, null); + getEntityType() { return 'log-viewer'; } @@ -108,6 +111,8 @@ export class UmbLogViewerWorkspaceContext extends UmbContextBase implements UmbW // TODO: Revisit usage of workspace for this case... currently no other workspace context provides them self with their own token, we need to update UMB_APP_LOG_VIEWER_CONTEXT to become a workspace context. [NL] this.provideContext(UMB_WORKSPACE_CONTEXT, this); this.#repository = new UmbLogViewerRepository(host); + + this.view.setBrowserTitle('#treeHeaders_logViewer'); } override hostConnected() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user/current-user-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user/current-user-modal.token.ts index 7247fda5a2..5084ba3d2c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user/current-user-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user/current-user-modal.token.ts @@ -4,5 +4,6 @@ export const UMB_CURRENT_USER_MODAL = new UmbModalToken('Umb.Modal.CurrentUser', modal: { type: 'sidebar', size: 'small', + title: '#general_user', }, }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/webhook/webhook/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/webhook/webhook/workspace/manifests.ts index 43f583c939..ae9c6cb1fd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/webhook/webhook/workspace/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/webhook/webhook/workspace/manifests.ts @@ -6,7 +6,7 @@ export const manifests: Array = [ type: 'workspace', kind: 'routable', alias: UMB_WEBHOOK_WORKSPACE_ALIAS, - name: 'Webhook Root Workspace', + name: 'Webhook Workspace', api: () => import('./webhook-workspace.context.js'), meta: { entityType: UMB_WEBHOOK_ENTITY_TYPE, diff --git a/src/Umbraco.Web.UI.Client/tsconfig.json b/src/Umbraco.Web.UI.Client/tsconfig.json index 010ad94033..1d040c15b2 100644 --- a/src/Umbraco.Web.UI.Client/tsconfig.json +++ b/src/Umbraco.Web.UI.Client/tsconfig.json @@ -58,9 +58,9 @@ DON'T EDIT THIS FILE DIRECTLY. It is generated by /devops/tsconfig/index.js "@umbraco-cms/backoffice/collection": ["./src/packages/core/collection/index.ts"], "@umbraco-cms/backoffice/components": ["./src/packages/core/components/index.ts"], "@umbraco-cms/backoffice/const": ["./src/packages/core/const/index.ts"], + "@umbraco-cms/backoffice/content-picker": ["./src/packages/property-editors/content-picker/index.ts"], "@umbraco-cms/backoffice/content-type": ["./src/packages/content/content-type/index.ts"], "@umbraco-cms/backoffice/content": ["./src/packages/content/content/index.ts"], - "@umbraco-cms/backoffice/content-picker": ["./src/packages/property-editors/content-picker/index.ts"], "@umbraco-cms/backoffice/culture": ["./src/packages/core/culture/index.ts"], "@umbraco-cms/backoffice/current-user": ["./src/packages/user/current-user/index.ts"], "@umbraco-cms/backoffice/dashboard": ["./src/packages/core/dashboard/index.ts"], @@ -76,8 +76,8 @@ DON'T EDIT THIS FILE DIRECTLY. It is generated by /devops/tsconfig/index.js "@umbraco-cms/backoffice/entity-create-option-action": [ "./src/packages/core/entity-create-option-action/index.ts" ], - "@umbraco-cms/backoffice/entity": ["./src/packages/core/entity/index.ts"], "@umbraco-cms/backoffice/entity-item": ["./src/packages/core/entity-item/index.ts"], + "@umbraco-cms/backoffice/entity": ["./src/packages/core/entity/index.ts"], "@umbraco-cms/backoffice/event": ["./src/packages/core/event/index.ts"], "@umbraco-cms/backoffice/extension-registry": ["./src/packages/core/extension-registry/index.ts"], "@umbraco-cms/backoffice/health-check": ["./src/packages/health-check/index.ts"], @@ -97,9 +97,9 @@ DON'T EDIT THIS FILE DIRECTLY. It is generated by /devops/tsconfig/index.js "@umbraco-cms/backoffice/media-type": ["./src/packages/media/media-types/index.ts"], "@umbraco-cms/backoffice/media": ["./src/packages/media/media/index.ts"], "@umbraco-cms/backoffice/member-group": ["./src/packages/members/member-group/index.ts"], + "@umbraco-cms/backoffice/member-public-access": ["./src/packages/members/member-public-access/index.ts"], "@umbraco-cms/backoffice/member-type": ["./src/packages/members/member-type/index.ts"], "@umbraco-cms/backoffice/member": ["./src/packages/members/member/index.ts"], - "@umbraco-cms/backoffice/member-public-access": ["./src/packages/members/member-public-access/index.ts"], "@umbraco-cms/backoffice/menu": ["./src/packages/core/menu/index.ts"], "@umbraco-cms/backoffice/modal": ["./src/packages/core/modal/index.ts"], "@umbraco-cms/backoffice/models": ["./src/packages/core/models/index.ts"], @@ -125,9 +125,10 @@ DON'T EDIT THIS FILE DIRECTLY. It is generated by /devops/tsconfig/index.js "@umbraco-cms/backoffice/search": ["./src/packages/search/index.ts"], "@umbraco-cms/backoffice/section": ["./src/packages/core/section/index.ts"], "@umbraco-cms/backoffice/segment": ["./src/packages/segment/index.ts"], - "@umbraco-cms/backoffice/server": ["./src/packages/core/server/index.ts"], "@umbraco-cms/backoffice/server-file-system": ["./src/packages/core/server-file-system/index.ts"], + "@umbraco-cms/backoffice/server": ["./src/packages/core/server/index.ts"], "@umbraco-cms/backoffice/settings": ["./src/packages/settings/index.ts"], + "@umbraco-cms/backoffice/shortcut": ["./src/packages/core/shortcut/index.ts"], "@umbraco-cms/backoffice/sorter": ["./src/packages/core/sorter/index.ts"], "@umbraco-cms/backoffice/static-file": ["./src/packages/static-file/index.ts"], "@umbraco-cms/backoffice/store": ["./src/packages/core/store/index.ts"],