From 42f901aa41d8347dd9d6b0a94dd17d2cc876012d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 4 Apr 2023 15:48:36 +0200 Subject: [PATCH] Proff of concept for navigating to edit path when created --- .../libs/controller/controller-host.mixin.ts | 23 +++++++++---- .../generate-route-path-builder.function.ts | 21 ++++++++++++ .../libs/router/index.ts | 1 + .../libs/router/route.context.ts | 23 ++----------- .../workspace/actions/save/save.action.ts | 33 +++---------------- .../context/workspace-context.interface.ts | 5 +-- .../language-workspace-edit.element.ts | 2 +- .../language/language-workspace.context.ts | 20 ++++++++++- .../language/language-workspace.element.ts | 20 +++++++++++ ...language-details-workspace-view.element.ts | 6 ++-- .../workspace-context/workspace-context.ts | 4 +-- 11 files changed, 93 insertions(+), 65 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/libs/router/generate-route-path-builder.function.ts diff --git a/src/Umbraco.Web.UI.Client/libs/controller/controller-host.mixin.ts b/src/Umbraco.Web.UI.Client/libs/controller/controller-host.mixin.ts index 221da8d54b..fc5df147a6 100644 --- a/src/Umbraco.Web.UI.Client/libs/controller/controller-host.mixin.ts +++ b/src/Umbraco.Web.UI.Client/libs/controller/controller-host.mixin.ts @@ -5,6 +5,7 @@ export declare class UmbControllerHostElement extends HTMLElement { hasController(controller: UmbControllerInterface): boolean; getControllers(filterMethod: (ctrl: UmbControllerInterface) => boolean): UmbControllerInterface[]; addController(controller: UmbControllerInterface): void; + removeControllerByUnique(unique: UmbControllerInterface['unique']): void; removeController(controller: UmbControllerInterface): void; } @@ -43,13 +44,7 @@ export const UmbControllerHostMixin = (superCl */ addController(ctrl: UmbControllerInterface): void { // Check if there is one already with same unique - if (ctrl.unique) { - this.#controllers.forEach((x) => { - if (x.unique === ctrl.unique) { - this.removeController(x); - } - }); - } + this.removeControllerByUnique(ctrl.unique); this.#controllers.push(ctrl); if (this.#attached) { @@ -57,6 +52,20 @@ export const UmbControllerHostMixin = (superCl } } + /** + * Remove a controller from this element, by its unique/alias. + * @param {unknown} unique/alias + */ + removeControllerByUnique(unique: UmbControllerInterface['unique']): void { + if (unique) { + this.#controllers.forEach((x) => { + if (x.unique === unique) { + this.removeController(x); + } + }); + } + } + /** * Remove a controller from this element. * Notice this will also destroy the controller. diff --git a/src/Umbraco.Web.UI.Client/libs/router/generate-route-path-builder.function.ts b/src/Umbraco.Web.UI.Client/libs/router/generate-route-path-builder.function.ts new file mode 100644 index 0000000000..152465e701 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/router/generate-route-path-builder.function.ts @@ -0,0 +1,21 @@ +import type { ISlashOptions } from '@umbraco-cms/internal/router'; + +const PARAM_IDENTIFIER = /:([^\\/]+)/g; + +function stripSlash(path: string): string { + return slashify(path, { start: false, end: false }); +} + +function slashify(path: string, { start = true, end = true }: Partial = {}): string { + path = start && !path.startsWith('/') ? `/${path}` : !start && path.startsWith('/') ? path.slice(1) : path; + return end && !path.endsWith('/') ? `${path}/` : !end && path.endsWith('/') ? path.slice(0, path.length - 1) : path; +} + +export function generateRoutePathBuilder(path: string) { + return (params: { [key: string]: string | number }) => + stripSlash( + path.replace(PARAM_IDENTIFIER, (substring: string, ...args: string[]) => { + return params[args[0]].toString(); + }) + ); +} diff --git a/src/Umbraco.Web.UI.Client/libs/router/index.ts b/src/Umbraco.Web.UI.Client/libs/router/index.ts index 5a49b1769d..45b593eca7 100644 --- a/src/Umbraco.Web.UI.Client/libs/router/index.ts +++ b/src/Umbraco.Web.UI.Client/libs/router/index.ts @@ -1,3 +1,4 @@ export * from './route-location.interface'; export * from './route.context'; export * from './route.interface'; +export * from './generate-route-path-builder.function'; diff --git a/src/Umbraco.Web.UI.Client/libs/router/route.context.ts b/src/Umbraco.Web.UI.Client/libs/router/route.context.ts index da5364d1df..fe561c2121 100644 --- a/src/Umbraco.Web.UI.Client/libs/router/route.context.ts +++ b/src/Umbraco.Web.UI.Client/libs/router/route.context.ts @@ -1,5 +1,6 @@ import type { IRoutingInfo, ISlashOptions } from 'router-slot'; import { UmbRoute } from './route.interface'; +import { generateRoutePathBuilder } from '.'; import { UmbContextConsumerController, UmbContextProviderController, @@ -10,17 +11,6 @@ import { UMB_MODAL_CONTEXT_TOKEN, UmbModalRouteRegistration } from '@umbraco-cms const EmptyDiv = document.createElement('div'); -const PARAM_IDENTIFIER = /:([^\\/]+)/g; - -function stripSlash(path: string): string { - return slashify(path, { start: false, end: false }); -} - -function slashify(path: string, { start = true, end = true }: Partial = {}): string { - path = start && !path.startsWith('/') ? `/${path}` : !start && path.startsWith('/') ? path.slice(1) : path; - return end && !path.endsWith('/') ? `${path}/` : !end && path.endsWith('/') ? path.slice(0, path.length - 1) : path; -} - export class UmbRouteContext { #modalRegistrations: UmbModalRouteRegistration[] = []; #modalContext?: typeof UMB_MODAL_CONTEXT_TOKEN.TYPE; @@ -125,16 +115,9 @@ export class UmbRouteContext { if (!this.#routerBasePath) return; const routeBasePath = this.#routerBasePath.endsWith('/') ? this.#routerBasePath : this.#routerBasePath + '/'; - const localPath = `modal/${modalRegistration.alias.toString()}/${modalRegistration.path}`; + const localPath = routeBasePath + `modal/${modalRegistration.alias.toString()}/${modalRegistration.path}`; - const urlBuilder = (params: { [key: string]: string | number }) => { - const localRoutePath = stripSlash( - localPath.replace(PARAM_IDENTIFIER, (substring: string, ...args: string[]) => { - return params[args[0]].toString(); - }) - ); - return routeBasePath + localRoutePath; - }; + const urlBuilder = generateRoutePathBuilder(localPath); modalRegistration._internal_setRouteBuilder(urlBuilder); }; diff --git a/src/Umbraco.Web.UI.Client/libs/workspace/actions/save/save.action.ts b/src/Umbraco.Web.UI.Client/libs/workspace/actions/save/save.action.ts index b2f986912f..9cc3d5089c 100644 --- a/src/Umbraco.Web.UI.Client/libs/workspace/actions/save/save.action.ts +++ b/src/Umbraco.Web.UI.Client/libs/workspace/actions/save/save.action.ts @@ -6,6 +6,9 @@ import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controlle export class UmbSaveWorkspaceAction extends UmbWorkspaceActionBase { constructor(host: UmbControllerHostElement) { super(host); + + // TODO: Could we make change label depending on the state? + // So its called 'Create' when the workspace is new and 'Save' when the workspace is not new. } /* TODO: we need a solution for all actions to notify the system that is has been executed. @@ -14,35 +17,7 @@ export class UmbSaveWorkspaceAction extends UmbWorkspaceActionBase { host: UmbControllerHostElement; repository: any; // TODO: add type - isNew: Observable; - getIsNew(): boolean; + isNew: Observable; + getIsNew(): boolean | undefined; setIsNew(value: boolean): void; // TODO: should we consider another name than entity type. File system files are not entities but still have this type. getEntityType(): string; getData(): DataType | undefined; + save(): Promise; destroy(): void; // TODO: temp solution to bubble validation errors to the UI setValidationErrors?(errorMap: any): void; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/language-workspace-edit.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/language-workspace-edit.element.ts index 2647b15a1c..594f876c75 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/language-workspace-edit.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/language-workspace-edit.element.ts @@ -45,7 +45,7 @@ export class UmbLanguageWorkspaceEditElement extends UmbLitElement { _language?: LanguageResponseModel; @state() - _isNew = false; + _isNew?: boolean; constructor() { super(); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/language-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/language-workspace.context.ts index 5a7ce69293..f51bfe3075 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/language-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/language-workspace.context.ts @@ -76,7 +76,25 @@ export class UmbLanguageWorkspaceContext } async save() { - throw new Error('Save method not implemented.'); + const data = this.getData(); + if (!data) return; + + if (this.getIsNew()) { + const { error } = await this.repository.create(data); + // TODO: this is temp solution to bubble validation errors to the UI + if (error) { + if (error.type === 'validation') { + this.setValidationErrors?.(error.errors); + } + } else { + this.setValidationErrors?.(undefined); + // TODO: do not make it the buttons responsibility to set the workspace to not new. + this.setIsNew(false); + } + } else { + await this.repository.save(data); + // TODO: Show validation errors as warnings? + } } destroy(): void { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/language-workspace.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/language-workspace.element.ts index 7574254fbe..a903589b0f 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/language-workspace.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/language-workspace.element.ts @@ -7,6 +7,7 @@ import { UmbRouterSlotInitEvent } from '@umbraco-cms/internal/router'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; import './language-workspace-edit.element'; +import { generateRoutePathBuilder } from '@umbraco-cms/backoffice/router'; @customElement('umb-language-workspace') export class UmbLanguageWorkspaceElement extends UmbLitElement { @@ -23,6 +24,7 @@ export class UmbLanguageWorkspaceElement extends UmbLitElement { path: 'edit/:isoCode', component: () => this.#element, setup: (component: HTMLElement, info: IRoutingInfo) => { + this.removeControllerByUnique('_observeIsNew'); this.#languageWorkspaceContext.load(info.match.params.isoCode); }, }, @@ -31,6 +33,24 @@ export class UmbLanguageWorkspaceElement extends UmbLitElement { component: () => this.#element, setup: async () => { this.#languageWorkspaceContext.createScaffold(); + + // Navigate to edit route when language is created: + this.observe( + this.#languageWorkspaceContext.isNew, + (isNew) => { + console.log('observe', isNew); + if (isNew === false) { + const isoCode = this.#languageWorkspaceContext.getEntityId(); + if (this.#routerPath && isoCode) { + const routeBasePath = this.#routerPath.endsWith('/') ? this.#routerPath : this.#routerPath + '/'; + // TODO: Revisit if this is the right way to change URL: + const newPath = generateRoutePathBuilder(routeBasePath + 'edit/:isoCode')({ isoCode }); + window.history.pushState({}, '', newPath); + } + } + }, + '_observeIsNew' + ); }, }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/details/language-details-workspace-view.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/details/language-details-workspace-view.element.ts index a2c0d105d7..c1b79f9add 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/details/language-details-workspace-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/details/language-details-workspace-view.element.ts @@ -52,7 +52,7 @@ export class UmbLanguageDetailsWorkspaceViewElement extends UmbLitElement { _isDefaultLanguage = false; @state() - _isNew = false; + _isNew?: boolean; @state() _validationErrors?: { [key: string]: Array }; @@ -81,8 +81,8 @@ export class UmbLanguageDetailsWorkspaceViewElement extends UmbLitElement { } }); - this.observe(this.#languageWorkspaceContext.isNew, (value) => { - this._isNew = value; + this.observe(this.#languageWorkspaceContext.isNew, (isNew) => { + this._isNew = isNew; }); this.observe(this.#languageWorkspaceContext.validationErrors, (value) => { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-context.ts index 9d6b905264..796b258bd3 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-context.ts @@ -1,7 +1,7 @@ import { UmbEntityWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; import { UmbContextProviderController, UMB_ENTITY_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/context-api'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; -import { DeepState } from '@umbraco-cms/backoffice/observable-api'; +import { BooleanState } from '@umbraco-cms/backoffice/observable-api'; import type { BaseEntity } from '@umbraco-cms/backoffice/models'; /* @@ -15,7 +15,7 @@ export abstract class UmbWorkspaceContext public host: UmbControllerHostElement; public repository: T; - #isNew = new DeepState(false); + #isNew = new BooleanState(undefined); isNew = this.#isNew.asObservable(); constructor(host: UmbControllerHostElement, repository: T) {