diff --git a/src/Umbraco.Web.UI.Client/.eslintrc.json b/src/Umbraco.Web.UI.Client/.eslintrc.json index 88caa66773..5d886c4d8f 100644 --- a/src/Umbraco.Web.UI.Client/.eslintrc.json +++ b/src/Umbraco.Web.UI.Client/.eslintrc.json @@ -44,6 +44,7 @@ "local-rules/enforce-element-suffix-on-element-class-name": "error", "local-rules/prefer-umbraco-cms-imports": "error", "local-rules/no-external-imports": "error", + "local-rules/umb-class-prefix": "error", "@typescript-eslint/no-non-null-assertion": "off" }, "settings": { diff --git a/src/Umbraco.Web.UI.Client/.storybook/preview.js b/src/Umbraco.Web.UI.Client/.storybook/preview.js index 822d951f3a..e55d5c7558 100644 --- a/src/Umbraco.Web.UI.Client/.storybook/preview.js +++ b/src/Umbraco.Web.UI.Client/.storybook/preview.js @@ -13,7 +13,7 @@ import { UmbDocumentTypeStore } from '../src/backoffice/documents/document-types import { UmbDocumentStore } from '../src/backoffice/documents/documents/repository/document.store.ts'; import { UmbDocumentTreeStore } from '../src/backoffice/documents/documents/repository/document.tree.store.ts'; -import customElementManifests from '../custom-elements.json'; +import customElementManifests from '../dist/libs/custom-elements.json'; import { UmbIconStore } from '../src/core/stores/icon/icon.store'; import { onUnhandledRequest } from '../src/core/mocks/browser'; import { handlers } from '../src/core/mocks/browser-handlers'; diff --git a/src/Umbraco.Web.UI.Client/eslint-local-rules.cjs b/src/Umbraco.Web.UI.Client/eslint-local-rules.cjs index ed1c5036cd..5d121e8cfb 100644 --- a/src/Umbraco.Web.UI.Client/eslint-local-rules.cjs +++ b/src/Umbraco.Web.UI.Client/eslint-local-rules.cjs @@ -205,4 +205,31 @@ module.exports = { }; }, }, + + /** @type {import('eslint').Rule.RuleModule} */ + 'umb-class-prefix': { + meta: { + type: 'problem', + docs: { + description: 'Ensure that all class declarations are prefixed with "Umb"', + category: 'Best Practices', + recommended: true, + }, + schema: [], + }, + create: function (context) { + function checkClassName(node) { + if (node.id && node.id.name && !node.id.name.startsWith('Umb')) { + context.report({ + node: node.id, + message: 'Class declaration should be prefixed with "Umb"', + }); + } + } + + return { + ClassDeclaration: checkClassName, + }; + }, + }, }; diff --git a/src/Umbraco.Web.UI.Client/libs/backend-api/src/models/PropertyTypeResponseModelBaseModel.ts b/src/Umbraco.Web.UI.Client/libs/backend-api/src/models/PropertyTypeResponseModelBaseModel.ts index ad3a59bbbf..72adb84e18 100644 --- a/src/Umbraco.Web.UI.Client/libs/backend-api/src/models/PropertyTypeResponseModelBaseModel.ts +++ b/src/Umbraco.Web.UI.Client/libs/backend-api/src/models/PropertyTypeResponseModelBaseModel.ts @@ -6,15 +6,14 @@ import type { PropertyTypeAppearanceModel } from './PropertyTypeAppearanceModel' import type { PropertyTypeValidationModel } from './PropertyTypeValidationModel'; export type PropertyTypeResponseModelBaseModel = { - id?: string; - containerId?: string | null; - alias?: string; - name?: string; - description?: string | null; - dataTypeId?: string; - variesByCulture?: boolean; - variesBySegment?: boolean; - validation?: PropertyTypeValidationModel; - appearance?: PropertyTypeAppearanceModel; + id?: string; + containerId?: string | null; + alias?: string; + name?: string; + description?: string | null; + dataTypeId?: string; + variesByCulture?: boolean; + variesBySegment?: boolean; + validation?: PropertyTypeValidationModel; + appearance?: PropertyTypeAppearanceModel; }; - diff --git a/src/Umbraco.Web.UI.Client/libs/context-api/consume/context-consumer.test.ts b/src/Umbraco.Web.UI.Client/libs/context-api/consume/context-consumer.test.ts index bc0c5f2e1e..09a575938f 100644 --- a/src/Umbraco.Web.UI.Client/libs/context-api/consume/context-consumer.test.ts +++ b/src/Umbraco.Web.UI.Client/libs/context-api/consume/context-consumer.test.ts @@ -5,7 +5,7 @@ import { UmbContextRequestEventImplementation, umbContextRequestEventType } from const testContextAlias = 'my-test-context'; -class MyClass { +class UmbTestContextConsumerClass { prop = 'value from provider'; } @@ -39,16 +39,20 @@ describe('UmbContextConsumer', () => { }); it('works with UmbContextProvider', (done) => { - const provider = new UmbContextProvider(document.body, testContextAlias, new MyClass()); + const provider = new UmbContextProvider(document.body, testContextAlias, new UmbTestContextConsumerClass()); provider.hostConnected(); const element = document.createElement('div'); document.body.appendChild(element); - const localConsumer = new UmbContextConsumer(element, testContextAlias, (_instance: MyClass) => { - expect(_instance.prop).to.eq('value from provider'); - done(); - }); + const localConsumer = new UmbContextConsumer( + element, + testContextAlias, + (_instance: UmbTestContextConsumerClass) => { + expect(_instance.prop).to.eq('value from provider'); + done(); + } + ); localConsumer.hostConnected(); provider.hostDisconnected(); diff --git a/src/Umbraco.Web.UI.Client/libs/context-api/provide/context-provider.controller.test.ts b/src/Umbraco.Web.UI.Client/libs/context-api/provide/context-provider.controller.test.ts index 7946d8b30b..d09e43fb44 100644 --- a/src/Umbraco.Web.UI.Client/libs/context-api/provide/context-provider.controller.test.ts +++ b/src/Umbraco.Web.UI.Client/libs/context-api/provide/context-provider.controller.test.ts @@ -3,21 +3,21 @@ import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; import { UmbContextConsumer } from '../consume/context-consumer'; import { UmbContextProviderController } from './context-provider.controller'; -class MyClass { +class UmbTestContextProviderControllerClass { prop = 'value from provider'; } -class ControllerHostElement extends UmbLitElement {} -const controllerHostElement = defineCE(ControllerHostElement); +class UmbTestControllerHostElement extends UmbLitElement {} +const controllerHostElement = defineCE(UmbTestControllerHostElement); describe('UmbContextProviderController', () => { - let instance: MyClass; + let instance: UmbTestContextProviderControllerClass; let provider: UmbContextProviderController; let element: UmbLitElement; beforeEach(async () => { element = await fixture(`<${controllerHostElement}>`); - instance = new MyClass(); + instance = new UmbTestContextProviderControllerClass(); provider = new UmbContextProviderController(element, 'my-test-context', instance); }); @@ -39,11 +39,15 @@ describe('UmbContextProviderController', () => { }); it('works with UmbContextConsumer', (done) => { - const localConsumer = new UmbContextConsumer(element, 'my-test-context', (_instance: MyClass) => { - expect(_instance.prop).to.eq('value from provider'); - done(); - localConsumer.hostDisconnected(); - }); + const localConsumer = new UmbContextConsumer( + element, + 'my-test-context', + (_instance: UmbTestContextProviderControllerClass) => { + expect(_instance.prop).to.eq('value from provider'); + done(); + localConsumer.hostDisconnected(); + } + ); localConsumer.hostConnected(); }); diff --git a/src/Umbraco.Web.UI.Client/libs/context-api/provide/context-provider.test.ts b/src/Umbraco.Web.UI.Client/libs/context-api/provide/context-provider.test.ts index d39e026a2f..ce82bcb9b5 100644 --- a/src/Umbraco.Web.UI.Client/libs/context-api/provide/context-provider.test.ts +++ b/src/Umbraco.Web.UI.Client/libs/context-api/provide/context-provider.test.ts @@ -3,16 +3,16 @@ import { UmbContextConsumer } from '../consume/context-consumer'; import { UmbContextRequestEventImplementation } from '../consume/context-request.event'; import { UmbContextProvider } from './context-provider'; -class MyClass { +class UmbTestContextProviderClass { prop = 'value from provider'; } describe('UmbContextProvider', () => { - let instance: MyClass; + let instance: UmbTestContextProviderClass; let provider: UmbContextProvider; beforeEach(() => { - instance = new MyClass(); + instance = new UmbTestContextProviderClass(); provider = new UmbContextProvider(document.body, 'my-test-context', instance); provider.hostConnected(); }); @@ -40,10 +40,13 @@ describe('UmbContextProvider', () => { }); it('handles context request events', (done) => { - const event = new UmbContextRequestEventImplementation('my-test-context', (_instance: MyClass) => { - expect(_instance.prop).to.eq('value from provider'); - done(); - }); + const event = new UmbContextRequestEventImplementation( + 'my-test-context', + (_instance: UmbTestContextProviderClass) => { + expect(_instance.prop).to.eq('value from provider'); + done(); + } + ); document.body.dispatchEvent(event); }); @@ -52,11 +55,15 @@ describe('UmbContextProvider', () => { const element = document.createElement('div'); document.body.appendChild(element); - const localConsumer = new UmbContextConsumer(element, 'my-test-context', (_instance: MyClass) => { - expect(_instance.prop).to.eq('value from provider'); - done(); - localConsumer.hostDisconnected(); - }); + const localConsumer = new UmbContextConsumer( + element, + 'my-test-context', + (_instance: UmbTestContextProviderClass) => { + expect(_instance.prop).to.eq('value from provider'); + done(); + localConsumer.hostDisconnected(); + } + ); localConsumer.hostConnected(); }); }); diff --git a/src/Umbraco.Web.UI.Client/libs/context-api/token/context-token.test.ts b/src/Umbraco.Web.UI.Client/libs/context-api/token/context-token.test.ts index f8b0a2046d..01cdf69427 100644 --- a/src/Umbraco.Web.UI.Client/libs/context-api/token/context-token.test.ts +++ b/src/Umbraco.Web.UI.Client/libs/context-api/token/context-token.test.ts @@ -5,13 +5,13 @@ import { UmbContextToken } from './context-token'; const testContextAlias = 'my-test-context'; -class MyClass { +class UmbTestContextTokenClass { prop = 'value from provider'; } describe('ContextAlias', () => { - const contextAlias = new UmbContextToken(testContextAlias); - const typedProvider = new UmbContextProvider(document.body, contextAlias, new MyClass()); + const contextAlias = new UmbContextToken(testContextAlias); + const typedProvider = new UmbContextProvider(document.body, contextAlias, new UmbTestContextTokenClass()); typedProvider.hostConnected(); after(() => { @@ -27,7 +27,7 @@ describe('ContextAlias', () => { document.body.appendChild(element); const localConsumer = new UmbContextConsumer(element, contextAlias, (_instance) => { - expect(_instance).to.be.instanceOf(MyClass); + expect(_instance).to.be.instanceOf(UmbTestContextTokenClass); expect(_instance.prop).to.eq('value from provider'); done(); }); @@ -39,8 +39,8 @@ describe('ContextAlias', () => { const element = document.createElement('div'); document.body.appendChild(element); - const localConsumer = new UmbContextConsumer(element, testContextAlias, (_instance: MyClass) => { - expect(_instance).to.be.instanceOf(MyClass); + const localConsumer = new UmbContextConsumer(element, testContextAlias, (_instance: UmbTestContextTokenClass) => { + expect(_instance).to.be.instanceOf(UmbTestContextTokenClass); expect(_instance.prop).to.eq('value from provider'); done(); }); diff --git a/src/Umbraco.Web.UI.Client/libs/controller/controller.test.ts b/src/Umbraco.Web.UI.Client/libs/controller/controller.test.ts index 993bc546ca..deda6a09e1 100644 --- a/src/Umbraco.Web.UI.Client/libs/controller/controller.test.ts +++ b/src/Umbraco.Web.UI.Client/libs/controller/controller.test.ts @@ -3,18 +3,18 @@ import { customElement } from 'lit/decorators.js'; import { UmbControllerHostElement, UmbControllerHostMixin } from './controller-host.mixin'; import { UmbContextProviderController } from '@umbraco-cms/backoffice/context-api'; -class MyClass { +class UmbTestContext { prop = 'value from provider'; } @customElement('test-my-controller-host') -export class MyHostElement extends UmbControllerHostMixin(HTMLElement) {} +export class UmbTestControllerHostElement extends UmbControllerHostMixin(HTMLElement) {} describe('UmbContextProvider', () => { type NewType = UmbControllerHostElement; let hostElement: NewType; - const contextInstance = new MyClass(); + const contextInstance = new UmbTestContext(); beforeEach(() => { hostElement = document.createElement('test-my-controller-host') as UmbControllerHostElement; @@ -35,7 +35,7 @@ describe('UmbContextProvider', () => { describe('Unique controllers replace each other', () => { it('has a host property', () => { const firstCtrl = new UmbContextProviderController(hostElement, 'my-test-context', contextInstance); - const secondCtrl = new UmbContextProviderController(hostElement, 'my-test-context', new MyClass()); + const secondCtrl = new UmbContextProviderController(hostElement, 'my-test-context', new UmbTestContext()); expect(hostElement.hasController(firstCtrl)).to.be.false; expect(hostElement.hasController(secondCtrl)).to.be.true; diff --git a/src/Umbraco.Web.UI.Client/libs/entity-action/actions/delete-folder/delete-folder.action.ts b/src/Umbraco.Web.UI.Client/libs/entity-action/actions/delete-folder/delete-folder.action.ts index 9a2adadb0b..594d0be5f4 100644 --- a/src/Umbraco.Web.UI.Client/libs/entity-action/actions/delete-folder/delete-folder.action.ts +++ b/src/Umbraco.Web.UI.Client/libs/entity-action/actions/delete-folder/delete-folder.action.ts @@ -2,9 +2,10 @@ import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action'; import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN, UMB_CONFIRM_MODAL } from '@umbraco-cms/backoffice/modal'; +import { UmbFolderRepository, UmbItemRepository } from '@umbraco-cms/backoffice/repository'; export class UmbDeleteFolderEntityAction< - T extends { deleteFolder(unique: string): Promise; requestTreeItems(uniques: Array): any } + T extends UmbItemRepository & UmbFolderRepository > extends UmbEntityActionBase { #modalContext?: UmbModalContext; @@ -19,7 +20,7 @@ export class UmbDeleteFolderEntityAction< async execute() { if (!this.repository || !this.#modalContext) return; - const { data } = await this.repository.requestTreeItems([this.unique]); + const { data } = await this.repository.requestItems([this.unique]); if (data) { const item = data[0]; diff --git a/src/Umbraco.Web.UI.Client/libs/entity-action/actions/delete/delete.action.ts b/src/Umbraco.Web.UI.Client/libs/entity-action/actions/delete/delete.action.ts index d290a1f912..c53a418502 100644 --- a/src/Umbraco.Web.UI.Client/libs/entity-action/actions/delete/delete.action.ts +++ b/src/Umbraco.Web.UI.Client/libs/entity-action/actions/delete/delete.action.ts @@ -2,9 +2,10 @@ import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action'; import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN, UMB_CONFIRM_MODAL } from '@umbraco-cms/backoffice/modal'; +import { UmbDetailRepository, UmbItemRepository } from '@umbraco-cms/backoffice/repository'; export class UmbDeleteEntityAction< - T extends { delete(unique: string): Promise; requestTreeItems(uniques: Array): any } + T extends UmbDetailRepository & UmbItemRepository > extends UmbEntityActionBase { #modalContext?: UmbModalContext; @@ -19,7 +20,7 @@ export class UmbDeleteEntityAction< async execute() { if (!this.repository || !this.#modalContext) return; - const { data } = await this.repository.requestTreeItems([this.unique]); + const { data } = await this.repository.requestItems([this.unique]); if (data) { const item = data[0]; diff --git a/src/Umbraco.Web.UI.Client/libs/entity-action/actions/move/move.action.ts b/src/Umbraco.Web.UI.Client/libs/entity-action/actions/move/move.action.ts index 2744029e82..dd4a7f2905 100644 --- a/src/Umbraco.Web.UI.Client/libs/entity-action/actions/move/move.action.ts +++ b/src/Umbraco.Web.UI.Client/libs/entity-action/actions/move/move.action.ts @@ -1,6 +1,8 @@ import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; +// TODO: investigate what we need to finish the generic move action. We would need to open a picker, which requires a modal token, +// maybe we can use kinds to make a specific manifest to the move action. export class UmbMoveEntityAction }> extends UmbEntityActionBase { constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) { super(host, repositoryAlias, unique); diff --git a/src/Umbraco.Web.UI.Client/libs/entity-action/actions/trash/trash.action.ts b/src/Umbraco.Web.UI.Client/libs/entity-action/actions/trash/trash.action.ts index 2b86b4d56c..1ed2a69beb 100644 --- a/src/Umbraco.Web.UI.Client/libs/entity-action/actions/trash/trash.action.ts +++ b/src/Umbraco.Web.UI.Client/libs/entity-action/actions/trash/trash.action.ts @@ -2,9 +2,10 @@ import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action'; import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN, UMB_CONFIRM_MODAL } from '@umbraco-cms/backoffice/modal'; +import { UmbItemRepository } from '@umbraco-cms/backoffice/repository'; export class UmbTrashEntityAction< - T extends { trash(unique: Array): Promise; requestTreeItems(uniques: Array): any } + T extends UmbItemRepository & { trash(unique: Array): Promise } > extends UmbEntityActionBase { #modalContext?: UmbModalContext; @@ -19,7 +20,7 @@ export class UmbTrashEntityAction< async execute() { if (!this.repository) return; - const { data } = await this.repository.requestTreeItems([this.unique]); + const { data } = await this.repository.requestItems([this.unique]); if (data) { const item = data[0]; diff --git a/src/Umbraco.Web.UI.Client/libs/extensions-registry/collection-view.models.ts b/src/Umbraco.Web.UI.Client/libs/extensions-registry/collection-view.models.ts index 2eb599c1b8..823ca3bc21 100644 --- a/src/Umbraco.Web.UI.Client/libs/extensions-registry/collection-view.models.ts +++ b/src/Umbraco.Web.UI.Client/libs/extensions-registry/collection-view.models.ts @@ -6,8 +6,24 @@ export interface ManifestCollectionView extends ManifestElement, ManifestWithCon } export interface MetaCollectionView { + /** + * The friendly name of the collection view + */ label: string; + + /** + * An icon to represent the collection view + * + * @examples [ + * "umb:box", + * "umb:grid" + * ] + */ icon: string; + + /** + * The URL pathname for this collection view that can be deep linked to by sharing the url + */ pathName: string; } diff --git a/src/Umbraco.Web.UI.Client/libs/extensions-registry/dashboard.models.ts b/src/Umbraco.Web.UI.Client/libs/extensions-registry/dashboard.models.ts index cf40a2748c..033e554623 100644 --- a/src/Umbraco.Web.UI.Client/libs/extensions-registry/dashboard.models.ts +++ b/src/Umbraco.Web.UI.Client/libs/extensions-registry/dashboard.models.ts @@ -6,10 +6,34 @@ export interface ManifestDashboard extends ManifestElement, ManifestWithConditio } export interface MetaDashboard { + /** + * This is the URL path for the dashboard which is used for navigating or deep linking directly to the dashboard + * https://yoursite.com/section/settings/dashboard/my-dashboard-path + * + * @example my-dashboard-path + * @examples [ + * "my-dashboard-path" + * ] + */ pathname: string; + + /** + * The displayed name (label) for the tab of the dashboard + */ label?: string; } export interface ConditionsDashboard { + /** + * An array of section aliases that the dashboard should be available in + * + * @uniqueItems true + * @minItems 1 + * @items.examples [ + * "Umb.Section.Content", + * "Umb.Section.Settings" + * ] + * + */ sections: string[]; } diff --git a/src/Umbraco.Web.UI.Client/libs/extensions-registry/entity-action.models.ts b/src/Umbraco.Web.UI.Client/libs/extensions-registry/entity-action.models.ts index dbea8f833f..4816f084fd 100644 --- a/src/Umbraco.Web.UI.Client/libs/extensions-registry/entity-action.models.ts +++ b/src/Umbraco.Web.UI.Client/libs/extensions-registry/entity-action.models.ts @@ -1,5 +1,9 @@ import type { ManifestElement } from './models'; +/** + * An action to perform on an entity + * For example for content you may wish to create a new document etc + */ export interface ManifestEntityAction extends ManifestElement { type: 'entityAction'; meta: MetaEntityAction; @@ -7,9 +11,38 @@ export interface ManifestEntityAction extends ManifestElement { } export interface MetaEntityAction { + /** + * An icon to represent the action to be performed + * + * @examples [ + * "umb:box", + * "umb:grid" + * ] + */ icon?: string; + + /** + * The friendly name of the action to perform + * + * @examples [ + * "Create", + * "Create Content Template" + * ] + */ label: string; + + /** + * @TJS-ignore + */ api: any; // create interface + + /** + * The alias for the repsoitory of the entity type this action is for + * such as 'Umb.Repository.Documents' + * @examples [ + * "Umb.Repository.Documents" + * ] + */ repositoryAlias: string; } diff --git a/src/Umbraco.Web.UI.Client/libs/extensions-registry/entity-bulk-action.models.ts b/src/Umbraco.Web.UI.Client/libs/extensions-registry/entity-bulk-action.models.ts index b7bd23a997..5564bb5b5d 100644 --- a/src/Umbraco.Web.UI.Client/libs/extensions-registry/entity-bulk-action.models.ts +++ b/src/Umbraco.Web.UI.Client/libs/extensions-registry/entity-bulk-action.models.ts @@ -1,13 +1,33 @@ import type { ManifestElement, ManifestWithConditions } from './models'; +/** + * An action to perform on multiple entities + * For example for content you may wish to move one or more documents in bulk + */ export interface ManifestEntityBulkAction extends ManifestElement, ManifestWithConditions { type: 'entityBulkAction'; meta: MetaEntityBulkAction; } export interface MetaEntityBulkAction { + /** + * A friendly label for the action + */ label: string; + + /** + * @TJS-ignore + */ api: any; // create interface + + /** + * The alias for the repsoitory of the entity type this action is for + * such as 'Umb.Repository.Documents' + * + * @examples [ + * "Umb.Repository.Documents" + * ] + */ repositoryAlias: string; } diff --git a/src/Umbraco.Web.UI.Client/libs/extensions-registry/header-app.models.ts b/src/Umbraco.Web.UI.Client/libs/extensions-registry/header-app.models.ts index 4834a8d9bf..e6e990c591 100644 --- a/src/Umbraco.Web.UI.Client/libs/extensions-registry/header-app.models.ts +++ b/src/Umbraco.Web.UI.Client/libs/extensions-registry/header-app.models.ts @@ -1,10 +1,15 @@ import type { ManifestElement } from './models'; +/** + * Header apps are displayed in the top right corner of the backoffice + * The two provided header apps are the search and the user menu + */ export interface ManifestHeaderApp extends ManifestElement { type: 'headerApp'; //meta: MetaHeaderApp; } +// TODO: Warren these don't seem to be used anywhere export interface MetaHeaderApp { pathname: string; label: string; diff --git a/src/Umbraco.Web.UI.Client/libs/extensions-registry/models.ts b/src/Umbraco.Web.UI.Client/libs/extensions-registry/models.ts index b1ccb5111b..67116df1b7 100644 --- a/src/Umbraco.Web.UI.Client/libs/extensions-registry/models.ts +++ b/src/Umbraco.Web.UI.Client/libs/extensions-registry/models.ts @@ -25,7 +25,7 @@ import type { ManifestWorkspaceView } from './workspace-view.models'; import type { ManifestWorkspaceViewCollection } from './workspace-view-collection.models'; import type { ManifestRepository } from './repository.models'; import type { ManifestModal } from './modal.models'; -import type { ManifestStore, ManifestTreeStore } from './store.models'; +import type { ManifestStore, ManifestTreeStore, ManifestItemStore } from './store.models'; import type { ClassConstructor } from '@umbraco-cms/backoffice/models'; export * from './collection-view.models'; @@ -92,6 +92,7 @@ export type ManifestTypes = | ManifestModal | ManifestStore | ManifestTreeStore + | ManifestItemStore | ManifestBase; export type ManifestStandardTypes = ManifestTypes['type']; @@ -104,10 +105,31 @@ export type SpecificManifestTypeOrManifestBase { + /** + * Set the conditions for when the extension should be loaded + */ conditions: ConditionsType; } export interface ManifestWithLoader extends ManifestBase { + /** + * @TJS-ignore + */ loader?: () => Promise; } +/** + * The type of extension such as dashboard etc... + */ export interface ManifestClass extends ManifestWithLoader { //type: ManifestStandardTypes; + + /** + * The file location of the javascript file to load + * @TJS-required + */ js?: string; + + /** + * @TJS-ignore + */ className?: string; + + /** + * @TJS-ignore + */ class?: ClassConstructor; //loader?: () => Promise; } @@ -141,10 +185,26 @@ export interface ManifestClassWithClassConstructor extends ManifestClass { export interface ManifestElement extends ManifestWithLoader { //type: ManifestStandardTypes; + + /** + * The file location of the javascript file to load + * + * @TJS-require + */ js?: string; + + /** + * The HTML web component name to use such as 'my-dashboard' + * Note it is NOT but just the name + */ elementName?: string; + //loader?: () => Promise; - meta?: any; + + /** + * This contains properties specific to the type of extension + */ + meta?: unknown; } export interface ManifestWithView extends ManifestElement { @@ -158,22 +218,29 @@ export interface MetaManifestWithView { } export interface ManifestElementWithElementName extends ManifestElement { + /** + * The HTML web component name to use such as 'my-dashboard' + * Note it is NOT but just the name + */ elementName: string; } -// TODO: Remove Custom as it has no purpose currently: -/* -export interface ManifestCustom extends ManifestBase { - type: 'custom'; - meta?: unknown; -} -*/ - export interface ManifestWithMeta extends ManifestBase { + /** + * This contains properties specific to the type of extension + */ meta: unknown; } +/** + * This type of extension gives full control and will simply load the specified JS file + * You could have custom logic to decide which extensions to load/register by using extensionRegistry + */ export interface ManifestEntrypoint extends ManifestBase { type: 'entrypoint'; + + /** + * The file location of the javascript file to load in the backoffice + */ js: string; } diff --git a/src/Umbraco.Web.UI.Client/libs/extensions-registry/store.models.ts b/src/Umbraco.Web.UI.Client/libs/extensions-registry/store.models.ts index 18e8c10c17..dbca5d6956 100644 --- a/src/Umbraco.Web.UI.Client/libs/extensions-registry/store.models.ts +++ b/src/Umbraco.Web.UI.Client/libs/extensions-registry/store.models.ts @@ -1,5 +1,5 @@ import type { ManifestClass } from './models'; -import { UmbStoreBase, UmbTreeStore } from '@umbraco-cms/backoffice/store'; +import { UmbItemStore, UmbStoreBase, UmbTreeStore } from '@umbraco-cms/backoffice/store'; export interface ManifestStore extends ManifestClass { type: 'store'; @@ -8,3 +8,7 @@ export interface ManifestStore extends ManifestClass { export interface ManifestTreeStore extends ManifestClass { type: 'treeStore'; } + +export interface ManifestItemStore extends ManifestClass { + type: 'itemStore'; +} diff --git a/src/Umbraco.Web.UI.Client/libs/extensions-registry/theme.models.ts b/src/Umbraco.Web.UI.Client/libs/extensions-registry/theme.models.ts index 81b84ee0bb..0e18a0164a 100644 --- a/src/Umbraco.Web.UI.Client/libs/extensions-registry/theme.models.ts +++ b/src/Umbraco.Web.UI.Client/libs/extensions-registry/theme.models.ts @@ -1,7 +1,17 @@ import type { ManifestWithLoader } from './models'; // TODO: make or find type for JS Module with default export: Would be nice to support css file directly. + +/** + * Theme manifest for styling the backoffice of Umbraco such as dark, high contrast etc + */ export interface ManifestTheme extends ManifestWithLoader { type: 'theme'; + + /** + * File location of the CSS file of the theme + * + * @examples ["themes/dark.theme.css"] + */ css?: string; } diff --git a/src/Umbraco.Web.UI.Client/libs/extensions-registry/umbraco-package.ts b/src/Umbraco.Web.UI.Client/libs/extensions-registry/umbraco-package.ts new file mode 100644 index 0000000000..c594bada93 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/extensions-registry/umbraco-package.ts @@ -0,0 +1,28 @@ +import type { ManifestTypes } from './models'; + +/** + * Umbraco package manifest JSON + */ +export class UmbracoPackage { + /** + * @title The name of the Umbraco package + */ + name?: string; + + /** + * @title The version of the Umbraco package in the style of semver + * @examples ["0.1.0"] + */ + version?: string; + + /** + * @title Decides if the package sends telemetry data for collection + * @default true + */ + allowTelemetry?: boolean; + + /** + * @title An array of Umbraco package manifest types that will be installed + */ + extensions?: ManifestTypes[]; +} diff --git a/src/Umbraco.Web.UI.Client/libs/extensions-registry/workspace-action.models.ts b/src/Umbraco.Web.UI.Client/libs/extensions-registry/workspace-action.models.ts index d55d4f4f89..2cb7595388 100644 --- a/src/Umbraco.Web.UI.Client/libs/extensions-registry/workspace-action.models.ts +++ b/src/Umbraco.Web.UI.Client/libs/extensions-registry/workspace-action.models.ts @@ -1,6 +1,5 @@ import type { InterfaceColor, InterfaceLook } from '@umbraco-ui/uui-base/lib/types/index'; import type { ManifestElement } from './models'; -import { UmbWorkspaceAction } from '@umbraco-cms/backoffice/workspace'; import type { ClassConstructor } from '@umbraco-cms/backoffice/models'; export interface ManifestWorkspaceAction extends ManifestElement { @@ -13,7 +12,7 @@ export interface MetaWorkspaceAction { label?: string; //TODO: Use or implement additional label-key look?: InterfaceLook; color?: InterfaceColor; - api: ClassConstructor; + api: ClassConstructor; } export interface ConditionsWorkspaceAction { diff --git a/src/Umbraco.Web.UI.Client/libs/modal/token/data-type-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/libs/modal/token/data-type-picker-modal.token.ts new file mode 100644 index 0000000000..51d90eb871 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/modal/token/data-type-picker-modal.token.ts @@ -0,0 +1,18 @@ +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; + +export interface UmbDataTypePickerModalData { + selection?: Array; + multiple?: boolean; +} + +export interface UmbDataTypePickerModalResult { + selection: Array; +} + +export const UMB_DATA_TYPE_PICKER_MODAL = new UmbModalToken( + 'Umb.Modal.DataTypePicker', + { + type: 'sidebar', + size: 'small', + } +); diff --git a/src/Umbraco.Web.UI.Client/libs/modal/token/icon-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/libs/modal/token/icon-picker-modal.token.ts index 0ae41a9fbb..e144bf70e4 100644 --- a/src/Umbraco.Web.UI.Client/libs/modal/token/icon-picker-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/libs/modal/token/icon-picker-modal.token.ts @@ -1,8 +1,8 @@ import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; export interface UmbIconPickerModalData { - multiple: boolean; - selection: string[]; + color: string | undefined; + icon: string | undefined; } export interface UmbIconPickerModalResult { diff --git a/src/Umbraco.Web.UI.Client/libs/modal/token/index.ts b/src/Umbraco.Web.UI.Client/libs/modal/token/index.ts index fda2d066e5..fdd391d1ff 100644 --- a/src/Umbraco.Web.UI.Client/libs/modal/token/index.ts +++ b/src/Umbraco.Web.UI.Client/libs/modal/token/index.ts @@ -27,3 +27,4 @@ export * from './user-group-picker-modal.token'; export * from './user-picker-modal.token'; export * from './code-editor-modal.token'; export * from './folder-modal.token'; +export * from './data-type-picker-modal.token'; diff --git a/src/Umbraco.Web.UI.Client/libs/observable-api/append-to-frozen-array.function.ts b/src/Umbraco.Web.UI.Client/libs/observable-api/append-to-frozen-array.function.ts index 97ff56848c..23510e754c 100644 --- a/src/Umbraco.Web.UI.Client/libs/observable-api/append-to-frozen-array.function.ts +++ b/src/Umbraco.Web.UI.Client/libs/observable-api/append-to-frozen-array.function.ts @@ -5,7 +5,7 @@ * @param {(mappable: T) => R} mappingFunction - Method to return the part for this Observable to return. * @param {(previousResult: R, currentResult: R) => boolean} [memoizationFunction] - Method to Compare if the data has changed. Should return true when data is different. * @description - Creates a RxJS Observable from RxJS Subject. - * @example Example append new entry for a ArrayState or a part of DeepState/ObjectState it which is an array. Where the key is unique and the item will be updated if matched with existing. + * @example Example append new entry for a ArrayState or a part of UmbDeepState/UmbObjectState it which is an array. Where the key is unique and the item will be updated if matched with existing. * const entry = {id: 'myKey', value: 'myValue'}; * const newDataSet = appendToFrozenArray(mySubject.getValue(), entry, x => x.id === id); * mySubject.next(newDataSet); diff --git a/src/Umbraco.Web.UI.Client/libs/observable-api/array-state.test.ts b/src/Umbraco.Web.UI.Client/libs/observable-api/array-state.test.ts index 854ec3ee92..1a22dae157 100644 --- a/src/Umbraco.Web.UI.Client/libs/observable-api/array-state.test.ts +++ b/src/Umbraco.Web.UI.Client/libs/observable-api/array-state.test.ts @@ -1,11 +1,11 @@ import { expect } from '@open-wc/testing'; -import { ArrayState } from './array-state'; +import { UmbArrayState } from './array-state'; describe('ArrayState', () => { type ObjectType = { key: string; another: string }; type ArrayType = ObjectType[]; - let subject: ArrayState; + let subject: UmbArrayState; let initialData: ArrayType; beforeEach(() => { @@ -14,7 +14,7 @@ describe('ArrayState', () => { { key: '2', another: 'myValue2' }, { key: '3', another: 'myValue3' }, ]; - subject = new ArrayState(initialData, (x) => x.key); + subject = new UmbArrayState(initialData, (x) => x.key); }); it('replays latests, no matter the amount of subscriptions.', (done) => { diff --git a/src/Umbraco.Web.UI.Client/libs/observable-api/array-state.ts b/src/Umbraco.Web.UI.Client/libs/observable-api/array-state.ts index 7572f89d8b..a4f67b2708 100644 --- a/src/Umbraco.Web.UI.Client/libs/observable-api/array-state.ts +++ b/src/Umbraco.Web.UI.Client/libs/observable-api/array-state.ts @@ -1,17 +1,17 @@ -import { DeepState } from './deep-state'; +import { UmbDeepState } from './deep-state'; import { partialUpdateFrozenArray } from './partial-update-frozen-array.function'; import { pushToUniqueArray } from './push-to-unique-array.function'; /** * @export - * @class ArrayState - * @extends {DeepState} + * @class UmbArrayState + * @extends {UmbDeepState} * @description - A RxJS BehaviorSubject which deepFreezes the object-data to ensure its not manipulated from any implementations. * Additionally the Subject ensures the data is unique, not updating any Observes unless there is an actual change of the content. * * The ArrayState provides methods to append data when the data is an Object. */ -export class ArrayState extends DeepState { +export class UmbArrayState extends UmbDeepState { #getUnique?: (entry: T) => unknown; #sortMethod?: (a: T, b: T) => number; @@ -29,7 +29,7 @@ export class ArrayState extends DeepState { * { key: 1, value: 'foo'}, * { key: 2, value: 'bar'} * ]; - * const myState = new ArrayState(data, (x) => x.key); + * const myState = new UmbArrayState(data, (x) => x.key); * myState.sortBy((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0)); */ sortBy(sortMethod?: (a: T, b: T) => number) { @@ -48,14 +48,14 @@ export class ArrayState extends DeepState { /** * @method remove * @param {unknown[]} uniques - The unique values to remove. - * @return {ArrayState} Reference to it self. + * @return {UmbArrayState} Reference to it self. * @description - Remove some new data of this Subject. * @example Example remove entry with id '1' and '2' * const data = [ * { id: 1, value: 'foo'}, * { id: 2, value: 'bar'} * ]; - * const myState = new ArrayState(data, (x) => x.id); + * const myState = new UmbArrayState(data, (x) => x.id); * myState.remove([1, 2]); */ remove(uniques: unknown[]) { @@ -77,14 +77,14 @@ export class ArrayState extends DeepState { /** * @method removeOne * @param {unknown} unique - The unique value to remove. - * @return {ArrayState} Reference to it self. + * @return {UmbArrayState} Reference to it self. * @description - Remove some new data of this Subject. * @example Example remove entry with id '1' * const data = [ * { id: 1, value: 'foo'}, * { id: 2, value: 'bar'} * ]; - * const myState = new ArrayState(data, (x) => x.id); + * const myState = new UmbArrayState(data, (x) => x.id); * myState.removeOne(1); */ removeOne(unique: unknown) { @@ -104,7 +104,7 @@ export class ArrayState extends DeepState { /** * @method filter * @param {unknown} filterMethod - The unique value to remove. - * @return {ArrayState} Reference to it self. + * @return {UmbArrayState} Reference to it self. * @description - Remove some new data of this Subject. * @example Example remove entry with key '1' * const data = [ @@ -112,7 +112,7 @@ export class ArrayState extends DeepState { * { key: 2, value: 'bar'}, * { key: 3, value: 'poo'} * ]; - * const myState = new ArrayState(data, (x) => x.key); + * const myState = new UmbArrayState(data, (x) => x.key); * myState.filter((entry) => entry.key !== 1); * * Result: @@ -130,14 +130,14 @@ export class ArrayState extends DeepState { /** * @method appendOne * @param {T} entry - new data to be added in this Subject. - * @return {ArrayState} Reference to it self. + * @return {UmbArrayState} Reference to it self. * @description - Append some new data to this Subject. * @example Example append some data. * const data = [ * { key: 1, value: 'foo'}, * { key: 2, value: 'bar'} * ]; - * const myState = new ArrayState(data); + * const myState = new UmbArrayState(data); * myState.append({ key: 1, value: 'replaced-foo'}); */ appendOne(entry: T) { @@ -154,14 +154,14 @@ export class ArrayState extends DeepState { /** * @method append * @param {T[]} entries - A array of new data to be added in this Subject. - * @return {ArrayState} Reference to it self. + * @return {UmbArrayState} Reference to it self. * @description - Append some new data to this Subject, if it compares to existing data it will replace it. * @example Example append some data. * const data = [ * { key: 1, value: 'foo'}, * { key: 2, value: 'bar'} * ]; - * const myState = new ArrayState(data); + * const myState = new UmbArrayState(data); * myState.append([ * { key: 1, value: 'replaced-foo'}, * { key: 3, value: 'another-bla'} @@ -184,14 +184,14 @@ export class ArrayState extends DeepState { * @method updateOne * @param {unknown} unique - Unique value to find entry to update. * @param {Partial} entry - new data to be added in this Subject. - * @return {ArrayState} Reference to it self. + * @return {UmbArrayState} Reference to it self. * @description - Update a item with some new data, requires the ArrayState to be constructed with a getUnique method. * @example Example append some data. * const data = [ * { key: 1, value: 'foo'}, * { key: 2, value: 'bar'} * ]; - * const myState = new ArrayState(data, (x) => x.key); + * const myState = new UmbArrayState(data, (x) => x.key); * myState.updateOne(2, {value: 'updated-bar'}); */ updateOne(unique: unknown, entry: Partial) { diff --git a/src/Umbraco.Web.UI.Client/libs/observable-api/basic-state.ts b/src/Umbraco.Web.UI.Client/libs/observable-api/basic-state.ts index 57d7f8581b..30972d0ef4 100644 --- a/src/Umbraco.Web.UI.Client/libs/observable-api/basic-state.ts +++ b/src/Umbraco.Web.UI.Client/libs/observable-api/basic-state.ts @@ -2,11 +2,11 @@ import { BehaviorSubject } from 'rxjs'; /** * @export - * @class BasicState + * @class UmbBasicState * @extends {BehaviorSubject} * @description - A RxJS BehaviorSubject this Subject ensures the data is unique, not updating any Observes unless there is an actual change of the value. */ -export class BasicState extends BehaviorSubject { +export class UmbBasicState extends BehaviorSubject { constructor(initialData: T) { super(initialData); } diff --git a/src/Umbraco.Web.UI.Client/libs/observable-api/boolean-state.ts b/src/Umbraco.Web.UI.Client/libs/observable-api/boolean-state.ts index 5404d43651..85a91d895d 100644 --- a/src/Umbraco.Web.UI.Client/libs/observable-api/boolean-state.ts +++ b/src/Umbraco.Web.UI.Client/libs/observable-api/boolean-state.ts @@ -1,12 +1,12 @@ -import { BasicState } from './basic-state'; +import { UmbBasicState } from './basic-state'; /** * @export - * @class BooleanState + * @class UmbBooleanState * @extends {BehaviorSubject} * @description - A RxJS BehaviorSubject this Subject ensures the data is unique, not updating any Observes unless there is an actual change of the value. */ -export class BooleanState extends BasicState { +export class UmbBooleanState extends UmbBasicState { constructor(initialData: T | boolean) { super(initialData); } diff --git a/src/Umbraco.Web.UI.Client/libs/observable-api/class-state.ts b/src/Umbraco.Web.UI.Client/libs/observable-api/class-state.ts index 11c38dc6b9..586710e38b 100644 --- a/src/Umbraco.Web.UI.Client/libs/observable-api/class-state.ts +++ b/src/Umbraco.Web.UI.Client/libs/observable-api/class-state.ts @@ -1,16 +1,16 @@ import { BehaviorSubject } from 'rxjs'; -interface ClassStateData { - equal(otherClass: ClassStateData): boolean; +interface UmbClassStateData { + equal(otherClass: UmbClassStateData): boolean; } /** * @export - * @class ClassState + * @class UmbClassState * @extends {BehaviorSubject} * @description - A RxJS BehaviorSubject which can hold class instance which has a equal method to compare in coming instances for changes. */ -export class ClassState extends BehaviorSubject { +export class UmbClassState extends BehaviorSubject { constructor(initialData: T) { super(initialData); } diff --git a/src/Umbraco.Web.UI.Client/libs/observable-api/deep-state.test.ts b/src/Umbraco.Web.UI.Client/libs/observable-api/deep-state.test.ts index f605564749..f1971dfa40 100644 --- a/src/Umbraco.Web.UI.Client/libs/observable-api/deep-state.test.ts +++ b/src/Umbraco.Web.UI.Client/libs/observable-api/deep-state.test.ts @@ -1,30 +1,27 @@ import { expect } from '@open-wc/testing'; -import { DeepState } from './deep-state'; +import { UmbDeepState } from './deep-state'; -describe('DeepState', () => { +describe('UmbDeepState', () => { + type ObjectType = { key: string; another: string }; - type ObjectType = {key: string, another: string}; - - let subject: DeepState; + let subject: UmbDeepState; let initialData: ObjectType; beforeEach(() => { - initialData = {key: 'some', another: 'myValue'}; - subject = new DeepState(initialData); + initialData = { key: 'some', another: 'myValue' }; + subject = new UmbDeepState(initialData); }); - it('getValue gives the initial data', () => { expect(subject.value.another).to.be.equal(initialData.another); }); it('update via next', () => { - subject.next({key: 'some', another: 'myNewValue'}); + subject.next({ key: 'some', another: 'myNewValue' }); expect(subject.value.another).to.be.equal('myNewValue'); }); it('replays latests, no matter the amount of subscriptions.', (done) => { - const observer = subject.asObservable(); observer.subscribe((value) => { expect(value).to.be.equal(initialData); @@ -33,28 +30,24 @@ describe('DeepState', () => { expect(value).to.be.equal(initialData); done(); }); - }); it('use gObservablePart, updates on its specific change.', (done) => { - let amountOfCallbacks = 0; - const subObserver = subject.getObservablePart(data => data.another); + const subObserver = subject.getObservablePart((data) => data.another); subObserver.subscribe((value) => { amountOfCallbacks++; - if(amountOfCallbacks === 1) { + if (amountOfCallbacks === 1) { expect(value).to.be.equal('myValue'); } - if(amountOfCallbacks === 2) { + if (amountOfCallbacks === 2) { expect(value).to.be.equal('myNewValue'); done(); } }); - subject.next({key: 'change_this_first_should_not_trigger_update', another: 'myValue'}); - subject.next({key: 'some', another: 'myNewValue'}); - + subject.next({ key: 'change_this_first_should_not_trigger_update', another: 'myValue' }); + subject.next({ key: 'some', another: 'myNewValue' }); }); - }); diff --git a/src/Umbraco.Web.UI.Client/libs/observable-api/deep-state.ts b/src/Umbraco.Web.UI.Client/libs/observable-api/deep-state.ts index 201ab479c8..c67daa927c 100644 --- a/src/Umbraco.Web.UI.Client/libs/observable-api/deep-state.ts +++ b/src/Umbraco.Web.UI.Client/libs/observable-api/deep-state.ts @@ -7,12 +7,12 @@ import { naiveObjectComparison } from './naive-object-comparison'; /** * @export - * @class DeepState + * @class UmbDeepState * @extends {BehaviorSubject} * @description - A RxJS BehaviorSubject which deepFreezes the data to ensure its not manipulated from any implementations. * Additionally the Subject ensures the data is unique, not updating any Observes unless there is an actual change of the content. */ -export class DeepState extends BehaviorSubject { +export class UmbDeepState extends BehaviorSubject { constructor(initialData: T) { super(deepFreeze(initialData)); } diff --git a/src/Umbraco.Web.UI.Client/libs/observable-api/filter-frozen-array.function.ts b/src/Umbraco.Web.UI.Client/libs/observable-api/filter-frozen-array.function.ts new file mode 100644 index 0000000000..7d3e5a2e0c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/observable-api/filter-frozen-array.function.ts @@ -0,0 +1,13 @@ +/** + * @export + * @method filterFrozenArray + * @param {Array} data - RxJS Subject to use for this Observable. + * @param {(entry: T) => boolean} filterMethod - Method to filter the array. + * @description - Creates a RxJS Observable from RxJS Subject. + * @example Example remove an entry of a ArrayState or a part of DeepState/ObjectState it which is an array. Where the key is unique and the item will be updated if matched with existing. + * const newDataSet = filterFrozenArray(mySubject.getValue(), x => x.id !== "myKey"); + * mySubject.next(newDataSet); + */ +export function filterFrozenArray(data: T[], filterMethod: (entry: T) => boolean): T[] { + return [...data].filter((x) => filterMethod(x)); +} diff --git a/src/Umbraco.Web.UI.Client/libs/observable-api/index.ts b/src/Umbraco.Web.UI.Client/libs/observable-api/index.ts index c18d4495b7..e0edfe4be9 100644 --- a/src/Umbraco.Web.UI.Client/libs/observable-api/index.ts +++ b/src/Umbraco.Web.UI.Client/libs/observable-api/index.ts @@ -10,5 +10,6 @@ export * from './array-state'; export * from './object-state'; export * from './create-observable-part.function'; export * from './append-to-frozen-array.function'; +export * from './filter-frozen-array.function'; export * from './partial-update-frozen-array.function'; export * from './mapping-function'; diff --git a/src/Umbraco.Web.UI.Client/libs/observable-api/number-state.ts b/src/Umbraco.Web.UI.Client/libs/observable-api/number-state.ts index ded3defeb9..5556d8956c 100644 --- a/src/Umbraco.Web.UI.Client/libs/observable-api/number-state.ts +++ b/src/Umbraco.Web.UI.Client/libs/observable-api/number-state.ts @@ -1,12 +1,12 @@ -import { BasicState } from './basic-state'; +import { UmbBasicState } from './basic-state'; /** * @export - * @class NumberState + * @class UmbNumberState * @extends {BehaviorSubject} * @description - A RxJS BehaviorSubject this Subject ensures the data is unique, not updating any Observes unless there is an actual change of the value. */ -export class NumberState extends BasicState { +export class UmbNumberState extends UmbBasicState { constructor(initialData: T | number) { super(initialData); } diff --git a/src/Umbraco.Web.UI.Client/libs/observable-api/object-state.test.ts b/src/Umbraco.Web.UI.Client/libs/observable-api/object-state.test.ts index 66b1e0deef..75b4d3bbe2 100644 --- a/src/Umbraco.Web.UI.Client/libs/observable-api/object-state.test.ts +++ b/src/Umbraco.Web.UI.Client/libs/observable-api/object-state.test.ts @@ -1,21 +1,18 @@ import { expect } from '@open-wc/testing'; -import { ObjectState } from './object-state'; +import { UmbObjectState } from './object-state'; -describe('ObjectState', () => { +describe('UmbObjectState', () => { + type ObjectType = { key: string; another: string }; - type ObjectType = {key: string, another: string}; - - let subject: ObjectState; + let subject: UmbObjectState; let initialData: ObjectType; beforeEach(() => { - initialData = {key: 'some', another: 'myValue'}; - subject = new ObjectState(initialData); + initialData = { key: 'some', another: 'myValue' }; + subject = new UmbObjectState(initialData); }); - it('replays latests, no matter the amount of subscriptions.', (done) => { - const observer = subject.asObservable(); observer.subscribe((value) => { expect(value).to.be.equal(initialData); @@ -24,28 +21,24 @@ describe('ObjectState', () => { expect(value).to.be.equal(initialData); done(); }); - }); it('use getObservablePart, updates on its specific change.', (done) => { - let amountOfCallbacks = 0; - const subObserver = subject.getObservablePart(data => data.another); + const subObserver = subject.getObservablePart((data) => data.another); subObserver.subscribe((value) => { amountOfCallbacks++; - if(amountOfCallbacks === 1) { + if (amountOfCallbacks === 1) { expect(value).to.be.equal('myValue'); } - if(amountOfCallbacks === 2) { + if (amountOfCallbacks === 2) { expect(value).to.be.equal('myNewValue'); done(); } }); - subject.update({key: 'change_this_first_should_not_trigger_update'}); - subject.update({another: 'myNewValue'}); - + subject.update({ key: 'change_this_first_should_not_trigger_update' }); + subject.update({ another: 'myNewValue' }); }); - }); diff --git a/src/Umbraco.Web.UI.Client/libs/observable-api/object-state.ts b/src/Umbraco.Web.UI.Client/libs/observable-api/object-state.ts index 04c72dd9b3..6c4b32a115 100644 --- a/src/Umbraco.Web.UI.Client/libs/observable-api/object-state.ts +++ b/src/Umbraco.Web.UI.Client/libs/observable-api/object-state.ts @@ -1,23 +1,23 @@ -import { DeepState } from './deep-state'; +import { UmbDeepState } from './deep-state'; /** * @export - * @class ObjectState - * @extends {DeepState} + * @class UmbObjectState + * @extends {UmbDeepState} * @description - A RxJS BehaviorSubject which deepFreezes the object-data to ensure its not manipulated from any implementations. * Additionally the Subject ensures the data is unique, not updating any Observes unless there is an actual change of the content. * - * The ObjectState provides methods to append data when the data is an Object. + * The UmbObjectState provides methods to append data when the data is an Object. */ -export class ObjectState extends DeepState { +export class UmbObjectState extends UmbDeepState { /** * @method update * @param {Partial} partialData - A object containing some of the data to update in this Subject. * @description - Append some new data to this Object. - * @return {ObjectState} Reference to it self. + * @return {UmbObjectState} Reference to it self. * @example Example append some data. * const data = {key: 'myKey', value: 'myInitialValue'}; - * const myState = new ObjectState(data); + * const myState = new UmbObjectState(data); * myState.update({value: 'myNewValue'}); */ update(partialData: Partial) { diff --git a/src/Umbraco.Web.UI.Client/libs/observable-api/partial-update-frozen-array.function.ts b/src/Umbraco.Web.UI.Client/libs/observable-api/partial-update-frozen-array.function.ts index 14a2e4c5b6..b19c322594 100644 --- a/src/Umbraco.Web.UI.Client/libs/observable-api/partial-update-frozen-array.function.ts +++ b/src/Umbraco.Web.UI.Client/libs/observable-api/partial-update-frozen-array.function.ts @@ -5,7 +5,7 @@ * @param {(mappable: T) => R} mappingFunction - Method to return the part for this Observable to return. * @param {(previousResult: R, currentResult: R) => boolean} [memoizationFunction] - Method to Compare if the data has changed. Should return true when data is different. * @description - Creates a RxJS Observable from RxJS Subject. - * @example Example append new entry for a ArrayState or a part of DeepState/ObjectState it which is an array. Where the key is unique and the item will be updated if matched with existing. + * @example Example append new entry for a ArrayState or a part of UmbDeepState/UmbObjectState it which is an array. Where the key is unique and the item will be updated if matched with existing. * const partialEntry = {value: 'myValue'}; * const newDataSet = partialUpdateFrozenArray(mySubject.getValue(), partialEntry, x => x.key === 'myKey'); * mySubject.next(newDataSet); diff --git a/src/Umbraco.Web.UI.Client/libs/observable-api/string-state.ts b/src/Umbraco.Web.UI.Client/libs/observable-api/string-state.ts index d3e9689a7b..632c28e1b1 100644 --- a/src/Umbraco.Web.UI.Client/libs/observable-api/string-state.ts +++ b/src/Umbraco.Web.UI.Client/libs/observable-api/string-state.ts @@ -1,12 +1,12 @@ -import { BasicState } from './basic-state'; +import { UmbBasicState } from './basic-state'; /** * @export - * @class StringState - * @extends {BasicState} + * @class UmbStringState + * @extends {UmbBasicState} * @description - A RxJS BehaviorSubject this Subject ensures the data is unique, not updating any Observes unless there is an actual change of the value. */ -export class StringState extends BasicState { +export class UmbStringState extends UmbBasicState { constructor(initialData: T | string) { super(initialData); } diff --git a/src/Umbraco.Web.UI.Client/libs/picker-input/index.ts b/src/Umbraco.Web.UI.Client/libs/picker-input/index.ts new file mode 100644 index 0000000000..74b8745179 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/picker-input/index.ts @@ -0,0 +1 @@ +export * from './picker-input.context'; diff --git a/src/Umbraco.Web.UI.Client/libs/picker-input/picker-input.context.ts b/src/Umbraco.Web.UI.Client/libs/picker-input/picker-input.context.ts new file mode 100644 index 0000000000..3c8bae91c2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/picker-input/picker-input.context.ts @@ -0,0 +1,136 @@ +import { UmbItemRepository } from '@umbraco-cms/backoffice/repository'; +import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; +import { UmbArrayState, UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; +import { createExtensionClass, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extensions-api'; +import { + UMB_CONFIRM_MODAL, + UMB_MODAL_CONTEXT_TOKEN, + UmbModalContext, + UmbModalToken, +} from '@umbraco-cms/backoffice/modal'; +import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; +import { ItemResponseModelBaseModel } from '@umbraco-cms/backoffice/backend-api'; +import { UmbChangeEvent } from '@umbraco-cms/backoffice/events'; + +export class UmbPickerInputContext { + host: UmbControllerHostElement; + modalAlias: string | UmbModalToken; + repository?: UmbItemRepository; + #getUnique: (entry: ItemType) => string | undefined; + + public modalContext?: UmbModalContext; + + #selection = new UmbArrayState([]); + selection = this.#selection.asObservable(); + + #selectedItems = new UmbArrayState([]); + selectedItems = this.#selectedItems.asObservable(); + + #selectedItemsObserver?: UmbObserverController; + + max = Infinity; + min = 0; + + /* TODO: find a better way to have a getUniqueMethod. If we want to support trees/items of different types, + then it need to be bound to the type and can't be a generic method we pass in. */ + constructor( + host: UmbControllerHostElement, + repositoryAlias: string, + modalAlias: string | UmbModalToken, + getUniqueMethod?: (entry: ItemType) => string | undefined + ) { + this.host = host; + this.modalAlias = modalAlias; + this.#getUnique = getUniqueMethod || ((entry) => entry.id || ''); + + // TODO: unsure a method can't be called before everything is initialized + new UmbObserverController( + this.host, + + // TODO: this code is reused in multiple places, so it should be extracted to a function + umbExtensionsRegistry.getByTypeAndAlias('repository', repositoryAlias), + async (repositoryManifest) => { + if (!repositoryManifest) return; + + try { + const result = await createExtensionClass>(repositoryManifest, [this.host]); + this.repository = result; + } catch (error) { + throw new Error('Could not create repository with alias: ' + repositoryAlias + ''); + } + } + ); + + new UmbContextConsumerController(this.host, UMB_MODAL_CONTEXT_TOKEN, (instance) => { + this.modalContext = instance; + }); + } + + getSelection() { + return this.#selection.value; + } + + setSelection(selection: string[]) { + this.#selection.next(selection); + } + + // TODO: revisit this method. How do we best pass picker data? + // If modalAlias is a ModalToken, then via TS, we should get the correct type for pickerData. Otherwise fallback to unknown. + openPicker(pickerData?: any) { + if (!this.modalContext) throw new Error('Modal context is not initialized'); + + const modalHandler = this.modalContext.open(this.modalAlias, { + multiple: this.max === 1 ? false : true, + selection: [...this.getSelection()], + ...pickerData, + }); + + modalHandler?.onSubmit().then(({ selection }: any) => { + this.setSelection(selection); + this.host.dispatchEvent(new UmbChangeEvent()); + // TODO: we only want to request items that are not already in the selectedItems array + this.#requestItems(); + }); + } + + async requestRemoveItem(unique: string) { + if (!this.repository) throw new Error('Repository is not initialized'); + + // TODO: id won't always be available on the model, so we need to get the unique property from somewhere. Maybe the repository? + const item = this.#selectedItems.value.find((item) => this.#getUnique(item) === unique); + if (!item) throw new Error('Could not find item with unique: ' + unique); + + const modalHandler = this.modalContext?.open(UMB_CONFIRM_MODAL, { + color: 'danger', + headline: `Remove ${item.name}?`, + content: 'Are you sure you want to remove this item', + confirmLabel: 'Remove', + }); + + await modalHandler?.onSubmit(); + this.#removeItem(unique); + } + + async #requestItems() { + if (!this.repository) throw new Error('Repository is not initialized'); + if (this.#selectedItemsObserver) this.#selectedItemsObserver.destroy(); + + const { asObservable } = await this.repository.requestItems(this.getSelection()); + + if (asObservable) { + this.#selectedItemsObserver = new UmbObserverController(this.host, asObservable(), (data) => + this.#selectedItems.next(data) + ); + } + } + + #removeItem(unique: string) { + const newSelection = this.getSelection().filter((value) => value !== unique); + this.setSelection(newSelection); + + // remove items items from selectedItems array + // TODO: id won't always be available on the model, so we need to get the unique property from somewhere. Maybe the repository? + const newSelectedItems = this.#selectedItems.value.filter((item) => this.#getUnique(item) !== unique); + this.#selectedItems.next(newSelectedItems); + } +} diff --git a/src/Umbraco.Web.UI.Client/libs/repository/copy-repository.interface.ts b/src/Umbraco.Web.UI.Client/libs/repository/copy-repository.interface.ts new file mode 100644 index 0000000000..f9621e0c30 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/repository/copy-repository.interface.ts @@ -0,0 +1,5 @@ +import { UmbRepositoryResponse } from './detail-repository.interface'; + +export interface UmbCopyRepository { + copy(unique: string, targetUnique: string): Promise>; +} diff --git a/src/Umbraco.Web.UI.Client/libs/repository/data-source/copy-data-source.interface.ts b/src/Umbraco.Web.UI.Client/libs/repository/data-source/copy-data-source.interface.ts new file mode 100644 index 0000000000..b9d41d7971 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/repository/data-source/copy-data-source.interface.ts @@ -0,0 +1,5 @@ +import type { DataSourceResponse } from '@umbraco-cms/backoffice/repository'; + +export interface UmbCopyDataSource { + copy(unique: string, targetUnique: string): Promise>; +} diff --git a/src/Umbraco.Web.UI.Client/libs/repository/data-source/data-source-response.interface.ts b/src/Umbraco.Web.UI.Client/libs/repository/data-source/data-source-response.interface.ts index a3ee6e90f0..2360da75a8 100644 --- a/src/Umbraco.Web.UI.Client/libs/repository/data-source/data-source-response.interface.ts +++ b/src/Umbraco.Web.UI.Client/libs/repository/data-source/data-source-response.interface.ts @@ -1,6 +1,9 @@ import type { ProblemDetailsModel } from '@umbraco-cms/backoffice/backend-api'; -export interface DataSourceResponse { +export interface DataSourceResponse extends UmbDataSourceErrorResponse { data?: T; +} + +export interface UmbDataSourceErrorResponse { error?: ProblemDetailsModel; } diff --git a/src/Umbraco.Web.UI.Client/libs/repository/data-source/index.ts b/src/Umbraco.Web.UI.Client/libs/repository/data-source/index.ts index 905a6849fe..08d765462a 100644 --- a/src/Umbraco.Web.UI.Client/libs/repository/data-source/index.ts +++ b/src/Umbraco.Web.UI.Client/libs/repository/data-source/index.ts @@ -2,3 +2,6 @@ export * from './data-source-response.interface'; export * from './data-source.interface'; export * from './folder-data-source.interface'; export * from './tree-data-source.interface'; +export * from './item-data-source.interface'; +export * from './move-data-source.interface'; +export * from './copy-data-source.interface'; diff --git a/src/Umbraco.Web.UI.Client/libs/repository/data-source/item-data-source.interface.ts b/src/Umbraco.Web.UI.Client/libs/repository/data-source/item-data-source.interface.ts new file mode 100644 index 0000000000..88fb741ee3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/repository/data-source/item-data-source.interface.ts @@ -0,0 +1,5 @@ +import type { DataSourceResponse } from '@umbraco-cms/backoffice/repository'; + +export interface UmbItemDataSource { + getItems(unique: Array): Promise>>; +} diff --git a/src/Umbraco.Web.UI.Client/libs/repository/data-source/move-data-source.interface.ts b/src/Umbraco.Web.UI.Client/libs/repository/data-source/move-data-source.interface.ts new file mode 100644 index 0000000000..c0639e31ce --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/repository/data-source/move-data-source.interface.ts @@ -0,0 +1,5 @@ +import type { UmbDataSourceErrorResponse } from '@umbraco-cms/backoffice/repository'; + +export interface UmbMoveDataSource { + move(unique: string, targetUnique: string): Promise; +} diff --git a/src/Umbraco.Web.UI.Client/libs/repository/data-source/tree-data-source.interface.ts b/src/Umbraco.Web.UI.Client/libs/repository/data-source/tree-data-source.interface.ts index ae57ef57af..52f623e13b 100644 --- a/src/Umbraco.Web.UI.Client/libs/repository/data-source/tree-data-source.interface.ts +++ b/src/Umbraco.Web.UI.Client/libs/repository/data-source/tree-data-source.interface.ts @@ -3,5 +3,7 @@ import type { DataSourceResponse } from '@umbraco-cms/backoffice/repository'; export interface UmbTreeDataSource { getRootItems(): Promise>; getChildrenOf(parentUnique: string): Promise>; + + // TODO: remove this when all repositories are migrated to the new items interface getItems(unique: Array): Promise>>; } diff --git a/src/Umbraco.Web.UI.Client/libs/repository/index.ts b/src/Umbraco.Web.UI.Client/libs/repository/index.ts index 9dcba782ba..ba407c2083 100644 --- a/src/Umbraco.Web.UI.Client/libs/repository/index.ts +++ b/src/Umbraco.Web.UI.Client/libs/repository/index.ts @@ -2,3 +2,6 @@ export * from './data-source'; export * from './detail-repository.interface'; export * from './tree-repository.interface'; export * from './folder-repository.interface'; +export * from './item-repository.interface'; +export * from './move-repository.interface'; +export * from './copy-repository.interface'; diff --git a/src/Umbraco.Web.UI.Client/libs/repository/item-repository.interface.ts b/src/Umbraco.Web.UI.Client/libs/repository/item-repository.interface.ts new file mode 100644 index 0000000000..3f92cb70e8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/repository/item-repository.interface.ts @@ -0,0 +1,11 @@ +import type { Observable } from 'rxjs'; +import { ItemResponseModelBaseModel, ProblemDetailsModel } from '@umbraco-cms/backoffice/backend-api'; + +export interface UmbItemRepository { + requestItems: (uniques: string[]) => Promise<{ + data?: Array | undefined; + error?: ProblemDetailsModel | undefined; + asObservable?: () => Observable>; + }>; + items: (uniques: string[]) => Promise>>; +} diff --git a/src/Umbraco.Web.UI.Client/libs/repository/move-repository.interface.ts b/src/Umbraco.Web.UI.Client/libs/repository/move-repository.interface.ts new file mode 100644 index 0000000000..f8e5889300 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/repository/move-repository.interface.ts @@ -0,0 +1,5 @@ +import { UmbRepositoryErrorResponse } from './detail-repository.interface'; + +export interface UmbMoveRepository { + move(unique: string, targetUnique: string): Promise; +} diff --git a/src/Umbraco.Web.UI.Client/libs/repository/tree-repository.interface.ts b/src/Umbraco.Web.UI.Client/libs/repository/tree-repository.interface.ts index 4ef19c3920..17ddc1a2d4 100644 --- a/src/Umbraco.Web.UI.Client/libs/repository/tree-repository.interface.ts +++ b/src/Umbraco.Web.UI.Client/libs/repository/tree-repository.interface.ts @@ -17,7 +17,9 @@ export interface UmbTreeRepository Observable; }>; - requestTreeItems: (uniques: string[]) => Promise<{ + + // TODO: remove this when all repositories are migrated to the new interface items interface + requestItemsLegacy?: (uniques: string[]) => Promise<{ data: Array | undefined; error: ProblemDetailsModel | undefined; asObservable?: () => Observable; @@ -25,5 +27,7 @@ export interface UmbTreeRepository Promise>; treeItemsOf: (parentUnique: string | null) => Promise>; - treeItems: (uniques: string[]) => Promise>; + + // TODO: remove this when all repositories are migrated to the new items interface + itemsLegacy?: (uniques: string[]) => Promise>; } diff --git a/src/Umbraco.Web.UI.Client/libs/router/index.ts b/src/Umbraco.Web.UI.Client/libs/router/index.ts index 3cfb67ff4a..7bfd8cff7a 100644 --- a/src/Umbraco.Web.UI.Client/libs/router/index.ts +++ b/src/Umbraco.Web.UI.Client/libs/router/index.ts @@ -1,4 +1,4 @@ -export type * from 'router-slot/model'; +export * from 'router-slot/model'; export * from 'router-slot/util'; export * from './route-location.interface'; export * from './route.context'; diff --git a/src/Umbraco.Web.UI.Client/libs/sorter/index.ts b/src/Umbraco.Web.UI.Client/libs/sorter/index.ts new file mode 100644 index 0000000000..c3024038f4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/sorter/index.ts @@ -0,0 +1 @@ +export * from './sorter.controller'; diff --git a/src/Umbraco.Web.UI.Client/libs/sorter/sorter.angular.txt b/src/Umbraco.Web.UI.Client/libs/sorter/sorter.angular.txt new file mode 100644 index 0000000000..a46a4a6c01 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/sorter/sorter.angular.txt @@ -0,0 +1,685 @@ +(function () { + 'use strict'; + + function isWithinRect(x, y, rect, modifier) { + return ( + x > rect.left - modifier && x < rect.right + modifier && y > rect.top - modifier && y < rect.bottom + modifier + ); + } + + function getParentScrollElement(el, includeSelf) { + // skip to window + if (!el || !el.getBoundingClientRect) return null; + var elem = el; + var gotSelf = false; + + while (elem) { + // we don't need to get elem css if it isn't even overflowing in the first place (performance) + if (elem.clientWidth < elem.scrollWidth || elem.clientHeight < elem.scrollHeight) { + var elemCSS = getComputedStyle(elem); + + if ( + (elem.clientHeight < elem.scrollHeight && (elemCSS.overflowY == 'auto' || elemCSS.overflowY == 'scroll')) || + (elem.clientWidth < elem.scrollWidth && (elemCSS.overflowX == 'auto' || elemCSS.overflowX == 'scroll')) + ) { + if (!elem.getBoundingClientRect || elem === document.body) return null; + if (gotSelf || includeSelf) return elem; + gotSelf = true; + } + } + + if (elem.parentNode === document) { + return null; + } else if (elem.parentNode instanceof DocumentFragment) { + elem = elem.parentNode.host; + } else { + elem = elem.parentNode; + } + } + + return null; + } + + const DefaultConfig = { + compareElementToModel: (el, modelEntry) => modelEntry.contentUdi === el.dataset.elementUdi, + querySelectModelToElement: (container, modelEntry) => + container.querySelector(`[data-element-udi='${modelEntry.contentUdi}']`), + identifier: 'UmbBlockGridSorter', + containerSelector: 'ol', // To find container and to connect with others. + ignorerSelector: 'a, img, iframe', + itemSelector: 'li', + placeholderClass: 'umb-drag-placeholder', + }; + + function UmbBlockGridSorter() { + function link(scope, element) { + let observer = new MutationObserver(function (mutations) { + mutations.forEach(function (mutation) { + mutation.addedNodes.forEach(function (addedNode) { + if (addedNode.matches && addedNode.matches(scope.config.itemSelector)) { + setupItem(addedNode); + } + }); + mutation.removedNodes.forEach(function (removedNode) { + if (removedNode.matches && removedNode.matches(scope.config.itemSelector)) { + destroyItem(removedNode); + } + }); + }); + }); + + let vm = {}; + + const config = { ...DefaultConfig, ...scope.config }; + + vm.identifier = config.identifier; + vm.ownerVM = config.ownerVM || null; + + let scrollElement = null; + + let containerEl = config.containerSelector ? element[0].closest(config.containerSelector) : element[0]; + if (!containerEl) { + console.error('Could not initialize umb block grid sorter.', element[0]); + return; + } + + function init() { + containerEl['umbBlockGridSorter:vm'] = () => { + return vm; + }; + containerEl.addEventListener('dragover', preventDragOver); + + observer.observe(containerEl, { childList: true, subtree: false }); + } + init(); + + function preventDragOver(e) { + e.preventDefault(); + } + + function setupItem(element) { + setupIgnorerElements(element); + + element.draggable = true; + element.addEventListener('dragstart', handleDragStart); + } + + function destroyItem(element) { + destroyIgnorerElements(element); + + element.removeEventListener('dragstart', handleDragStart); + } + + function setupIgnorerElements(element) { + config.ignorerSelector.split(',').forEach(function (criteria) { + element.querySelectorAll(criteria.trim()).forEach(setupPreventEvent); + }); + } + function destroyIgnorerElements(element) { + config.ignorerSelector.split(',').forEach(function (criteria) { + element.querySelectorAll(criteria.trim()).forEach(destroyPreventEvent); + }); + } + function setupPreventEvent(element) { + element.draggable = false; + } + function destroyPreventEvent(element) { + element.removeAttribute('draggable'); + } + + let currentContainerElement = containerEl; + let currentContainerVM = vm; + + let rqaId = null; + let currentItem = null; + let currentElement = null; + let currentDragElement = null; + let currentDragRect = null; + let dragX = 0; + let dragY = 0; + + function handleDragStart(event) { + if (currentElement) { + handleDragEnd(); + } + + event.stopPropagation(); + event.dataTransfer.effectAllowed = 'move'; // copyMove when we enhance the drag with clipboard data. + event.dataTransfer.dropEffect = 'none'; // visual feedback when dropped. + + if (!scrollElement) { + scrollElement = getParentScrollElement(containerEl, true); + } + + const element = event.target.closest(config.itemSelector); + + currentElement = element; + currentDragElement = config.draggableSelector + ? currentElement.querySelector(config.draggableSelector) + : currentElement; + currentDragRect = currentDragElement.getBoundingClientRect(); + currentItem = vm.getItemOfElement(currentElement); + if (!currentItem) { + console.error('Could not find item related to this element.'); + return; + } + + currentElement.style.transform = 'translateZ(0)'; // Solves problem with FireFox and ShadowDom in the drag-image. + + if (config.dataTransferResolver) { + config.dataTransferResolver(event.dataTransfer, currentItem); + } + + if (config.onStart) { + config.onStart({ item: currentItem, element: currentElement }); + } + + window.addEventListener('dragover', handleDragMove); + window.addEventListener('dragend', handleDragEnd); + + // We must wait one frame before changing the look of the block. + rqaId = requestAnimationFrame(() => { + // It should be okay to use the same refId, as the move does not or is okay not to happen on first frame/drag-move. + rqaId = null; + currentElement.style.transform = ''; + currentElement.classList.add(config.placeholderClass); + }); + } + + function handleDragEnd() { + if (!currentElement) { + return; + } + + window.removeEventListener('dragover', handleDragMove); + window.removeEventListener('dragend', handleDragEnd); + currentElement.style.transform = ''; + currentElement.classList.remove(config.placeholderClass); + + stopAutoScroll(); + removeAllowIndication(); + + if (currentContainerVM.sync(currentElement, vm) === false) { + // Sync could not succeed, might be because item is not allowed here. + + currentContainerVM = vm; + if (config.onContainerChange) { + config.onContainerChange({ + item: currentItem, + element: currentElement, + ownerVM: currentContainerVM.ownerVM, + }); + } + + // Lets move the Element back to where it came from: + const movingItemIndex = scope.model.indexOf(currentItem); + if (movingItemIndex < scope.model.length - 1) { + const afterItem = scope.model[movingItemIndex + 1]; + const afterEl = config.querySelectModelToElement(containerEl, afterItem); + containerEl.insertBefore(currentElement, afterEl); + } else { + containerEl.appendChild(currentElement); + } + } + + if (config.onEnd) { + config.onEnd({ item: currentItem, element: currentElement }); + } + + if (rqaId) { + cancelAnimationFrame(rqaId); + } + + currentContainerElement = containerEl; + currentContainerVM = vm; + + rqaId = null; + currentItem = null; + currentElement = null; + currentDragElement = null; + currentDragRect = null; + dragX = 0; + dragY = 0; + } + + function handleDragMove(event) { + if (!currentElement) { + return; + } + + const clientX = (event.touches ? event.touches[0] : event).clientX; + const clientY = (event.touches ? event.touches[1] : event).clientY; + if (clientX !== 0 && clientY !== 0) { + if (dragX === clientX && dragY === clientY) { + return; + } + dragX = clientX; + dragY = clientY; + + handleAutoScroll(dragX, dragY); + + currentDragRect = currentDragElement.getBoundingClientRect(); + const insideCurrentRect = isWithinRect(dragX, dragY, currentDragRect, 0); + if (!insideCurrentRect) { + if (rqaId === null) { + rqaId = requestAnimationFrame(moveCurrentElement); + } + } + } + } + + function moveCurrentElement() { + rqaId = null; + if (!currentElement) { + return; + } + + const currentElementRect = currentElement.getBoundingClientRect(); + const insideCurrentRect = isWithinRect(dragX, dragY, currentElementRect); + if (insideCurrentRect) { + return; + } + + // If we have a boundarySelector, try it, if we didn't get anything fall back to currentContainerElement. + var currentBoundaryElement = + (config.boundarySelector + ? currentContainerElement.closest(config.boundarySelector) + : currentContainerElement) || currentContainerElement; + + var currentBoundaryRect = currentBoundaryElement.getBoundingClientRect(); + + const currentContainerHasItems = currentContainerVM.hasOtherItemsThan(currentItem); + + // if empty we will be move likely to accept an item (add 20px to the bounding box) + // If we have items we must be 10 within the container to accept the move. + const offsetEdge = currentContainerHasItems ? -10 : 20; + if (!isWithinRect(dragX, dragY, currentBoundaryRect, offsetEdge)) { + // we are outside the current container boundary, so lets see if there is a parent we can move. + var parentContainer = currentContainerElement.parentNode.closest(config.containerSelector); + if (parentContainer) { + const parentContainerVM = parentContainer['umbBlockGridSorter:vm'](); + if (parentContainerVM.identifier === vm.identifier) { + currentContainerElement = parentContainer; + currentContainerVM = parentContainerVM; + if (config.onContainerChange) { + config.onContainerChange({ + item: currentItem, + element: currentElement, + ownerVM: currentContainerVM.ownerVM, + }); + } + } + } + } + + // We want to retrieve the children of the container, every time to ensure we got the right order and index + const orderedContainerElements = Array.from(currentContainerElement.children); + + var currentContainerRect = currentContainerElement.getBoundingClientRect(); + + // gather elements on the same row. + let elementsInSameRow = []; + let placeholderIsInThisRow = false; + for (const el of orderedContainerElements) { + const elRect = el.getBoundingClientRect(); + // gather elements on the same row. + if (dragY >= elRect.top && dragY <= elRect.bottom) { + const dragElement = config.draggableSelector ? el.querySelector(config.draggableSelector) : el; + const dragElementRect = dragElement.getBoundingClientRect(); + if (el !== currentElement) { + elementsInSameRow.push({ el: el, dragRect: dragElementRect }); + } else { + placeholderIsInThisRow = true; + } + } + } + + let lastDistance = 99999; + let foundEl = null; + let foundElDragRect = null; + let placeAfter = false; + elementsInSameRow.forEach((sameRow) => { + const centerX = sameRow.dragRect.left + sameRow.dragRect.width * 0.5; + let distance = Math.abs(dragX - centerX); + if (distance < lastDistance) { + foundEl = sameRow.el; + foundElDragRect = sameRow.dragRect; + lastDistance = Math.abs(distance); + placeAfter = dragX > centerX; + } + }); + + // If we are on top or closest to our self, we should not do anything. + if (foundEl === currentElement) { + return; + } + + if (foundEl) { + const isInsideFound = isWithinRect(dragX, dragY, foundElDragRect, 0); + + // If we are inside the found element, lets look for sub containers. + // use the itemHasNestedContainersResolver, if not configured fallback to looking for the existence of a container via DOM. + if ( + isInsideFound && config.itemHasNestedContainersResolver + ? config.itemHasNestedContainersResolver(foundEl) + : foundEl.querySelector(config.containerSelector) + ) { + // Find all sub containers: + const subLayouts = foundEl.querySelectorAll(config.containerSelector); + for (const subLayoutEl of subLayouts) { + // Use boundary element or fallback to container element. + var subBoundaryElement = + (config.boundarySelector ? subLayoutEl.closest(config.boundarySelector) : subLayoutEl) || subLayoutEl; + var subBoundaryRect = subBoundaryElement.getBoundingClientRect(); + + const subContainerHasItems = subLayoutEl.querySelector( + config.itemSelector + ':not(.' + config.placeholderClass + ')' + ); + // gather elements on the same row. + const subOffsetEdge = subContainerHasItems ? -10 : 20; + if (isWithinRect(dragX, dragY, subBoundaryRect, subOffsetEdge)) { + var subVm = subLayoutEl['umbBlockGridSorter:vm'](); + if (subVm.identifier === vm.identifier) { + currentContainerElement = subLayoutEl; + currentContainerVM = subVm; + if (config.onContainerChange) { + config.onContainerChange({ + item: currentItem, + element: currentElement, + ownerVM: currentContainerVM.ownerVM, + }); + } + moveCurrentElement(); + return; + } + } + } + } + + // Indication if drop is good: + if (updateAllowIndication(currentContainerVM, currentItem) === false) { + return; + } + + let verticalDirection = scope.config.resolveVerticalDirection + ? scope.config.resolveVerticalDirection({ + containerElement: currentContainerElement, + containerRect: currentContainerRect, + item: currentItem, + element: currentElement, + elementRect: currentElementRect, + relatedElement: foundEl, + relatedRect: foundElDragRect, + placeholderIsInThisRow: placeholderIsInThisRow, + horizontalPlaceAfter: placeAfter, + }) + : true; + + if (verticalDirection) { + placeAfter = dragY > foundElDragRect.top + foundElDragRect.height * 0.5; + } + + if (verticalDirection) { + let el; + if (placeAfter === false) { + let lastLeft = foundElDragRect.left; + elementsInSameRow.findIndex((x) => { + if (x.dragRect.left < lastLeft) { + lastLeft = x.dragRect.left; + el = x.el; + } + }); + } else { + let lastRight = foundElDragRect.right; + elementsInSameRow.findIndex((x) => { + if (x.dragRect.right > lastRight) { + lastRight = x.dragRect.right; + el = x.el; + } + }); + } + if (el) { + foundEl = el; + } + } + + const foundElIndex = orderedContainerElements.indexOf(foundEl); + const placeAt = placeAfter ? foundElIndex + 1 : foundElIndex; + + move(orderedContainerElements, placeAt); + + return; + } + // We skipped the above part cause we are above or below container: + + // Indication if drop is good: + if (updateAllowIndication(currentContainerVM, currentItem) === false) { + return; + } + + if (dragY < currentContainerRect.top) { + move(orderedContainerElements, 0); + } else if (dragY > currentContainerRect.bottom) { + move(orderedContainerElements, -1); + } + } + + function move(orderedContainerElements, newElIndex) { + newElIndex = newElIndex === -1 ? orderedContainerElements.length : newElIndex; + + const placeBeforeElement = orderedContainerElements[newElIndex]; + if (placeBeforeElement) { + // We do not need to move this, if the element to be placed before is it self. + if (placeBeforeElement !== currentElement) { + currentContainerElement.insertBefore(currentElement, placeBeforeElement); + } + } else { + currentContainerElement.appendChild(currentElement); + } + + if (config.onChange) { + config.onChange({ element: currentElement, item: currentItem, ownerVM: currentContainerVM.ownerVM }); + } + } + + /** Removes an element from container and returns its items-data entry */ + vm.getItemOfElement = function (element) { + if (!element) { + return null; + } + return scope.model.find((entry) => config.compareElementToModel(element, entry)); + }; + vm.removeItem = function (item) { + if (!item) { + return null; + } + const oldIndex = scope.model.indexOf(item); + if (oldIndex !== -1) { + return scope.model.splice(oldIndex, 1)[0]; + } + return null; + }; + + vm.hasOtherItemsThan = function (item) { + return scope.model.filter((x) => x !== item).length > 0; + }; + + vm.sync = function (element, fromVm) { + const movingItem = fromVm.getItemOfElement(element); + if (!movingItem) { + console.error('Could not find item of sync item'); + return false; + } + if (vm.notifyRequestDrop({ item: movingItem }) === false) { + return false; + } + if (fromVm.removeItem(movingItem) === null) { + console.error('Sync could not remove item'); + return false; + } + + /** Find next element, to then find the index of that element in items-data, to use as a safe reference to where the item will go in our items-data. + * This enables the container to contain various other elements and as well having these elements change while sorting is occurring. + */ + + // find next valid element (This assumes the next element in DOM is presented in items-data, aka. only moving one item between each sync) + let nextEl; + let loopEl = element; + while ((loopEl = loopEl.nextElementSibling)) { + if (loopEl.matches && loopEl.matches(config.itemSelector)) { + nextEl = loopEl; + break; + } + } + + let newIndex = scope.model.length; + if (nextEl) { + // We had a reference element, we want to get the index of it. + // This is problem if a item is being moved forward? + newIndex = scope.model.findIndex((entry) => config.compareElementToModel(nextEl, entry)); + } + + scope.model.splice(newIndex, 0, movingItem); + + const eventData = { item: movingItem, fromController: fromVm, toController: vm }; + if (fromVm !== vm) { + fromVm.notifySync(eventData); + } + vm.notifySync(eventData); + + return true; + }; + + var _lastIndicationContainerVM = null; + function updateAllowIndication(contextVM, item) { + // Remove old indication: + if (_lastIndicationContainerVM !== null && _lastIndicationContainerVM !== contextVM) { + _lastIndicationContainerVM.notifyAllowed(); + } + _lastIndicationContainerVM = contextVM; + + if (contextVM.notifyRequestDrop({ item: item }) === true) { + contextVM.notifyAllowed(); + return true; + } + + contextVM.notifyDisallowed(); // This block is not accepted to we will indicate that its not allowed. + return false; + } + function removeAllowIndication() { + // Remove old indication: + if (_lastIndicationContainerVM !== null) { + _lastIndicationContainerVM.notifyAllowed(); + } + _lastIndicationContainerVM = null; + } + + let autoScrollRAF; + let autoScrollEl; + const autoScrollSensitivity = 50; + const autoScrollSpeed = 16; + let autoScrollX = 0; + let autoScrollY = 0; + + function handleAutoScroll(clientX, clientY) { + let scrollRect = null; + if (scrollElement) { + autoScrollEl = scrollElement; + scrollRect = scrollElement.getBoundingClientRect(); + } else { + autoScrollEl = document.scrollingElement || document.documentElement; + scrollRect = { + top: 0, + left: 0, + bottom: window.innerHeight, + right: window.innerWidth, + height: window.innerHeight, + width: window.innerWidth, + }; + } + + const scrollWidth = autoScrollEl.scrollWidth; + const scrollHeight = autoScrollEl.scrollHeight; + const canScrollX = scrollRect.width < scrollWidth; + const canScrollY = scrollRect.height < scrollHeight; + const scrollPosX = autoScrollEl.scrollLeft; + const scrollPosY = autoScrollEl.scrollTop; + + cancelAnimationFrame(autoScrollRAF); + + if (canScrollX || canScrollY) { + autoScrollX = + (Math.abs(scrollRect.right - clientX) <= autoScrollSensitivity && + scrollPosX + scrollRect.width < scrollWidth) - + (Math.abs(scrollRect.left - clientX) <= autoScrollSensitivity && !!scrollPosX); + autoScrollY = + (Math.abs(scrollRect.bottom - clientY) <= autoScrollSensitivity && + scrollPosY + scrollRect.height < scrollHeight) - + (Math.abs(scrollRect.top - clientY) <= autoScrollSensitivity && !!scrollPosY); + autoScrollRAF = requestAnimationFrame(performAutoScroll); + } + } + function performAutoScroll() { + autoScrollEl.scrollLeft += autoScrollX * autoScrollSpeed; + autoScrollEl.scrollTop += autoScrollY * autoScrollSpeed; + autoScrollRAF = requestAnimationFrame(performAutoScroll); + } + function stopAutoScroll() { + cancelAnimationFrame(autoScrollRAF); + autoScrollRAF = null; + } + + vm.notifySync = function (data) { + if (config.onSync) { + config.onSync(data); + } + }; + vm.notifyDisallowed = function () { + if (config.onDisallowed) { + config.onDisallowed(); + } + }; + vm.notifyAllowed = function () { + if (config.onAllowed) { + config.onAllowed(); + } + }; + vm.notifyRequestDrop = function (data) { + if (config.onRequestDrop) { + return config.onRequestDrop(data); + } + return true; + }; + + scope.$on('$destroy', () => { + if (currentElement) { + handleDragEnd(); + } + + _lastIndicationContainerVM = null; + + containerEl['umbBlockGridSorter:vm'] = null; + containerEl.removeEventListener('dragover', preventDragOver); + + observer.disconnect(); + observer = null; + containerEl = null; + scrollElement = null; + vm = null; + }); + } + + var directive = { + restrict: 'A', + scope: { + config: '=umbBlockGridSorter', + model: '=umbBlockGridSorterModel', + }, + link: link, + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbBlockGridSorter', UmbBlockGridSorter); +})(); diff --git a/src/Umbraco.Web.UI.Client/libs/sorter/sorter.controller.test.ts b/src/Umbraco.Web.UI.Client/libs/sorter/sorter.controller.test.ts new file mode 100644 index 0000000000..99aaa06f40 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/sorter/sorter.controller.test.ts @@ -0,0 +1,60 @@ +import { expect, fixture, html } from '@open-wc/testing'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; +import { customElement } from 'lit/decorators.js'; +import { UmbSorterConfig, UmbSorterController } from './sorter.controller'; + +type SortEntryType = { + id: string; + value: string; +}; + +const sorterConfig: UmbSorterConfig = { + compareElementToModel: (element: HTMLElement, model: SortEntryType) => { + return element.getAttribute('id') === model.id; + }, + querySelectModelToElement: (container: HTMLElement, modelEntry: SortEntryType) => { + return container.querySelector('data-sort-entry-id[' + modelEntry.id + ']'); + }, + identifier: 'test-sorter', + itemSelector: 'li', +}; + +const model: Array = [ + { + id: '0', + value: 'Entry 0', + }, + { + id: '1', + value: 'Entry 1', + }, + { + id: '2', + value: 'Entry 2', + }, +]; + +@customElement('test-my-sorter-controller') +class UmbTestSorterControllerElement extends UmbLitElement { + public sorter; + + constructor() { + super(); + + this.sorter = new UmbSorterController(this, sorterConfig); + this.sorter.setModel(model); + } +} + +describe('UmbContextConsumer', () => { + let hostElement: UmbTestSorterControllerElement; + + beforeEach(async () => { + hostElement = await fixture(html` `); + }); + + // TODO: Testing ideas: + // - Test that the model is updated correctly? + // - Test that the DOM is updated correctly? + // - Use the controller to sort the DOM and test that the model is updated correctly... +}); diff --git a/src/Umbraco.Web.UI.Client/libs/sorter/sorter.controller.ts b/src/Umbraco.Web.UI.Client/libs/sorter/sorter.controller.ts new file mode 100644 index 0000000000..86a3288439 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/sorter/sorter.controller.ts @@ -0,0 +1,838 @@ +import { UmbControllerInterface, UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; + +const autoScrollSensitivity = 50; +const autoScrollSpeed = 16; + +function isWithinRect(x: number, y: number, rect: DOMRect, modifier = 0) { + return x > rect.left - modifier && x < rect.right + modifier && y > rect.top - modifier && y < rect.bottom + modifier; +} + +function getParentScrollElement(el: Element, includeSelf: boolean) { + if (!el || !el.getBoundingClientRect) return null; + + let elem = el; + let gotSelf = false; + + while (elem) { + // we don't need to get elem css if it isn't even overflowing in the first place (performance) + if (elem.clientWidth < elem.scrollWidth || elem.clientHeight < elem.scrollHeight) { + const elemCSS = getComputedStyle(elem); + + if ( + (elem.clientHeight < elem.scrollHeight && (elemCSS.overflowY == 'auto' || elemCSS.overflowY == 'scroll')) || + (elem.clientWidth < elem.scrollWidth && (elemCSS.overflowX == 'auto' || elemCSS.overflowX == 'scroll')) + ) { + if (!elem.getBoundingClientRect || elem === document.body) return null; + if (gotSelf || includeSelf) return elem; + gotSelf = true; + } + } + + if (elem.parentNode === document) { + return null; + } else if (elem.parentNode instanceof ShadowRoot) { + elem = elem.parentNode.host; + } else { + elem = elem.parentNode as Element; + } + } + + return null; +} + +function preventDragOver(e: Event) { + e.preventDefault(); +} + +function setupIgnorerElements(element: HTMLElement, ignorerSelectors: string) { + ignorerSelectors.split(',').forEach(function (criteria) { + element.querySelectorAll(criteria.trim()).forEach(setupPreventEvent); + }); +} +function destroyIgnorerElements(element: HTMLElement, ignorerSelectors: string) { + ignorerSelectors.split(',').forEach(function (criteria: string) { + element.querySelectorAll(criteria.trim()).forEach(destroyPreventEvent); + }); +} +function setupPreventEvent(element: Element) { + (element as HTMLElement).draggable = false; +} +function destroyPreventEvent(element: Element) { + element.removeAttribute('draggable'); +} + +type INTERNAL_UmbSorterConfig = { + compareElementToModel: (el: HTMLElement, modelEntry: T) => boolean; + querySelectModelToElement: (container: HTMLElement, modelEntry: T) => HTMLElement | null; + identifier: string; + itemSelector: string; + disabledItemSelector?: string; + containerSelector: string; + ignorerSelector: string; + placeholderClass: string; + draggableSelector?: string; + boundarySelector?: string; + dataTransferResolver?: (dataTransfer: DataTransfer | null, currentItem: T) => void; + onStart?: (argument: { item: T; element: HTMLElement }) => void; + onChange?: (argument: { item: T; element: HTMLElement }) => void; + onContainerChange?: (argument: { item: T; element: HTMLElement }) => void; + onEnd?: (argument: { item: T; element: HTMLElement }) => void; + onSync?: (argument: { + item: T; + fromController: UmbSorterController; + toController: UmbSorterController; + }) => void; + itemHasNestedContainersResolver?: (element: HTMLElement) => boolean; + onDisallowed?: () => void; + onAllowed?: () => void; + onRequestDrop?: (argument: { item: T }) => boolean | void; + resolveVerticalDirection?: (argument: { + containerElement: Element; + containerRect: DOMRect; + item: T; + element: HTMLElement; + elementRect: DOMRect; + relatedElement: HTMLElement; + relatedRect: DOMRect; + placeholderIsInThisRow: boolean; + horizontalPlaceAfter: boolean; + }) => void; + performItemInsert?: (argument: { item: T; newIndex: number }) => Promise | boolean; + performItemRemove?: (argument: { item: T }) => Promise | boolean; +}; + +// External type with some properties optional, as they have defaults: +export type UmbSorterConfig = Omit< + INTERNAL_UmbSorterConfig, + 'placeholderClass' | 'ignorerSelector' | 'containerSelector' +> & + Partial, 'placeholderClass' | 'ignorerSelector' | 'containerSelector'>>; + +/** + * @export + * @class UmbSorterController + * @implements {UmbControllerInterface} + * @description This controller can make user able to sort items. + */ +export class UmbSorterController implements UmbControllerInterface { + #host; + #config: INTERNAL_UmbSorterConfig; + #observer; + + #model: Array = []; + #rqaId?: number; + + #containerElement!: HTMLElement; + #currentContainerVM = this; + #currentContainerElement: Element | null = null; + + #scrollElement?: Element | null; + #currentElement?: HTMLElement; + #currentDragElement?: Element; + #currentDragRect?: DOMRect; + #currentItem?: T | null; + + #dragX = 0; + #dragY = 0; + + private _lastIndicationContainerVM: UmbSorterController | null = null; + + public get unique() { + return this.#config.identifier; + } + + constructor(host: UmbControllerHostElement, config: UmbSorterConfig) { + this.#host = host; + + // Set defaults: + config.ignorerSelector ??= 'a, img, iframe'; + config.placeholderClass ??= '--umb-sorter-placeholder'; + + this.#config = config as INTERNAL_UmbSorterConfig; + host.addController(this); + + //this.#currentContainerElement = host; + + this.#observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + mutation.addedNodes.forEach((addedNode) => { + if ((addedNode as HTMLElement).matches && (addedNode as HTMLElement).matches(this.#config.itemSelector)) { + this.setupItem(addedNode as HTMLElement); + } + }); + mutation.removedNodes.forEach((removedNode) => { + if ((removedNode as HTMLElement).matches && (removedNode as HTMLElement).matches(this.#config.itemSelector)) { + this.destroyItem(removedNode as HTMLElement); + } + }); + }); + }); + } + + setModel(model: Array) { + if (this.#model) { + // TODO: Some updates might need to be done, as the modal is about to changed? Do make the changes after setting the model?.. + } + this.#model = model; + } + + hostConnected() { + requestAnimationFrame(this._onFirstRender); + } + private _onFirstRender = () => { + const containerEl = + (this.#config.containerSelector + ? this.#host.shadowRoot!.querySelector(this.#config.containerSelector) + : this.#host) ?? this.#host; + + if (this.#currentContainerElement === this.#containerElement) { + this.#currentContainerElement = containerEl; + } + this.#containerElement = containerEl as HTMLElement; + this.#containerElement.addEventListener('dragover', preventDragOver); + + (this.#containerElement as any)['__umbBlockGridSorterController'] = () => { + return this; + }; + + console.log('containerEl', this.#containerElement.shadowRoot ?? this.#containerElement); + + // TODO: Clean up?? + this.#observer.disconnect(); + + const containerElement = this.#containerElement.shadowRoot ?? this.#containerElement; + containerElement.querySelectorAll(this.#config.itemSelector).forEach((child) => { + if (child.matches && child.matches(this.#config.itemSelector)) { + this.setupItem(child as HTMLElement); + } + }); + this.#observer.observe(containerElement, { + childList: true, + subtree: false, + }); + }; + hostDisconnected() { + // TODO: Clean up?? + this.#observer.disconnect(); + (this.#containerElement as any)['__umbBlockGridSorterController'] = undefined; + this.#containerElement.removeEventListener('dragover', preventDragOver); + (this.#containerElement as any) = undefined; + } + + setupItem(element: HTMLElement) { + if (this.#config.ignorerSelector) { + setupIgnorerElements(element, this.#config.ignorerSelector); + } + + if (!this.#config.disabledItemSelector || !element.matches(this.#config.disabledItemSelector)) { + element.draggable = true; + element.addEventListener('dragstart', this.handleDragStart); + } + } + + destroyItem(element: HTMLElement) { + if (this.#config.ignorerSelector) { + destroyIgnorerElements(element, this.#config.ignorerSelector); + } + + element.removeEventListener('dragstart', this.handleDragStart); + } + + handleDragStart = (event: DragEvent) => { + if (this.#currentElement) { + this.handleDragEnd(); + } + + event.stopPropagation(); + if (event.dataTransfer) { + event.dataTransfer.effectAllowed = 'move'; // copyMove when we enhance the drag with clipboard data. + event.dataTransfer.dropEffect = 'none'; // visual feedback when dropped. + } + + if (!this.#scrollElement) { + this.#scrollElement = getParentScrollElement(this.#containerElement, true); + } + + const element = (event.target as HTMLElement).closest(this.#config.itemSelector); + + if (!element) return; + + this.#currentDragElement = this.#config.draggableSelector + ? element.querySelector(this.#config.draggableSelector) ?? undefined + : element; + + if (!this.#currentDragElement) { + throw new Error( + 'Could not find drag element, query was made with the `draggableSelector` of "' + + this.#config.draggableSelector + + '"' + ); + return; + } + + this.#currentElement = element as HTMLElement; + this.#currentDragRect = this.#currentDragElement.getBoundingClientRect(); + this.#currentItem = this.getItemOfElement(this.#currentElement); + if (!this.#currentItem) { + console.error('Could not find item related to this element.'); + return; + } + + this.#currentElement.style.transform = 'translateZ(0)'; // Solves problem with FireFox and ShadowDom in the drag-image. + + if (this.#config.dataTransferResolver) { + this.#config.dataTransferResolver(event.dataTransfer, this.#currentItem); + } + + if (this.#config.onStart) { + this.#config.onStart({ item: this.#currentItem!, element: this.#currentElement }); + } + + window.addEventListener('dragover', this.handleDragMove); + window.addEventListener('dragend', this.handleDragEnd); + + // We must wait one frame before changing the look of the block. + this.#rqaId = requestAnimationFrame(() => { + // It should be okay to use the same rqaId, as the move does not or is okay not to happen on first frame/drag-move. + this.#rqaId = undefined; + if (this.#currentElement) { + this.#currentElement.style.transform = ''; + this.#currentElement.classList.add(this.#config.placeholderClass); + } + }); + }; + + handleDragEnd = async () => { + if (!this.#currentElement || !this.#currentItem) { + return; + } + + window.removeEventListener('dragover', this.handleDragMove); + window.removeEventListener('dragend', this.handleDragEnd); + this.#currentElement.style.transform = ''; + this.#currentElement.classList.remove(this.#config.placeholderClass); + + this.stopAutoScroll(); + this.removeAllowIndication(); + + if ((await this.#currentContainerVM.sync(this.#currentElement, this)) === false) { + // Sync could not succeed, might be because item is not allowed here. + + this.#currentContainerVM = this; + if (this.#config.onContainerChange) { + this.#config.onContainerChange({ + item: this.#currentItem, + element: this.#currentElement, + //ownerVM: this.#currentContainerVM.ownerVM, + }); + } + + // Lets move the Element back to where it came from: + const movingItemIndex = this.#model.indexOf(this.#currentItem); + if (movingItemIndex < this.#model.length - 1) { + const afterItem = this.#model[movingItemIndex + 1]; + const afterEl = this.#config.querySelectModelToElement(this.#containerElement, afterItem); + if (afterEl) { + this.#containerElement.insertBefore(this.#currentElement, afterEl); + } else { + this.#containerElement.appendChild(this.#currentElement); + } + } else { + this.#containerElement.appendChild(this.#currentElement); + } + } + + if (this.#config.onEnd) { + this.#config.onEnd({ item: this.#currentItem, element: this.#currentElement }); + } + + if (this.#rqaId) { + cancelAnimationFrame(this.#rqaId); + } + + this.#currentContainerElement = this.#containerElement; + this.#currentContainerVM = this; + + this.#rqaId = undefined; + this.#currentItem = undefined; + this.#currentElement = undefined; + this.#currentDragElement = undefined; + this.#currentDragRect = undefined; + this.#dragX = 0; + this.#dragY = 0; + }; + + handleDragMove = (event: DragEvent) => { + if (!this.#currentElement) { + return; + } + + const clientX = (event as unknown as TouchEvent).touches + ? (event as unknown as TouchEvent).touches[0].clientX + : event.clientX; + const clientY = (event as unknown as TouchEvent).touches + ? (event as unknown as TouchEvent).touches[0].clientY + : event.clientY; + if (clientX !== 0 && clientY !== 0) { + if (this.#dragX === clientX && this.#dragY === clientY) { + return; + } + this.#dragX = clientX; + this.#dragY = clientY; + + this.handleAutoScroll(this.#dragX, this.#dragY); + + this.#currentDragRect = this.#currentDragElement!.getBoundingClientRect(); + const insideCurrentRect = isWithinRect(this.#dragX, this.#dragY, this.#currentDragRect); + if (!insideCurrentRect) { + if (this.#rqaId === undefined) { + this.#rqaId = requestAnimationFrame(this.moveCurrentElement); + } + } + } + }; + + moveCurrentElement = () => { + this.#rqaId = undefined; + if (!this.#currentElement || !this.#currentContainerElement || !this.#currentItem) { + return; + } + + const currentElementRect = this.#currentElement.getBoundingClientRect(); + const insideCurrentRect = isWithinRect(this.#dragX, this.#dragY, currentElementRect); + if (insideCurrentRect) { + return; + } + + // If we have a boundarySelector, try it, if we didn't get anything fall back to currentContainerElement. + const currentBoundaryElement = + (this.#config.boundarySelector + ? this.#currentContainerElement.closest(this.#config.boundarySelector) + : this.#currentContainerElement) ?? this.#currentContainerElement; + + const currentBoundaryRect = currentBoundaryElement.getBoundingClientRect(); + + const currentContainerHasItems = this.#currentContainerVM.hasOtherItemsThan(this.#currentItem!); + + // if empty we will be move likely to accept an item (add 20px to the bounding box) + // If we have items we must be 10 within the container to accept the move. + const offsetEdge = currentContainerHasItems ? -10 : 20; + if (!isWithinRect(this.#dragX, this.#dragY, currentBoundaryRect, offsetEdge)) { + // we are outside the current container boundary, so lets see if there is a parent we can move. + const parentNode = this.#currentContainerElement.parentNode; + if (parentNode) { + // TODO: support multiple parent shadowDOMs? + const parentContainer = this.#config.containerSelector + ? (parentNode as HTMLElement).closest(this.#config.containerSelector) + : null; + if (parentContainer) { + const parentContainerVM = (parentContainer as any)['__umbBlockGridSorterController'](); + if (parentContainerVM.unique === this.unique) { + this.#currentContainerElement = parentContainer as Element; + this.#currentContainerVM = parentContainerVM; + if (this.#config.onContainerChange) { + this.#config.onContainerChange({ + item: this.#currentItem, + element: this.#currentElement, + //ownerVM: this.#currentContainerVM.ownerVM, + }); + } + } + } + } + } + + // We want to retrieve the children of the container, every time to ensure we got the right order and index + const orderedContainerElements = Array.from( + this.#currentContainerElement.shadowRoot + ? this.#currentContainerElement.shadowRoot.querySelectorAll(this.#config.itemSelector) + : this.#currentContainerElement.querySelectorAll(this.#config.itemSelector) + ); + + const currentContainerRect = this.#currentContainerElement.getBoundingClientRect(); + + // gather elements on the same row. + const elementsInSameRow = []; + let placeholderIsInThisRow = false; + for (const el of orderedContainerElements) { + const elRect = el.getBoundingClientRect(); + // gather elements on the same row. + if (this.#dragY >= elRect.top && this.#dragY <= elRect.bottom) { + const dragElement = this.#config.draggableSelector ? el.querySelector(this.#config.draggableSelector) : el; + if (dragElement) { + const dragElementRect = dragElement.getBoundingClientRect(); + if (el !== this.#currentElement) { + elementsInSameRow.push({ el: el, dragRect: dragElementRect }); + } else { + placeholderIsInThisRow = true; + } + } + } + } + + let lastDistance = 99999; + let foundEl: Element | null = null; + let foundElDragRect!: DOMRect; + let placeAfter = false; + elementsInSameRow.forEach((sameRow) => { + const centerX = sameRow.dragRect.left + sameRow.dragRect.width * 0.5; + const distance = Math.abs(this.#dragX - centerX); + if (distance < lastDistance) { + foundEl = sameRow.el; + foundElDragRect = sameRow.dragRect; + lastDistance = distance; + placeAfter = this.#dragX > centerX; + } + }); + + if (foundEl) { + // If we are on top or closest to our self, we should not do anything. + if (foundEl === this.#currentElement) { + return; + } + const isInsideFound = isWithinRect(this.#dragX, this.#dragY, foundElDragRect, 0); + + // If we are inside the found element, lets look for sub containers. + // use the itemHasNestedContainersResolver, if not configured fallback to looking for the existence of a container via DOM. + // TODO: Ability to look into shadowDOMs for sub containers? + if ( + isInsideFound && this.#config.itemHasNestedContainersResolver + ? this.#config.itemHasNestedContainersResolver(foundEl) + : (foundEl as HTMLElement).querySelector(this.#config.containerSelector) + ) { + // Find all sub containers: + const subLayouts = (foundEl as HTMLElement).querySelectorAll(this.#config.containerSelector); + for (const subLayoutEl of subLayouts) { + // Use boundary element or fallback to container element. + const subBoundaryElement = + (this.#config.boundarySelector ? subLayoutEl.closest(this.#config.boundarySelector) : subLayoutEl) || + subLayoutEl; + const subBoundaryRect = subBoundaryElement.getBoundingClientRect(); + + const subContainerHasItems = subLayoutEl.querySelector( + this.#config.itemSelector + ':not(.' + this.#config.placeholderClass + ')' + ); + // gather elements on the same row. + const subOffsetEdge = subContainerHasItems ? -10 : 20; + if (isWithinRect(this.#dragX, this.#dragY, subBoundaryRect, subOffsetEdge)) { + const subVm = (subLayoutEl as any)['__umbBlockGridSorterController'](); + if (subVm.unique === this.unique) { + this.#currentContainerElement = subLayoutEl as HTMLElement; + this.#currentContainerVM = subVm; + if (this.#config.onContainerChange) { + this.#config.onContainerChange({ + item: this.#currentItem, + element: this.#currentElement, + //ownerVM: this.#currentContainerVM.ownerVM, + }); + } + this.moveCurrentElement(); + return; + } + } + } + } + + // Indication if drop is good: + if (this.updateAllowIndication(this.#currentContainerVM, this.#currentItem) === false) { + return; + } + + const verticalDirection = this.#config.resolveVerticalDirection + ? this.#config.resolveVerticalDirection({ + containerElement: this.#currentContainerElement, + containerRect: currentContainerRect, + item: this.#currentItem, + element: this.#currentElement, + elementRect: currentElementRect, + relatedElement: foundEl, + relatedRect: foundElDragRect, + placeholderIsInThisRow: placeholderIsInThisRow, + horizontalPlaceAfter: placeAfter, + }) + : true; + + if (verticalDirection) { + placeAfter = this.#dragY > foundElDragRect.top + foundElDragRect.height * 0.5; + } + + if (verticalDirection) { + let el; + if (placeAfter === false) { + let lastLeft = foundElDragRect.left; + elementsInSameRow.findIndex((x) => { + if (x.dragRect.left < lastLeft) { + lastLeft = x.dragRect.left; + el = x.el; + } + }); + } else { + let lastRight = foundElDragRect.right; + elementsInSameRow.findIndex((x) => { + if (x.dragRect.right > lastRight) { + lastRight = x.dragRect.right; + el = x.el; + } + }); + } + if (el) { + foundEl = el; + } + } + + const foundElIndex = orderedContainerElements.indexOf(foundEl); + const placeAt = placeAfter ? foundElIndex + 1 : foundElIndex; + + this.move(orderedContainerElements, placeAt); + + return; + } + // We skipped the above part cause we are above or below container: + + // Indication if drop is good: + if (this.updateAllowIndication(this.#currentContainerVM, this.#currentItem) === false) { + return; + } + + if (this.#dragY < currentContainerRect.top) { + this.move(orderedContainerElements, 0); + } else if (this.#dragY > currentContainerRect.bottom) { + this.move(orderedContainerElements, -1); + } + }; + + move(orderedContainerElements: Array, newElIndex: number) { + if (!this.#currentElement || !this.#currentItem || !this.#currentContainerElement) return; + + newElIndex = newElIndex === -1 ? orderedContainerElements.length : newElIndex; + + const containerElement = this.#currentContainerElement.shadowRoot ?? this.#currentContainerElement; + + const placeBeforeElement = orderedContainerElements[newElIndex]; + if (placeBeforeElement) { + // We do not need to move this, if the element to be placed before is it self. + if (placeBeforeElement !== this.#currentElement) { + containerElement.insertBefore(this.#currentElement, placeBeforeElement); + } + } else { + containerElement.appendChild(this.#currentElement); + } + + if (this.#config.onChange) { + this.#config.onChange({ + element: this.#currentElement, + item: this.#currentItem, + //ownerVM: this.#currentContainerVM.ownerVM + }); + } + } + + /** Management methods: */ + + public getItemOfElement(element: HTMLElement) { + if (!element) { + return null; + } + return this.#model.find((entry: T) => this.#config.compareElementToModel(element, entry)); + } + + public async removeItem(item: T) { + if (!item) { + return null; + } + + if (this.#config.performItemRemove) { + return await this.#config.performItemRemove({ item }); + } else { + const oldIndex = this.#model.indexOf(item); + if (oldIndex !== -1) { + return this.#model.splice(oldIndex, 1)[0]; + } + } + return null; + } + + hasOtherItemsThan(item: T) { + return this.#model.filter((x) => x !== item).length > 0; + } + + public async sync(element: HTMLElement, fromVm: UmbSorterController) { + const movingItem = fromVm.getItemOfElement(element); + if (!movingItem) { + console.error('Could not find item of sync item'); + return false; + } + if (this.notifyRequestDrop({ item: movingItem }) === false) { + return false; + } + if (fromVm.removeItem(movingItem) === null) { + console.error('Sync could not remove item'); + return false; + } + + /** Find next element, to then find the index of that element in items-data, to use as a safe reference to where the item will go in our items-data. + * This enables the container to contain various other elements and as well having these elements change while sorting is occurring. + */ + + // find next valid element (This assumes the next element in DOM is presented in items-data, aka. only moving one item between each sync) + let nextEl: Element | null = null; + let loopEl: Element | null = element; + while ((loopEl = loopEl?.nextElementSibling)) { + if (loopEl.matches && loopEl.matches(this.#config.itemSelector)) { + nextEl = loopEl; + break; + } + } + + let newIndex = this.#model.length; + + const movingItemIndex = this.#model.indexOf(movingItem); + + if (movingItemIndex !== -1 && movingItemIndex <= movingItemIndex) { + newIndex--; + } + if (nextEl) { + // We had a reference element, we want to get the index of it. + // This is might a problem if a item is being moved forward? (was also like this in the AngularJS version...) + newIndex = this.#model.findIndex((entry) => this.#config.compareElementToModel(nextEl! as HTMLElement, entry)); + } + + if (this.#config.performItemInsert) { + const result = await this.#config.performItemInsert({ item: movingItem, newIndex }); + if (result === false) { + return false; + } + } else { + this.#model.splice(newIndex, 0, movingItem); + } + + const eventData = { item: movingItem, fromController: fromVm, toController: this }; + if (fromVm !== this) { + fromVm.notifySync(eventData); + } + this.notifySync(eventData); + + return true; + } + + updateAllowIndication(contextVM: UmbSorterController, item: T) { + // Remove old indication: + if (this._lastIndicationContainerVM !== null && this._lastIndicationContainerVM !== contextVM) { + this._lastIndicationContainerVM.notifyAllowed(); + } + this._lastIndicationContainerVM = contextVM; + + if (contextVM.notifyRequestDrop({ item: item }) === true) { + contextVM.notifyAllowed(); + return true; + } + + contextVM.notifyDisallowed(); // This block is not accepted to we will indicate that its not allowed. + return false; + } + removeAllowIndication() { + // Remove old indication: + if (this._lastIndicationContainerVM !== null) { + this._lastIndicationContainerVM.notifyAllowed(); + } + this._lastIndicationContainerVM = null; + } + + // TODO: Move auto scroll into its own class? + #autoScrollRAF: number | null = null; + #autoScrollEl?: Element; + private autoScrollX = 0; + private autoScrollY = 0; + + private handleAutoScroll(clientX: number, clientY: number) { + let scrollRect: DOMRect | null = null; + if (this.#scrollElement) { + this.#autoScrollEl = this.#scrollElement; + scrollRect = this.#autoScrollEl.getBoundingClientRect(); + } else { + this.#autoScrollEl = document.scrollingElement || document.documentElement; + scrollRect = { + top: 0, + left: 0, + bottom: window.innerHeight, + right: window.innerWidth, + height: window.innerHeight, + width: window.innerWidth, + } as DOMRect; + } + + const scrollWidth = this.#autoScrollEl.scrollWidth; + const scrollHeight = this.#autoScrollEl.scrollHeight; + const canScrollX = scrollRect.width < scrollWidth; + const canScrollY = scrollRect.height < scrollHeight; + const scrollPosX = this.#autoScrollEl.scrollLeft; + const scrollPosY = this.#autoScrollEl.scrollTop; + + cancelAnimationFrame(this.#autoScrollRAF!); + + if (canScrollX || canScrollY) { + this.autoScrollX = + Math.abs(scrollRect.right - clientX) <= autoScrollSensitivity && scrollPosX + scrollRect.width < scrollWidth + ? 1 + : Math.abs(scrollRect.left - clientX) <= autoScrollSensitivity && !!scrollPosX + ? -1 + : 0; + + this.autoScrollY = + Math.abs(scrollRect.bottom - clientY) <= autoScrollSensitivity && scrollPosY + scrollRect.height < scrollHeight + ? 1 + : Math.abs(scrollRect.top - clientY) <= autoScrollSensitivity && !!scrollPosY + ? -1 + : 0; + + this.#autoScrollRAF = requestAnimationFrame(this._performAutoScroll); + } + } + private _performAutoScroll() { + this.#autoScrollEl!.scrollLeft += this.autoScrollX * autoScrollSpeed; + this.#autoScrollEl!.scrollTop += this.autoScrollY * autoScrollSpeed; + this.#autoScrollRAF = requestAnimationFrame(this._performAutoScroll); + } + private stopAutoScroll() { + cancelAnimationFrame(this.#autoScrollRAF!); + this.#autoScrollRAF = null; + } + + public notifySync(data: any) { + if (this.#config.onSync) { + this.#config.onSync(data); + } + } + public notifyDisallowed() { + if (this.#config.onDisallowed) { + this.#config.onDisallowed(); + } + } + public notifyAllowed() { + if (this.#config.onAllowed) { + this.#config.onAllowed(); + } + } + public notifyRequestDrop(data: any) { + if (this.#config.onRequestDrop) { + return this.#config.onRequestDrop(data) || false; + } + return true; + } + + destroy() { + // Do something when host element is destroyed. + if (this.#currentElement) { + this.handleDragEnd(); + } + + this._lastIndicationContainerVM = null; + + // TODO: Clean up items?? + this.#observer.disconnect(); + + // For auto scroller: + this.#scrollElement = null; + this.#autoScrollEl = undefined; + } +} diff --git a/src/Umbraco.Web.UI.Client/libs/store/entity-tree-store.ts b/src/Umbraco.Web.UI.Client/libs/store/entity-tree-store.ts index ea94db043a..fb032ac309 100644 --- a/src/Umbraco.Web.UI.Client/libs/store/entity-tree-store.ts +++ b/src/Umbraco.Web.UI.Client/libs/store/entity-tree-store.ts @@ -1,49 +1,27 @@ import { EntityTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; -import { ArrayState, partialUpdateFrozenArray } from '@umbraco-cms/backoffice/observable-api'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; import { UmbStoreBase, UmbTreeStore } from '@umbraco-cms/backoffice/store'; +import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; /** * @export * @class UmbEntityTreeStore * @extends {UmbStoreBase} - * @description - General Tree Data Store + * @description - Entity Tree Store */ -export class UmbEntityTreeStore extends UmbStoreBase implements UmbTreeStore { - #data = new ArrayState([], (x) => x.id); - - /** - * Appends items to the store - * @param {Array} items - * @memberof UmbEntityTreeStore - */ - appendItems(items: Array) { - this.#data.append(items); - } - - /** - * Updates an item in the store - * @param {string} id - * @param {Partial} data - * @memberof UmbEntityTreeStore - */ - updateItem(id: string, data: Partial) { - this.#data.next(partialUpdateFrozenArray(this.#data.getValue(), data, (entry) => entry.id === id)); - } - - /** - * Removes an item from the store - * @param {string} id - * @memberof UmbEntityTreeStore - */ - removeItem(id: string) { - this.#data.removeOne(id); +export class UmbEntityTreeStore + extends UmbStoreBase + implements UmbTreeStore +{ + constructor(host: UmbControllerHostElement, storeAlias: string) { + super(host, storeAlias, new UmbArrayState([], (x) => x.id)); } /** * An observable to observe the root items * @memberof UmbEntityTreeStore */ - rootItems = this.#data.getObservablePart((items) => items.filter((item) => item.parentId === null)); + rootItems = this._data.getObservablePart((items) => items.filter((item) => item.parentId === null)); /** * Returns an observable to observe the children of a given parent @@ -52,7 +30,7 @@ export class UmbEntityTreeStore extends UmbStoreBase implements UmbTreeStore items.filter((item) => item.parentId === parentId)); + return this._data.getObservablePart((items) => items.filter((item) => item.parentId === parentId)); } /** @@ -62,6 +40,6 @@ export class UmbEntityTreeStore extends UmbStoreBase implements UmbTreeStore) { - return this.#data.getObservablePart((items) => items.filter((item) => ids.includes(item.id ?? ''))); + return this._data.getObservablePart((items) => items.filter((item) => ids.includes(item.id ?? ''))); } } diff --git a/src/Umbraco.Web.UI.Client/libs/store/file-system-tree.store.ts b/src/Umbraco.Web.UI.Client/libs/store/file-system-tree.store.ts index 8afa8ca62e..68c9352ce5 100644 --- a/src/Umbraco.Web.UI.Client/libs/store/file-system-tree.store.ts +++ b/src/Umbraco.Web.UI.Client/libs/store/file-system-tree.store.ts @@ -1,49 +1,27 @@ import { FileSystemTreeItemPresentationModel } from '@umbraco-cms/backoffice/backend-api'; -import { ArrayState, partialUpdateFrozenArray } from '@umbraco-cms/backoffice/observable-api'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; import { UmbStoreBase, UmbTreeStore } from '@umbraco-cms/backoffice/store'; +import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; /** * @export * @class UmbFileSystemTreeStore * @extends {UmbStoreBase} - * @description - General Tree Data Store + * @description - File System Tree Store */ -export class UmbFileSystemTreeStore extends UmbStoreBase implements UmbTreeStore { - #data = new ArrayState([], (x) => x.path); - - /** - * Appends items to the store - * @param {Array} items - * @memberof UmbFileSystemTreeStore - */ - appendItems(items: Array) { - this.#data.append(items); - } - - /** - * Updates an item in the store - * @param {string} path - * @param {Partial} data - * @memberof UmbFileSystemTreeStore - */ - updateItem(path: string, data: Partial) { - this.#data.appendOne(data); - } - - /** - * Removes an item from the store - * @param {string} path - * @memberof UmbFileSystemTreeStore - */ - removeItem(path: string) { - this.#data.removeOne(path); +export class UmbFileSystemTreeStore + extends UmbStoreBase + implements UmbTreeStore +{ + constructor(host: UmbControllerHostElement, storeAlias: string) { + super(host, storeAlias, new UmbArrayState([], (x) => x.path)); } /** * An observable to observe the root items * @memberof UmbFileSystemTreeStore */ - rootItems = this.#data.getObservablePart((items) => items.filter((item) => item.path?.includes('/') === false)); + rootItems = this._data.getObservablePart((items) => items.filter((item) => item.path?.includes('/') === false)); /** * Returns an observable to observe the children of a given parent @@ -52,7 +30,7 @@ export class UmbFileSystemTreeStore extends UmbStoreBase implements UmbTreeStore * @memberof UmbFileSystemTreeStore */ childrenOf(parentPath: string | null) { - return this.#data.getObservablePart((items) => items.filter((item) => item.path?.startsWith(parentPath + '/'))); + return this._data.getObservablePart((items) => items.filter((item) => item.path?.startsWith(parentPath + '/'))); } /** @@ -62,6 +40,6 @@ export class UmbFileSystemTreeStore extends UmbStoreBase implements UmbTreeStore * @memberof UmbFileSystemTreeStore */ items(paths: Array) { - return this.#data.getObservablePart((items) => items.filter((item) => paths.includes(item.path ?? ''))); + return this._data.getObservablePart((items) => items.filter((item) => paths.includes(item.path ?? ''))); } } diff --git a/src/Umbraco.Web.UI.Client/libs/store/index.ts b/src/Umbraco.Web.UI.Client/libs/store/index.ts index f9152d3ebd..a72d304eca 100644 --- a/src/Umbraco.Web.UI.Client/libs/store/index.ts +++ b/src/Umbraco.Web.UI.Client/libs/store/index.ts @@ -3,3 +3,4 @@ export * from './store-base'; export * from './entity-tree-store'; export * from './file-system-tree.store'; export * from './tree-store.interface'; +export * from './item-store.interface'; diff --git a/src/Umbraco.Web.UI.Client/libs/store/item-store.interface.ts b/src/Umbraco.Web.UI.Client/libs/store/item-store.interface.ts new file mode 100644 index 0000000000..a68e83b5c1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/store/item-store.interface.ts @@ -0,0 +1,7 @@ +import type { Observable } from 'rxjs'; +import { ItemResponseModelBaseModel } from '../backend-api'; +import { UmbStore } from './store.interface'; + +export interface UmbItemStore extends UmbStore { + items: (uniques: Array) => Observable>; +} diff --git a/src/Umbraco.Web.UI.Client/libs/store/store-base.ts b/src/Umbraco.Web.UI.Client/libs/store/store-base.ts index 4021307cd6..dcd9244391 100644 --- a/src/Umbraco.Web.UI.Client/libs/store/store-base.ts +++ b/src/Umbraco.Web.UI.Client/libs/store/store-base.ts @@ -1,9 +1,48 @@ +import { UmbStore } from './store.interface'; import { UmbContextProviderController } from '@umbraco-cms/backoffice/context-api'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; // TODO: Make a Store interface? -export class UmbStoreBase { - constructor(protected _host: UmbControllerHostElement, public readonly storeAlias: string) { +export class UmbStoreBase implements UmbStore { + protected _host: UmbControllerHostElement; + protected _data: UmbArrayState; + + public readonly storeAlias: string; + + constructor(_host: UmbControllerHostElement, storeAlias: string, data: UmbArrayState) { + this._host = _host; + this.storeAlias = storeAlias; + this._data = data; + new UmbContextProviderController(_host, storeAlias, this); } + + /** + * Appends items to the store + * @param {Array} items + * @memberof UmbEntityTreeStore + */ + appendItems(items: Array) { + this._data.append(items); + } + + /** + * Updates an item in the store + * @param {string} id + * @param {Partial} data + * @memberof UmbEntityTreeStore + */ + updateItem(unique: string, data: Partial) { + this._data.updateOne(unique, data); + } + + /** + * Removes an item from the store + * @param {string} id + * @memberof UmbEntityTreeStore + */ + removeItem(unique: string) { + this._data.removeOne(unique); + } } diff --git a/src/Umbraco.Web.UI.Client/libs/store/store.interface.ts b/src/Umbraco.Web.UI.Client/libs/store/store.interface.ts new file mode 100644 index 0000000000..0f094a1d4f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/store/store.interface.ts @@ -0,0 +1,5 @@ +export interface UmbStore { + appendItems: (items: Array) => void; + updateItem: (unique: string, item: Partial) => void; + removeItem: (unique: string) => void; +} diff --git a/src/Umbraco.Web.UI.Client/libs/store/store.ts b/src/Umbraco.Web.UI.Client/libs/store/store.ts index 4961162767..b2c48aedde 100644 --- a/src/Umbraco.Web.UI.Client/libs/store/store.ts +++ b/src/Umbraco.Web.UI.Client/libs/store/store.ts @@ -1,3 +1,4 @@ +// TODO: delete when the last usages are gone import type { Observable } from 'rxjs'; export interface UmbDataStoreIdentifiers { diff --git a/src/Umbraco.Web.UI.Client/libs/store/tree-store.interface.ts b/src/Umbraco.Web.UI.Client/libs/store/tree-store.interface.ts index 80a366ada5..cbe7b563d8 100644 --- a/src/Umbraco.Web.UI.Client/libs/store/tree-store.interface.ts +++ b/src/Umbraco.Web.UI.Client/libs/store/tree-store.interface.ts @@ -1,12 +1,10 @@ import type { Observable } from 'rxjs'; import { TreeItemPresentationModel } from '../backend-api'; +import { UmbStore } from './store.interface'; -export interface UmbTreeStore { - appendItems: (items: Array) => void; - updateItem: (unique: string, item: Partial) => void; - removeItem: (unique: string) => void; - +export interface UmbTreeStore extends UmbStore { rootItems: Observable>; childrenOf: (parentUnique: string | null) => Observable>; + // TODO: remove this one when all repositories are using an item store items: (uniques: Array) => Observable>; } diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index 59986a80be..42e0676d6f 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -72,6 +72,7 @@ "storybook": "^7.0.2", "tiny-glob": "^0.2.9", "typescript": "^5.0.3", + "typescript-json-schema": "^0.55.0", "uglify-js": "^3.17.4", "vite": "^4.2.1", "vite-plugin-static-copy": "^0.13.0", @@ -2004,6 +2005,28 @@ "node": ">=0.1.90" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -4737,6 +4760,30 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", + "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", + "dev": true + }, "node_modules/@types/accepts": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.5.tgz", @@ -7053,6 +7100,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/address": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/address/-/address-1.2.2.tgz", @@ -7264,6 +7320,12 @@ "node": ">=10" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -8617,6 +8679,12 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, "node_modules/cross-fetch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", @@ -13555,6 +13623,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, "node_modules/make-iterator": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", @@ -14691,6 +14765,12 @@ "tslib": "^2.0.3" } }, + "node_modules/path-equal": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/path-equal/-/path-equal-1.2.5.tgz", + "integrity": "sha512-i73IctDr3F2W+bsOWDyyVm/lqsXO47aY9nsFZUjTT/aljSbkxHxxCoyZ9UUrM8jK0JVod+An+rl48RCsvWM+9g==", + "dev": true + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -16204,6 +16284,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-stable-stringify": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.2.tgz", + "integrity": "sha512-gMxvPJYhP0O9n2pvcfYfIuYgbledAOJFcqRThtPRmjscaipiwcwPPKLytpVzMkG2HAN87Qmo2d4PtGiri1dSLA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -17264,6 +17353,58 @@ "node": ">=6.10" } }, + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/ts-simple-type": { "version": "2.0.0-next.0", "resolved": "https://registry.npmjs.org/ts-simple-type/-/ts-simple-type-2.0.0-next.0.tgz", @@ -17419,6 +17560,64 @@ "node": ">=12.20" } }, + "node_modules/typescript-json-schema": { + "version": "0.55.0", + "resolved": "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.55.0.tgz", + "integrity": "sha512-BXaivYecUdiXWWNiUqXgY6A9cMWerwmhtO+lQE7tDZGs7Mf38sORDeQZugfYOZOHPZ9ulsD+w0LWjFDOQoXcwg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "@types/node": "^16.9.2", + "glob": "^7.1.7", + "path-equal": "^1.1.2", + "safe-stable-stringify": "^2.2.0", + "ts-node": "^10.9.1", + "typescript": "~4.8.2", + "yargs": "^17.1.1" + }, + "bin": { + "typescript-json-schema": "bin/typescript-json-schema" + } + }, + "node_modules/typescript-json-schema/node_modules/@types/node": { + "version": "16.18.16", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.16.tgz", + "integrity": "sha512-ZOzvDRWp8dCVBmgnkIqYCArgdFOO9YzocZp8Ra25N/RStKiWvMOXHMz+GjSeVNe5TstaTmTWPucGJkDw0XXJWA==", + "dev": true + }, + "node_modules/typescript-json-schema/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typescript-json-schema/node_modules/typescript": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", + "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/typical": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", @@ -17737,6 +17936,12 @@ "integrity": "sha512-dsNgbLaTrd6l3MMxTtouOCFw4CBFc/3a+GgYA2YyrJvyQ1u6q4pcu3ktLoUZ/VN/Aw9WsauazbgsgdfVWgAKQg==", "dev": true }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, "node_modules/v8-to-istanbul": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", @@ -18380,6 +18585,15 @@ "node": ">= 4.0.0" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -19741,6 +19955,27 @@ "dev": true, "optional": true }, + "@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + } + } + }, "@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -21623,6 +21858,30 @@ "magic-string": "^0.27.0" } }, + "@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "@tsconfig/node16": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", + "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", + "dev": true + }, "@types/accepts": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.5.tgz", @@ -23668,6 +23927,12 @@ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true }, + "acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true + }, "address": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/address/-/address-1.2.2.tgz", @@ -23833,6 +24098,12 @@ "readable-stream": "^3.6.0" } }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -24848,6 +25119,12 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, "cross-fetch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", @@ -28520,6 +28797,12 @@ "semver": "^6.0.0" } }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, "make-iterator": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", @@ -29380,6 +29663,12 @@ "tslib": "^2.0.3" } }, + "path-equal": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/path-equal/-/path-equal-1.2.5.tgz", + "integrity": "sha512-i73IctDr3F2W+bsOWDyyVm/lqsXO47aY9nsFZUjTT/aljSbkxHxxCoyZ9UUrM8jK0JVod+An+rl48RCsvWM+9g==", + "dev": true + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -30465,6 +30754,12 @@ "is-regex": "^1.1.4" } }, + "safe-stable-stringify": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.2.tgz", + "integrity": "sha512-gMxvPJYhP0O9n2pvcfYfIuYgbledAOJFcqRThtPRmjscaipiwcwPPKLytpVzMkG2HAN87Qmo2d4PtGiri1dSLA==", + "dev": true + }, "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -31327,6 +31622,35 @@ "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", "dev": true }, + "ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "requires": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "dependencies": { + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + } + } + }, "ts-simple-type": { "version": "2.0.0-next.0", "resolved": "https://registry.npmjs.org/ts-simple-type/-/ts-simple-type-2.0.0-next.0.tgz", @@ -31438,6 +31762,50 @@ "integrity": "sha512-xv8mOEDnigb/tN9PSMTwSEqAnUvkoXMQlicOb0IUVDBSQCgBSaAAROUZYy2IcUy5qU6XajK5jjjO7TMWqBTKZA==", "dev": true }, + "typescript-json-schema": { + "version": "0.55.0", + "resolved": "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.55.0.tgz", + "integrity": "sha512-BXaivYecUdiXWWNiUqXgY6A9cMWerwmhtO+lQE7tDZGs7Mf38sORDeQZugfYOZOHPZ9ulsD+w0LWjFDOQoXcwg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "@types/node": "^16.9.2", + "glob": "^7.1.7", + "path-equal": "^1.1.2", + "safe-stable-stringify": "^2.2.0", + "ts-node": "^10.9.1", + "typescript": "~4.8.2", + "yargs": "^17.1.1" + }, + "dependencies": { + "@types/node": { + "version": "16.18.16", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.16.tgz", + "integrity": "sha512-ZOzvDRWp8dCVBmgnkIqYCArgdFOO9YzocZp8Ra25N/RStKiWvMOXHMz+GjSeVNe5TstaTmTWPucGJkDw0XXJWA==", + "dev": true + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "typescript": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", + "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", + "dev": true + } + } + }, "typical": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", @@ -31666,6 +32034,12 @@ "integrity": "sha512-dsNgbLaTrd6l3MMxTtouOCFw4CBFc/3a+GgYA2YyrJvyQ1u6q4pcu3ktLoUZ/VN/Aw9WsauazbgsgdfVWgAKQg==", "dev": true }, + "v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, "v8-to-istanbul": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", @@ -32131,6 +32505,12 @@ "integrity": "sha512-RXRJzMiK6U2ye0BlGGZnmpwJDPgakn6aNQ0A7gHRbD4I0uvK4TW6UqkK1V0pp9jskjJBAXd3dRrbzWkqJ+6cxA==", "dev": true }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + }, "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index c095c233da..b8407e48b3 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -28,7 +28,7 @@ "scripts": { "dev": "vite", "build": "tsc && vite build --mode staging", - "build:libs": "npm run wc-analyze && npm run wc-analyze:vscode && rollup -c rollup-libs.config.js && node utils/move-libs.js", + "build:libs": "npm run wc-analyze && npm run wc-analyze:vscode && npm run generate:jsonschema && rollup -c rollup-libs.config.js && node utils/move-libs.js", "build:for:static": "tsc && vite build", "build:for:cms": "tsc && npm run build:libs && vite build -c vite.cms.config.ts", "build:for:cms:watch": "tsc && npm run build:libs && vite build -c vite.cms.config.ts --watch", @@ -45,12 +45,13 @@ "format:fix": "npm run format -- --write", "generate:api": "openapi --input https://raw.githubusercontent.com/umbraco/Umbraco-CMS/v13/dev/src/Umbraco.Cms.Api.Management/OpenApi.json --output libs/backend-api/src --postfix Resource --useOptions", "generate:api-dev": "openapi --input http://localhost:11000/umbraco/swagger/v1/swagger.json --output libs/backend-api/src --postfix Resource --useOptions", + "generate:jsonschema": "typescript-json-schema --required --include \"./libs/extensions-registry/*.ts\" --out dist/libs/umbraco-package-schema.json tsconfig.json UmbracoPackage", "storybook": "npm run wc-analyze && storybook dev -p 6006", "storybook:build": "npm run wc-analyze && storybook build", "build-storybook": "npm run wc-analyze && storybook build", "generate:icons": "node ./devops/icons/index.js", - "wc-analyze": "wca **/*.element.ts --outFile custom-elements.json", - "wc-analyze:vscode": "wca **/*.element.ts --format vscode --outFile vscode-html-custom-data.json", + "wc-analyze": "wca **/*.element.ts --outFile dist/libs/custom-elements.json", + "wc-analyze:vscode": "wca **/*.element.ts --format vscode --outFile dist/libs/vscode-html-custom-data.json", "new-extension": "plop --plopfile ./devops/plop/plop.js", "compile": "tsc", "check": "npm run lint && npm run compile && npm run build-storybook", @@ -123,7 +124,8 @@ "rollup-plugin-url": "^3.0.1", "storybook": "^7.0.2", "tiny-glob": "^0.2.9", - "typescript": "^5.0.3", + "typescript": "^5.0.3", + "typescript-json-schema": "^0.55.0", "uglify-js": "^3.17.4", "vite": "^4.2.1", "vite-plugin-static-copy": "^0.13.0", diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts index c4590ec1ee..116be1ac07 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts @@ -71,7 +71,8 @@ export class UmbBackofficeElement extends UmbLitElement { this.provideContext(UMB_CURRENT_USER_HISTORY_STORE_CONTEXT_TOKEN, new UmbCurrentUserHistoryStore()); // Register All Stores - this.observe(umbExtensionsRegistry.extensionsOfTypes(['store', 'treeStore']), (stores) => { + // TODO: can we use kinds here so we don't have to hardcode the types? + this.observe(umbExtensionsRegistry.extensionsOfTypes(['store', 'treeStore', 'itemStore']), (stores) => { stores.forEach((store) => createExtensionClass(store, [this])); }); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-blueprints/document-blueprint.detail.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-blueprints/document-blueprint.detail.store.ts index f60b478a36..d7efe4701c 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-blueprints/document-blueprint.detail.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-blueprints/document-blueprint.detail.store.ts @@ -1,6 +1,6 @@ import type { DocumentBlueprintDetails } from '@umbraco-cms/backoffice/models'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import { ArrayState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; import { UmbStoreBase } from '@umbraco-cms/backoffice/store'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; @@ -11,11 +11,14 @@ import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; * @description - Data Store for Document Blueprints */ export class UmbDocumentBlueprintStore extends UmbStoreBase { - // TODO: use the right type: - #data = new ArrayState([], (x) => x.id); - constructor(host: UmbControllerHostElement) { - super(host, UMB_DOCUMENT_BLUEPRINT_STORE_CONTEXT_TOKEN.toString()); + super( + host, + UMB_DOCUMENT_BLUEPRINT_STORE_CONTEXT_TOKEN.toString(), + // TODO: use the right type: + + new UmbArrayState([], (x) => x.id) + ); } /** @@ -29,10 +32,10 @@ export class UmbDocumentBlueprintStore extends UmbStoreBase { fetch(`/umbraco/management/api/v1/document-blueprint/details/${id}`) .then((res) => res.json()) .then((data) => { - this.#data.append(data); + this._data.append(data); }); - return this.#data.getObservablePart((documents) => documents.find((document) => document.id === id)); + return this._data.getObservablePart((documents) => documents.find((document) => document.id === id)); } getScaffold(entityType: string, parentId: string | null) { @@ -68,7 +71,7 @@ export class UmbDocumentBlueprintStore extends UmbStoreBase { }) .then((res) => res.json()) .then((data: Array) => { - this.#data.append(data); + this._data.append(data); }); } @@ -89,7 +92,7 @@ export class UmbDocumentBlueprintStore extends UmbStoreBase { }, }); - this.#data.remove(ids); + this._data.remove(ids); } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/document-type.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/document-type.repository.ts index 78c9ac203c..14656bb826 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/document-type.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/document-type.repository.ts @@ -1,11 +1,15 @@ -import { DocumentTypeTreeServerDataSource } from './sources/document-type.tree.server.data'; +import { UmbDocumentTypeTreeServerDataSource } from './sources/document-type.tree.server.data'; import { UmbDocumentTypeServerDataSource } from './sources/document-type.server.data'; import { UmbDocumentTypeTreeStore, UMB_DOCUMENT_TYPE_TREE_STORE_CONTEXT_TOKEN } from './document-type.tree.store'; import { UmbDocumentTypeStore, UMB_DOCUMENT_TYPE_STORE_CONTEXT_TOKEN } from './document-type.store'; import type { UmbTreeDataSource, UmbTreeRepository, UmbDetailRepository } from '@umbraco-cms/backoffice/repository'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; -import { ProblemDetailsModel, DocumentTypeResponseModel } from '@umbraco-cms/backoffice/backend-api'; +import { + ProblemDetailsModel, + DocumentTypeResponseModel, + FolderTreeItemResponseModel, +} from '@umbraco-cms/backoffice/backend-api'; import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/notification'; type ItemType = DocumentTypeResponseModel; @@ -27,7 +31,7 @@ export class UmbDocumentTypeRepository implements UmbTreeRepository, U this.#host = host; // TODO: figure out how spin up get the correct data source - this.#treeSource = new DocumentTypeTreeServerDataSource(this.#host); + this.#treeSource = new UmbDocumentTypeTreeServerDataSource(this.#host); this.#detailDataSource = new UmbDocumentTypeServerDataSource(this.#host); this.#init = Promise.all([ @@ -77,7 +81,7 @@ export class UmbDocumentTypeRepository implements UmbTreeRepository, U return { data, error, asObservable: () => this.#treeStore!.childrenOf(parentId) }; } - async requestTreeItems(ids: Array) { + async requestItemsLegacy(ids: Array) { await this.#init; if (!ids) { @@ -100,7 +104,7 @@ export class UmbDocumentTypeRepository implements UmbTreeRepository, U return this.#treeStore!.childrenOf(parentId); } - async treeItems(ids: Array) { + async itemsLegacy(ids: Array) { await this.#init; return this.#treeStore!.items(ids); } @@ -141,19 +145,22 @@ export class UmbDocumentTypeRepository implements UmbTreeRepository, U // Could potentially be general methods: - async create(template: ItemType) { - if (!template || !template.id) throw new Error('Template is missing'); + async create(documentType: ItemType) { + if (!documentType || !documentType.id) throw new Error('Template is missing'); await this.#init; - const { error } = await this.#detailDataSource.insert(template); + const { error } = await this.#detailDataSource.insert(documentType); if (!error) { + const treeItem = createTreeItem(documentType); + this.#treeStore?.appendItems([treeItem]); + const notification = { data: { message: `Document created` } }; this.#notificationContext?.peek('positive', notification); // TODO: we currently don't use the detail store for anything. // Consider to look up the data before fetching from the server - this.#detailStore?.append(template); + this.#detailStore?.append(documentType); // TODO: Update tree store with the new item? or ask tree to request the new item? } @@ -206,3 +213,20 @@ export class UmbDocumentTypeRepository implements UmbTreeRepository, U return { error }; } } + +export const createTreeItem = (item: ItemType): FolderTreeItemResponseModel => { + if (!item) throw new Error('item is null or undefined'); + if (!item.id) throw new Error('item.id is null or undefined'); + + // TODO: needs parentID, this is missing in the current model. Should be good when updated to a createModel. + return { + $type: 'FolderTreeItemResponseModel', + type: 'data-type', + parentId: null, + name: item.name, + id: item.id, + isFolder: false, + isContainer: false, + hasChildren: false, + }; +}; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/document-type.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/document-type.store.ts index 23b64c0942..e04813169c 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/document-type.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/document-type.store.ts @@ -1,6 +1,6 @@ import { DocumentTypeResponseModel } from '@umbraco-cms/backoffice/backend-api'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import { ArrayState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; import { UmbStoreBase } from '@umbraco-cms/backoffice/store'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; @@ -11,15 +11,17 @@ import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; * @description - Data Store for Document Types */ export class UmbDocumentTypeStore extends UmbStoreBase { - #data = new ArrayState([], (x) => x.id); - /** * Creates an instance of UmbDocumentTypeStore. * @param {UmbControllerHostElement} host * @memberof UmbDocumentTypeStore */ constructor(host: UmbControllerHostElement) { - super(host, UMB_DOCUMENT_TYPE_STORE_CONTEXT_TOKEN.toString()); + super( + host, + UMB_DOCUMENT_TYPE_STORE_CONTEXT_TOKEN.toString(), + new UmbArrayState([], (x) => x.id) + ); } /** @@ -28,7 +30,7 @@ export class UmbDocumentTypeStore extends UmbStoreBase { * @memberof UmbDocumentTypeStore */ append(document: DocumentTypeResponseModel) { - this.#data.append([document]); + this._data.append([document]); } /** @@ -37,7 +39,7 @@ export class UmbDocumentTypeStore extends UmbStoreBase { * @memberof UmbDocumentTypeStore */ byId(id: DocumentTypeResponseModel['id']) { - return this.#data.getObservablePart((x) => x.find((y) => y.id === id)); + return this._data.getObservablePart((x) => x.find((y) => y.id === id)); } /** @@ -46,7 +48,7 @@ export class UmbDocumentTypeStore extends UmbStoreBase { * @memberof UmbDocumentTypeStore */ remove(uniques: Array) { - this.#data.remove(uniques); + this._data.remove(uniques); } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/sources/document-type.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/sources/document-type.server.data.ts index f6bff744b2..5915dc9907 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/sources/document-type.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/sources/document-type.server.data.ts @@ -66,11 +66,7 @@ export class UmbDocumentTypeServerDataSource implements UmbDataSource} ids * @return {*} - * @memberof DocumentTreeServerDataSource + * @memberof UmbDocumentTypeTreeServerDataSource */ async getItems(ids: Array) { if (ids) { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/document-type-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/document-type-workspace-editor.element.ts index 21d8c3e526..21c13cfe67 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/document-type-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/document-type-workspace-editor.element.ts @@ -3,13 +3,115 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { css, html } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import { UmbDocumentTypeWorkspaceContext } from './document-type-workspace.context'; -import type { DocumentTypeResponseModel } from '@umbraco-cms/backoffice/backend-api'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN, UMB_ICON_PICKER_MODAL } from '@umbraco-cms/backoffice/modal'; import { UMB_ENTITY_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/context-api'; @customElement('umb-document-type-workspace-editor') export class UmbDocumentTypeWorkspaceEditorElement extends UmbLitElement { + @state() + private _icon?: string; + + @state() + private _iconColorAlias?: string; + // TODO: Color should be using an alias, and look up in some dictionary/key/value) of project-colors. + + #workspaceContext?: UmbDocumentTypeWorkspaceContext; + + @state() + private _name?: string; + + @state() + private _alias?: string; + + private _modalContext?: UmbModalContext; + + constructor() { + super(); + + this.consumeContext(UMB_ENTITY_WORKSPACE_CONTEXT, (instance) => { + this.#workspaceContext = instance as UmbDocumentTypeWorkspaceContext; + this.#observeDocumentType(); + }); + + this.consumeContext(UMB_MODAL_CONTEXT_TOKEN, (instance) => { + this._modalContext = instance; + }); + } + + #observeDocumentType() { + if (!this.#workspaceContext) return; + this.observe(this.#workspaceContext.name, (name) => (this._name = name)); + this.observe(this.#workspaceContext.alias, (alias) => (this._alias = alias)); + this.observe(this.#workspaceContext.icon, (icon) => (this._icon = icon)); + } + + // TODO. find a way where we don't have to do this for all workspaces. + private _handleNameInput(event: UUIInputEvent) { + if (event instanceof UUIInputEvent) { + const target = event.composedPath()[0] as UUIInputElement; + + if (typeof target?.value === 'string') { + this.#workspaceContext?.setName(target.value); + } + } + } + + // TODO. find a way where we don't have to do this for all workspaces. + private _handleAliasInput(event: UUIInputEvent) { + if (event instanceof UUIInputEvent) { + const target = event.composedPath()[0] as UUIInputElement; + + if (typeof target?.value === 'string') { + this.#workspaceContext?.setAlias(target.value); + } + } + event.stopPropagation(); + } + + private async _handleIconClick() { + const modalHandler = this._modalContext?.open(UMB_ICON_PICKER_MODAL, { + icon: this._icon, + color: this._iconColorAlias, + }); + + modalHandler?.onSubmit().then((saved) => { + if (saved.icon) this.#workspaceContext?.setIcon(saved.icon); + // TODO: save color ALIAS as well + }); + } + + render() { + return html` + + + +
+ + + Keyboard Shortcuts + + ALT + + + shift + + + k + + +
+
+ `; + } + static styles = [ UUITextStyles, css` @@ -42,107 +144,6 @@ export class UmbDocumentTypeWorkspaceEditorElement extends UmbLitElement { } `, ]; - - // TODO: notice this format is not acceptable: - @state() - private _icon = { - color: '#000000', - name: 'umb:document-dashed-line', - }; - - #workspaceContext?: UmbDocumentTypeWorkspaceContext; - - //@state() - //private _documentType?: DocumentTypeResponseModel; - @state() - private _name?: string; - - @state() - private _alias?: string; - - private _modalContext?: UmbModalContext; - - constructor() { - super(); - - this.consumeContext(UMB_ENTITY_WORKSPACE_CONTEXT, (instance) => { - this.#workspaceContext = instance as UmbDocumentTypeWorkspaceContext; - this.#observeDocumentType(); - }); - - this.consumeContext(UMB_MODAL_CONTEXT_TOKEN, (instance) => { - this._modalContext = instance; - }); - } - - #observeDocumentType() { - if (!this.#workspaceContext) return; - //this.observe(this.#workspaceContext.data, (data) => (this._documentType = data)); - this.observe(this.#workspaceContext.name, (name) => (this._name = name)); - this.observe(this.#workspaceContext.alias, (alias) => (this._alias = alias)); - } - - // TODO. find a way where we don't have to do this for all workspaces. - private _handleNameInput(event: UUIInputEvent) { - if (event instanceof UUIInputEvent) { - const target = event.composedPath()[0] as UUIInputElement; - - if (typeof target?.value === 'string') { - this.#workspaceContext?.setName(target.value); - } - } - } - - // TODO. find a way where we don't have to do this for all workspaces. - private _handleAliasInput(event: UUIInputEvent) { - if (event instanceof UUIInputEvent) { - const target = event.composedPath()[0] as UUIInputElement; - - if (typeof target?.value === 'string') { - this.#workspaceContext?.setAlias(target.value); - } - } - event.stopPropagation(); - } - - private async _handleIconClick() { - const modalHandler = this._modalContext?.open(UMB_ICON_PICKER_MODAL); - - modalHandler?.onSubmit().then((saved) => { - if (saved.icon) this.#workspaceContext?.setIcon(saved.icon); - // TODO save color ALIAS as well - }); - } - - render() { - return html` - - - -
- - Keyboard Shortcuts - - ALT - + - shift - + - k - - -
-
- `; - } } export default UmbDocumentTypeWorkspaceEditorElement; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/document-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/document-type-workspace.context.ts index 4ee41fc2b7..9545be917d 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/document-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/document-type-workspace.context.ts @@ -2,7 +2,11 @@ import { UmbWorkspaceContext } from '../../../shared/components/workspace/worksp import { UmbDocumentTypeRepository } from '../repository/document-type.repository'; import { UmbWorkspacePropertyStructureManager } from '../../../shared/components/workspace/workspace-context/workspace-structure-manager.class'; import { UmbEntityWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; -import type { DocumentTypeResponseModel } from '@umbraco-cms/backoffice/backend-api'; +import type { + ContentTypeCompositionModel, + ContentTypeSortModel, + DocumentTypeResponseModel, +} from '@umbraco-cms/backoffice/backend-api'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; type EntityType = DocumentTypeResponseModel; @@ -80,12 +84,43 @@ export class UmbDocumentTypeWorkspaceContext setAlias(alias: string) { this.structure.updateRootDocumentType({ alias }); } + setDescription(description: string) { + this.structure.updateRootDocumentType({ description }); + } - // TODO => manage setting icon color + // TODO: manage setting icon color alias? setIcon(icon: string) { this.structure.updateRootDocumentType({ icon }); } + setAllowedAsRoot(allowedAsRoot: boolean) { + this.structure.updateRootDocumentType({ allowedAsRoot }); + } + setVariesByCulture(variesByCulture: boolean) { + this.structure.updateRootDocumentType({ variesByCulture }); + } + setVariesBySegment(variesBySegment: boolean) { + this.structure.updateRootDocumentType({ variesBySegment }); + } + setIsElement(isElement: boolean) { + this.structure.updateRootDocumentType({ isElement }); + } + setAllowedContentTypes(allowedContentTypes: Array) { + this.structure.updateRootDocumentType({ allowedContentTypes }); + } + setCompositions(compositions: Array) { + this.structure.updateRootDocumentType({ compositions }); + } + + // Document type specific: + setAllowedTemplateIds(allowedTemplateIds: Array) { + console.log('setAllowedTemplateIds', allowedTemplateIds); + this.structure.updateRootDocumentType({ allowedTemplateIds }); + } + setDefaultTemplateId(defaultTemplateId: string) { + this.structure.updateRootDocumentType({ defaultTemplateId }); + } + async createScaffold(parentId: string) { const { data } = await this.structure.createScaffold(parentId); if (!data) return undefined; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-edit-properties.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-edit-properties.element.ts index 73eea4e334..10e37d583a 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-edit-properties.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-edit-properties.element.ts @@ -2,17 +2,53 @@ import { css, html } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, property, state } from 'lit/decorators.js'; import { repeat } from 'lit/directives/repeat.js'; +import { ifDefined } from 'lit/directives/if-defined.js'; import { UmbWorkspacePropertyStructureHelper } from '../../../../../shared/components/workspace/workspace-context/workspace-property-structure-helper.class'; import { PropertyContainerTypes } from '../../../../../shared/components/workspace/workspace-context/workspace-structure-manager.class'; +import { UmbSorterController, UmbSorterConfig } from '@umbraco-cms/backoffice/sorter'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; import { DocumentTypePropertyTypeResponseModel } from '@umbraco-cms/backoffice/backend-api'; import { UMB_MODAL_CONTEXT_TOKEN, UMB_PROPERTY_SETTINGS_MODAL } from '@umbraco-cms/backoffice/modal'; import './document-type-workspace-view-edit-property.element'; +const SORTER_CONFIG: UmbSorterConfig = { + compareElementToModel: (element: HTMLElement, model: DocumentTypePropertyTypeResponseModel) => { + return element.getAttribute('data-umb-property-id') === model.id; + }, + querySelectModelToElement: (container: HTMLElement, modelEntry: DocumentTypePropertyTypeResponseModel) => { + return container.querySelector('data-umb-property-id[' + modelEntry.id + ']'); + }, + identifier: 'content-type-property-sorter', + itemSelector: '[data-umb-property-id]', + disabledItemSelector: '[inherited]', + containerSelector: '#property-list', +}; @customElement('umb-document-type-workspace-view-edit-properties') export class UmbDocumentTypeWorkspaceViewEditPropertiesElement extends UmbLitElement { + #propertySorter = new UmbSorterController(this, { + ...SORTER_CONFIG, + performItemInsert: (args) => { + let sortOrder = 0; + if (this._propertyStructure.length > 0) { + if (args.newIndex === 0) { + // TODO: Remove 'as any' when sortOrder is added to the model: + sortOrder = ((this._propertyStructure[0] as any).sortOrder ?? 0) - 1; + } else { + sortOrder = + ((this._propertyStructure[Math.min(args.newIndex, this._propertyStructure.length - 1)] as any).sortOrder ?? + 0) + 1; + } + } + return this._propertyStructureHelper.insertProperty(args.item, sortOrder); + }, + performItemRemove: (args) => { + return this._propertyStructureHelper.removeProperty(args.item.id!); + }, + }); + private _containerId: string | undefined; + @property({ type: String, attribute: 'container-id', reflect: false }) public get containerId(): string | undefined { return this._containerId; } @@ -52,6 +88,7 @@ export class UmbDocumentTypeWorkspaceViewEditPropertiesElement extends UmbLitEle this.consumeContext(UMB_MODAL_CONTEXT_TOKEN, (instance) => (this.#modalContext = instance)); this.observe(this._propertyStructureHelper.propertyStructure, (propertyStructure) => { this._propertyStructure = propertyStructure; + this.#propertySorter.setModel(this._propertyStructure); }); } @@ -70,50 +107,29 @@ export class UmbDocumentTypeWorkspaceViewEditPropertiesElement extends UmbLitEle } render() { - return html`${repeat( - this._propertyStructure, - (property) => property.alias, - (property) => - html` { - this._propertyStructureHelper.partialUpdateProperty(property.id, event.detail); - }}>` - )} Add property `; + return html`
+ ${repeat( + this._propertyStructure, + (property) => property.alias ?? '' + property.containerId ?? '' + (property as any).sortOrder ?? '', + (property) => + html` { + this._propertyStructureHelper.partialUpdateProperty(property.id, event.detail); + }}>` + )} +
+ Add property `; } static styles = [ UUITextStyles, css` - .property:first-of-type { - padding-top: 0; - } - .property { - border-bottom: 1px solid var(--uui-color-divider); - } - .property:last-child { - border-bottom: 0; - } - - .property { - display: grid; - grid-template-columns: 200px auto; - column-gap: var(--uui-size-layout-2); - border-bottom: 1px solid var(--uui-color-divider); - padding: var(--uui-size-layout-1) 0; - container-type: inline-size; - } - - .property > div { - grid-column: span 2; - } - - @container (width > 600px) { - .property:not([orientation='vertical']) > div { - grid-column: span 1; - } - } - #add { width: 100%; } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-edit-property.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-edit-property.element.ts index 45fad85fcb..fe56c18d60 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-edit-property.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-edit-property.element.ts @@ -12,13 +12,23 @@ import { PropertyTypeResponseModelBaseModel } from '@umbraco-cms/backoffice/back export class UmbDocumentTypeWorkspacePropertyElement extends LitElement { /** * Property, the data object for the property. - * @type {string} + * @type {PropertyTypeResponseModelBaseModel} * @attr - * @default '' + * @default undefined */ @property({ type: Object }) public property?: PropertyTypeResponseModelBaseModel; + /** + * Inherited, Determines if the property is part of the main document type thats being edited. + * If true, then the property is inherited from another document type, not a part of the main document type. + * @type {boolean} + * @attr + * @default undefined + */ + @property({ type: Boolean }) + public inherited?: boolean; + _firePartialUpdate(propertyName: string, value: string | number | boolean | null | undefined) { const partialObject = {} as any; partialObject[propertyName] = value; @@ -26,8 +36,20 @@ export class UmbDocumentTypeWorkspacePropertyElement extends LitElement { this.dispatchEvent(new CustomEvent('partial-property-update', { detail: partialObject })); } - render() { - // TODO: Only show alias on label if user has access to DocumentType within settings: + renderInheritedProperty() { + return this.property + ? html` + +
+ ` + : ''; + } + + renderEditableProperty() { return this.property ? html` -
+
` : ''; } + render() { + // TODO: Only show alias on label if user has access to DocumentType within settings: + return this.inherited ? this.renderInheritedProperty() : this.renderEditableProperty(); + } + static styles = [ UUITextStyles, css` @@ -81,9 +108,28 @@ export class UmbDocumentTypeWorkspacePropertyElement extends LitElement { border-bottom: none; } - :host-context(umb-variantable-property:first-of-type) { + :host(:first-of-type) { padding-top: 0; } + :host([draggable='true']) { + cursor: grab; + } + + /* Placeholder style, used when property is being dragged.*/ + :host(.--umb-sorter-placeholder) { + height: 2px; + } + :host(.--umb-sorter-placeholder) > div { + display: none; + } + :host(.--umb-sorter-placeholder)::after { + content: ''; + grid-column: span 2; + width: 100%; + border-top: 2px solid blue; + border-radius: 1px; + /* TODO: Make use of same highlight color as UUI and the same Animation. Consider making this a component/(available style) in UUI? */ + } p { margin-bottom: 0; @@ -95,6 +141,10 @@ export class UmbDocumentTypeWorkspacePropertyElement extends LitElement { height: min-content; z-index: 2; } + + #editor { + background-color: var(--uui-color-background); + } `, ]; } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-edit-tab.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-edit-tab.element.ts index 51782de0b7..8bf2ac1618 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-edit-tab.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-edit-tab.element.ts @@ -78,7 +78,7 @@ export class UmbDocumentTypeWorkspaceViewEditTabElement extends UmbLitElement { #onAddGroup = () => { // Idea, maybe we can gather the sortOrder from the last group rendered and add 1 to it? - this._groupStructureHelper.addGroup(this._ownerTabId); + this._groupStructureHelper.addContainer(this._ownerTabId); }; render() { @@ -87,6 +87,7 @@ export class UmbDocumentTypeWorkspaceViewEditTabElement extends UmbLitElement { ? html` @@ -97,6 +98,7 @@ export class UmbDocumentTypeWorkspaceViewEditTabElement extends UmbLitElement { (group) => group.name, (group) => html` ` diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-edit.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-edit.element.ts index b09416588d..f6fe6e032d 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-edit.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-edit.element.ts @@ -120,6 +120,11 @@ export class UmbDocumentTypeWorkspaceViewEditElement extends UmbLitElement { this._routes = routes; } + #requestRemoveTab(tabId: string | undefined) { + // TODO: If this tab is composed of other tabs, then notify that it will only delete the local tab. + // TODO: Update URL when removing tab. + this.#remove(tabId); + } #remove(tabId: string | undefined) { if (!tabId) return; this._workspaceContext?.structure.removeContainer(null, tabId); @@ -142,19 +147,31 @@ export class UmbDocumentTypeWorkspaceViewEditElement extends UmbLitElement { : ''} ${repeat( this._tabs, - (tab) => tab.id, + (tab) => tab.id! + tab.name, (tab) => { // TODO: make better url folder name: const path = this._routerPath + '/tab/' + encodeURI(tab.name || ''); return html` - ${path === this._activePath - ? html` - + ${path === this._activePath && this._tabsStructureHelper.isOwnerContainer(tab.id!) + ? html` { + const newName = (e.target as HTMLInputElement).value; + this._tabsStructureHelper.partialUpdateContainer(tab.id, { + name: newName, + }); + + // Update the current URL, so we are still on this specific tab: + window.history.replaceState(null, '', this._routerPath + '/tab/' + encodeURI(newName)); + }}> this.#remove(tab.id)} + @click=${() => this.#requestRemoveTab(tab.id)} compact> diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/details/document-type-workspace-view-details.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/details/document-type-workspace-view-details.element.ts index 803fcf76a0..b81255e9c4 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/details/document-type-workspace-view-details.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/details/document-type-workspace-view-details.element.ts @@ -1,13 +1,102 @@ import { css, html } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, state } from 'lit/decorators.js'; +import type { UUIToggleElement } from '@umbraco-ui/uui'; import { UmbDocumentTypeWorkspaceContext } from '../../document-type-workspace.context'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; -import type { DocumentTypeResponseModel } from '@umbraco-cms/backoffice/backend-api'; import { UMB_ENTITY_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/context-api'; @customElement('umb-document-type-workspace-view-details') export class UmbDocumentTypeWorkspaceViewDetailsElement extends UmbLitElement { + #workspaceContext?: UmbDocumentTypeWorkspaceContext; + + @state() + private _variesByCulture?: boolean; + @state() + private _variesBySegment?: boolean; + @state() + private _isElement?: boolean; + + constructor() { + super(); + + // TODO: Figure out if this is the best way to consume the context or if it can be strongly typed with an UmbContextToken + this.consumeContext(UMB_ENTITY_WORKSPACE_CONTEXT, (documentTypeContext) => { + this.#workspaceContext = documentTypeContext as UmbDocumentTypeWorkspaceContext; + this._observeDocumentType(); + }); + } + + private _observeDocumentType() { + if (!this.#workspaceContext) return; + this.observe( + this.#workspaceContext.variesByCulture, + (variesByCulture) => (this._variesByCulture = variesByCulture) + ); + this.observe( + this.#workspaceContext.variesBySegment, + (variesBySegment) => (this._variesBySegment = variesBySegment) + ); + this.observe(this.#workspaceContext.isElement, (isElement) => (this._isElement = isElement)); + } + + render() { + return html` + + +
Allow editors to create content of different languages.
+
+ { + this.#workspaceContext?.setVariesByCulture((e.target as UUIToggleElement).checked); + }} + label="Vary by culture"> +
+
+ +
Allow editors to segment their content.
+
+ { + this.#workspaceContext?.setVariesBySegment((e.target as UUIToggleElement).checked); + }} + label="Vary by segments"> +
+
+ +
+ An Element Type is used for content instances in Property Editors, like the Block Editors. +
+
+ { + this.#workspaceContext?.setIsElement((e.target as UUIToggleElement).checked); + }} + label="Element type"> +
+
+
+ + +
+ Allow overriding the global history cleanup settings. (TODO: this ui is not working.. ) +
+
+ + + Keep all versions newer than X days + + Keep latest version per day for X days + +
+
+
+ `; + } + static styles = [ UUITextStyles, css` @@ -26,57 +115,6 @@ export class UmbDocumentTypeWorkspaceViewDetailsElement extends UmbLitElement { } `, ]; - - private _workspaceContext?: UmbDocumentTypeWorkspaceContext; - - constructor() { - super(); - - // TODO: Figure out if this is the best way to consume the context or if it can be strongly typed with an UmbContextToken - this.consumeContext(UMB_ENTITY_WORKSPACE_CONTEXT, (documentTypeContext) => { - this._workspaceContext = documentTypeContext as UmbDocumentTypeWorkspaceContext; - this._observeDocumentType(); - }); - } - - private _observeDocumentType() { - if (!this._workspaceContext) return; - } - - render() { - return html` - - -
Allow editors to create content of different languages.
-
-
- -
Allow editors to segment their content.
-
-
- -
- An Element Type is used for content instances in Property Editors, like the Block Editors. -
-
-
-
- - -
- Allow overriding the global history cleanup settings. (TODO: this ui is not working.. ) -
-
- - Keep all versions newer than X days - - Keep latest version per day for X days - -
-
-
- `; - } } export default UmbDocumentTypeWorkspaceViewDetailsElement; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/structure/document-type-workspace-view-structure.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/structure/document-type-workspace-view-structure.element.ts index 94ee23568c..a5a0e97b64 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/structure/document-type-workspace-view-structure.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/structure/document-type-workspace-view-structure.element.ts @@ -1,13 +1,90 @@ import { css, html } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, state } from 'lit/decorators.js'; +import type { UUIToggleElement } from '@umbraco-ui/uui'; import { UmbDocumentTypeWorkspaceContext } from '../../document-type-workspace.context'; +import type { UmbInputDocumentTypePickerElement } from '../../../../../../backoffice/shared/components/input-document-type-picker/input-document-type-picker.element'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; -import type { DocumentTypeResponseModel } from '@umbraco-cms/backoffice/backend-api'; import { UMB_ENTITY_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/context-api'; @customElement('umb-document-type-workspace-view-structure') export class UmbDocumentTypeWorkspaceViewStructureElement extends UmbLitElement { + #workspaceContext?: UmbDocumentTypeWorkspaceContext; + + @state() + private _allowedAsRoot?: boolean; + + @state() + private _allowedContentTypeIDs?: Array; + + constructor() { + super(); + + // TODO: Figure out if this is the best way to consume the context or if it can be strongly typed with an UmbContextToken + this.consumeContext(UMB_ENTITY_WORKSPACE_CONTEXT, (documentTypeContext) => { + this.#workspaceContext = documentTypeContext as UmbDocumentTypeWorkspaceContext; + this._observeDocumentType(); + }); + } + + private _observeDocumentType() { + if (!this.#workspaceContext) return; + this.observe(this.#workspaceContext.allowedAsRoot, (allowedAsRoot) => (this._allowedAsRoot = allowedAsRoot)); + this.observe(this.#workspaceContext.allowedContentTypes, (allowedContentTypes) => { + this._allowedContentTypeIDs = allowedContentTypes + ?.map((x) => x.id) + .filter((x) => x !== undefined) as Array; + console.log('this._allowedContentTypeIDs', this._allowedContentTypeIDs); + }); + } + + render() { + return html` + + +
Allow editors to create content of this type in the root of the content tree.
+
+ { + this.#workspaceContext?.setAllowedAsRoot((e.target as UUIToggleElement).checked); + }}> +
+
+ +
+ Allow content of the specified types to be created underneath content of this type. +
+
+ + + +
+
+
+ + +
+ Use this document as a collection, displaying its children in a Collection View. This could be a list or a + table. +
+
+
+
+ `; + } + static styles = [ UUITextStyles, css` @@ -26,51 +103,6 @@ export class UmbDocumentTypeWorkspaceViewStructureElement extends UmbLitElement } `, ]; - - private _workspaceContext?: UmbDocumentTypeWorkspaceContext; - - constructor() { - super(); - - // TODO: Figure out if this is the best way to consume the context or if it can be strongly typed with an UmbContextToken - this.consumeContext(UMB_ENTITY_WORKSPACE_CONTEXT, (documentTypeContext) => { - this._workspaceContext = documentTypeContext as UmbDocumentTypeWorkspaceContext; - this._observeDocumentType(); - }); - } - - private _observeDocumentType() { - if (!this._workspaceContext) return; - } - - render() { - return html` - - -
Allow editors to create content of this type in the root of the content tree.
-
-
- -
- Allow content of the specified types to be created underneath content of this type. -
-
- - -
-
-
- - -
- Use this document as a collection, displaying its children in a Collection View. This could be a list or a - table. -
-
-
-
- `; - } } export default UmbDocumentTypeWorkspaceViewStructureElement; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/templates/document-type-workspace-view-templates.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/templates/document-type-workspace-view-templates.element.ts index 8563844b9f..6c549d0db5 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/templates/document-type-workspace-view-templates.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/templates/document-type-workspace-view-templates.element.ts @@ -1,12 +1,63 @@ import { css, html } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; -import { customElement } from 'lit/decorators.js'; +import { customElement, state } from 'lit/decorators.js'; import { UmbDocumentTypeWorkspaceContext } from '../../document-type-workspace.context'; +import type { UmbInputTemplateElement } from '../../../../../../backoffice/shared/components/input-template/input-template.element'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; import { UMB_ENTITY_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/context-api'; @customElement('umb-document-type-workspace-view-templates') export class UmbDocumentTypeWorkspaceViewTemplatesElement extends UmbLitElement { + #workspaceContext?: UmbDocumentTypeWorkspaceContext; + + @state() + private _defaultTemplateId?: string | null; + + @state() + private _allowedTemplateIds?: Array; + + constructor() { + super(); + this.consumeContext(UMB_ENTITY_WORKSPACE_CONTEXT, (documentTypeContext) => { + this.#workspaceContext = documentTypeContext as UmbDocumentTypeWorkspaceContext; + this._observeDocumentType(); + }); + } + + private _observeDocumentType() { + if (!this.#workspaceContext) return; + this.observe( + this.#workspaceContext.defaultTemplateId, + (defaultTemplateId) => (this._defaultTemplateId = defaultTemplateId) + ); + this.observe(this.#workspaceContext.allowedTemplateIds, (allowedTemplateIds) => { + console.log('allowedTemplateIds', allowedTemplateIds); + this._allowedTemplateIds = allowedTemplateIds; + }); + } + + #templateInputChange(e: CustomEvent) { + console.log('change', e); + // save new allowed ids + const input = e.target as UmbInputTemplateElement; + this.#workspaceContext?.setAllowedTemplateIds(input.selectedIds); + this.#workspaceContext?.setDefaultTemplateId(input.defaultId); + } + + render() { + return html` + +
Choose which templates editors are allowed to use on content of this type
+
+ +
+
+
`; + } + static styles = [ UUITextStyles, css` @@ -34,45 +85,6 @@ export class UmbDocumentTypeWorkspaceViewTemplatesElement extends UmbLitElement } `, ]; - - private _workspaceContext?: UmbDocumentTypeWorkspaceContext; - - constructor() { - super(); - this.consumeContext(UMB_ENTITY_WORKSPACE_CONTEXT, (documentTypeContext) => { - this._workspaceContext = documentTypeContext as UmbDocumentTypeWorkspaceContext; - this._observeDocumentType(); - }); - } - - private _observeDocumentType() { - if (!this._workspaceContext) return; - } - - async #changeDefaultId(e: CustomEvent) { - // save new default id - console.log('workspace: default template id', e); - } - - #changeAllowedKeys(e: CustomEvent) { - // save new allowed ids - console.log('workspace: allowed templates changed', e); - } - - render() { - return html` - -
Choose which templates editors are allowed to use on content of this type
-
- -
-
-
`; - } } export default UmbDocumentTypeWorkspaceViewTemplatesElement; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/document.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/document.repository.ts index 91d485f05c..cde51f788c 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/document.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/document.repository.ts @@ -1,7 +1,7 @@ import { UmbDocumentServerDataSource } from './sources/document.server.data'; import { UmbDocumentStore, UMB_DOCUMENT_STORE_CONTEXT_TOKEN } from './document.store'; import { UmbDocumentTreeStore, UMB_DOCUMENT_TREE_STORE_CONTEXT_TOKEN } from './document.tree.store'; -import { DocumentTreeServerDataSource } from './sources/document.tree.server.data'; +import { UmbDocumentTreeServerDataSource } from './sources/document.tree.server.data'; import type { UmbTreeDataSource, UmbTreeRepository, UmbDetailRepository } from '@umbraco-cms/backoffice/repository'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; @@ -32,7 +32,7 @@ export class UmbDocumentRepository implements UmbTreeRepository, UmbDe this.#host = host; // TODO: figure out how spin up get the correct data source - this.#treeSource = new DocumentTreeServerDataSource(this.#host); + this.#treeSource = new UmbDocumentTreeServerDataSource(this.#host); this.#detailDataSource = new UmbDocumentServerDataSource(this.#host); this.#init = Promise.all([ @@ -82,7 +82,7 @@ export class UmbDocumentRepository implements UmbTreeRepository, UmbDe return { data, error, asObservable: () => this.#treeStore!.childrenOf(parentId) }; } - async requestTreeItems(ids: Array) { + async requestItemsLegacy(ids: Array) { await this.#init; if (!ids) { @@ -105,7 +105,7 @@ export class UmbDocumentRepository implements UmbTreeRepository, UmbDe return this.#treeStore!.childrenOf(parentId); } - async treeItems(ids: Array) { + async itemsLegacy(ids: Array) { await this.#init; return this.#treeStore!.items(ids); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/document.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/document.store.ts index d53818f95e..b1f023bd78 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/document.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/document.store.ts @@ -1,6 +1,6 @@ import { DocumentResponseModel } from '@umbraco-cms/backoffice/backend-api'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import { ArrayState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; import { UmbStoreBase } from '@umbraco-cms/backoffice/store'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; @@ -11,15 +11,13 @@ import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; * @description - Data Store for Template Details */ export class UmbDocumentStore extends UmbStoreBase { - #data = new ArrayState([], (x) => x.id); - /** * Creates an instance of UmbDocumentDetailStore. * @param {UmbControllerHostElement} host * @memberof UmbDocumentDetailStore */ constructor(host: UmbControllerHostElement) { - super(host, UMB_DOCUMENT_STORE_CONTEXT_TOKEN.toString()); + super(host, UMB_DOCUMENT_STORE_CONTEXT_TOKEN.toString(), new UmbArrayState([], (x) => x.id)); } /** @@ -28,7 +26,7 @@ export class UmbDocumentStore extends UmbStoreBase { * @memberof UmbDocumentDetailStore */ append(document: DocumentResponseModel) { - this.#data.append([document]); + this._data.append([document]); } /** @@ -37,7 +35,7 @@ export class UmbDocumentStore extends UmbStoreBase { * @memberof UmbDocumentStore */ byKey(id: DocumentResponseModel['id']) { - return this.#data.getObservablePart((x) => x.find((y) => y.id === id)); + return this._data.getObservablePart((x) => x.find((y) => y.id === id)); } /** @@ -46,7 +44,7 @@ export class UmbDocumentStore extends UmbStoreBase { * @memberof UmbDocumentDetailStore */ remove(uniques: Array) { - this.#data.remove(uniques); + this._data.remove(uniques); } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/sources/document.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/sources/document.server.data.ts index ae6a501479..00aca296d9 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/sources/document.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/sources/document.server.data.ts @@ -90,26 +90,7 @@ export class UmbDocumentServerDataSource async insert(document: CreateDocumentRequestModel & { id: string }) { if (!document.id) throw new Error('Id is missing'); - let body: string; - - try { - body = JSON.stringify(document); - } catch (error) { - console.error(error); - return Promise.reject(); - } - //return tryExecuteAndNotify(this.#host, DocumentResource.postDocument(payload)); - // TODO: use resources when end point is ready: - return tryExecuteAndNotify( - this.#host, - fetch('/umbraco/management/api/v1/document/save', { - method: 'POST', - body: body, - headers: { - 'Content-Type': 'application/json', - }, - }) as any - ); + return tryExecuteAndNotify(this.#host, DocumentResource.postDocument({ requestBody: document })); } /** @@ -118,33 +99,10 @@ export class UmbDocumentServerDataSource * @return {*} * @memberof UmbDocumentServerDataSource */ - async update(id: string, document: DocumentResponseModel) { - if (!document.id) { - const error: ProblemDetailsModel = { title: 'Document id is missing' }; - return { error }; - } - //const payload = { id: document.id, requestBody: document }; + async update(id: string, document: UpdateDocumentRequestModel) { + if (!id) throw new Error('Id is missing'); - let body: string; - - try { - body = JSON.stringify(document); - } catch (error) { - const myError: ProblemDetailsModel = { title: 'JSON could not parse' }; - return { error: myError }; - } - - // TODO: use resources when end point is ready: - return tryExecuteAndNotify( - this.#host, - fetch('/umbraco/management/api/v1/document/save', { - method: 'POST', - body: body, - headers: { - 'Content-Type': 'application/json', - }, - }) as any - ); + return tryExecuteAndNotify(this.#host, DocumentResource.putDocumentById({ id, requestBody: document })); } /** diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/sources/document.tree.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/sources/document.tree.server.data.ts index 076b707b0b..9d09c3bcc3 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/sources/document.tree.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/sources/document.tree.server.data.ts @@ -6,10 +6,10 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; /** * A data source for the Document tree that fetches data from the server * @export - * @class DocumentTreeServerDataSource - * @implements {DocumentTreeDataSource} + * @class UmbDocumentTreeServerDataSource + * @implements {UmbTreeDataSource} */ -export class DocumentTreeServerDataSource implements UmbTreeDataSource { +export class UmbDocumentTreeServerDataSource implements UmbTreeDataSource { #host: UmbControllerHostElement; // TODO: how do we handle trashed items? @@ -42,9 +42,9 @@ export class DocumentTreeServerDataSource implements UmbTreeDataSource { } /** - * Creates an instance of DocumentTreeServerDataSource. + * Creates an instance of UmbDocumentTreeServerDataSource. * @param {UmbControllerHostElement} host - * @memberof DocumentTreeServerDataSource + * @memberof UmbDocumentTreeServerDataSource */ constructor(host: UmbControllerHostElement) { this.#host = host; @@ -53,7 +53,7 @@ export class DocumentTreeServerDataSource implements UmbTreeDataSource { /** * Fetches the root items for the tree from the server * @return {*} - * @memberof DocumentTreeServerDataSource + * @memberof UmbDocumentTreeServerDataSource */ async getRootItems() { return tryExecuteAndNotify(this.#host, DocumentResource.getTreeDocumentRoot({})); @@ -63,7 +63,7 @@ export class DocumentTreeServerDataSource implements UmbTreeDataSource { * Fetches the children of a given parent id from the server * @param {(string | null)} parentId * @return {*} - * @memberof DocumentTreeServerDataSource + * @memberof UmbDocumentTreeServerDataSource */ async getChildrenOf(parentId: string | null) { if (!parentId) { @@ -83,7 +83,7 @@ export class DocumentTreeServerDataSource implements UmbTreeDataSource { * Fetches the items for the given ids from the server * @param {Array} ids * @return {*} - * @memberof DocumentTreeServerDataSource + * @memberof UmbDocumentTreeServerDataSource */ async getItems(ids: Array) { if (!ids) { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace-editor.element.ts index dd2f4c5fdf..defdf43afb 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace-editor.element.ts @@ -14,17 +14,6 @@ import { UMB_ENTITY_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/context-ap @customElement('umb-document-workspace-editor') export class UmbDocumentWorkspaceEditorElement extends UmbLitElement { - static styles = [ - UUITextStyles, - css` - :host { - display: block; - width: 100%; - height: 100%; - } - `, - ]; - //private _defaultVariant?: VariantViewModelBaseModel; private splitViewElement = new UmbDocumentWorkspaceSplitViewElement(); @@ -132,6 +121,17 @@ export class UmbDocumentWorkspaceEditorElement extends UmbLitElement { >` : ''; } + + static styles = [ + UUITextStyles, + css` + :host { + display: block; + width: 100%; + height: 100%; + } + `, + ]; } export default UmbDocumentWorkspaceEditorElement; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace-split-view.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace-split-view.element.ts index 936b5c7783..1f32d29b73 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace-split-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace-split-view.element.ts @@ -10,30 +10,6 @@ import { UMB_ENTITY_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/context-ap @customElement('umb-document-workspace-split-view') export class UmbDocumentWorkspaceSplitViewElement extends UmbLitElement { - static styles = [ - UUITextStyles, - css` - :host { - width: 100%; - height: 100%; - - display: flex; - flex: 1; - flex-direction: column; - } - - #splitViews { - display: flex; - width: 100%; - height: calc(100% - var(--umb-footer-layout-height)); - } - - #breadcrumbs { - margin: 0 var(--uui-size-layout-1); - } - `, - ]; - private _workspaceContext?: UmbDocumentWorkspaceContext; @state() @@ -82,6 +58,30 @@ export class UmbDocumentWorkspaceSplitViewElement extends UmbLitElement { ` : nothing; } + + static styles = [ + UUITextStyles, + css` + :host { + width: 100%; + height: 100%; + + display: flex; + flex: 1; + flex-direction: column; + } + + #splitViews { + display: flex; + width: 100%; + height: calc(100% - var(--umb-footer-layout-height)); + } + + #breadcrumbs { + margin: 0 var(--uui-size-layout-1); + } + `, + ]; } export default UmbDocumentWorkspaceSplitViewElement; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace.context.ts index e4d3f0b984..3b38145096 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace.context.ts @@ -6,7 +6,11 @@ import { UmbVariantId } from '../../../shared/variants/variant-id.class'; import { UmbWorkspacePropertyStructureManager } from '../../../shared/components/workspace/workspace-context/workspace-structure-manager.class'; import { UmbWorkspaceSplitViewManager } from '../../../shared/components/workspace/workspace-context/workspace-split-view-manager.class'; import type { CreateDocumentRequestModel, DocumentResponseModel } from '@umbraco-cms/backoffice/backend-api'; -import { partialUpdateFrozenArray, ObjectState, UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; +import { + partialUpdateFrozenArray, + UmbObjectState, + UmbObserverController, +} from '@umbraco-cms/backoffice/observable-api'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; // TODO: should this context be called DocumentDraft instead of workspace? or should the draft be part of this? @@ -22,12 +26,12 @@ export class UmbDocumentWorkspaceContext * For now lets not share this publicly as it can become confusing. * TODO: Use this to compare, for variants with changes. */ - #document = new ObjectState(undefined); + #document = new UmbObjectState(undefined); /** * The document is the current state/draft version of the document. */ - #draft = new ObjectState(undefined); + #draft = new UmbObjectState(undefined); readonly unique = this.#draft.getObservablePart((data) => data?.id); readonly documentTypeKey = this.#draft.getObservablePart((data) => data?.contentTypeId); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace.element.ts index 19301cf90a..bd99def08f 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace.element.ts @@ -9,8 +9,6 @@ import './document-workspace-editor.element'; @customElement('umb-document-workspace') export class UmbDocumentWorkspaceElement extends UmbLitElement { - static styles = [UUITextStyles]; - #workspaceContext = new UmbDocumentWorkspaceContext(this); #element = document.createElement('umb-document-workspace-editor'); @@ -40,6 +38,8 @@ export class UmbDocumentWorkspaceElement extends UmbLitElement { render() { return html``; } + + static styles = [UUITextStyles]; } export default UmbDocumentWorkspaceElement; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.detail.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.detail.store.ts index d6ef9c0fa9..e258ca33cf 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.detail.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.detail.store.ts @@ -1,7 +1,7 @@ import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; import { UmbStoreBase } from '@umbraco-cms/backoffice/store'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; -import { ArrayState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; import type { MediaTypeDetails } from '@umbraco-cms/backoffice/models'; /** @@ -11,18 +11,16 @@ import type { MediaTypeDetails } from '@umbraco-cms/backoffice/models'; * @description - Details Data Store for Media Types */ export class UmbMediaTypeStore extends UmbStoreBase { - #data = new ArrayState([], (x) => x.id); - constructor(host: UmbControllerHostElement) { - super(host, UMB_MEDIA_TYPE_STORE_CONTEXT_TOKEN.toString()); + super(host, UMB_MEDIA_TYPE_STORE_CONTEXT_TOKEN.toString(), new UmbArrayState([], (x) => x.id)); } append(mediaType: MediaTypeDetails) { - this.#data.append([mediaType]); + this._data.append([mediaType]); } remove(uniques: string[]) { - this.#data.remove(uniques); + this._data.remove(uniques); } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.repository.ts index 898e1ab966..32fdedecbd 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.repository.ts @@ -1,7 +1,7 @@ import { UmbMediaTypeTreeStore, UMB_MEDIA_TYPE_TREE_STORE_CONTEXT_TOKEN } from './media-type.tree.store'; import { UmbMediaTypeDetailServerDataSource } from './sources/media-type.detail.server.data'; import { UmbMediaTypeStore, UMB_MEDIA_TYPE_STORE_CONTEXT_TOKEN } from './media-type.detail.store'; -import { MediaTypeTreeServerDataSource } from './sources/media-type.tree.server.data'; +import { UmbMediaTypeTreeServerDataSource } from './sources/media-type.tree.server.data'; import { ProblemDetailsModel } from '@umbraco-cms/backoffice/backend-api'; import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; @@ -26,7 +26,7 @@ export class UmbMediaTypeRepository implements UmbTreeRepository { this.#host = host; // TODO: figure out how spin up get the correct data source - this.#treeSource = new MediaTypeTreeServerDataSource(this.#host); + this.#treeSource = new UmbMediaTypeTreeServerDataSource(this.#host); this.#detailSource = new UmbMediaTypeDetailServerDataSource(this.#host); this.#init = Promise.all([ @@ -73,7 +73,7 @@ export class UmbMediaTypeRepository implements UmbTreeRepository { return { data, error, asObservable: () => this.#treeStore!.childrenOf(parentId) }; } - async requestTreeItems(ids: Array) { + async requestItemsLegacy(ids: Array) { await this.#init; if (!ids) { @@ -96,7 +96,7 @@ export class UmbMediaTypeRepository implements UmbTreeRepository { return this.#treeStore!.childrenOf(parentId); } - async treeItems(ids: Array) { + async itemsLegacy(ids: Array) { await this.#init; return this.#treeStore!.items(ids); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/sources/media-type.tree.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/sources/media-type.tree.server.data.ts index 6d164599f4..12401f3fe9 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/sources/media-type.tree.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/sources/media-type.tree.server.data.ts @@ -6,10 +6,10 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; /** * A data source for the MediaType tree that fetches data from the server * @export - * @class MediaTypeTreeServerDataSource - * @implements {MediaTypeTreeDataSource} + * @class UmbMediaTypeTreeServerDataSource + * @implements {UmbTreeDataSource} */ -export class MediaTypeTreeServerDataSource implements UmbTreeDataSource { +export class UmbMediaTypeTreeServerDataSource implements UmbTreeDataSource { #host: UmbControllerHostElement; /** @@ -24,7 +24,7 @@ export class MediaTypeTreeServerDataSource implements UmbTreeDataSource { /** * Fetches the root items for the tree from the server * @return {*} - * @memberof MediaTypeTreeServerDataSource + * @memberof UmbMediaTypeTreeServerDataSource */ async getRootItems() { return tryExecuteAndNotify(this.#host, MediaTypeResource.getTreeMediaTypeRoot({})); @@ -34,7 +34,7 @@ export class MediaTypeTreeServerDataSource implements UmbTreeDataSource { * Fetches the children of a given parent id from the server * @param {(string | null)} parentId * @return {*} - * @memberof MediaTypeTreeServerDataSource + * @memberof UmbMediaTypeTreeServerDataSource */ async getChildrenOf(parentId: string | null) { if (!parentId) { @@ -54,7 +54,7 @@ export class MediaTypeTreeServerDataSource implements UmbTreeDataSource { * Fetches the items for the given ids from the server * @param {Array} ids * @return {*} - * @memberof MediaTypeTreeServerDataSource + * @memberof UmbMediaTypeTreeServerDataSource */ async getItems(ids: Array) { if (!ids || ids.length === 0) { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/workspace/media-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/workspace/media-type-workspace.context.ts index 648c880015..921a41b6b1 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/workspace/media-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/workspace/media-type-workspace.context.ts @@ -2,7 +2,7 @@ import { UmbWorkspaceContext } from '../../../shared/components/workspace/worksp import { UmbMediaTypeRepository } from '../repository/media-type.repository'; import { UmbEntityWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; -import { ObjectState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; import type { MediaTypeDetails } from '@umbraco-cms/backoffice/models'; type EntityType = MediaTypeDetails; @@ -10,7 +10,7 @@ export class UmbWorkspaceMediaTypeContext extends UmbWorkspaceContext implements UmbEntityWorkspaceContextInterface { - #data = new ObjectState(undefined); + #data = new UmbObjectState(undefined); data = this.#data.asObservable(); name = this.#data.getObservablePart((data) => data?.name); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/entity-bulk-actions/trash/trash.action.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/entity-bulk-actions/trash/trash.action.ts index 4a40576216..ef1200f2e9 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/entity-bulk-actions/trash/trash.action.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/entity-bulk-actions/trash/trash.action.ts @@ -21,7 +21,7 @@ export class UmbMediaTrashEntityBulkAction extends UmbEntityBulkActionBase { @@ -96,7 +96,7 @@ export class UmbMediaRepository return { data, error, asObservable: () => this.#treeStore!.childrenOf(parentId) }; } - async requestTreeItems(ids: Array) { + async requestItemsLegacy(ids: Array) { await this.#init; if (!ids) { @@ -119,7 +119,7 @@ export class UmbMediaRepository return this.#treeStore!.childrenOf(parentId); } - async treeItems(ids: Array) { + async itemsLegacy(ids: Array) { await this.#init; return this.#treeStore!.items(ids); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/media.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/media.store.ts index ddc1a8b5fc..ba59e42ce4 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/media.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/media.store.ts @@ -1,6 +1,6 @@ import type { MediaDetails } from '../'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import { ArrayState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; import { UmbStoreBase } from '@umbraco-cms/backoffice/store'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; @@ -11,15 +11,13 @@ import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; * @description - Data Store for Template Details */ export class UmbMediaStore extends UmbStoreBase { - #data = new ArrayState([], (x) => x.id); - /** * Creates an instance of UmbMediaStore. * @param {UmbControllerHostElement} host * @memberof UmbMediaStore */ constructor(host: UmbControllerHostElement) { - super(host, UMB_MEDIA_STORE_CONTEXT_TOKEN.toString()); + super(host, UMB_MEDIA_STORE_CONTEXT_TOKEN.toString(), new UmbArrayState([], (x) => x.id)); } /** @@ -28,7 +26,7 @@ export class UmbMediaStore extends UmbStoreBase { * @memberof UmbMediaStore */ append(media: MediaDetails) { - this.#data.append([media]); + this._data.append([media]); } /** @@ -37,7 +35,7 @@ export class UmbMediaStore extends UmbStoreBase { * @memberof UmbMediaStore */ remove(uniques: string[]) { - this.#data.remove(uniques); + this._data.remove(uniques); } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/media.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/media.tree.store.ts index 9c63d9c946..babdf21049 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/media.tree.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/media.tree.store.ts @@ -1,6 +1,6 @@ import { EntityTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import { ArrayState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; import { UmbEntityTreeStore } from '@umbraco-cms/backoffice/store'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; @@ -13,7 +13,7 @@ export const UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken([], (x) => x.id); + #data = new UmbArrayState([], (x) => x.id); /** * Creates an instance of UmbMediaTreeStore. diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/sources/media.tree.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/sources/media.tree.server.data.ts index 096d2b2814..ee50c0ce1d 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/sources/media.tree.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/sources/media.tree.server.data.ts @@ -6,10 +6,10 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; /** * A data source for the Media tree that fetches data from the server * @export - * @class MediaTreeServerDataSource - * @implements {MediaTreeDataSource} + * @class UmbMediaTreeServerDataSource + * @implements {UmbTreeDataSource} */ -export class MediaTreeServerDataSource implements UmbTreeDataSource { +export class UmbMediaTreeServerDataSource implements UmbTreeDataSource { #host: UmbControllerHostElement; // TODO: how do we handle trashed items? @@ -42,9 +42,9 @@ export class MediaTreeServerDataSource implements UmbTreeDataSource { } /** - * Creates an instance of MediaTreeServerDataSource. + * Creates an instance of UmbMediaTreeServerDataSource. * @param {UmbControllerHostElement} host - * @memberof MediaTreeServerDataSource + * @memberof UmbMediaTreeServerDataSource */ constructor(host: UmbControllerHostElement) { this.#host = host; @@ -53,7 +53,7 @@ export class MediaTreeServerDataSource implements UmbTreeDataSource { /** * Fetches the root items for the tree from the server * @return {*} - * @memberof MediaTreeServerDataSource + * @memberof UmbMediaTreeServerDataSource */ async getRootItems() { return tryExecuteAndNotify(this.#host, MediaResource.getTreeMediaRoot({})); @@ -63,7 +63,7 @@ export class MediaTreeServerDataSource implements UmbTreeDataSource { * Fetches the children of a given parent id from the server * @param {(string | null)} parentId * @return {*} - * @memberof MediaTreeServerDataSource + * @memberof UmbMediaTreeServerDataSource */ async getChildrenOf(parentId: string | null) { if (!parentId) { @@ -83,7 +83,7 @@ export class MediaTreeServerDataSource implements UmbTreeDataSource { * Fetches the items for the given ids from the server * @param {Array} ids * @return {*} - * @memberof MediaTreeServerDataSource + * @memberof UmbMediaTreeServerDataSource */ async getItems(ids: Array) { if (!ids) { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/workspace/media-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/workspace/media-workspace.context.ts index 751b587560..9e50b63768 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/workspace/media-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/workspace/media-workspace.context.ts @@ -2,7 +2,7 @@ import { UmbWorkspaceContext } from '../../../shared/components/workspace/worksp import { UmbMediaRepository } from '../repository/media.repository'; import type { MediaDetails } from '../'; import type { UmbEntityWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; -import { appendToFrozenArray, ObjectState } from '@umbraco-cms/backoffice/observable-api'; +import { appendToFrozenArray, UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; type EntityType = MediaDetails; @@ -10,7 +10,7 @@ export class UmbMediaWorkspaceContext extends UmbWorkspaceContext implements UmbEntityWorkspaceContextInterface { - #data = new ObjectState(undefined); + #data = new UmbObjectState(undefined); data = this.#data.asObservable(); name = this.#data.getObservablePart((data) => data?.name); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/member-group.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/member-group.repository.ts index 572e6d7fd0..a405678c1a 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/member-group.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/member-group.repository.ts @@ -1,7 +1,7 @@ import { UmbMemberGroupTreeStore, UMB_MEMBER_GROUP_TREE_STORE_CONTEXT_TOKEN } from './member-group.tree.store'; import { UmbMemberGroupDetailServerDataSource } from './sources/member-group.detail.server.data'; import { UmbMemberGroupStore, UMB_MEMBER_GROUP_STORE_CONTEXT_TOKEN } from './member-group.store'; -import { MemberGroupTreeServerDataSource } from './sources/member-group.tree.server.data'; +import { UmbMemberGroupTreeServerDataSource } from './sources/member-group.tree.server.data'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/notification'; import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; @@ -26,7 +26,7 @@ export class UmbMemberGroupRepository implements UmbTreeRepository, UmbDetailRep constructor(host: UmbControllerHostElement) { this.#host = host; // TODO: figure out how spin up get the correct data source - this.#treeSource = new MemberGroupTreeServerDataSource(this.#host); + this.#treeSource = new UmbMemberGroupTreeServerDataSource(this.#host); this.#detailSource = new UmbMemberGroupDetailServerDataSource(this.#host); new UmbContextConsumerController(this.#host, UMB_MEMBER_GROUP_TREE_STORE_CONTEXT_TOKEN, (instance) => { @@ -59,7 +59,7 @@ export class UmbMemberGroupRepository implements UmbTreeRepository, UmbDetailRep return { data: undefined, error }; } - async requestTreeItems(ids: Array) { + async requestItemsLegacy(ids: Array) { await this.#init; if (!ids) { @@ -82,7 +82,7 @@ export class UmbMemberGroupRepository implements UmbTreeRepository, UmbDetailRep return this.#treeStore!.childrenOf(parentId); } - async treeItems(ids: Array) { + async itemsLegacy(ids: Array) { await this.#init; return this.#treeStore!.items(ids); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/member-group.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/member-group.store.ts index 38fd8ede11..e379594fa5 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/member-group.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/member-group.store.ts @@ -1,6 +1,6 @@ import type { MemberGroupDetails } from '@umbraco-cms/backoffice/models'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import { ArrayState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; import { UmbStoreBase } from '@umbraco-cms/backoffice/store'; @@ -11,18 +11,22 @@ import { UmbStoreBase } from '@umbraco-cms/backoffice/store'; * @description - Data Store for Member Groups */ export class UmbMemberGroupStore extends UmbStoreBase { - #data = new ArrayState([], (x) => x.id); + #data = new UmbArrayState([], (x) => x.id); constructor(host: UmbControllerHostElement) { - super(host, UMB_MEMBER_GROUP_STORE_CONTEXT_TOKEN.toString()); + super( + host, + UMB_MEMBER_GROUP_STORE_CONTEXT_TOKEN.toString(), + new UmbArrayState([], (x) => x.id) + ); } append(memberGroup: MemberGroupDetails) { - this.#data.append([memberGroup]); + this._data.append([memberGroup]); } remove(uniques: string[]) { - this.#data.remove(uniques); + this._data.remove(uniques); } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/sources/member-group.tree.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/sources/member-group.tree.server.data.ts index bfb95a7473..7535e756ea 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/sources/member-group.tree.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/sources/member-group.tree.server.data.ts @@ -6,16 +6,16 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; /** * A data source for the Member Group tree that fetches data from the server * @export - * @class MemberGroupTreeServerDataSource - * @implements {MemberGroupTreeDataSource} + * @class UmbMemberGroupTreeServerDataSource + * @implements {UmbTreeDataSource} */ -export class MemberGroupTreeServerDataSource implements UmbTreeDataSource { +export class UmbMemberGroupTreeServerDataSource implements UmbTreeDataSource { #host: UmbControllerHostElement; /** - * Creates an instance of MemberGroupTreeServerDataSource. + * Creates an instance of UmbMemberGroupTreeServerDataSource. * @param {UmbControllerHostElement} host - * @memberof MemberGroupTreeServerDataSource + * @memberof UmbMemberGroupTreeServerDataSource */ constructor(host: UmbControllerHostElement) { this.#host = host; @@ -24,7 +24,7 @@ export class MemberGroupTreeServerDataSource implements UmbTreeDataSource { /** * Fetches the root items for the tree from the server * @return {*} - * @memberof MemberGroupTreeServerDataSource + * @memberof UmbMemberGroupTreeServerDataSource */ async getRootItems() { return tryExecuteAndNotify(this.#host, MemberGroupResource.getTreeMemberGroupRoot({})); @@ -34,7 +34,7 @@ export class MemberGroupTreeServerDataSource implements UmbTreeDataSource { * Fetches the children of a given parent id from the server * @param {(string | null)} parentId * @return {*} - * @memberof MemberGroupTreeServerDataSource + * @memberof UmbMemberGroupTreeServerDataSource */ async getChildrenOf(parentId: string | null) { // Not implemented for this tree @@ -45,7 +45,7 @@ export class MemberGroupTreeServerDataSource implements UmbTreeDataSource { * Fetches the items for the given ids from the server * @param {Array} ids * @return {*} - * @memberof MemberGroupTreeServerDataSource + * @memberof UmbMemberGroupTreeServerDataSource */ async getItems(ids: Array) { if (!ids || ids.length === 0) { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.context.ts index ecf64222eb..bf8f38eb1e 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.context.ts @@ -3,14 +3,14 @@ import { UmbMemberGroupRepository } from '../repository/member-group.repository' import { UmbEntityWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; import type { MemberGroupDetails } from '@umbraco-cms/backoffice/models'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; -import { ObjectState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; type EntityType = MemberGroupDetails; export class UmbWorkspaceMemberGroupContext extends UmbWorkspaceContext implements UmbEntityWorkspaceContextInterface { - #data = new ObjectState(undefined); + #data = new UmbObjectState(undefined); data = this.#data.asObservable(); name = this.#data.getObservablePart((data) => data?.name); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/member-type.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/member-type.repository.ts index 620347e0b4..960be5ef86 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/member-type.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/member-type.repository.ts @@ -1,4 +1,4 @@ -import { MemberTypeTreeServerDataSource } from './sources/member-type.tree.server.data'; +import { UmbMemberTypeTreeServerDataSource } from './sources/member-type.tree.server.data'; import { UmbMemberTypeTreeStore, UMB_MEMBER_TYPE_TREE_STORE_CONTEXT_TOKEN } from './member-type.tree.store'; import { UmbMemberTypeStore, UMB_MEMBER_TYPE_STORE_CONTEXT_TOKEN } from './member-type.store'; import { UmbMemberTypeDetailServerDataSource } from './sources/member-type.detail.server.data'; @@ -30,7 +30,7 @@ export class UmbMemberTypeRepository implements UmbTreeRepository, this.#host = host; // TODO: figure out how spin up get the correct data source - this.#treeSource = new MemberTypeTreeServerDataSource(this.#host); + this.#treeSource = new UmbMemberTypeTreeServerDataSource(this.#host); this.#detailSource = new UmbMemberTypeDetailServerDataSource(this.#host); this.#init = Promise.all([ @@ -77,7 +77,7 @@ export class UmbMemberTypeRepository implements UmbTreeRepository, return { data, error, asObservable: () => this.#treeStore!.childrenOf(parentId) }; } - async requestTreeItems(ids: Array) { + async requestItemsLegacy(ids: Array) { await this.#init; if (!ids) { @@ -100,7 +100,7 @@ export class UmbMemberTypeRepository implements UmbTreeRepository, return this.#treeStore!.childrenOf(parentId); } - async treeItems(ids: Array) { + async itemsLegacy(ids: Array) { await this.#init; return this.#treeStore!.items(ids); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/member-type.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/member-type.store.ts index b9afd506a2..5dca6dd718 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/member-type.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/member-type.store.ts @@ -1,7 +1,7 @@ import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; import { UmbStoreBase } from '@umbraco-cms/backoffice/store'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; -import { ArrayState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; import type { MemberTypeDetails } from '@umbraco-cms/backoffice/models'; /** @@ -11,18 +11,16 @@ import type { MemberTypeDetails } from '@umbraco-cms/backoffice/models'; * @description - Data Store for Member Types */ export class UmbMemberTypeStore extends UmbStoreBase { - #data = new ArrayState([], (x) => x.id); - constructor(host: UmbControllerHostElement) { - super(host, UMB_MEMBER_TYPE_STORE_CONTEXT_TOKEN.toString()); + super(host, UMB_MEMBER_TYPE_STORE_CONTEXT_TOKEN.toString(), new UmbArrayState([], (x) => x.id)); } append(MemberType: MemberTypeDetails) { - this.#data.append([MemberType]); + this._data.append([MemberType]); } remove(uniques: string[]) { - this.#data.remove(uniques); + this._data.remove(uniques); } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/sources/member-type.tree.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/sources/member-type.tree.server.data.ts index 0c1abf952a..40c09a25fc 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/sources/member-type.tree.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/sources/member-type.tree.server.data.ts @@ -6,10 +6,10 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; /** * A data source for the MemberType tree that fetches data from the server * @export - * @class MemberTypeTreeServerDataSource - * @implements {MemberTypeTreeDataSource} + * @class UmbMemberTypeTreeServerDataSource + * @implements {UmbTreeDataSource} */ -export class MemberTypeTreeServerDataSource implements UmbTreeDataSource { +export class UmbMemberTypeTreeServerDataSource implements UmbTreeDataSource { #host: UmbControllerHostElement; /** @@ -24,7 +24,7 @@ export class MemberTypeTreeServerDataSource implements UmbTreeDataSource { /** * Fetches the root items for the tree from the server * @return {*} - * @memberof MemberTypeTreeServerDataSource + * @memberof UmbMemberTypeTreeServerDataSource */ async getRootItems() { return tryExecuteAndNotify(this.#host, MemberTypeResource.getTreeMemberTypeRoot({})); @@ -34,7 +34,7 @@ export class MemberTypeTreeServerDataSource implements UmbTreeDataSource { * Fetches the children of a given parent id from the server * @param {(string | null)} parentId * @return {*} - * @memberof MemberTypeTreeServerDataSource + * @memberof UmbMemberTypeTreeServerDataSource */ async getChildrenOf(parentId: string | null) { const error: ProblemDetailsModel = { title: 'Not implemented for Member Type' }; @@ -45,7 +45,7 @@ export class MemberTypeTreeServerDataSource implements UmbTreeDataSource { * Fetches the items for the given ids from the server * @param {Array} ids * @return {*} - * @memberof MemberTypeTreeServerDataSource + * @memberof UmbMemberTypeTreeServerDataSource */ async getItems(ids: Array) { if (!ids || ids.length === 0) { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/workspace/member-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/workspace/member-type-workspace.context.ts index 8274bec279..9acaa2b9f7 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/workspace/member-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/workspace/member-type-workspace.context.ts @@ -1,7 +1,7 @@ import { UmbWorkspaceContext } from '../../../shared/components/workspace/workspace-context/workspace-context'; import { UmbMemberTypeRepository } from '../repository/member-type.repository'; import { UmbEntityWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; -import { ObjectState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; // TODO => use correct tpye @@ -11,7 +11,7 @@ export class UmbMemberTypeWorkspaceContext extends UmbWorkspaceContext implements UmbEntityWorkspaceContextInterface { - #data = new ObjectState(undefined); + #data = new UmbObjectState(undefined); name = this.#data.getObservablePart((data) => data?.name); constructor(host: UmbControllerHostElement) { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/member.detail.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/member.detail.store.ts index 973f79f5c4..95520e4d2b 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/member.detail.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/member.detail.store.ts @@ -2,7 +2,7 @@ import { Observable } from 'rxjs'; import { umbMemberData } from '../../../core/mocks/data/member.data'; import type { MemberDetails, MemberGroupDetails } from '@umbraco-cms/backoffice/models'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import { ArrayState, createObservablePart } from '@umbraco-cms/backoffice/observable-api'; +import { UmbArrayState, createObservablePart } from '@umbraco-cms/backoffice/observable-api'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; import { UmbEntityDetailStore, UmbStoreBase } from '@umbraco-cms/backoffice/store'; @@ -13,11 +13,8 @@ import { UmbEntityDetailStore, UmbStoreBase } from '@umbraco-cms/backoffice/stor * @description - Data Store for Members */ export class UmbMemberStore extends UmbStoreBase implements UmbEntityDetailStore { - #data = new ArrayState([], (x) => x.id); - public groups = this.#data.asObservable(); - constructor(private host: UmbControllerHostElement) { - super(host, UMB_MEMBER_STORE_CONTEXT_TOKEN.toString()); + super(host, UMB_MEMBER_STORE_CONTEXT_TOKEN.toString(), new UmbArrayState([], (x) => x.id)); } getScaffold(entityType: string, parentId: string | null) { @@ -40,10 +37,10 @@ export class UmbMemberStore extends UmbStoreBase implements UmbEntityDetailStore // temp until Resource is updated const member = umbMemberData.getById(id); if (member) { - this.#data.appendOne(member); + this._data.appendOne(member); } - return createObservablePart(this.#data, (members) => members.find((member) => member.id === id) as MemberDetails); + return createObservablePart(this._data, (members) => members.find((member) => member.id === id) as MemberDetails); } async save(member: Array): Promise { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/repository/member.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/repository/member.repository.ts index cf91bf0bf9..e5020375a1 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/repository/member.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/repository/member.repository.ts @@ -1,5 +1,5 @@ import { UmbMemberTreeStore, UMB_MEMBER_TREE_STORE_CONTEXT_TOKEN } from './member.tree.store'; -import { MemberTreeServerDataSource } from './sources/member.tree.server.data'; +import { UmbMemberTreeServerDataSource } from './sources/member.tree.server.data'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/notification'; import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; @@ -8,7 +8,7 @@ import { ProblemDetailsModel } from '@umbraco-cms/backoffice/backend-api'; export class UmbMemberRepository implements UmbTreeRepository { #host: UmbControllerHostElement; - #dataSource: MemberTreeServerDataSource; + #dataSource: UmbMemberTreeServerDataSource; #treeStore?: UmbMemberTreeStore; #notificationContext?: UmbNotificationContext; #initResolver?: () => void; @@ -17,7 +17,7 @@ export class UmbMemberRepository implements UmbTreeRepository { constructor(host: UmbControllerHostElement) { this.#host = host; // TODO: figure out how spin up get the correct data source - this.#dataSource = new MemberTreeServerDataSource(this.#host); + this.#dataSource = new UmbMemberTreeServerDataSource(this.#host); new UmbContextConsumerController(this.#host, UMB_MEMBER_TREE_STORE_CONTEXT_TOKEN, (instance) => { this.#treeStore = instance; @@ -58,7 +58,7 @@ export class UmbMemberRepository implements UmbTreeRepository { return { data: undefined, error }; } - async requestTreeItems(ids: Array) { + async requestItemsLegacy(ids: Array) { await this.#init; if (!ids) { @@ -81,7 +81,7 @@ export class UmbMemberRepository implements UmbTreeRepository { return this.#treeStore!.childrenOf(parentId); } - async treeItems(ids: Array) { + async itemsLegacy(ids: Array) { await this.#init; return this.#treeStore!.items(ids); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/repository/member.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/repository/member.store.ts index 4b01b1a8d8..49a7339a76 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/repository/member.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/repository/member.store.ts @@ -1,7 +1,7 @@ import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; import { UmbStoreBase } from '@umbraco-cms/backoffice/store'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; -import { ArrayState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; import type { MemberDetails } from '@umbraco-cms/backoffice/models'; /** @@ -11,18 +11,16 @@ import type { MemberDetails } from '@umbraco-cms/backoffice/models'; * @description - Data Store for Members */ export class UmbMemberStore extends UmbStoreBase { - #data = new ArrayState([], (x) => x.id); - constructor(host: UmbControllerHostElement) { - super(host, UMB_MEMBER_STORE_CONTEXT_TOKEN.toString()); + super(host, UMB_MEMBER_STORE_CONTEXT_TOKEN.toString(), new UmbArrayState([], (x) => x.id)); } append(member: MemberDetails) { - this.#data.append([member]); + this._data.append([member]); } remove(uniques: string[]) { - this.#data.remove(uniques); + this._data.remove(uniques); } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/repository/sources/member.tree.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/repository/sources/member.tree.server.data.ts index b386fb52e2..903c63ef9f 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/repository/sources/member.tree.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/repository/sources/member.tree.server.data.ts @@ -4,16 +4,16 @@ import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; /** * A data source for the Member tree that fetches data from the server * @export - * @class MemberTreeServerDataSource + * @class UmbMemberTreeServerDataSource * @implements {MemberTreeDataSource} */ -export class MemberTreeServerDataSource implements MemberTreeDataSource { +export class UmbMemberTreeServerDataSource implements MemberTreeDataSource { #host: UmbControllerHostElement; /** - * Creates an instance of MemberTreeServerDataSource. + * Creates an instance of UmbMemberTreeServerDataSource. * @param {UmbControllerHostElement} host - * @memberof MemberTreeServerDataSource + * @memberof UmbMemberTreeServerDataSource */ constructor(host: UmbControllerHostElement) { this.#host = host; @@ -22,7 +22,7 @@ export class MemberTreeServerDataSource implements MemberTreeDataSource { /** * Fetches the root items for the tree from the server * @return {*} - * @memberof MemberTreeServerDataSource + * @memberof UmbMemberTreeServerDataSource */ async getRootItems() { const response = await fetch('/umbraco/management/api/v1/tree/member/root'); @@ -36,7 +36,7 @@ export class MemberTreeServerDataSource implements MemberTreeDataSource { * Fetches the items for the given ids from the server * @param {Array} ids * @return {*} - * @memberof MemberTreeServerDataSource + * @memberof UmbMemberTreeServerDataSource */ async getItems(ids: Array) { const response = await fetch('/umbraco/management/api/v1/tree/member/item'); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/packages/package-builder/workspace/workspace-package-builder.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/packages/package-builder/workspace/workspace-package-builder.element.ts index 9dce5a6dca..ccb98189a6 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/packages/package-builder/workspace/workspace-package-builder.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/packages/package-builder/workspace/workspace-package-builder.element.ts @@ -259,7 +259,7 @@ export class UmbWorkspacePackageBuilderElement extends UmbLitElement { #renderDataTypeSection() { return html`
- +
`; } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/packages/repository/package.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/packages/repository/package.store.ts index 8ed4792471..13a9ce6aa9 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/packages/repository/package.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/packages/repository/package.store.ts @@ -5,7 +5,7 @@ import { UmbStoreBase } from '@umbraco-cms/backoffice/store'; import type { UmbPackage } from '@umbraco-cms/backoffice/models'; import type { PackageMigrationStatusResponseModel } from '@umbraco-cms/backoffice/backend-api'; import type { ManifestBase } from '@umbraco-cms/backoffice/extensions-registry'; -import { ArrayState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; export const UMB_PACKAGE_STORE_TOKEN = new UmbContextToken('UmbPackageStore'); @@ -21,9 +21,9 @@ export class UmbPackageStore extends UmbStoreBase { */ #packages = new ReplaySubject>(1); - #extensions = new ArrayState([], (e) => e.alias); + #extensions = new UmbArrayState([], (e) => e.alias); - #migrations = new ArrayState([], (e) => e.packageName); + #migrations = new UmbArrayState([], (e) => e.packageName); /** * Observable of packages with extensions @@ -42,7 +42,9 @@ export class UmbPackageStore extends UmbStoreBase { * @memberof PackageStore */ constructor(host: UmbControllerHostElement) { - super(host, UMB_PACKAGE_STORE_TOKEN.toString()); + // TODO: revisit this store. Is it ok to have multiple data sets? + // temp hack to satisfy the base class + super(host, UMB_PACKAGE_STORE_TOKEN.toString(), new UmbArrayState([], (x) => x.name)); } /** diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/models-builder/dashboard-models-builder.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/models-builder/dashboard-models-builder.element.ts index 249718f61e..34bc597c23 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/models-builder/dashboard-models-builder.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/models-builder/dashboard-models-builder.element.ts @@ -135,7 +135,7 @@ export class UmbDashboardModelsBuilderElement extends UmbLitElement {

${this._modelsBuilder?.lastError ? html`

Last generation failed with the following error:

- ${this._modelsBuilder.lastError}` + ${this._modelsBuilder.lastError}` : nothing} `; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/components/data-type-input/data-type-input.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/components/data-type-input/data-type-input.context.ts new file mode 100644 index 0000000000..06b3e5762e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/components/data-type-input/data-type-input.context.ts @@ -0,0 +1,10 @@ +import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input'; +import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; +import { UMB_DATA_TYPE_PICKER_MODAL } from '@umbraco-cms/backoffice/modal'; +import { DataTypeItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; + +export class UmbDataTypePickerContext extends UmbPickerInputContext { + constructor(host: UmbControllerHostElement) { + super(host, 'Umb.Repository.DataType', UMB_DATA_TYPE_PICKER_MODAL); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/components/data-type-input/data-type-input.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/components/data-type-input/data-type-input.element.ts new file mode 100644 index 0000000000..c8c28070e2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/components/data-type-input/data-type-input.element.ts @@ -0,0 +1,138 @@ +import { css, html } from 'lit'; +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { customElement, property, state } from 'lit/decorators.js'; +import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins'; +import { UmbDataTypePickerContext } from './data-type-input.context'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; +import type { DataTypeItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; + +@customElement('umb-data-type-input') +export class UmbDataTypeInputElement extends FormControlMixin(UmbLitElement) { + /** + * This is a minimum amount of selected items in this input. + * @type {number} + * @attr + * @default 0 + */ + @property({ type: Number }) + public get min(): number { + return this.#pickerContext.min; + } + public set min(value: number) { + this.#pickerContext.min = value; + } + + /** + * Min validation message. + * @type {boolean} + * @attr + * @default + */ + @property({ type: String, attribute: 'min-message' }) + minMessage = 'This field need more items'; + + /** + * This is a maximum amount of selected items in this input. + * @type {number} + * @attr + * @default Infinity + */ + @property({ type: Number }) + public get max(): number { + return this.#pickerContext.max; + } + public set max(value: number) { + this.#pickerContext.max = value; + } + + /** + * Max validation message. + * @type {boolean} + * @attr + * @default + */ + @property({ type: String, attribute: 'min-message' }) + maxMessage = 'This field exceeds the allowed amount of items'; + + public get selectedIds(): Array { + return this.#pickerContext.getSelection(); + } + public set selectedIds(ids: Array) { + this.#pickerContext.setSelection(ids); + } + + @property() + public set value(idsString: string) { + // Its with full purpose we don't call super.value, as thats being handled by the observation of the context selection. + this.selectedIds = idsString.split(/[ ,]+/); + } + + @state() + private _items?: Array; + + #pickerContext = new UmbDataTypePickerContext(this); + + constructor() { + super(); + + this.addValidator( + 'rangeUnderflow', + () => this.minMessage, + () => !!this.min && this.#pickerContext.getSelection().length < this.min + ); + + this.addValidator( + 'rangeOverflow', + () => this.maxMessage, + () => !!this.max && this.#pickerContext.getSelection().length > this.max + ); + + this.observe(this.#pickerContext.selection, (selection) => (super.value = selection.join(','))); + this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems)); + } + + protected getFormElement() { + return undefined; + } + + render() { + return html` + ${this._items?.map((item) => this._renderItem(item))} + this.#pickerContext.openPicker()} label="open" + >Add + `; + } + + private _renderItem(item: DataTypeItemResponseModel) { + if (!item.id) return; + return html` + + + this.#pickerContext.requestRemoveItem(item.id!)} + label="Remove Data Type ${item.name}" + >Remove + + + `; + } + + static styles = [ + UUITextStyles, + css` + #add-button { + width: 100%; + } + `, + ]; +} + +export default UmbDataTypeInputElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-data-type-input': UmbDataTypeInputElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/components/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/components/index.ts new file mode 100644 index 0000000000..c076c5f9a3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/components/index.ts @@ -0,0 +1 @@ +import './data-type-input/data-type-input.element'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/copy/copy.action.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/copy/copy.action.ts new file mode 100644 index 0000000000..9b2fd19b53 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/copy/copy.action.ts @@ -0,0 +1,27 @@ +import { UmbDataTypeRepository } from '../../repository/data-type.repository'; +import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action'; +import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; +import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN, UMB_DATA_TYPE_PICKER_MODAL } from '@umbraco-cms/backoffice/modal'; +import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; + +// TODO: investigate what we need to make a generic copy action +export class UmbCopyDataTypeEntityAction extends UmbEntityActionBase { + #modalContext?: UmbModalContext; + + constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) { + super(host, repositoryAlias, unique); + + new UmbContextConsumerController(this.host, UMB_MODAL_CONTEXT_TOKEN, (instance) => { + this.#modalContext = instance; + }); + } + + async execute() { + if (!this.#modalContext) throw new Error('Modal context is not available'); + if (!this.repository) throw new Error('Repository is not available'); + + const modalHandler = this.#modalContext?.open(UMB_DATA_TYPE_PICKER_MODAL); + const { selection } = await modalHandler.onSubmit(); + await this.repository.copy(this.unique, selection[0]); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/copy/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/copy/manifests.ts new file mode 100644 index 0000000000..726637162b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/copy/manifests.ts @@ -0,0 +1,24 @@ +import { DATA_TYPE_ENTITY_TYPE } from '../..'; +import { DATA_TYPE_REPOSITORY_ALIAS } from '../../repository/manifests'; +import { UmbCopyDataTypeEntityAction } from './copy.action'; +import { ManifestTypes } from '@umbraco-cms/backoffice/extensions-registry'; + +const entityActions: Array = [ + { + type: 'entityAction', + alias: 'Umb.EntityAction.DataType.Copy', + name: 'Copy Data Type Entity Action', + weight: 900, + meta: { + icon: 'umb:documents', + label: 'Copy to...', + repositoryAlias: DATA_TYPE_REPOSITORY_ALIAS, + api: UmbCopyDataTypeEntityAction, + }, + conditions: { + entityType: DATA_TYPE_ENTITY_TYPE, + }, + }, +]; + +export const manifests = [...entityActions]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/manifests.ts index 54e8288b16..64d90c4142 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/manifests.ts @@ -1,5 +1,9 @@ +import { DATA_TYPE_ENTITY_TYPE } from '..'; import { DATA_TYPE_REPOSITORY_ALIAS } from '../repository/manifests'; import { manifests as createManifests } from './create/manifests'; +import { manifests as moveManifests } from './move/manifests'; +import { manifests as copyManifests } from './copy/manifests'; + import { UmbDeleteEntityAction, UmbDeleteFolderEntityAction, @@ -20,7 +24,7 @@ const entityActions: Array = [ api: UmbDeleteEntityAction, }, conditions: { - entityType: 'data-type', + entityType: DATA_TYPE_ENTITY_TYPE, }, }, { @@ -35,7 +39,7 @@ const entityActions: Array = [ api: UmbDeleteFolderEntityAction, }, conditions: { - entityType: 'data-type', + entityType: DATA_TYPE_ENTITY_TYPE, }, }, { @@ -50,9 +54,9 @@ const entityActions: Array = [ api: UmbFolderUpdateEntityAction, }, conditions: { - entityType: 'data-type', + entityType: DATA_TYPE_ENTITY_TYPE, }, }, ]; -export const manifests = [...entityActions, ...createManifests]; +export const manifests = [...entityActions, ...createManifests, ...moveManifests, ...copyManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/move/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/move/manifests.ts new file mode 100644 index 0000000000..5e7ff4e7c6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/move/manifests.ts @@ -0,0 +1,24 @@ +import { DATA_TYPE_ENTITY_TYPE } from '../..'; +import { DATA_TYPE_REPOSITORY_ALIAS } from '../../repository/manifests'; +import { UmbMoveDataTypeEntityAction } from './move.action'; +import { ManifestTypes } from '@umbraco-cms/backoffice/extensions-registry'; + +const entityActions: Array = [ + { + type: 'entityAction', + alias: 'Umb.EntityAction.DataType.Move', + name: 'Move Data Type Entity Action', + weight: 900, + meta: { + icon: 'umb:enter', + label: 'Move to...', + repositoryAlias: DATA_TYPE_REPOSITORY_ALIAS, + api: UmbMoveDataTypeEntityAction, + }, + conditions: { + entityType: DATA_TYPE_ENTITY_TYPE, + }, + }, +]; + +export const manifests = [...entityActions]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/move/move.action.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/move/move.action.ts new file mode 100644 index 0000000000..9d24d65844 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/entity-actions/move/move.action.ts @@ -0,0 +1,27 @@ +import { UmbDataTypeRepository } from '../../repository/data-type.repository'; +import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action'; +import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; +import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN, UMB_DATA_TYPE_PICKER_MODAL } from '@umbraco-cms/backoffice/modal'; +import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; + +// TODO: investigate what we need to make a generic move action +export class UmbMoveDataTypeEntityAction extends UmbEntityActionBase { + #modalContext?: UmbModalContext; + + constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) { + super(host, repositoryAlias, unique); + + new UmbContextConsumerController(this.host, UMB_MODAL_CONTEXT_TOKEN, (instance) => { + this.#modalContext = instance; + }); + } + + async execute() { + if (!this.#modalContext) throw new Error('Modal context is not available'); + if (!this.repository) throw new Error('Repository is not available'); + + const modalHandler = this.#modalContext?.open(UMB_DATA_TYPE_PICKER_MODAL); + const { selection } = await modalHandler.onSubmit(); + await this.repository.move(this.unique, selection[0]); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/index.ts new file mode 100644 index 0000000000..44cb2b0223 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/index.ts @@ -0,0 +1,3 @@ +import './components'; + +export const DATA_TYPE_ENTITY_TYPE = 'data-type'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/manifests.ts index fac58d2628..28e0f18938 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/manifests.ts @@ -3,6 +3,7 @@ import { manifests as repositoryManifests } from './repository/manifests'; import { manifests as menuItemManifests } from './menu-item/manifests'; import { manifests as treeManifests } from './tree/manifests'; import { manifests as workspaceManifests } from './workspace/manifests'; +import { manifests as modalManifests } from './modal/manifests'; export const manifests = [ ...entityActions, @@ -10,4 +11,5 @@ export const manifests = [ ...menuItemManifests, ...treeManifests, ...workspaceManifests, + ...modalManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/modal/data-type-picker/data-type-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/modal/data-type-picker/data-type-picker-modal.element.ts new file mode 100644 index 0000000000..ea1cae5dfb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/modal/data-type-picker/data-type-picker-modal.element.ts @@ -0,0 +1,75 @@ +import { css, html } from 'lit'; +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { customElement, property, state } from 'lit/decorators.js'; +import type { UmbTreeElement } from '../../../../shared/components/tree/tree.element'; +import { + UmbDataTypePickerModalData, + UmbDataTypePickerModalResult, + UmbModalHandler, +} from '@umbraco-cms/backoffice/modal'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; + +// TODO: make use of UmbPickerLayoutBase +@customElement('umb-data-type-picker-modal') +export class UmbDataTypePickerModalElement extends UmbLitElement { + static styles = [UUITextStyles, css``]; + + @property({ attribute: false }) + modalHandler?: UmbModalHandler; + + @property({ type: Object, attribute: false }) + data?: UmbDataTypePickerModalData; + + @state() + _selection: Array = []; + + @state() + _multiple = false; + + connectedCallback() { + super.connectedCallback(); + this._selection = this.data?.selection ?? []; + this._multiple = this.data?.multiple ?? false; + } + + #onSelectionChange(e: CustomEvent) { + e.stopPropagation(); + const element = e.target as UmbTreeElement; + this._selection = element.selection; + } + + #submit() { + this.modalHandler?.submit({ selection: this._selection }); + } + + #close() { + this.modalHandler?.reject(); + } + + render() { + return html` + + + + +
+ + +
+
+ `; + } +} + +export default UmbDataTypePickerModalElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-data-type-picker-modal': UmbDataTypePickerModalElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/modal/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/modal/manifests.ts new file mode 100644 index 0000000000..838aba346b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/modal/manifests.ts @@ -0,0 +1,12 @@ +import type { ManifestModal } from '@umbraco-cms/backoffice/extensions-registry'; + +const modals: Array = [ + { + type: 'modal', + alias: 'Umb.Modal.DataTypePicker', + name: 'Data Type Picker Modal', + loader: () => import('./data-type-picker/data-type-picker-modal.element'), + }, +]; + +export const manifests = [...modals]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/data-type-item.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/data-type-item.store.ts new file mode 100644 index 0000000000..8fe3a5205a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/data-type-item.store.ts @@ -0,0 +1,36 @@ +import { DataTypeItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; +import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; +import { UmbItemStore, UmbStoreBase } from '@umbraco-cms/backoffice/store'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; + +/** + * @export + * @class UmbDataTypeItemStore + * @extends {UmbStoreBase} + * @description - Data Store for Data Type items + */ + +export class UmbDataTypeItemStore + extends UmbStoreBase + implements UmbItemStore +{ + /** + * Creates an instance of UmbDataTypeItemStore. + * @param {UmbControllerHostElement} host + * @memberof UmbDataTypeItemStore + */ + constructor(host: UmbControllerHostElement) { + super( + host, + UMB_DATA_TYPE_ITEM_STORE_CONTEXT_TOKEN.toString(), + new UmbArrayState([], (x) => x.id) + ); + } + + items(ids: Array) { + return this._data.getObservablePart((items) => items.filter((item) => ids.includes(item.id ?? ''))); + } +} + +export const UMB_DATA_TYPE_ITEM_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbDataTypeItemStore'); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/data-type.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/data-type.repository.ts index 21a4d02041..fb2608ef19 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/data-type.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/data-type.repository.ts @@ -1,44 +1,55 @@ import { UmbDataTypeTreeServerDataSource } from './sources/data-type.tree.server.data'; +import { UmbDataTypeMoveServerDataSource } from './sources/data-type-move.server.data'; import { UmbDataTypeStore, UMB_DATA_TYPE_STORE_CONTEXT_TOKEN } from './data-type.store'; import { UmbDataTypeServerDataSource } from './sources/data-type.server.data'; import { UmbDataTypeTreeStore, UMB_DATA_TYPE_TREE_STORE_CONTEXT_TOKEN } from './data-type.tree.store'; import { UmbDataTypeFolderServerDataSource } from './sources/data-type-folder.server.data'; +import { UmbDataTypeItemServerDataSource } from './sources/data-type-item.server.data'; +import { UMB_DATA_TYPE_ITEM_STORE_CONTEXT_TOKEN, UmbDataTypeItemStore } from './data-type-item.store'; +import { UmbDataTypeCopyServerDataSource } from './sources/data-type-copy.server.data'; import type { - UmbTreeDataSource, UmbTreeRepository, UmbDetailRepository, - UmbFolderDataSource, - UmbDataSource, + UmbItemRepository, + UmbFolderRepository, + UmbMoveRepository, + UmbCopyRepository, } from '@umbraco-cms/backoffice/repository'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; import { CreateDataTypeRequestModel, CreateFolderRequestModel, + DataTypeItemResponseModel, DataTypeResponseModel, FolderModelBaseModel, FolderTreeItemResponseModel, UpdateDataTypeRequestModel, } from '@umbraco-cms/backoffice/backend-api'; import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/notification'; -import { UmbFolderRepository } from '@umbraco-cms/backoffice/repository'; - export class UmbDataTypeRepository implements + UmbItemRepository, UmbTreeRepository, UmbDetailRepository, - UmbFolderRepository + UmbFolderRepository, + UmbMoveRepository, + UmbCopyRepository { - #init!: Promise; + #init: Promise; #host: UmbControllerHostElement; - #treeSource: UmbTreeDataSource; - #detailSource: UmbDataSource; - #folderSource: UmbFolderDataSource; + #treeSource: UmbDataTypeTreeServerDataSource; + #detailSource: UmbDataTypeServerDataSource; + #folderSource: UmbDataTypeFolderServerDataSource; + #itemSource: UmbDataTypeItemServerDataSource; + #moveSource: UmbDataTypeMoveServerDataSource; + #copySource: UmbDataTypeCopyServerDataSource; #detailStore?: UmbDataTypeStore; #treeStore?: UmbDataTypeTreeStore; + #itemStore?: UmbDataTypeItemStore; #notificationContext?: UmbNotificationContext; @@ -49,22 +60,30 @@ export class UmbDataTypeRepository this.#treeSource = new UmbDataTypeTreeServerDataSource(this.#host); this.#detailSource = new UmbDataTypeServerDataSource(this.#host); this.#folderSource = new UmbDataTypeFolderServerDataSource(this.#host); + this.#itemSource = new UmbDataTypeItemServerDataSource(this.#host); + this.#moveSource = new UmbDataTypeMoveServerDataSource(this.#host); + this.#copySource = new UmbDataTypeCopyServerDataSource(this.#host); this.#init = Promise.all([ - new UmbContextConsumerController(this.#host, UMB_DATA_TYPE_TREE_STORE_CONTEXT_TOKEN, (instance) => { - this.#treeStore = instance; - }), - new UmbContextConsumerController(this.#host, UMB_DATA_TYPE_STORE_CONTEXT_TOKEN, (instance) => { this.#detailStore = instance; - }), + }).asPromise(), + + new UmbContextConsumerController(this.#host, UMB_DATA_TYPE_TREE_STORE_CONTEXT_TOKEN, (instance) => { + this.#treeStore = instance; + }).asPromise(), + + new UmbContextConsumerController(this.#host, UMB_DATA_TYPE_ITEM_STORE_CONTEXT_TOKEN, (instance) => { + this.#itemStore = instance; + }).asPromise(), new UmbContextConsumerController(this.#host, UMB_NOTIFICATION_CONTEXT_TOKEN, (instance) => { this.#notificationContext = instance; - }), + }).asPromise(), ]); } + // TREE: async requestRootTreeItems() { await this.#init; @@ -90,15 +109,6 @@ export class UmbDataTypeRepository return { data, error, asObservable: () => this.#treeStore!.childrenOf(parentId) }; } - async requestTreeItems(ids: Array) { - if (!ids) throw new Error('Keys are missing'); - await this.#init; - - const { data, error } = await this.#treeSource.getItems(ids); - - return { data, error, asObservable: () => this.#treeStore!.items(ids) }; - } - async rootTreeItems() { await this.#init; return this.#treeStore!.rootItems; @@ -110,13 +120,26 @@ export class UmbDataTypeRepository return this.#treeStore!.childrenOf(parentId); } - async treeItems(ids: Array) { + // ITEMS: + async requestItems(ids: Array) { + if (!ids) throw new Error('Keys are missing'); await this.#init; - return this.#treeStore!.items(ids); + + const { data, error } = await this.#itemSource.getItems(ids); + + if (data) { + this.#itemStore?.appendItems(data); + } + + return { data, error, asObservable: () => this.#itemStore!.items(ids) }; + } + + async items(ids: Array) { + await this.#init; + return this.#itemStore!.items(ids); } // DETAILS: - async createScaffold(parentId: string | null) { if (parentId === undefined) throw new Error('Parent id is missing'); await this.#init; @@ -146,7 +169,6 @@ export class UmbDataTypeRepository async create(dataType: CreateDataTypeRequestModel) { if (!dataType) throw new Error('Data Type is missing'); if (!dataType.id) throw new Error('Data Type id is missing'); - await this.#init; const { error } = await this.#detailSource.insert(dataType); @@ -167,7 +189,6 @@ export class UmbDataTypeRepository async save(id: string, updatedDataType: UpdateDataTypeRequestModel) { if (!id) throw new Error('Data Type id is missing'); if (!updatedDataType) throw new Error('Data Type is missing'); - await this.#init; const { error } = await this.#detailSource.update(id, updatedDataType); @@ -187,8 +208,6 @@ export class UmbDataTypeRepository return { error }; } - // General: - async delete(id: string) { if (!id) throw new Error('Data Type id is missing'); await this.#init; @@ -210,9 +229,10 @@ export class UmbDataTypeRepository return { error }; } - // folder + // Folder: async createFolderScaffold(parentId: string | null) { if (parentId === undefined) throw new Error('Parent id is missing'); + await this.#init; return this.#folderSource.createScaffold(parentId); } @@ -234,6 +254,7 @@ export class UmbDataTypeRepository async deleteFolder(id: string) { if (!id) throw new Error('Key is missing'); + await this.#init; const { error } = await this.#folderSource.delete(id); @@ -247,6 +268,7 @@ export class UmbDataTypeRepository async updateFolder(id: string, folder: FolderModelBaseModel) { if (!id) throw new Error('Key is missing'); if (!folder) throw new Error('Folder data is missing'); + await this.#init; const { error } = await this.#folderSource.update(id, folder); @@ -259,6 +281,7 @@ export class UmbDataTypeRepository async requestFolder(id: string) { if (!id) throw new Error('Key is missing'); + await this.#init; const { data, error } = await this.#folderSource.get(id); @@ -268,6 +291,41 @@ export class UmbDataTypeRepository return { data, error }; } + + // Actions + async move(id: string, targetId: string) { + await this.#init; + const { error } = await this.#moveSource.move(id, targetId); + + if (!error) { + // TODO: Be aware about this responsibility. + this.#treeStore?.updateItem(id, { parentId: targetId }); + this.#treeStore?.updateItem(targetId, { hasChildren: true }); + + const notification = { data: { message: `Data type moved` } }; + this.#notificationContext?.peek('positive', notification); + } + + return { error }; + } + + async copy(id: string, targetId: string) { + await this.#init; + const { data: dataTypeCopyId, error } = await this.#copySource.copy(id, targetId); + if (error) return { error }; + + if (dataTypeCopyId) { + const { data: dataTypeCopy } = await this.requestById(dataTypeCopyId); + if (!dataTypeCopy) throw new Error('Could not find copied data type'); + this.#treeStore?.appendItems([dataTypeCopy]); + this.#treeStore?.updateItem(targetId, { hasChildren: true }); + + const notification = { data: { message: `Data type copied` } }; + this.#notificationContext?.peek('positive', notification); + } + + return { data: dataTypeCopyId }; + } } export const createTreeItem = (item: CreateDataTypeRequestModel): FolderTreeItemResponseModel => { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/data-type.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/data-type.store.ts index 71db9b0013..c2d25be707 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/data-type.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/data-type.store.ts @@ -1,6 +1,6 @@ import type { DataTypeResponseModel } from '@umbraco-cms/backoffice/backend-api'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import { ArrayState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; import { UmbStoreBase } from '@umbraco-cms/backoffice/store'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; @@ -13,15 +13,17 @@ export const UMB_DATA_TYPE_STORE_CONTEXT_TOKEN = new UmbContextToken([], (x) => x.id); - /** * Creates an instance of UmbDataTypeStore. * @param {UmbControllerHostElement} host * @memberof UmbDataTypeStore */ constructor(host: UmbControllerHostElement) { - super(host, UMB_DATA_TYPE_STORE_CONTEXT_TOKEN.toString()); + super( + host, + UMB_DATA_TYPE_STORE_CONTEXT_TOKEN.toString(), + new UmbArrayState([], (x) => x.id) + ); } /** @@ -30,7 +32,7 @@ export class UmbDataTypeStore extends UmbStoreBase { * @memberof UmbDataTypeStore */ append(dataType: DataTypeResponseModel) { - this.#data.append([dataType]); + this._data.append([dataType]); } /** @@ -39,7 +41,7 @@ export class UmbDataTypeStore extends UmbStoreBase { * @memberof UmbDataTypeStore */ byId(id: DataTypeResponseModel['id']) { - return this.#data.getObservablePart((x) => x.find((y) => y.id === id)); + return this._data.getObservablePart((x) => x.find((y) => y.id === id)); } /** @@ -48,6 +50,6 @@ export class UmbDataTypeStore extends UmbStoreBase { * @memberof UmbDataTypeStore */ remove(uniques: Array) { - this.#data.remove(uniques); + this._data.remove(uniques); } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/manifests.ts index 0217359bf7..60ce0e41e6 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/manifests.ts @@ -1,7 +1,13 @@ import { UmbDataTypeRepository } from '../repository/data-type.repository'; +import { UmbDataTypeItemStore } from './data-type-item.store'; import { UmbDataTypeStore } from './data-type.store'; import { UmbDataTypeTreeStore } from './data-type.tree.store'; -import type { ManifestStore, ManifestTreeStore, ManifestRepository } from '@umbraco-cms/backoffice/extensions-registry'; +import type { + ManifestStore, + ManifestTreeStore, + ManifestRepository, + ManifestItemStore, +} from '@umbraco-cms/backoffice/extensions-registry'; export const DATA_TYPE_REPOSITORY_ALIAS = 'Umb.Repository.DataType'; @@ -14,6 +20,7 @@ const repository: ManifestRepository = { export const DATA_TYPE_STORE_ALIAS = 'Umb.Store.DataType'; export const DATA_TYPE_TREE_STORE_ALIAS = 'Umb.Store.DataTypeTree'; +export const DATA_TYPE_ITEM_STORE_ALIAS = 'Umb.Store.DataTypeItem'; const store: ManifestStore = { type: 'store', @@ -29,4 +36,11 @@ const treeStore: ManifestTreeStore = { class: UmbDataTypeTreeStore, }; -export const manifests = [repository, store, treeStore]; +const itemStore: ManifestItemStore = { + type: 'itemStore', + alias: DATA_TYPE_ITEM_STORE_ALIAS, + name: 'Data Type Item Store', + class: UmbDataTypeItemStore, +}; + +export const manifests = [repository, store, treeStore, itemStore]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/sources/data-type-copy.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/sources/data-type-copy.server.data.ts new file mode 100644 index 0000000000..d322063e48 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/sources/data-type-copy.server.data.ts @@ -0,0 +1,43 @@ +import { DataTypeResource } from '@umbraco-cms/backoffice/backend-api'; +import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; +import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; +import { UmbCopyDataSource } from '@umbraco-cms/backoffice/repository'; + +/** + * A data source for Data Type items that fetches data from the server + * @export + * @class UmbDataTypeCopyServerDataSource + */ +export class UmbDataTypeCopyServerDataSource implements UmbCopyDataSource { + #host: UmbControllerHostElement; + + /** + * Creates an instance of UmbDataTypeCopyServerDataSource. + * @param {UmbControllerHostElement} host + * @memberof UmbDataTypeCopyServerDataSource + */ + constructor(host: UmbControllerHostElement) { + this.#host = host; + } + + /** + * Copy an item for the given id to the target id + * @param {Array} id + * @return {*} + * @memberof UmbDataTypeCopyServerDataSource + */ + async copy(id: string, targetId: string) { + if (!id) throw new Error('Id is missing'); + if (!targetId) throw new Error('Target Id is missing'); + + return tryExecuteAndNotify( + this.#host, + DataTypeResource.postDataTypeByIdCopy({ + id, + requestBody: { + targetId, + }, + }) + ); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/sources/data-type-item.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/sources/data-type-item.server.data.ts new file mode 100644 index 0000000000..ae7943c5ab --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/sources/data-type-item.server.data.ts @@ -0,0 +1,39 @@ +import type { UmbItemDataSource } from '@umbraco-cms/backoffice/repository'; +import { DataTypeItemResponseModel, DataTypeResource } from '@umbraco-cms/backoffice/backend-api'; +import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; +import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; + +/** + * A data source for Data Type items that fetches data from the server + * @export + * @class UmbDataTypeItemServerDataSource + * @implements {DocumentTreeDataSource} + */ +export class UmbDataTypeItemServerDataSource implements UmbItemDataSource { + #host: UmbControllerHostElement; + + /** + * Creates an instance of UmbDataTypeItemServerDataSource. + * @param {UmbControllerHostElement} host + * @memberof UmbDataTypeItemServerDataSource + */ + constructor(host: UmbControllerHostElement) { + this.#host = host; + } + + /** + * Fetches the items for the given ids from the server + * @param {Array} ids + * @return {*} + * @memberof UmbDataTypeItemServerDataSource + */ + async getItems(ids: Array) { + if (!ids) throw new Error('Ids are missing'); + return tryExecuteAndNotify( + this.#host, + DataTypeResource.getDataTypeItem({ + id: ids, + }) + ); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/sources/data-type-move.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/sources/data-type-move.server.data.ts new file mode 100644 index 0000000000..c6759d7676 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/sources/data-type-move.server.data.ts @@ -0,0 +1,43 @@ +import { DataTypeResource } from '@umbraco-cms/backoffice/backend-api'; +import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; +import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; +import { UmbMoveDataSource } from '@umbraco-cms/backoffice/repository'; + +/** + * A data source for Data Type items that fetches data from the server + * @export + * @class UmbDataTypeMoveServerDataSource + */ +export class UmbDataTypeMoveServerDataSource implements UmbMoveDataSource { + #host: UmbControllerHostElement; + + /** + * Creates an instance of UmbDataTypeMoveServerDataSource. + * @param {UmbControllerHostElement} host + * @memberof UmbDataTypeMoveServerDataSource + */ + constructor(host: UmbControllerHostElement) { + this.#host = host; + } + + /** + * Move an item for the given id to the target id + * @param {Array} id + * @return {*} + * @memberof UmbDataTypeMoveServerDataSource + */ + async move(id: string, targetId: string) { + if (!id) throw new Error('Id is missing'); + if (!targetId) throw new Error('Target Id is missing'); + + return tryExecuteAndNotify( + this.#host, + DataTypeResource.postDataTypeByIdMove({ + id, + requestBody: { + targetId, + }, + }) + ); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/sources/data-type.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/sources/data-type.server.data.ts index 0d70c24d85..c8108c42ea 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/sources/data-type.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/sources/data-type.server.data.ts @@ -67,7 +67,7 @@ export class UmbDataTypeServerDataSource * @return {*} * @memberof UmbDataTypeServerDataSource */ - async insert(dataType: CreateDataTypeRequestModel & { id: string }) { + async insert(dataType: CreateDataTypeRequestModel) { if (!dataType) throw new Error('Data Type is missing'); if (!dataType.id) throw new Error('Data Type id is missing'); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/workspace/data-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/workspace/data-type-workspace.context.ts index 127850dc1f..15a5714fc1 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/workspace/data-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/workspace/data-type-workspace.context.ts @@ -2,7 +2,7 @@ import { UmbWorkspaceContext } from '../../../shared/components/workspace/worksp import { UmbDataTypeRepository } from '../repository/data-type.repository'; import { UmbEntityWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; import type { CreateDataTypeRequestModel, DataTypeResponseModel } from '@umbraco-cms/backoffice/backend-api'; -import { appendToFrozenArray, ObjectState } from '@umbraco-cms/backoffice/observable-api'; +import { appendToFrozenArray, UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; export class UmbDataTypeWorkspaceContext @@ -10,7 +10,7 @@ export class UmbDataTypeWorkspaceContext implements UmbEntityWorkspaceContextInterface { // TODO: revisit. temp solution because the create and response models are different. - #data = new ObjectState(undefined); + #data = new UmbObjectState(undefined); data = this.#data.asObservable(); name = this.#data.getObservablePart((data) => data?.name); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/index.ts index a578c12d5c..c86644f739 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/index.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/index.ts @@ -11,6 +11,8 @@ import { manifests as logviewerManifests } from './logviewer/manifests'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extensions-api'; import { ManifestTypes } from '@umbraco-cms/backoffice/extensions-registry'; +import './data-types/components'; + export const manifests = [ ...settingsSectionManifests, ...settingsMenuManifests, diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/app-language-select/app-language.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/app-language-select/app-language.context.ts index 6f85f5c271..4ed5119e48 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/app-language-select/app-language.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/app-language-select/app-language.context.ts @@ -1,5 +1,5 @@ import { UmbLanguageRepository } from '../repository/language.repository'; -import { ObjectState, UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; +import { UmbObjectState, UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; import { LanguageResponseModel } from '@umbraco-cms/backoffice/backend-api'; @@ -10,7 +10,7 @@ export class UmbAppLanguageContext { #languages: Array = []; - #appLanguage = new ObjectState(undefined); + #appLanguage = new UmbObjectState(undefined); appLanguage = this.#appLanguage.asObservable(); constructor(host: UmbControllerHostElement) { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/language-item.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/language-item.store.ts new file mode 100644 index 0000000000..e475a0d96b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/language-item.store.ts @@ -0,0 +1,31 @@ +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; +import { UmbStoreBase } from '@umbraco-cms/backoffice/store'; +import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; +import { LanguageResponseModel } from '@umbraco-cms/backoffice/backend-api'; +import type { UmbItemStore } from '@umbraco-cms/backoffice/store'; + +export const UMB_LANGUAGE_ITEM_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbLanguageItemStore'); + +/** + * @export + * @class UmbLanguageItemStore + * @extends {UmbStoreBase} + * @description - Store for Languages items + */ +export class UmbLanguageItemStore + extends UmbStoreBase + implements UmbItemStore +{ + constructor(host: UmbControllerHostElement) { + super( + host, + UMB_LANGUAGE_ITEM_STORE_CONTEXT_TOKEN.toString(), + new UmbArrayState([], (x) => x.isoCode) + ); + } + + items(isoCodes: Array) { + return this._data.getObservablePart((items) => items.filter((item) => isoCodes.includes(item.isoCode ?? ''))); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/language.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/language.repository.ts index d37dada762..243a7f3791 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/language.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/language.repository.ts @@ -1,17 +1,26 @@ import { UmbLanguageServerDataSource } from './sources/language.server.data'; import { UmbLanguageStore, UMB_LANGUAGE_STORE_CONTEXT_TOKEN } from './language.store'; +import { UmbLanguageItemServerDataSource } from './sources/language-item.server.data'; +import { UMB_LANGUAGE_ITEM_STORE_CONTEXT_TOKEN, UmbLanguageItemStore } from './language-item.store'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/notification'; -import { LanguageResponseModel, ProblemDetailsModel } from '@umbraco-cms/backoffice/backend-api'; +import { + LanguageItemResponseModel, + LanguageResponseModel, + ProblemDetailsModel, +} from '@umbraco-cms/backoffice/backend-api'; +import { UmbItemRepository } from '@umbraco-cms/backoffice/repository'; -export class UmbLanguageRepository { - #init!: Promise; +export class UmbLanguageRepository implements UmbItemRepository { + #init: Promise; #host: UmbControllerHostElement; #dataSource: UmbLanguageServerDataSource; + #itemDataSource: UmbLanguageItemServerDataSource; #languageStore?: UmbLanguageStore; + #languageItemStore?: UmbLanguageItemStore; #notificationContext?: UmbNotificationContext; @@ -19,15 +28,20 @@ export class UmbLanguageRepository { this.#host = host; this.#dataSource = new UmbLanguageServerDataSource(this.#host); + this.#itemDataSource = new UmbLanguageItemServerDataSource(this.#host); this.#init = Promise.all([ new UmbContextConsumerController(this.#host, UMB_NOTIFICATION_CONTEXT_TOKEN, (instance) => { this.#notificationContext = instance; - }), + }).asPromise(), new UmbContextConsumerController(this.#host, UMB_LANGUAGE_STORE_CONTEXT_TOKEN, (instance) => { this.#languageStore = instance; }).asPromise(), + + new UmbContextConsumerController(this.#host, UMB_LANGUAGE_ITEM_STORE_CONTEXT_TOKEN, (instance) => { + this.#languageItemStore = instance; + }).asPromise(), ]); } @@ -59,19 +73,19 @@ export class UmbLanguageRepository { } async requestItems(isoCodes: Array) { - // HACK: filter client side until we have a proper server side endpoint - // TODO: we will get a different size model here, how do we handle that in the store? - const { data, error } = await this.requestLanguages(); - - let items = undefined; + await this.#init; + const { data, error } = await this.#itemDataSource.getItems(isoCodes); if (data) { - // TODO: how do we best handle this? They might have a smaller data set than the details - items = data.items = data.items.filter((x) => isoCodes.includes(x.isoCode!)); - data.items.forEach((x) => this.#languageStore?.append(x)); + this.#languageItemStore?.appendItems(data); } - return { data: items, error, asObservable: () => this.#languageStore!.items(isoCodes) }; + return { data, error, asObservable: () => this.#languageItemStore!.items(isoCodes) }; + } + + async items(isoCodes: Array) { + await this.#init; + return this.#languageItemStore!.items(isoCodes); } /** diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/language.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/language.store.ts index 820b66fc50..34e3f4878c 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/language.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/language.store.ts @@ -1,7 +1,7 @@ import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; import { UmbStoreBase } from '@umbraco-cms/backoffice/store'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; -import { ArrayState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; import { LanguageResponseModel } from '@umbraco-cms/backoffice/backend-api'; export const UMB_LANGUAGE_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbLanguageStore'); @@ -13,23 +13,26 @@ export const UMB_LANGUAGE_STORE_CONTEXT_TOKEN = new UmbContextToken([], (x) => x.isoCode); - data = this.#data.asObservable(); + public readonly data = this._data.asObservable(); constructor(host: UmbControllerHostElement) { - super(host, UMB_LANGUAGE_STORE_CONTEXT_TOKEN.toString()); + super( + host, + UMB_LANGUAGE_STORE_CONTEXT_TOKEN.toString(), + new UmbArrayState([], (x) => x.isoCode) + ); } append(language: LanguageResponseModel) { - this.#data.append([language]); + this._data.append([language]); } remove(uniques: string[]) { - this.#data.remove(uniques); + this._data.remove(uniques); } // TODO: how do we best handle this? They might have a smaller data set than the details items(isoCodes: Array) { - return this.#data.getObservablePart((items) => items.filter((item) => isoCodes.includes(item.isoCode ?? ''))); + return this._data.getObservablePart((items) => items.filter((item) => isoCodes.includes(item.isoCode ?? ''))); } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/manifests.ts index 75dcb127c4..e0811d94c1 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/manifests.ts @@ -1,5 +1,6 @@ import { UmbLanguageRepository } from '../repository/language.repository'; import { UmbLanguageStore } from './language.store'; +import { UmbLanguageItemStore } from './language-item.store'; import type { ManifestStore, ManifestRepository } from '@umbraco-cms/backoffice/extensions-registry'; export const LANGUAGE_REPOSITORY_ALIAS = 'Umb.Repository.Language'; @@ -12,6 +13,7 @@ const repository: ManifestRepository = { }; export const LANGUAGE_STORE_ALIAS = 'Umb.Store.Language'; +export const LANGUAGE_ITEM_STORE_ALIAS = 'Umb.Store.LanguageItem'; const store: ManifestStore = { type: 'store', @@ -20,4 +22,11 @@ const store: ManifestStore = { class: UmbLanguageStore, }; -export const manifests = [repository, store]; +const itemStore = { + type: 'itemStore', + alias: LANGUAGE_ITEM_STORE_ALIAS, + name: 'Language Item Store', + class: UmbLanguageItemStore, +}; + +export const manifests = [repository, store, itemStore]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/sources/language-item.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/sources/language-item.server.data.ts new file mode 100644 index 0000000000..807565c841 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/sources/language-item.server.data.ts @@ -0,0 +1,35 @@ +import { LanguageResource, LanguageItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; +import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; +import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; +import { UmbItemDataSource } from '@umbraco-cms/backoffice/repository'; + +/** + * A data source for Languages that fetches Language items from the server + * @export + * @class UmbLanguageItemServerDataSource + * @implements {UmbItemDataSource} + */ +export class UmbLanguageItemServerDataSource implements UmbItemDataSource { + #host: UmbControllerHostElement; + + constructor(host: UmbControllerHostElement) { + this.#host = host; + } + + /** + * Fetches Language items the given iso codes from the server + * @param {string[]} isoCodes + * @return {*} + * @memberof UmbLanguageItemServerDataSource + */ + async getItems(isoCodes: string[]) { + if (!isoCodes) throw new Error('Iso Codes are missing'); + + return tryExecuteAndNotify( + this.#host, + LanguageResource.getLanguageItem({ + isoCode: isoCodes, + }) + ); + } +} 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 f51bfe3075..782f623ff0 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 @@ -2,18 +2,18 @@ import { UmbLanguageRepository } from '../../repository/language.repository'; import { UmbWorkspaceContext } from '../../../../shared/components/workspace/workspace-context/workspace-context'; import { UmbEntityWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; import type { LanguageResponseModel } from '@umbraco-cms/backoffice/backend-api'; -import { ObjectState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; export class UmbLanguageWorkspaceContext extends UmbWorkspaceContext implements UmbEntityWorkspaceContextInterface { - #data = new ObjectState(undefined); + #data = new UmbObjectState(undefined); data = this.#data.asObservable(); // TODO: this is a temp solution to bubble validation errors to the UI - #validationErrors = new ObjectState(undefined); + #validationErrors = new UmbObjectState(undefined); validationErrors = this.#validationErrors.asObservable(); constructor(host: UmbControllerHostElement) { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/logviewer.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/logviewer.context.ts index 6bec1b10e9..73c793e4dd 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/logviewer.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/logviewer.context.ts @@ -1,11 +1,11 @@ import { UmbLogViewerRepository } from '../repository/log-viewer.repository'; import { - BasicState, - ArrayState, + UmbBasicState, + UmbArrayState, createObservablePart, - DeepState, - ObjectState, - StringState, + UmbDeepState, + UmbObjectState, + UmbStringState, } from '@umbraco-cms/backoffice/observable-api'; import { DirectionModel, @@ -58,38 +58,38 @@ export class UmbLogViewerWorkspaceContext { endDate: this.today, }; - #savedSearches = new ObjectState(undefined); + #savedSearches = new UmbObjectState(undefined); savedSearches = createObservablePart(this.#savedSearches, (data) => data?.items); - #logCount = new DeepState(null); + #logCount = new UmbDeepState(null); logCount = createObservablePart(this.#logCount, (data) => data); - #dateRange = new DeepState(this.defaultDateRange); + #dateRange = new UmbDeepState(this.defaultDateRange); dateRange = createObservablePart(this.#dateRange, (data) => data); - #loggers = new DeepState(null); + #loggers = new UmbDeepState(null); loggers = createObservablePart(this.#loggers, (data) => data?.items); - #canShowLogs = new BasicState(null); + #canShowLogs = new UmbBasicState(null); canShowLogs = createObservablePart(this.#canShowLogs, (data) => data); - #filterExpression = new StringState(''); + #filterExpression = new UmbStringState(''); filterExpression = createObservablePart(this.#filterExpression, (data) => data); - #messageTemplates = new DeepState(null); + #messageTemplates = new UmbDeepState(null); messageTemplates = createObservablePart(this.#messageTemplates, (data) => data); - #logLevelsFilter = new ArrayState([]); + #logLevelsFilter = new UmbArrayState([]); logLevelsFilter = createObservablePart(this.#logLevelsFilter, (data) => data); - #logs = new DeepState(null); + #logs = new UmbDeepState(null); logs = createObservablePart(this.#logs, (data) => data?.items); logsTotal = createObservablePart(this.#logs, (data) => data?.total); - #polling = new ObjectState({ enabled: false, interval: 2000 }); + #polling = new UmbObjectState({ enabled: false, interval: 2000 }); polling = createObservablePart(this.#polling, (data) => data); - #sortingDirection = new BasicState(DirectionModel.ASCENDING); + #sortingDirection = new UmbBasicState(DirectionModel.ASCENDING); sortingDirection = createObservablePart(this.#sortingDirection, (data) => data); #intervalID: number | null = null; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/search/components/log-viewer-search-input-modal.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/search/components/log-viewer-search-input-modal.element.ts index 7fc279c8eb..7266e6c8d3 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/search/components/log-viewer-search-input-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/logviewer/workspace/views/search/components/log-viewer-search-input-modal.element.ts @@ -1,9 +1,9 @@ import { html, css } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, query, state } from 'lit/decorators.js'; +import { UUIInputElement } from '@umbraco-ui/uui'; import { UmbModalBaseElement } from '@umbraco-cms/internal/modal'; import { SavedLogSearchPresenationBaseModel } from '@umbraco-cms/backoffice/backend-api'; -import { UUIInputElement } from '@umbraco-ui/uui'; @customElement('umb-log-viewer-save-search-modal') export default class UmbLogViewerSaveSearchModalElement extends UmbModalBaseElement< diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/repository/relation-type.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/repository/relation-type.repository.ts index 52249a2896..6f92a1ce92 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/repository/relation-type.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/repository/relation-type.repository.ts @@ -1,8 +1,8 @@ import { UmbRelationTypeTreeStore, UMB_RELATION_TYPE_TREE_STORE_CONTEXT_TOKEN } from './relation-type.tree.store'; import { UmbRelationTypeServerDataSource } from './sources/relation-type.server.data'; import { UmbRelationTypeStore, UMB_RELATION_TYPE_STORE_CONTEXT_TOKEN } from './relation-type.store'; -import { RelationTypeTreeServerDataSource } from './sources/relation-type.tree.server.data'; -import { RelationTypeTreeDataSource } from './sources'; +import { UmbRelationTypeTreeServerDataSource } from './sources/relation-type.tree.server.data'; +import { UmbRelationTypeTreeDataSource } from './sources'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; import { @@ -23,7 +23,7 @@ export class UmbRelationTypeRepository #host: UmbControllerHostElement; - #treeSource: RelationTypeTreeDataSource; + #treeSource: UmbRelationTypeTreeDataSource; #treeStore?: UmbRelationTypeTreeStore; #detailDataSource: UmbRelationTypeServerDataSource; @@ -35,7 +35,7 @@ export class UmbRelationTypeRepository this.#host = host; // TODO: figure out how spin up get the correct data source - this.#treeSource = new RelationTypeTreeServerDataSource(this.#host); + this.#treeSource = new UmbRelationTypeTreeServerDataSource(this.#host); this.#detailDataSource = new UmbRelationTypeServerDataSource(this.#host); this.#init = Promise.all([ @@ -74,7 +74,7 @@ export class UmbRelationTypeRepository return { data: undefined, error }; } - async requestTreeItems(ids: Array) { + async requestItemsLegacy(ids: Array) { if (!ids) throw new Error('Ids are missing'); await this.#init; @@ -93,7 +93,7 @@ export class UmbRelationTypeRepository return this.#treeStore!.childrenOf(parentId); } - async treeItems(ids: Array) { + async itemsLegacy(ids: Array) { await this.#init; return this.#treeStore!.items(ids); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/repository/relation-type.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/repository/relation-type.store.ts index f8ac1dd072..708e9ba0e4 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/repository/relation-type.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/repository/relation-type.store.ts @@ -1,6 +1,6 @@ import type { RelationTypeResponseModel } from '@umbraco-cms/backoffice/backend-api'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import { ArrayState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; import { UmbStoreBase } from '@umbraco-cms/backoffice/store'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; @@ -13,15 +13,17 @@ export const UMB_RELATION_TYPE_STORE_CONTEXT_TOKEN = new UmbContextToken([], (x) => x.id); - /** * Creates an instance of UmbRelationTypeStore. * @param {UmbControllerHostElement} host * @memberof UmbRelationTypeStore */ constructor(host: UmbControllerHostElement) { - super(host, UMB_RELATION_TYPE_STORE_CONTEXT_TOKEN.toString()); + super( + host, + UMB_RELATION_TYPE_STORE_CONTEXT_TOKEN.toString(), + new UmbArrayState([], (x) => x.id) + ); } /** @@ -30,7 +32,7 @@ export class UmbRelationTypeStore extends UmbStoreBase { * @memberof UmbRelationTypeStore */ append(RelationType: RelationTypeResponseModel) { - this.#data.append([RelationType]); + this._data.append([RelationType]); } /** @@ -39,7 +41,7 @@ export class UmbRelationTypeStore extends UmbStoreBase { * @memberof UmbRelationTypeStore */ byKey(id: RelationTypeResponseModel['id']) { - return this.#data.getObservablePart((x) => x.find((y) => y.id === id)); + return this._data.getObservablePart((x) => x.find((y) => y.id === id)); } /** @@ -48,6 +50,6 @@ export class UmbRelationTypeStore extends UmbStoreBase { * @memberof UmbRelationTypeStore */ remove(uniques: Array) { - this.#data.remove(uniques); + this._data.remove(uniques); } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/repository/sources/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/repository/sources/index.ts index f8ff932071..7acf297b3e 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/repository/sources/index.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/repository/sources/index.ts @@ -1,7 +1,7 @@ import type { DataSourceResponse } from '@umbraco-cms/backoffice/repository'; import { ItemResponseModelBaseModel, PagedEntityTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; -export interface RelationTypeTreeDataSource { +export interface UmbRelationTypeTreeDataSource { getRootItems(): Promise>; getItems(ids: Array): Promise>; } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/repository/sources/relation-type.tree.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/repository/sources/relation-type.tree.server.data.ts index 1dc64b51b5..4529c77390 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/repository/sources/relation-type.tree.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/repository/sources/relation-type.tree.server.data.ts @@ -1,4 +1,4 @@ -import { RelationTypeTreeDataSource } from '.'; +import { UmbRelationTypeTreeDataSource } from '.'; import { ProblemDetailsModel, RelationTypeResource } from '@umbraco-cms/backoffice/backend-api'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; @@ -7,10 +7,10 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; /** * A data source for the RelationType tree that fetches data from the server * @export - * @class RelationTypeTreeServerDataSource - * @implements {RelationTypeTreeDataSource} + * @class UmbRelationTypeTreeServerDataSource + * @implements {UmbRelationTypeTreeDataSource} */ -export class RelationTypeTreeServerDataSource implements RelationTypeTreeDataSource { +export class UmbRelationTypeTreeServerDataSource implements UmbRelationTypeTreeDataSource { #host: UmbControllerHostElement; // TODO: how do we handle trashed items? @@ -47,9 +47,9 @@ export class RelationTypeTreeServerDataSource implements RelationTypeTreeDataSou } /** - * Creates an instance of RelationTypeTreeServerDataSource. + * Creates an instance of UmbRelationTypeTreeServerDataSource. * @param {UmbControllerHostElement} host - * @memberof RelationTypeTreeServerDataSource + * @memberof UmbRelationTypeTreeServerDataSource */ constructor(host: UmbControllerHostElement) { this.#host = host; @@ -58,7 +58,7 @@ export class RelationTypeTreeServerDataSource implements RelationTypeTreeDataSou /** * Fetches the root items for the tree from the server * @return {*} - * @memberof RelationTypeTreeServerDataSource + * @memberof UmbRelationTypeTreeServerDataSource */ async getRootItems() { return tryExecuteAndNotify(this.#host, RelationTypeResource.getTreeRelationTypeRoot({})); @@ -68,7 +68,7 @@ export class RelationTypeTreeServerDataSource implements RelationTypeTreeDataSou * Fetches the items for the given ids from the server * @param {Array} ids * @return {*} - * @memberof RelationTypeTreeServerDataSource + * @memberof UmbRelationTypeTreeServerDataSource */ async getItems(ids: Array) { if (ids) { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/workspace/relation-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/workspace/relation-type-workspace.context.ts index 81abf8f66f..3c87efd4c2 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/workspace/relation-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/relation-types/workspace/relation-type-workspace.context.ts @@ -3,14 +3,14 @@ import { UmbRelationTypeRepository } from '../repository/relation-type.repositor import { UmbEntityWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; import type { RelationTypeBaseModel, RelationTypeResponseModel } from '@umbraco-cms/backoffice/backend-api'; -import { ObjectState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; export class UmbRelationTypeWorkspaceContext extends UmbWorkspaceContext implements UmbEntityWorkspaceContextInterface { - #data = new ObjectState(undefined); + #data = new UmbObjectState(undefined); data = this.#data.asObservable(); name = this.#data.getObservablePart((data) => data?.name); id = this.#data.getObservablePart((data) => data?.id); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/collection/collection.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/collection/collection.context.ts index 9668a51d68..4f2512206c 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/collection/collection.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/collection/collection.context.ts @@ -2,7 +2,7 @@ import { Observable } from 'rxjs'; import type { EntityTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; import { UmbContextToken, UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; -import { ArrayState, UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; +import { UmbArrayState, UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; import { umbExtensionsRegistry, createExtensionClass } from '@umbraco-cms/backoffice/extensions-api'; import { UmbTreeRepository } from '@umbraco-cms/backoffice/repository'; @@ -17,15 +17,15 @@ export class UmbCollectionContext; - #data = new ArrayState(>[]); + #data = new UmbArrayState(>[]); public readonly data = this.#data.asObservable(); - #selection = new ArrayState(>[]); + #selection = new UmbArrayState(>[]); public readonly selection = this.#selection.asObservable(); /* TODO: - private _search = new StringState(''); + private _search = new UmbStringState(''); public readonly search = this._search.asObservable(); */ diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/backoffice-frame/backoffice.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/backoffice-frame/backoffice.context.ts index 9a5d46fcc1..f7562f6296 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/backoffice-frame/backoffice.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/backoffice-frame/backoffice.context.ts @@ -1,9 +1,9 @@ import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extensions-api'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import { StringState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbStringState } from '@umbraco-cms/backoffice/observable-api'; export class UmbBackofficeContext { - #activeSectionAlias = new StringState(undefined); + #activeSectionAlias = new UmbStringState(undefined); public readonly activeSectionAlias = this.#activeSectionAlias.asObservable(); public getAllowedSections() { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/code-block/code-block.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/code-block/code-block.element.ts index 99833cbcce..11a4372905 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/code-block/code-block.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/code-block/code-block.element.ts @@ -7,8 +7,8 @@ import { customElement } from 'lit/decorators.js'; * @slot the full message * */ -@customElement('uui-code-block') -export class UUICodeBlockElement extends LitElement { +@customElement('umb-code-block') +export class UmbCodeBlockElement extends LitElement { static styles = [ UUITextStyles, css` @@ -54,6 +54,6 @@ export class UUICodeBlockElement extends LitElement { declare global { interface HTMLElementTagNameMap { - 'uui-code-block': UUICodeBlockElement; + 'umb-code-block': UmbCodeBlockElement; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/code-block/code-block.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/code-block/code-block.stories.ts index f3f2d6ca24..46edd57d43 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/code-block/code-block.stories.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/code-block/code-block.stories.ts @@ -1,15 +1,15 @@ import { Meta, StoryObj } from '@storybook/web-components'; import { html } from 'lit'; import './code-block.element'; -import type { UUICodeBlockElement } from './code-block.element'; +import type { UmbCodeBlockElement } from './code-block.element'; -const meta: Meta = { +const meta: Meta = { title: 'Components/Code Block', - component: 'uui-code-block', + component: 'umb-code-block', }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Overview: Story = { args: {}, @@ -17,5 +17,5 @@ export const Overview: Story = { export const WithCode: Story = { decorators: [], - render: () => html` // Lets write some javascript alert("Hello World"); `, + render: () => html` // Lets write some javascript alert("Hello World"); `, }; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/code-editor/code-editor.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/code-editor/code-editor.stories.ts index e8d7805abe..8c7e95004a 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/code-editor/code-editor.stories.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/code-editor/code-editor.stories.ts @@ -111,12 +111,12 @@ const codeSnippets: Record = { "Smartypants, double quotes" and 'single quotes'`, typescript: `import { UmbTemplateRepository } from '../repository/template.repository'; import { UmbWorkspaceContext } from '../../../shared/components/workspace/workspace-context/workspace-context'; - import { createObservablePart, DeepState } from '@umbraco-cms/observable-api'; + import { createObservablePart, UmbDeepState } from '@umbraco-cms/observable-api'; import { TemplateModel } from '@umbraco-cms/backend-api'; import { UmbControllerHostElement } from '@umbraco-cms/controller'; export class UmbTemplateWorkspaceContext extends UmbWorkspaceContext { - #data = new DeepState(undefined); + #data = new UmbDeepState(undefined); data = this.#data.asObservable(); name = createObservablePart(this.#data, (data) => data?.name); content = createObservablePart(this.#data, (data) => data?.content); @@ -190,6 +190,7 @@ const codeSnippets: Record = { "@umbraco-cms/workspace": ["libs/workspace"], "@umbraco-cms/utils": ["libs/utils"], "@umbraco-cms/router": ["libs/router"], + "@umbraco-cms/sorter": ["libs/sorter"], "@umbraco-cms/test-utils": ["libs/test-utils"], "@umbraco-cms/repository": ["libs/repository"], "@umbraco-cms/resources": ["libs/resources"], diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/extension-slot/extension-slot.test.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/extension-slot/extension-slot.test.ts index a61451b20b..3fba5e41f1 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/extension-slot/extension-slot.test.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/extension-slot/extension-slot.test.ts @@ -3,10 +3,9 @@ import { expect, fixture, html } from '@open-wc/testing'; import { InitializedExtension, UmbExtensionSlotElement } from './extension-slot.element'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extensions-api'; import { ManifestDashboard } from '@umbraco-cms/backoffice/extensions-registry'; -import { defaultA11yConfig } from '@umbraco-cms/internal/test-utils'; -@customElement('test-extension-slot-manifest-element') -class MyExtensionSlotManifestElement extends HTMLElement {} +@customElement('umb-test-extension-slot-manifest-element') +class UmbTestExtensionSlotManifestElement extends HTMLElement {} function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); @@ -52,7 +51,7 @@ describe('UmbExtensionSlotElement', () => { type: 'dashboard', alias: 'unit-test-ext-slot-element-manifest', name: 'unit-test-extension', - elementName: 'test-extension-slot-manifest-element', + elementName: 'umb-test-extension-slot-manifest-element', meta: { pathname: 'test/test', }, @@ -75,7 +74,7 @@ describe('UmbExtensionSlotElement', () => { await sleep(0); - expect(element.shadowRoot!.firstElementChild).to.be.instanceOf(MyExtensionSlotManifestElement); + expect(element.shadowRoot!.firstElementChild).to.be.instanceOf(UmbTestExtensionSlotManifestElement); }); it('use the render method', async () => { @@ -90,7 +89,9 @@ describe('UmbExtensionSlotElement', () => { await sleep(0); expect(element.shadowRoot!.firstElementChild?.nodeName).to.be.equal('BLA'); - expect(element.shadowRoot!.firstElementChild?.firstElementChild).to.be.instanceOf(MyExtensionSlotManifestElement); + expect(element.shadowRoot!.firstElementChild?.firstElementChild).to.be.instanceOf( + UmbTestExtensionSlotManifestElement + ); }); }); }); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/index.ts index 8ae49d07be..683a77d61f 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/index.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/index.ts @@ -30,7 +30,7 @@ import './input-slider/input-slider.element'; import './input-tiny-mce/input-tiny-mce.element'; import './input-toggle/input-toggle.element'; import './input-upload-field/input-upload-field.element'; -import './input-template-picker/input-template-picker.element'; +import './input-template/input-template.element'; import './property-type-based-property/property-type-based-property.element'; import './ref-property-editor-ui/ref-property-editor-ui.element'; import './section/section-main/section-main.element'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-document-type-picker/input-document-type-picker.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-document-type-picker/input-document-type-picker.element.ts index c3f844b898..06930f64e7 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-document-type-picker/input-document-type-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-document-type-picker/input-document-type-picker.element.ts @@ -30,11 +30,12 @@ export class UmbInputDocumentTypePickerElement extends FormControlMixin(UmbLitEl // TODO: do we need both selectedIds and value? If we just use value we follow the same pattern as native form controls. private _selectedIds: Array = []; + @property({ type: Array }) public get selectedIds(): Array { return this._selectedIds; } public set selectedIds(ids: Array) { - this._selectedIds = ids; + this._selectedIds = ids ?? []; super.value = ids.join(','); this._observePickedDocuments(); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-language-picker/input-language-picker.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-language-picker/input-language-picker.context.ts new file mode 100644 index 0000000000..80568ccff4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-language-picker/input-language-picker.context.ts @@ -0,0 +1,10 @@ +import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input'; +import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; +import { UMB_LANGUAGE_PICKER_MODAL } from '@umbraco-cms/backoffice/modal'; +import type { LanguageItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; + +export class UmbLanguagePickerContext extends UmbPickerInputContext { + constructor(host: UmbControllerHostElement) { + super(host, 'Umb.Repository.Language', UMB_LANGUAGE_PICKER_MODAL, (item) => item.isoCode); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-language-picker/input-language-picker.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-language-picker/input-language-picker.element.ts index df04084af4..2be4babdbd 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-language-picker/input-language-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-language-picker/input-language-picker.element.ts @@ -3,36 +3,25 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, property, state } from 'lit/decorators.js'; import { ifDefined } from 'lit/directives/if-defined.js'; import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins'; -import { UmbLanguageRepository } from '../../../settings/languages/repository/language.repository'; -import { - UmbModalContext, - UMB_MODAL_CONTEXT_TOKEN, - UMB_CONFIRM_MODAL, - UMB_LANGUAGE_PICKER_MODAL, -} from '@umbraco-cms/backoffice/modal'; -import { UmbChangeEvent } from '@umbraco-cms/backoffice/events'; +import { UmbLanguagePickerContext } from './input-language-picker.context'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; import type { LanguageResponseModel } from '@umbraco-cms/backoffice/backend-api'; -import type { UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; @customElement('umb-input-language-picker') export class UmbInputLanguagePickerElement extends FormControlMixin(UmbLitElement) { - static styles = [ - UUITextStyles, - css` - #add-button { - width: 100%; - } - `, - ]; /** * This is a minimum amount of selected items in this input. * @type {number} * @attr - * @default undefined + * @default 0 */ @property({ type: Number }) - min?: number; + public get min(): number { + return this.#pickerContext.min; + } + public set min(value: number) { + this.#pickerContext.min = value; + } /** * Min validation message. @@ -47,10 +36,15 @@ export class UmbInputLanguagePickerElement extends FormControlMixin(UmbLitElemen * This is a maximum amount of selected items in this input. * @type {number} * @attr - * @default undefined + * @default Infinity */ @property({ type: Number }) - max?: number; + public get max(): number { + return this.#pickerContext.max; + } + public set max(value: number) { + this.#pickerContext.max = value; + } /** * Max validation message. @@ -64,29 +58,23 @@ export class UmbInputLanguagePickerElement extends FormControlMixin(UmbLitElemen @property({ type: Object, attribute: false }) public filter: (language: LanguageResponseModel) => boolean = () => true; - private _selectedIsoCodes: Array = []; public get selectedIsoCodes(): Array { - return this._selectedIsoCodes; + return this.#pickerContext.getSelection(); } - public set selectedIsoCodes(isoCodes: Array) { - this._selectedIsoCodes = isoCodes; - super.value = isoCodes.join(','); - this._observePickedItems(); + public set selectedIsoCodes(ids: Array) { + this.#pickerContext.setSelection(ids); } @property() public set value(isoCodesString: string) { - if (isoCodesString !== this._value) { - this.selectedIsoCodes = isoCodesString.split(/[ ,]+/); - } + // Its with full purpose we don't call super.value, as thats being handled by the observation of the context selection. + this.selectedIsoCodes = isoCodesString.split(/[ ,]+/); } @state() - private _items?: Array; + private _items: Array = []; - private _modalContext?: UmbModalContext; - private _repository = new UmbLanguageRepository(this); - private _pickedItemsObserver?: UmbObserverController; + #pickerContext = new UmbLanguagePickerContext(this); constructor() { super(); @@ -94,90 +82,65 @@ export class UmbInputLanguagePickerElement extends FormControlMixin(UmbLitElemen this.addValidator( 'rangeUnderflow', () => this.minMessage, - () => !!this.min && this._selectedIsoCodes.length < this.min + () => !!this.min && this.#pickerContext.getSelection().length < this.min ); this.addValidator( 'rangeOverflow', () => this.maxMessage, - () => !!this.max && this._selectedIsoCodes.length > this.max + () => !!this.max && this.#pickerContext.getSelection().length > this.max ); - this.consumeContext(UMB_MODAL_CONTEXT_TOKEN, (instance) => { - this._modalContext = instance; - }); + this.observe(this.#pickerContext.selection, (selection) => (super.value = selection.join(','))); + this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems)); } protected getFormElement() { return undefined; } - private async _observePickedItems() { - this._pickedItemsObserver?.destroy(); - if (!this._repository) return; - - const { asObservable } = await this._repository.requestItems(this._selectedIsoCodes); - - this._pickedItemsObserver = this.observe(asObservable(), (items) => { - this._items = items; - }); - } - private _openPicker() { - const modalHandler = this._modalContext?.open(UMB_LANGUAGE_PICKER_MODAL, { - multiple: this.max === 1 ? false : true, - selection: [...this._selectedIsoCodes], + this.#pickerContext.openPicker({ filter: this.filter, }); - - modalHandler?.onSubmit().then(({ selection }) => { - this._setSelection(selection); - }); - } - - private _removeItem(item: LanguageResponseModel) { - const modalHandler = this._modalContext?.open(UMB_CONFIRM_MODAL, { - color: 'danger', - headline: `Remove ${item.name}?`, - content: 'Are you sure you want to remove this item', - confirmLabel: 'Remove', - }); - - modalHandler?.onSubmit().then(() => { - const newSelection = this._selectedIsoCodes.filter((value) => value !== item.isoCode); - this._setSelection(newSelection); - }); - } - - private _setSelection(newSelection: Array) { - this.selectedIsoCodes = newSelection; - this.dispatchEvent(new UmbChangeEvent()); } render() { return html` - ${this._items?.map((item) => this._renderItem(item))} + ${this._items.map((item) => this._renderItem(item))} Add `; } private _renderItem(item: LanguageResponseModel) { + if (!item.isoCode) return; return html` - this._removeItem(item)} label="Remove ${item.name}">Remove + this.#pickerContext.requestRemoveItem(item.isoCode!)} label="Remove ${item.name}" + >Remove `; } + + static styles = [ + UUITextStyles, + css` + #add-button { + width: 100%; + } + `, + ]; } export default UmbInputLanguagePickerElement; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-media-picker/input-media-picker.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-media-picker/input-media-picker.element.ts index f324ca44be..cf3afb8554 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-media-picker/input-media-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-media-picker/input-media-picker.element.ts @@ -129,7 +129,7 @@ export class UmbInputMediaPickerElement extends FormControlMixin(UmbLitElement) this._pickedItemsObserver?.destroy(); // TODO: consider changing this to the list data endpoint when it is available - const { asObservable } = await this._repository.requestTreeItems(this._selectedIds); + const { asObservable } = await this._repository.requestItemsLegacy(this._selectedIds); if (!asObservable) return; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-template-picker/input-template-picker.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-template/input-template.element.ts similarity index 77% rename from src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-template-picker/input-template-picker.element.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-template/input-template.element.ts index aa47ff72d6..4fd0164550 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-template-picker/input-template-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-template/input-template.element.ts @@ -13,8 +13,8 @@ import { import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; import { TemplateResponseModel } from '@umbraco-cms/backoffice/backend-api'; -@customElement('umb-input-template-picker') -export class UmbInputTemplatePickerElement extends FormControlMixin(UmbLitElement) { +@customElement('umb-input-template') +export class UmbInputTemplateElement extends FormControlMixin(UmbLitElement) { /** * This is a minimum amount of selected items in this input. * @type {number} @@ -51,24 +51,24 @@ export class UmbInputTemplatePickerElement extends FormControlMixin(UmbLitElemen @property({ type: String, attribute: 'min-message' }) maxMessage = 'This field exceeds the allowed amount of items'; - _allowedKeys: Array = []; + _selectedIds: Array = []; @property({ type: Array }) - public get allowedKeys() { - return this._allowedKeys; + public get selectedIds() { + return this._selectedIds; } - public set allowedKeys(newKeys: Array) { - this._allowedKeys = newKeys; + public set selectedIds(newKeys: Array) { + this._selectedIds = newKeys; this.#observePickedTemplates(); } - _defaultKey = ''; + _defaultId = ''; @property({ type: String }) - public get defaultKey(): string { - return this._defaultKey; + public get defaultId(): string { + return this._defaultId; } - public set defaultKey(newKey: string) { - this._defaultKey = newKey; - super.value = newKey; + public set defaultId(newId: string) { + this._defaultId = newId; + super.value = newId; } private _modalContext?: UmbModalContext; @@ -87,7 +87,7 @@ export class UmbInputTemplatePickerElement extends FormControlMixin(UmbLitElemen async #observePickedTemplates() { this.observe( - await this._templateRepository.treeItems(this._allowedKeys), + await this._templateRepository.itemsLegacy(this._selectedIds), (data) => { this._pickedTemplates = data; }, @@ -99,23 +99,24 @@ export class UmbInputTemplatePickerElement extends FormControlMixin(UmbLitElemen return this; } - #changeDefault(e: CustomEvent) { + #onCardChange(e: CustomEvent) { e.stopPropagation(); const newKey = (e.target as UmbTemplateCardElement).value as string; - this.defaultKey = newKey; - this.dispatchEvent(new CustomEvent('change-default')); + this.defaultId = newKey; + this.dispatchEvent(new CustomEvent('change')); } #openPicker() { + // TODO: Change experience, so its not multi selectable. But instead already picked templates should be unpickable. (awaiting general picker features for such) const modalHandler = this._modalContext?.open(UMB_TEMPLATE_PICKER_MODAL, { multiple: true, - selection: [...this.allowedKeys], + selection: [...this.selectedIds], }); modalHandler?.onSubmit().then((data) => { if (!data.selection) return; - this.allowedKeys = data.selection; - this.dispatchEvent(new CustomEvent('change-allowed')); + this.selectedIds = data.selection; + this.dispatchEvent(new CustomEvent('change')); }); } @@ -129,7 +130,7 @@ export class UmbInputTemplatePickerElement extends FormControlMixin(UmbLitElemen In current backoffice we just prevent deleting a default when there are other templates. But if its the only one its okay. This is a weird experience, so we should make something that makes more sense. BTW. its weird cause the damage of removing the default template is equally bad when there is one or more templates. */ - this.allowedKeys = this.allowedKeys.filter((x) => x !== id); + this.selectedIds = this.selectedIds.filter((x) => x !== id); } #openTemplate(e: CustomEvent) { @@ -146,18 +147,17 @@ export class UmbInputTemplatePickerElement extends FormControlMixin(UmbLitElemen ${this._pickedTemplates.map( (template) => html` + ?default="${template.id === this.defaultId}"> - + ` @@ -188,10 +188,10 @@ export class UmbInputTemplatePickerElement extends FormControlMixin(UmbLitElemen ]; } -export default UmbInputTemplatePickerElement; +export default UmbInputTemplateElement; declare global { interface HTMLElementTagNameMap { - 'umb-input-template-picker': UmbInputTemplatePickerElement; + 'umb-input-template': UmbInputTemplateElement; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-upload-field/input-upload-field.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-upload-field/input-upload-field.element.ts index a5237a876d..f4727f6792 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-upload-field/input-upload-field.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-upload-field/input-upload-field.element.ts @@ -64,10 +64,10 @@ export class UmbInputUploadFieldElement extends FormControlMixin(UmbLitElement) multiple = false; @state() - _currentFiles: Blob[] = []; + _currentFiles: File[] = []; @state() - _currentFilesTemp?: Blob[]; + _currentFilesTemp?: File[]; @state() extensions?: string[]; @@ -114,12 +114,12 @@ export class UmbInputUploadFieldElement extends FormControlMixin(UmbLitElement) this.#setFiles(validated); } - #validateExtensions(): Blob[] { + #validateExtensions(): File[] { // TODO: Should property editor be able to handle allowed extensions like image/* ? - const filesValidated: Blob[] = []; + const filesValidated: File[] = []; this._currentFilesTemp?.forEach((temp) => { - const type = temp.type.slice(temp.type.lastIndexOf('/') + 1, temp.length); + const type = temp.type.slice(temp.type.lastIndexOf('/') + 1); if (this.fileExtensions?.find((x) => x === type)) filesValidated.push(temp); else this._notificationContext?.peek('danger', { @@ -129,7 +129,7 @@ export class UmbInputUploadFieldElement extends FormControlMixin(UmbLitElement) return filesValidated; } - #setFiles(files: Blob[]) { + #setFiles(files: File[]) { this._currentFiles = [...this._currentFiles, ...files]; //TODO: set keys when possible, not names diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section-sidebar/section-sidebar.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section-sidebar/section-sidebar.context.ts index c3a4264b2a..8185a4f502 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section-sidebar/section-sidebar.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section-sidebar/section-sidebar.context.ts @@ -1,19 +1,19 @@ import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; -import { StringState, BooleanState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbStringState, UmbBooleanState } from '@umbraco-cms/backoffice/observable-api'; export class UmbSectionSidebarContext { #host: UmbControllerHostElement; - #contextMenuIsOpen = new BooleanState(false); + #contextMenuIsOpen = new UmbBooleanState(false); contextMenuIsOpen = this.#contextMenuIsOpen.asObservable(); - #entityType = new StringState(undefined); + #entityType = new UmbStringState(undefined); entityType = this.#entityType.asObservable(); - #unique = new StringState(undefined); + #unique = new UmbStringState(undefined); unique = this.#unique.asObservable(); - #headline = new StringState(undefined); + #headline = new UmbStringState(undefined); headline = this.#headline.asObservable(); constructor(host: UmbControllerHostElement) { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section.context.ts index 2f90f58eca..628062ceb0 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section.context.ts @@ -1,14 +1,14 @@ import type { ManifestSection } from '@umbraco-cms/backoffice/extensions-registry'; import type { Entity } from '@umbraco-cms/backoffice/models'; -import { ObjectState, StringState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbObjectState, UmbStringState } from '@umbraco-cms/backoffice/observable-api'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; export type ActiveTreeItemType = Entity | undefined; export class UmbSectionContext { - #manifestAlias = new StringState(undefined); - #manifestPathname = new StringState(undefined); - #manifestLabel = new StringState(undefined); + #manifestAlias = new UmbStringState(undefined); + #manifestPathname = new UmbStringState(undefined); + #manifestLabel = new UmbStringState(undefined); public readonly alias = this.#manifestAlias.asObservable(); public readonly pathname = this.#manifestPathname.asObservable(); public readonly label = this.#manifestLabel.asObservable(); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/template-card/template-card.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/template-card/template-card.element.ts index ec7cbf1f29..aaebc484c6 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/template-card/template-card.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/template-card/template-card.element.ts @@ -9,10 +9,9 @@ import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; * @slot actions * @fires open * @fires selected - * - * */ - +// TODO: This should extends the UUICardElement, and the visual look of this should be like the UserCard or similarly. +// TOOD: Consider if this should be select in the 'persisted'-select style when it is selected as a default. (But its should not use the runtime-selection style) @customElement('umb-template-card') export class UmbTemplateCardElement extends FormControlMixin(UmbLitElement) { @property({ type: String }) @@ -39,12 +38,12 @@ export class UmbTemplateCardElement extends FormControlMixin(UmbLitElement) { e.preventDefault(); e.stopPropagation(); //this.selected = true; - this.dispatchEvent(new CustomEvent('change-default', { bubbles: true, composed: true })); + this.dispatchEvent(new CustomEvent('change', { bubbles: false, composed: true })); } #openTemplate(e: KeyboardEvent) { e.preventDefault(); e.stopPropagation(); - this.dispatchEvent(new CustomEvent('open', { bubbles: true, composed: true })); + this.dispatchEvent(new CustomEvent('open', { bubbles: false, composed: true })); } render() { @@ -54,7 +53,7 @@ export class UmbTemplateCardElement extends FormControlMixin(UmbLitElement) { ${this.name.length ? this.name : 'Untitled template'} - ${this.default ? '(Default template)' : 'Set default'} + ${this.default ? '(Default template)' : 'Make default'} `; @@ -88,14 +87,6 @@ export class UmbTemplateCardElement extends FormControlMixin(UmbLitElement) { padding: var(--uui-size-4); } - :host([default]) #card { - border: 1px solid var(--uui-color-selected); - outline: 1px solid var(--uui-color-selected); - } - #card:has(uui-button:hover) { - border: 1px solid var(--uui-color-selected); - } - #bottom { margin-top: auto; } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/context-menu/tree-context-menu-page.service.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/context-menu/tree-context-menu-page.service.ts index d82eb782bf..584b77456a 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/context-menu/tree-context-menu-page.service.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/context-menu/tree-context-menu-page.service.ts @@ -2,7 +2,7 @@ import { UUITextStyles } from '@umbraco-ui/uui-css'; import { css, nothing, PropertyValueMap } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; -import { DeepState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbDeepState } from '@umbraco-cms/backoffice/observable-api'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; // TODO: Refactor this, its not a service and the data should be handled by a context api. @@ -13,7 +13,7 @@ export class UmbTreeContextMenuPageServiceElement extends UmbLitElement { @property({ type: Object }) public actionEntity: any = { key: '', name: '' }; - #entity = new DeepState({ key: '', name: '' } as any); + #entity = new UmbDeepState({ key: '', name: '' } as any); public readonly entity = this.#entity.asObservable(); @state() diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree-item-base/tree-item-base.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree-item-base/tree-item-base.context.ts index 0465630182..9c81962047 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree-item-base/tree-item-base.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree-item-base/tree-item-base.context.ts @@ -7,7 +7,12 @@ import { UmbSectionContext, UMB_SECTION_CONTEXT_TOKEN } from '../../section/sect import { UmbTreeContextBase } from '../tree.context'; import { UmbTreeItemContext } from '../tree-item.context.interface'; import { ManifestEntityAction } from '@umbraco-cms/backoffice/extensions-registry'; -import { BooleanState, DeepState, StringState, UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; +import { + UmbBooleanState, + UmbDeepState, + UmbStringState, + UmbObserverController, +} from '@umbraco-cms/backoffice/observable-api'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; import { UmbContextConsumerController, @@ -27,28 +32,28 @@ export class UmbTreeItemContextBase(undefined); + #treeItem = new UmbDeepState(undefined); treeItem = this.#treeItem.asObservable(); - #hasChildren = new BooleanState(false); + #hasChildren = new UmbBooleanState(false); hasChildren = this.#hasChildren.asObservable(); - #isLoading = new BooleanState(false); + #isLoading = new UmbBooleanState(false); isLoading = this.#isLoading.asObservable(); - #isSelectable = new BooleanState(false); + #isSelectable = new UmbBooleanState(false); isSelectable = this.#isSelectable.asObservable(); - #isSelected = new BooleanState(false); + #isSelected = new UmbBooleanState(false); isSelected = this.#isSelected.asObservable(); - #isActive = new BooleanState(false); + #isActive = new UmbBooleanState(false); isActive = this.#isActive.asObservable(); - #hasActions = new BooleanState(false); + #hasActions = new UmbBooleanState(false); hasActions = this.#hasActions.asObservable(); - #path = new StringState(''); + #path = new UmbStringState(''); path = this.#path.asObservable(); treeContext?: UmbTreeContextBase; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree.context.ts index 89020e9acd..cc720084d3 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree.context.ts @@ -1,7 +1,7 @@ import type { Observable } from 'rxjs'; import { UmbTreeRepository } from '@umbraco-cms/backoffice/repository'; import type { ManifestTree } from '@umbraco-cms/backoffice/extensions-registry'; -import { DeepState, UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; +import { UmbBooleanState, UmbArrayState, UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; import { createExtensionClass, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extensions-api'; @@ -10,6 +10,7 @@ export interface UmbTreeContext { readonly selectable: Observable; readonly selection: Observable>; setSelectable(value: boolean): void; + setMultiple(value: boolean): void; setSelection(value: Array): void; select(id: string): void; } @@ -18,10 +19,13 @@ export class UmbTreeContextBase implements UmbTreeContext { host: UmbControllerHostElement; public tree: ManifestTree; - #selectable = new DeepState(false); + #selectable = new UmbBooleanState(false); public readonly selectable = this.#selectable.asObservable(); - #selection = new DeepState(>[]); + #multiple = new UmbBooleanState(false); + public readonly multiple = this.#multiple.asObservable(); + + #selection = new UmbArrayState(>[]); public readonly selection = this.#selection.asObservable(); repository?: UmbTreeRepository; @@ -69,22 +73,36 @@ export class UmbTreeContextBase implements UmbTreeContext { this.#selectable.next(value); } + public getSelectable() { + return this.#selectable.getValue(); + } + + public setMultiple(value: boolean) { + this.#multiple.next(value); + } + + public getMultiple() { + return this.#multiple.getValue(); + } + public setSelection(value: Array) { if (!value) return; this.#selection.next(value); } - public select(id: string) { - const oldSelection = this.#selection.getValue(); - if (oldSelection.indexOf(id) !== -1) return; + public getSelection() { + return this.#selection.getValue(); + } - const selection = [...oldSelection, id]; - this.#selection.next(selection); + public select(id: string) { + if (!this.getSelectable()) return; + const newSelection = this.getMultiple() ? [...this.getSelection(), id] : [id]; + this.#selection.next(newSelection); } public deselect(id: string) { - const selection = this.#selection.getValue(); - this.#selection.next(selection.filter((x) => x !== id)); + const newSelection = this.getSelection().filter((x) => x !== id); + this.#selection.next(newSelection); } public async requestRootItems() { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree.element.ts index 3466abc60b..e50d1e7930 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tree/tree.element.ts @@ -51,6 +51,18 @@ export class UmbTreeElement extends UmbLitElement { this._treeContext?.setSelection(newVal); } + private _multiple = false; + @property({ type: Boolean, reflect: true }) + get multiple() { + return this._multiple; + } + set multiple(newVal) { + const oldVal = this._multiple; + this._multiple = newVal; + this.requestUpdate('multiple', oldVal); + this._treeContext?.setMultiple(newVal); + } + @state() private _tree?: ManifestTree; @@ -86,6 +98,7 @@ export class UmbTreeElement extends UmbLitElement { this._treeContext = new UmbTreeContextBase(this, this._tree); this._treeContext.setSelectable(this.selectable); this._treeContext.setSelection(this.selection); + this._treeContext.setMultiple(this.multiple); this.#observeSelection(); this.#observeTreeRoot(); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace-property/workspace-property.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace-property/workspace-property.context.ts index e069c2a5b5..5cad4cae04 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace-property/workspace-property.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace-property/workspace-property.context.ts @@ -3,7 +3,12 @@ import { UmbWorkspaceVariableEntityContextInterface } from '../workspace/workspa import { UMB_WORKSPACE_VARIANT_CONTEXT_TOKEN } from '../workspace/workspace-variant/workspace-variant.context'; import type { DataTypeResponseModel } from '@umbraco-cms/backoffice/backend-api'; import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; -import { ClassState, ObjectState, StringState, UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; +import { + UmbClassState, + UmbObjectState, + UmbStringState, + UmbObserverController, +} from '@umbraco-cms/backoffice/observable-api'; import { UmbContextConsumerController, UmbContextProviderController, @@ -25,7 +30,7 @@ export class UmbWorkspacePropertyContext { private _providerController: UmbContextProviderController; - private _data = new ObjectState>({}); + private _data = new UmbObjectState>({}); public readonly alias = this._data.getObservablePart((data) => data.alias); public readonly label = this._data.getObservablePart((data) => data.label); @@ -35,10 +40,10 @@ export class UmbWorkspacePropertyContext { #workspaceVariantId?: UmbVariantId; - #variantId = new ClassState(undefined); + #variantId = new UmbClassState(undefined); public readonly variantId = this.#variantId.asObservable(); - private _variantDifference = new StringState(undefined); + private _variantDifference = new UmbStringState(undefined); public readonly variantDifference = this._variantDifference.asObservable(); private _workspaceContext?: UmbWorkspaceVariableEntityContextInterface; @@ -88,7 +93,7 @@ export class UmbWorkspacePropertyContext { this._data.update({ description }); } public setValue(value: WorkspacePropertyData['value']) { - // Note: Do not try to compare new / old value, as it can of any type. We trust the ObjectState in doing such. + // Note: Do not try to compare new / old value, as it can of any type. We trust the UmbObjectState in doing such. this._data.update({ value }); } public changeValue(value: WorkspacePropertyData['value']) { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/entity-manager-controller.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/entity-manager-controller.ts index 9704ce80c2..9eb0f5400c 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/entity-manager-controller.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/entity-manager-controller.ts @@ -6,7 +6,7 @@ import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN, } from '@umbraco-cms/backoffice/notification'; -import { ObjectState, UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; +import { UmbObjectState, UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; import type { EntityTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; import { UmbEntityDetailStore } from '@umbraco-cms/backoffice/store'; @@ -18,7 +18,7 @@ export class UmbEntityWorkspaceManager< > { private _host; - state = new ObjectState(undefined); + state = new UmbObjectState(undefined); protected _storeSubscription?: UmbObserverController; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-container-structure-helper.class.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-container-structure-helper.class.ts index 25b756dfbf..5a76262c13 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-container-structure-helper.class.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-container-structure-helper.class.ts @@ -3,10 +3,11 @@ import { PropertyContainerTypes } from './workspace-structure-manager.class'; import { PropertyTypeContainerResponseModelBaseModel } from '@umbraco-cms/backoffice/backend-api'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; import { UmbContextConsumerController, UMB_ENTITY_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/context-api'; -import { ArrayState, BooleanState, UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; +import { UmbArrayState, UmbBooleanState, UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; export class UmbWorkspaceContainerStructureHelper { #host: UmbControllerHostElement; + #init; #workspaceContext?: UmbDocumentWorkspaceContext; @@ -17,13 +18,14 @@ export class UmbWorkspaceContainerStructureHelper { private _ownerKey?: string; // Containers defined in data might be more than actual containers to display as we merge them by name. + // Owner containers are the containers defining the total of this container(Multiple containers with the same name and type) private _ownerContainers: PropertyTypeContainerResponseModelBaseModel[] = []; // State containing the merged containers (only one pr. name): - #containers = new ArrayState([], (x) => x.id); + #containers = new UmbArrayState([], (x) => x.id); readonly containers = this.#containers.asObservable(); - #hasProperties = new BooleanState(false); + #hasProperties = new UmbBooleanState(false); readonly hasProperties = this.#hasProperties.asObservable(); constructor(host: UmbControllerHostElement) { @@ -31,10 +33,10 @@ export class UmbWorkspaceContainerStructureHelper { this.#containers.sortBy((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0)); - new UmbContextConsumerController(host, UMB_ENTITY_WORKSPACE_CONTEXT, (context) => { + this.#init = new UmbContextConsumerController(host, UMB_ENTITY_WORKSPACE_CONTEXT, (context) => { this.#workspaceContext = context as UmbDocumentWorkspaceContext; this._observeOwnerContainers(); - }); + }).asPromise(); } public setType(value?: PropertyContainerTypes) { @@ -150,11 +152,27 @@ export class UmbWorkspaceContainerStructureHelper { }); }; + /** + * Returns true if the container is an owner container. + */ + isOwnerContainer(groupId?: string) { + if (!this.#workspaceContext || !groupId) return; + + return this._ownerContainers.find((x) => x.id === groupId) !== undefined; + } + /** Manipulate methods: */ - async addGroup(ownerKey?: string, sortOrder?: number) { + async addContainer(ownerId?: string, sortOrder?: number) { if (!this.#workspaceContext) return; - await this.#workspaceContext.structure.createContainer(null, ownerKey, this._childType, sortOrder); + await this.#workspaceContext.structure.createContainer(null, ownerId, this._childType, sortOrder); + } + + async partialUpdateContainer(groupId?: string, partialUpdate?: Partial) { + await this.#init; + if (!this.#workspaceContext || !groupId || !partialUpdate) return; + + return await this.#workspaceContext.structure.updateContainer(null, groupId, partialUpdate); } } 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 796b258bd3..05d6a77a39 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 { BooleanState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbBooleanState } 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 BooleanState(undefined); + #isNew = new UmbBooleanState(undefined); isNew = this.#isNew.asObservable(); constructor(host: UmbControllerHostElement, repository: T) { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-property-structure-helper.class.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-property-structure-helper.class.ts index 6941cd3daa..0df98340b0 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-property-structure-helper.class.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-property-structure-helper.class.ts @@ -1,12 +1,16 @@ import { UmbDocumentWorkspaceContext } from '../../../../documents/documents/workspace/document-workspace.context'; import { PropertyContainerTypes } from './workspace-structure-manager.class'; -import { DocumentTypePropertyTypeResponseModel } from '@umbraco-cms/backoffice/backend-api'; +import { + DocumentTypePropertyTypeResponseModel, + PropertyTypeResponseModelBaseModel, +} from '@umbraco-cms/backoffice/backend-api'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; import { UmbContextConsumerController, UMB_ENTITY_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/context-api'; -import { ArrayState, UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; +import { UmbArrayState, UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; export class UmbWorkspacePropertyStructureHelper { #host: UmbControllerHostElement; + #init; #workspaceContext?: UmbDocumentWorkspaceContext; @@ -14,15 +18,17 @@ export class UmbWorkspacePropertyStructureHelper { private _isRoot?: boolean; private _containerName?: string; - #propertyStructure = new ArrayState([], (x) => x.id); + #propertyStructure = new UmbArrayState([], (x) => x.id); readonly propertyStructure = this.#propertyStructure.asObservable(); constructor(host: UmbControllerHostElement) { this.#host = host; - new UmbContextConsumerController(host, UMB_ENTITY_WORKSPACE_CONTEXT, (context) => { + // TODO: Remove as any when sortOrder is implemented: + this.#propertyStructure.sortBy((a, b) => ((a as any).sortOrder ?? 0) - ((b as any).sortOrder ?? 0)); + this.#init = new UmbContextConsumerController(host, UMB_ENTITY_WORKSPACE_CONTEXT, (context) => { this.#workspaceContext = context as UmbDocumentWorkspaceContext; this._observeGroupContainers(); - }); + }).asPromise(); } public setContainerType(value?: PropertyContainerTypes) { @@ -85,11 +91,6 @@ export class UmbWorkspacePropertyStructureHelper { } }); - if (_propertyStructure.length > 0) { - // TODO: End-point: Missing sort order? - //_propertyStructure = _propertyStructure.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0)); - } - // Fire update to subscribers: this.#propertyStructure.next(_propertyStructure); }, @@ -97,16 +98,38 @@ export class UmbWorkspacePropertyStructureHelper { ); } + // TODO: consider moving this to another class, to separate 'viewer' from 'manipulator': /** Manipulate methods: */ - async addProperty(ownerKey?: string, sortOrder?: number) { + async addProperty(ownerId?: string, sortOrder?: number) { + await this.#init; if (!this.#workspaceContext) return; - return await this.#workspaceContext.structure.createProperty(null, ownerKey, sortOrder); + return await this.#workspaceContext.structure.createProperty(null, ownerId, sortOrder); + } + + async insertProperty(property: PropertyTypeResponseModelBaseModel, sortOrder = 0) { + await this.#init; + if (!this.#workspaceContext) return false; + + const newProperty = { ...property, sortOrder }; + + // TODO: Remove as any when server model has gotten sortOrder: + await this.#workspaceContext.structure.insertProperty(null, newProperty); + return true; + } + + async removeProperty(propertyId: string) { + await this.#init; + if (!this.#workspaceContext) return false; + + await this.#workspaceContext.structure.removeProperty(null, propertyId); + return true; } // Takes optional arguments as this is easier for the implementation in the view: async partialUpdateProperty(propertyKey?: string, partialUpdate?: Partial) { + await this.#init; if (!this.#workspaceContext || !propertyKey || !partialUpdate) return; return await this.#workspaceContext.structure.updateProperty(null, propertyKey, partialUpdate); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-split-view-manager.class.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-split-view-manager.class.ts index 3c238c96b0..b300be5ebf 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-split-view-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-split-view-manager.class.ts @@ -1,6 +1,6 @@ import { UmbVariantId } from '../../../variants/variant-id.class'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; -import { ArrayState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; export type ActiveVariant = { index: number; @@ -16,7 +16,7 @@ export type ActiveVariant = { export class UmbWorkspaceSplitViewManager { #host: UmbControllerHostElement; - #activeVariantsInfo = new ArrayState([], (x) => x.index); + #activeVariantsInfo = new UmbArrayState([], (x) => x.index); public readonly activeVariantsInfo = this.#activeVariantsInfo.asObservable(); constructor(host: UmbControllerHostElement) { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-structure-manager.class.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-structure-manager.class.ts index 8a7b24dc23..bbce77931d 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-structure-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-structure-manager.class.ts @@ -9,10 +9,12 @@ import { } from '@umbraco-cms/backoffice/backend-api'; import { UmbControllerHostElement, UmbControllerInterface } from '@umbraco-cms/backoffice/controller'; import { - ArrayState, + UmbArrayState, UmbObserverController, MappingFunction, partialUpdateFrozenArray, + appendToFrozenArray, + filterFrozenArray, } from '@umbraco-cms/backoffice/observable-api'; export type PropertyContainerTypes = 'Group' | 'Tab'; @@ -27,15 +29,15 @@ export class UmbWorkspacePropertyStructureManager(); - #documentTypes = new ArrayState([], (x) => x.id); + #documentTypes = new UmbArrayState([], (x) => x.id); readonly documentTypes = this.#documentTypes.asObservable(); private readonly _documentTypeContainers = this.#documentTypes.getObservablePart((x) => x.flatMap((x) => x.containers ?? []) ); - #containers = new ArrayState([], (x) => x.id); + #containers = new UmbArrayState([], (x) => x.id); constructor(host: UmbControllerHostElement, typeRepository: R) { this.#host = host; @@ -62,7 +64,7 @@ export class UmbWorkspacePropertyStructureManager x.find((y) => y.id === this.#rootDocumentTypeKey)); + return this.#documentTypes.getObservablePart((x) => x.find((y) => y.id === this.#rootDocumentTypeId)); } getRootDocumentType() { - return this.#documentTypes.getValue().find((y) => y.id === this.#rootDocumentTypeKey); + return this.#documentTypes.getValue().find((y) => y.id === this.#rootDocumentTypeId); } updateRootDocumentType(entry: T) { - this.#documentTypes.updateOne(this.#rootDocumentTypeKey, entry); + this.#documentTypes.updateOne(this.#rootDocumentTypeId, entry); } // We could move the actions to another class? @@ -157,7 +159,7 @@ export class UmbWorkspacePropertyStructureManager + ) { + await this.#init; + documentTypeId = documentTypeId ?? this.#rootDocumentTypeId!; + + const frozenContainers = this.#documentTypes.getValue().find((x) => x.id === documentTypeId)?.containers ?? []; + + const containers = partialUpdateFrozenArray(frozenContainers, partialUpdate, (x) => x.id === groupKey); + + this.#documentTypes.updateOne(documentTypeId, { containers }); + } + async removeContainer(documentTypeKey: string | null, containerId: string | null = null) { await this.#init; - documentTypeKey = documentTypeKey ?? this.#rootDocumentTypeKey!; + documentTypeKey = documentTypeKey ?? this.#rootDocumentTypeId!; const frozenContainers = this.#documentTypes.getValue().find((x) => x.id === documentTypeKey)?.containers ?? []; const containers = frozenContainers.filter((x) => x.id !== containerId); @@ -185,9 +202,9 @@ export class UmbWorkspacePropertyStructureManager x.id === documentTypeKey)?.properties ?? [])]; + const properties = [...(this.#documentTypes.getValue().find((x) => x.id === documentTypeId)?.properties ?? [])]; properties.push(property); - this.#documentTypes.updateOne(documentTypeKey, { properties }); + this.#documentTypes.updateOne(documentTypeId, { properties }); return property; } + async insertProperty(documentTypeId: string | null, property: PropertyTypeResponseModelBaseModel) { + await this.#init; + documentTypeId = documentTypeId ?? this.#rootDocumentTypeId!; + + const frozenProperties = this.#documentTypes.getValue().find((x) => x.id === documentTypeId)?.properties ?? []; + + const properties = appendToFrozenArray(frozenProperties, property, (x) => x.id === property.id); + + this.#documentTypes.updateOne(documentTypeId, { properties }); + } + + async removeProperty(documentTypeId: string | null, propertyId: string) { + await this.#init; + documentTypeId = documentTypeId ?? this.#rootDocumentTypeId!; + + const frozenProperties = this.#documentTypes.getValue().find((x) => x.id === documentTypeId)?.properties ?? []; + + const properties = filterFrozenArray(frozenProperties, (x) => x.id === propertyId); + + this.#documentTypes.updateOne(documentTypeId, { properties }); + } + async updateProperty( - documentTypeKey: string | null, - propertyKey: string, - partialUpdate: Partial + documentTypeId: string | null, + propertyId: string, + partialUpdate: Partial ) { await this.#init; - documentTypeKey = documentTypeKey ?? this.#rootDocumentTypeKey!; + documentTypeId = documentTypeId ?? this.#rootDocumentTypeId!; - const frozenProperties = this.#documentTypes.getValue().find((x) => x.id === documentTypeKey)?.properties ?? []; + const frozenProperties = this.#documentTypes.getValue().find((x) => x.id === documentTypeId)?.properties ?? []; - const properties = partialUpdateFrozenArray(frozenProperties, partialUpdate, (x) => x.id === propertyKey!); + const properties = partialUpdateFrozenArray(frozenProperties, partialUpdate, (x) => x.id === propertyId); - this.#documentTypes.updateOne(documentTypeKey, { properties }); + this.#documentTypes.updateOne(documentTypeId, { properties }); } /* @@ -229,7 +268,7 @@ export class UmbWorkspacePropertyStructureManager(mappingFunction: MappingFunction) { return this.#documentTypes.getObservablePart((docTypes) => { - const docType = docTypes.find((x) => x.id === this.#rootDocumentTypeKey); + const docType = docTypes.find((x) => x.id === this.#rootDocumentTypeId); return docType ? mappingFunction(docType) : undefined; }); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-layout/workspace-layout.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-layout/workspace-layout.element.ts index 2e635d9114..fe1ca13fe1 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-layout/workspace-layout.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-layout/workspace-layout.element.ts @@ -4,7 +4,7 @@ import { customElement, property, state } from 'lit/decorators.js'; import { map } from 'rxjs'; import { repeat } from 'lit/directives/repeat.js'; -import type { IRoute } from '@umbraco-cms/backoffice/router'; +import type { IRoute, PageComponent } from '@umbraco-cms/backoffice/router'; import type { UmbRouterSlotInitEvent, UmbRouterSlotChangeEvent } from '@umbraco-cms/internal/router'; import { createExtensionElement, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extensions-api'; import type { @@ -120,6 +120,11 @@ export class UmbWorkspaceLayoutElement extends UmbLitElement { ); } + // TODO: Move into a helper function: + private componentHasManifest(component: PageComponent): component is HTMLElement & { manifest: unknown } { + return component ? 'manifest' in component : false; + } + private _createRoutes() { this._routes = []; @@ -136,16 +141,8 @@ export class UmbWorkspaceLayoutElement extends UmbLitElement { return createExtensionElement(view); }, setup: (component, info) => { - if (component && 'manifest' in component) { + if (this.componentHasManifest(component)) { component.manifest = view; - } else { - /* - TODO: Too noisy for my taste, so I would investigate if there is otherwise to make this more visible. - console.group(`[UmbWorkspaceLayout] Failed to setup component for route: ${info.match.route.path}`); - console.log('Matched route', info.match.route); - console.error('Missing property "manifest" on component', component); - console.groupEnd(); - */ } }, }; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-variant/workspace-variant.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-variant/workspace-variant.context.ts index 0f621391b5..4364c6d3b8 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-variant/workspace-variant.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-variant/workspace-variant.context.ts @@ -9,7 +9,12 @@ import { UMB_ENTITY_WORKSPACE_CONTEXT, } from '@umbraco-cms/backoffice/context-api'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; -import { ClassState, NumberState, ObjectState, UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; +import { + UmbClassState, + UmbNumberState, + UmbObjectState, + UmbObserverController, +} from '@umbraco-cms/backoffice/observable-api'; import { DocumentVariantResponseModel } from '@umbraco-cms/backoffice/backend-api'; //type EntityType = DocumentModel; @@ -22,17 +27,17 @@ export class UmbWorkspaceVariantContext { return this.#workspaceContext; } - #index = new NumberState(undefined); + #index = new UmbNumberState(undefined); index = this.#index.asObservable(); - #currentVariant = new ObjectState(undefined); + #currentVariant = new UmbObjectState(undefined); currentVariant = this.#currentVariant.asObservable(); name = this.#currentVariant.getObservablePart((x) => x?.name); culture = this.#currentVariant.getObservablePart((x) => x?.culture); segment = this.#currentVariant.getObservablePart((x) => x?.segment); - #variantId = new ClassState(undefined); + #variantId = new UmbClassState(undefined); variantId = this.#variantId.asObservable(); private _currentVariantObserver?: UmbObserverController; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/icon-picker/icon-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/icon-picker/icon-picker-modal.element.ts index ff0cab31b1..4dadf09877 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/icon-picker/icon-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/icon-picker/icon-picker-modal.element.ts @@ -2,7 +2,7 @@ import type { UUIColorSwatchesEvent } from '@umbraco-ui/uui'; import { css, html } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; -import { customElement, property, state } from 'lit/decorators.js'; +import { customElement, state } from 'lit/decorators.js'; import { styleMap } from 'lit/directives/style-map.js'; import icons from '../../../../../public-assets/icons/icons.json'; @@ -13,6 +13,128 @@ import { UmbModalBaseElement } from '@umbraco-cms/internal/modal'; // TODO: to prevent element extension we need to move the Picker logic into a separate class we can reuse across all pickers @customElement('umb-icon-picker-modal') export class UmbIconPickerModalElement extends UmbModalBaseElement { + private _iconList = icons.map((icon) => icon.name); + + @state() + private _iconListFiltered: Array = []; + + @state() + private _colorList = [ + '#000000', + '#373737', + '#9e9e9e', + '#607d8b', + '#2196f3', + '#03a9f4', + '#3f51b5', + '#9c27b0', + '#673ab7', + '#00bcd4', + '#4caf50', + '#8bc34a', + '#cddc39', + '#ffeb3b', + '#ffc107', + '#ff9800', + '#ff5722', + '#f44336', + '#e91e63', + '#795548', + ]; + + @state() + private _currentColor?: string; + + @state() + private _currentIcon?: string; + + private _changeIcon(e: { target: HTMLInputElement; type: any; key: unknown }) { + if (e.type == 'click' || (e.type == 'keyup' && e.key == 'Enter')) { + this._currentIcon = e.target.id; + } + } + + private _filterIcons(e: { target: HTMLInputElement }) { + if (e.target.value) { + this._iconListFiltered = this._iconList.filter((icon) => icon.includes(e.target.value)); + } else { + this._iconListFiltered = this._iconList; + } + } + + private _close() { + this.modalHandler?.reject(); + } + + private _submit() { + this.modalHandler?.submit({ color: this._currentColor, icon: this._currentIcon }); + } + + private _onColorChange(e: UUIColorSwatchesEvent) { + this._currentColor = e.target.value; + } + + connectedCallback() { + super.connectedCallback(); + this._currentColor = this.data?.color ?? this._colorList[0]; + this._currentIcon = this.data?.icon ?? this._iconList[0]; + this._iconListFiltered = this._iconList; + } + + render() { + return html` + +
+ ${this.renderSearchbar()} +
+ + ${this._colorList.map( + (color) => + html` ` + )} + + +
+ ${this.renderIconSelection()} +
+ Close + + Submit + +
+ `; + } + + renderSearchbar() { + return html` + + `; + } + + renderIconSelection() { + return html`${this._iconListFiltered.map((icon) => { + return html` + + `; + })}`; + } + static styles = [ UUITextStyles, css` @@ -82,129 +204,6 @@ export class UmbIconPickerModalElement extends UmbModalBaseElement icon.name); - - @property({ type: Array }) - iconlistFiltered: Array = []; - - @property({ type: Array }) - colorlist = [ - '#000000', - '#373737', - '#9e9e9e', - '#607d8b', - '#2196f3', - '#03a9f4', - '#3f51b5', - '#9c27b0', - '#673ab7', - '#00bcd4', - '#4caf50', - '#8bc34a', - '#cddc39', - '#ffeb3b', - '#ffc107', - '#ff9800', - '#ff5722', - '#f44336', - '#e91e63', - '#795548', - ]; - - @state() - private _currentColor?: string; - - @state() - private _currentIcon?: string; - - private _changeIcon(e: { target: HTMLInputElement; type: any; key: unknown }) { - if (e.type == 'click' || (e.type == 'keyup' && e.key == 'Enter')) { - this._currentIcon = e.target.id; - } - } - - private _filterIcons(e: { target: HTMLInputElement }) { - if (e.target.value) { - this.iconlistFiltered = this.iconlist.filter((icon) => icon.includes(e.target.value)); - } else { - this.iconlistFiltered = this.iconlist; - } - } - - private _close() { - this.modalHandler?.reject(); - } - - private _save() { - this.modalHandler?.submit({ color: this._currentColor, icon: this._currentIcon }); - } - - private _onColorChange(e: UUIColorSwatchesEvent) { - this._currentColor = e.target.value; - } - - connectedCallback(): void { - super.connectedCallback(); - this._currentColor = this.colorlist[0]; - this._currentIcon = this.iconlist[0]; - this.iconlistFiltered = this.iconlist; - } - - render() { - return html` - -
- ${this.renderSearchbar()} -
- - ${this.colorlist.map( - (color) => - html` ` - )} - - -
- ${this.renderIconSelection()} -
- Close - - Save - -
- `; - } - - renderSearchbar() { - return html` - - `; - } - - renderIconSelection() { - return html`${this.iconlistFiltered.map((icon) => { - return html` - - `; - })}`; - } } export default UmbIconPickerModalElement; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/icon-picker/icon-picker-modal.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/icon-picker/icon-picker-modal.stories.ts index 53ee8a5bdb..17de382faf 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/icon-picker/icon-picker-modal.stories.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/icon-picker/icon-picker-modal.stories.ts @@ -14,8 +14,8 @@ export default { } as Meta; const data: UmbIconPickerModalData = { - multiple: true, - selection: [], + color: undefined, + icon: undefined, }; export const Overview: Story = () => html` diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/property-settings/property-settings-modal.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/property-settings/property-settings-modal.element.ts index c57165fe42..3e345aad1e 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/property-settings/property-settings-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/modals/property-settings/property-settings-modal.element.ts @@ -127,7 +127,7 @@ export class UmbPropertySettingsModalElement extends UmbModalBaseElement this.#treeStore!.childrenOf(path) }; } - async requestTreeItems(paths: Array) { + async requestItemsLegacy(paths: Array) { if (!paths) throw new Error('Paths are missing'); await this.#init; const { data, error } = await this.#treeDataSource.getItems(paths); @@ -94,7 +94,7 @@ export class UmbStylesheetRepository return this.#treeStore!.childrenOf(parentPath); } - async treeItems(paths: Array) { + async itemsLegacy(paths: Array) { if (!paths) throw new Error('Paths are missing'); await this.#init; return this.#treeStore!.items(paths); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/workspace/stylesheet-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/workspace/stylesheet-workspace.context.ts index a973a00a4b..e92b3f643f 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/workspace/stylesheet-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/stylesheets/workspace/stylesheet-workspace.context.ts @@ -2,10 +2,10 @@ import { UmbWorkspaceContext } from '../../../shared/components/workspace/worksp import { UmbStylesheetRepository } from '../repository/stylesheet.repository'; import { StylesheetDetails } from '..'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; -import { ObjectState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; export class UmbStylesheetWorkspaceContext extends UmbWorkspaceContext { - #data = new ObjectState(undefined); + #data = new UmbObjectState(undefined); data = this.#data.asObservable(); constructor(host: UmbControllerHostElement) { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/sources/template.tree.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/sources/template.tree.server.data.ts index ed7be4b8d9..ee94417b50 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/sources/template.tree.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/sources/template.tree.server.data.ts @@ -6,16 +6,16 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; /** * A data source for the Template tree that fetches data from the server * @export - * @class TemplateTreeServerDataSource + * @class UmbTemplateTreeServerDataSource * @implements {TemplateTreeDataSource} */ -export class TemplateTreeServerDataSource implements TemplateTreeDataSource { +export class UmbTemplateTreeServerDataSource implements TemplateTreeDataSource { #host: UmbControllerHostElement; /** - * Creates an instance of TemplateTreeServerDataSource. + * Creates an instance of UmbTemplateTreeServerDataSource. * @param {UmbControllerHostElement} host - * @memberof TemplateTreeServerDataSource + * @memberof UmbTemplateTreeServerDataSource */ constructor(host: UmbControllerHostElement) { this.#host = host; @@ -24,7 +24,7 @@ export class TemplateTreeServerDataSource implements TemplateTreeDataSource { /** * Fetches the root items for the tree from the server * @return {*} - * @memberof TemplateTreeServerDataSource + * @memberof UmbTemplateTreeServerDataSource */ async getRootItems() { return tryExecuteAndNotify(this.#host, TemplateResource.getTreeTemplateRoot({})); @@ -34,7 +34,7 @@ export class TemplateTreeServerDataSource implements TemplateTreeDataSource { * Fetches the children of a given parent id from the server * @param {(string | null)} parentId * @return {*} - * @memberof TemplateTreeServerDataSource + * @memberof UmbTemplateTreeServerDataSource */ async getChildrenOf(parentId: string | null) { if (!parentId) { @@ -54,7 +54,7 @@ export class TemplateTreeServerDataSource implements TemplateTreeDataSource { * Fetches the items for the given ids from the server * @param {Array} id * @return {*} - * @memberof TemplateTreeServerDataSource + * @memberof UmbTemplateTreeServerDataSource */ async getItems(ids: Array) { if (!ids) { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/template.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/template.repository.ts index 0ec0c4baa7..24a0663004 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/template.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/template.repository.ts @@ -1,5 +1,5 @@ import { UmbTemplateDetailServerDataSource } from './sources/template.detail.server.data'; -import { TemplateTreeServerDataSource } from './sources/template.tree.server.data'; +import { UmbTemplateTreeServerDataSource } from './sources/template.tree.server.data'; import { UmbTemplateStore, UMB_TEMPLATE_STORE_CONTEXT_TOKEN } from './template.store'; import { UmbTemplateTreeStore, UMB_TEMPLATE_TREE_STORE_CONTEXT_TOKEN } from './template.tree.store'; import type { UmbDetailRepository, UmbTreeRepository } from '@umbraco-cms/backoffice/repository'; @@ -21,7 +21,7 @@ export class UmbTemplateRepository #init; #host: UmbControllerHostElement; - #treeDataSource: TemplateTreeServerDataSource; + #treeDataSource: UmbTemplateTreeServerDataSource; #detailDataSource: UmbTemplateDetailServerDataSource; #treeStore?: UmbTemplateTreeStore; @@ -33,7 +33,7 @@ export class UmbTemplateRepository this.#host = host; // TODO: figure out how spin up get the correct data source - this.#treeDataSource = new TemplateTreeServerDataSource(this.#host); + this.#treeDataSource = new UmbTemplateTreeServerDataSource(this.#host); this.#detailDataSource = new UmbTemplateDetailServerDataSource(this.#host); this.#init = Promise.all([ @@ -82,7 +82,7 @@ export class UmbTemplateRepository return { data, error, asObservable: () => this.#treeStore!.childrenOf(parentId) }; } - async requestTreeItems(ids: Array) { + async requestItemsLegacy(ids: Array) { await this.#init; if (!ids) { @@ -105,7 +105,7 @@ export class UmbTemplateRepository return this.#treeStore!.childrenOf(parentId); } - async treeItems(ids: Array) { + async itemsLegacy(ids: Array) { await this.#init; return this.#treeStore!.items(ids); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/template.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/template.store.ts index edebf64d61..d6e9a4589a 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/template.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/repository/template.store.ts @@ -1,5 +1,5 @@ import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import { ArrayState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; import { UmbStoreBase } from '@umbraco-cms/backoffice/store'; import type { TemplateResponseModel } from '@umbraco-cms/backoffice/backend-api'; import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; @@ -11,15 +11,13 @@ import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controlle * @description - Data Store for Templates */ export class UmbTemplateStore extends UmbStoreBase { - #data = new ArrayState([], (x) => x.id); - /** * Creates an instance of UmbTemplateStore. * @param {UmbControllerHostElement} host * @memberof UmbTemplateStore */ constructor(host: UmbControllerHostElement) { - super(host, UMB_TEMPLATE_STORE_CONTEXT_TOKEN.toString()); + super(host, UMB_TEMPLATE_STORE_CONTEXT_TOKEN.toString(), new UmbArrayState([], (x) => x.id)); } /** @@ -28,7 +26,7 @@ export class UmbTemplateStore extends UmbStoreBase { * @memberof UmbTemplateStore */ append(template: TemplateResponseModel) { - this.#data.append([template]); + this._data.append([template]); } /** @@ -37,7 +35,7 @@ export class UmbTemplateStore extends UmbStoreBase { * @memberof UmbTemplateStore */ remove(uniques: string[]) { - this.#data.remove(uniques); + this._data.remove(uniques); } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/template-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/template-workspace.context.ts index d4f7541846..89983e09f5 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/template-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/template-workspace.context.ts @@ -1,11 +1,11 @@ import { UmbTemplateRepository } from '../repository/template.repository'; import { UmbWorkspaceContext } from '../../../shared/components/workspace/workspace-context/workspace-context'; -import { createObservablePart, DeepState } from '@umbraco-cms/backoffice/observable-api'; +import { createObservablePart, UmbDeepState } from '@umbraco-cms/backoffice/observable-api'; import { TemplateResponseModel } from '@umbraco-cms/backoffice/backend-api'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; export class UmbTemplateWorkspaceContext extends UmbWorkspaceContext { - #data = new DeepState(undefined); + #data = new UmbDeepState(undefined); data = this.#data.asObservable(); name = createObservablePart(this.#data, (data) => data?.name); content = createObservablePart(this.#data, (data) => data?.content); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/themes/theme.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/themes/theme.context.ts index 959c8f19ef..1d3829bf8a 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/themes/theme.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/themes/theme.context.ts @@ -1,7 +1,7 @@ import { map } from 'rxjs'; import { manifests } from './manifests'; import { UmbContextProviderController, UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import { StringState, UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; +import { UmbStringState, UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extensions-api'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; import { ManifestTheme } from '@umbraco-cms/backoffice/extensions-registry'; @@ -11,7 +11,7 @@ const LOCAL_STORAGE_KEY = 'umb-theme-alias'; export class UmbThemeContext { private _host: UmbControllerHostElement; - #theme = new StringState('umb-light-theme'); + #theme = new UmbStringState('umb-light-theme'); public readonly theme = this.#theme.asObservable(); private themeSubscription?: UmbObserverController; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.repository.ts index 0ce6e7db0f..8ee8ddda86 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.repository.ts @@ -1,7 +1,7 @@ import { UmbDictionaryStore, UMB_DICTIONARY_STORE_CONTEXT_TOKEN } from './dictionary.store'; import { UmbDictionaryDetailServerDataSource } from './sources/dictionary.detail.server.data'; import { UmbDictionaryTreeStore, UMB_DICTIONARY_TREE_STORE_CONTEXT_TOKEN } from './dictionary.tree.store'; -import { DictionaryTreeServerDataSource } from './sources/dictionary.tree.server.data'; +import { UmbDictionaryTreeServerDataSource } from './sources/dictionary.tree.server.data'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; import { UmbTreeDataSource, UmbDetailRepository, UmbTreeRepository } from '@umbraco-cms/backoffice/repository'; @@ -39,7 +39,7 @@ export class UmbDictionaryRepository this.#host = host; // TODO: figure out how spin up get the correct data source - this.#treeSource = new DictionaryTreeServerDataSource(this.#host); + this.#treeSource = new UmbDictionaryTreeServerDataSource(this.#host); this.#detailSource = new UmbDictionaryDetailServerDataSource(this.#host); this.#init = Promise.all([ @@ -86,7 +86,7 @@ export class UmbDictionaryRepository return { data, error, asObservable: () => this.#treeStore!.childrenOf(parentId) }; } - async requestTreeItems(ids: Array) { + async requestItemsLegacy(ids: Array) { await this.#init; if (!ids) { @@ -109,7 +109,7 @@ export class UmbDictionaryRepository return this.#treeStore!.childrenOf(parentId); } - async treeItems(ids: Array) { + async itemsLegacy(ids: Array) { await this.#init; return this.#treeStore!.items(ids); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.store.ts index c4b62e7c82..94347f6a59 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.store.ts @@ -1,7 +1,7 @@ import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; import { UmbStoreBase } from '@umbraco-cms/backoffice/store'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; -import { ArrayState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; import { DictionaryItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; /** @@ -11,18 +11,20 @@ import { DictionaryItemResponseModel } from '@umbraco-cms/backoffice/backend-api * @description - Data Store for Dictionary */ export class UmbDictionaryStore extends UmbStoreBase { - #data = new ArrayState([], (x) => x.id); - constructor(host: UmbControllerHostElement) { - super(host, UMB_DICTIONARY_STORE_CONTEXT_TOKEN.toString()); + super( + host, + UMB_DICTIONARY_STORE_CONTEXT_TOKEN.toString(), + new UmbArrayState([], (x) => x.id) + ); } append(dictionary: DictionaryItemResponseModel) { - this.#data.append([dictionary]); + this._data.append([dictionary]); } remove(uniques: string[]) { - this.#data.remove(uniques); + this._data.remove(uniques); } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/sources/dictionary.tree.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/sources/dictionary.tree.server.data.ts index a24572eb9f..e9d5ab9a40 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/sources/dictionary.tree.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/sources/dictionary.tree.server.data.ts @@ -6,10 +6,10 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; /** * A data source for the Dictionary tree that fetches data from the server * @export - * @class DictionaryTreeServerDataSource + * @class UmbDictionaryTreeServerDataSource * @implements {DictionaryTreeDataSource} */ -export class DictionaryTreeServerDataSource implements UmbTreeDataSource { +export class UmbDictionaryTreeServerDataSource implements UmbTreeDataSource { #host: UmbControllerHostElement; /** @@ -24,7 +24,7 @@ export class DictionaryTreeServerDataSource implements UmbTreeDataSource { /** * Fetches the root items for the tree from the server * @return {*} - * @memberof DictionaryTreeServerDataSource + * @memberof UmbDictionaryTreeServerDataSource */ async getRootItems() { return tryExecuteAndNotify(this.#host, DictionaryResource.getTreeDictionaryRoot({})); @@ -34,7 +34,7 @@ export class DictionaryTreeServerDataSource implements UmbTreeDataSource { * Fetches the children of a given parent id from the server * @param {(string | null)} parentId * @return {*} - * @memberof DictionaryTreeServerDataSource + * @memberof UmbDictionaryTreeServerDataSource */ async getChildrenOf(parentId: string | null) { if (!parentId) { @@ -54,7 +54,7 @@ export class DictionaryTreeServerDataSource implements UmbTreeDataSource { * Fetches the items for the given ids from the server * @param {Array} ids * @return {*} - * @memberof DictionaryTreeServerDataSource + * @memberof UmbDictionaryTreeServerDataSource */ async getItems(ids: Array) { if (!ids || ids.length === 0) { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/workspace/dictionary-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/workspace/dictionary-workspace.context.ts index 2d2eb3039d..3a1115d880 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/workspace/dictionary-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/workspace/dictionary-workspace.context.ts @@ -2,14 +2,14 @@ import { UmbDictionaryRepository } from '../repository/dictionary.repository'; import { UmbWorkspaceContext } from '../../../../backoffice/shared/components/workspace/workspace-context/workspace-context'; import { UmbEntityWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; -import { ObjectState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; import { DictionaryItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; export class UmbDictionaryWorkspaceContext extends UmbWorkspaceContext implements UmbEntityWorkspaceContextInterface { - #data = new ObjectState(undefined); + #data = new UmbObjectState(undefined); data = this.#data.asObservable(); name = this.#data.getObservablePart((data) => data?.name); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/current-user/current-user-history.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/current-user/current-user-history.store.ts index 2a5df8b61f..888278353b 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/users/current-user/current-user-history.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/current-user/current-user-history.store.ts @@ -1,5 +1,5 @@ import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import { DeepState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbDeepState } from '@umbraco-cms/backoffice/observable-api'; export type UmbModelType = 'dialog' | 'sidebar'; @@ -10,7 +10,7 @@ export type UmbCurrentUserHistoryItem = { }; export class UmbCurrentUserHistoryStore { - #history = new DeepState(>[]); + #history = new UmbDeepState(>[]); public readonly history = this.#history.asObservable(); public readonly latestHistory = this.#history.getObservablePart((historyItems) => historyItems.slice(-10)); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/current-user/current-user.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/current-user/current-user.store.ts index 70556528f1..cf4823147e 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/users/current-user/current-user.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/current-user/current-user.store.ts @@ -2,13 +2,13 @@ import { umbUsersData } from '../../../core/mocks/data/users.data'; import { umbracoPath } from '@umbraco-cms/backoffice/utils'; import type { UserDetails } from '@umbraco-cms/backoffice/models'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import { ObjectState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; export const UMB_CURRENT_USER_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbCurrentUserStore'); export class UmbCurrentUserStore { //TODO: Temp solution to get a current user. Replace when we have a real user service - private _currentUser = new ObjectState(umbUsersData.getAll()[0]); + private _currentUser = new UmbObjectState(umbUsersData.getAll()[0]); public readonly currentUser = this._currentUser.asObservable(); /** diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/repository/user-group.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/repository/user-group.store.ts index 64197bb05b..b3eb6f3cd6 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/repository/user-group.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/repository/user-group.store.ts @@ -1,7 +1,7 @@ import type { UserGroupDetails } from '@umbraco-cms/backoffice/models'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; -import { ArrayState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; import { UmbEntityDetailStore, UmbStoreBase } from '@umbraco-cms/backoffice/store'; // TODO: get rid of this type addition & { ... }: @@ -16,11 +16,11 @@ export const UMB_USER_GROUP_STORE_CONTEXT_TOKEN = new UmbContextToken { - #groups = new ArrayState([], (x) => x.id); + #groups = new UmbArrayState([], (x) => x.id); public groups = this.#groups.asObservable(); constructor(host: UmbControllerHostElement) { - super(host, UMB_USER_GROUP_STORE_CONTEXT_TOKEN.toString()); + super(host, UMB_USER_GROUP_STORE_CONTEXT_TOKEN.toString(), new UmbArrayState([], (x) => x.id)); } getScaffold(entityType: string, parentId: string | null) { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/user-section/views/users/section-view-users.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/user-section/views/users/section-view-users.element.ts index 2cebe636ce..64f9e96e23 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/users/user-section/views/users/section-view-users.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/user-section/views/users/section-view-users.element.ts @@ -11,7 +11,7 @@ import './workspace-view-users-selection.element'; import type { UserDetails } from '@umbraco-cms/backoffice/models'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; -import { DeepState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbDeepState } from '@umbraco-cms/backoffice/observable-api'; import type { ManifestWorkspace } from '@umbraco-cms/backoffice/extensions-registry'; @customElement('umb-section-view-users') @@ -37,13 +37,13 @@ export class UmbSectionViewUsersElement extends UmbLitElement { // TODO: This must be turned into context api: Maybe its a Collection View (SectionView Collection View)? private _userStore?: UmbUserStore; - #selection = new DeepState(>[]); + #selection = new UmbDeepState(>[]); public readonly selection = this.#selection.asObservable(); - #users = new DeepState(>[]); + #users = new UmbDeepState(>[]); public readonly users = this.#users.asObservable(); - #search = new DeepState(''); + #search = new UmbDeepState(''); public readonly search = this.#search.asObservable(); constructor() { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/users/repository/user.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/users/repository/user.store.ts index a52581484d..fa14ef77fb 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/users/users/repository/user.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/users/repository/user.store.ts @@ -1,5 +1,5 @@ import type { UserDetails } from '@umbraco-cms/backoffice/models'; -import { ArrayState, NumberState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbArrayState, UmbNumberState } from '@umbraco-cms/backoffice/observable-api'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; import { UmbEntityDetailStore, UmbStoreBase } from '@umbraco-cms/backoffice/store'; import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; @@ -15,14 +15,13 @@ export const UMB_USER_STORE_CONTEXT_TOKEN = new UmbContextToken('U * @description - Data Store for Users */ export class UmbUserStore extends UmbStoreBase implements UmbEntityDetailStore { - #users = new ArrayState([], (x) => x.id); - public users = this.#users.asObservable(); + public users = this._data.asObservable(); - #totalUsers = new NumberState(0); + #totalUsers = new UmbNumberState(0); public readonly totalUsers = this.#totalUsers.asObservable(); constructor(host: UmbControllerHostElement) { - super(host, UMB_USER_STORE_CONTEXT_TOKEN.toString()); + super(host, UMB_USER_STORE_CONTEXT_TOKEN.toString(), new UmbArrayState([], (x) => x.id)); } getScaffold(entityType: string, parentId: string | null) { @@ -52,7 +51,7 @@ export class UmbUserStore extends UmbStoreBase implements UmbEntityDetailStore res.json()) .then((data) => { this.#totalUsers.next(data.total); - this.#users.next(data.items); + this._data.next(data.items); }); return this.users; @@ -70,10 +69,10 @@ export class UmbUserStore extends UmbStoreBase implements UmbEntityDetailStore res.json()) .then((data) => { - this.#users.appendOne(data); + this._data.appendOne(data); }); - return this.#users.getObservablePart((users: Array) => + return this._data.getObservablePart((users: Array) => users.find((user: UmbUserStoreItemType) => user.id === id) ); } @@ -89,10 +88,10 @@ export class UmbUserStore extends UmbStoreBase implements UmbEntityDetailStore res.json()) .then((data) => { - this.#users.append(data); + this._data.append(data); }); - return this.#users.getObservablePart((users: Array) => + return this._data.getObservablePart((users: Array) => users.filter((user: UmbUserStoreItemType) => ids.includes(user.id)) ); } @@ -105,10 +104,10 @@ export class UmbUserStore extends UmbStoreBase implements UmbEntityDetailStore res.json()) .then((data) => { - this.#users.append(data); + this._data.append(data); }); - return this.#users.getObservablePart((users: Array) => + return this._data.getObservablePart((users: Array) => users.filter((user: UmbUserStoreItemType) => user.name.toLocaleLowerCase().includes(name)) ); } @@ -124,13 +123,13 @@ export class UmbUserStore extends UmbStoreBase implements UmbEntityDetailStore enabledKeys.includes(user.id)); + const storedUsers = this._data.getValue().filter((user) => enabledKeys.includes(user.id)); storedUsers.forEach((user) => { user.status = 'enabled'; }); - this.#users.append(storedUsers); + this._data.append(storedUsers); } catch (error) { console.error('Enable Users failed', error); } @@ -147,17 +146,17 @@ export class UmbUserStore extends UmbStoreBase implements UmbEntityDetailStore enabledKeys.includes(user.id)); + const storedUsers = this._data.getValue().filter((user) => enabledKeys.includes(user.id)); storedUsers.forEach((user) => { if (userKeys.includes(user.id)) { user.userGroups.push(userGroup); } else { - user.userGroups = user.userGroups.filter((group) => group !== userGroup); + user.userGroups = user.userGroups.filter((group: any) => group !== userGroup); } }); - this.#users.append(storedUsers); + this._data.append(storedUsers); } catch (error) { console.error('Add user group failed', error); } @@ -174,13 +173,13 @@ export class UmbUserStore extends UmbStoreBase implements UmbEntityDetailStore enabledKeys.includes(user.id)); + const storedUsers = this._data.getValue().filter((user) => enabledKeys.includes(user.id)); storedUsers.forEach((user) => { - user.userGroups = user.userGroups.filter((group) => group !== userGroup); + user.userGroups = user.userGroups.filter((group: any) => group !== userGroup); }); - this.#users.append(storedUsers); + this._data.append(storedUsers); } catch (error) { console.error('Remove user group failed', error); } @@ -197,13 +196,13 @@ export class UmbUserStore extends UmbStoreBase implements UmbEntityDetailStore disabledKeys.includes(user.id)); + const storedUsers = this._data.getValue().filter((user) => disabledKeys.includes(user.id)); storedUsers.forEach((user) => { user.status = 'disabled'; }); - this.#users.append(storedUsers); + this._data.append(storedUsers); } catch (error) { console.error('Disable Users failed', error); } @@ -220,7 +219,7 @@ export class UmbUserStore extends UmbStoreBase implements UmbEntityDetailStore { let element: UmbContextProviderElement; - let consumer: ContextTestElement; + let consumer: UmbContextTestElement; const contextValue = 'test-value'; beforeEach(async () => { @@ -25,7 +25,7 @@ describe('UmbContextProvider', () => { ` ); - consumer = element.getElementsByTagName('umb-context-test')[0] as ContextTestElement; + consumer = element.getElementsByTagName('umb-context-test')[0] as UmbContextTestElement; }); it('is defined with its own instance', () => { diff --git a/src/Umbraco.Web.UI.Client/src/core/controller-host/controller-host-test.test.ts b/src/Umbraco.Web.UI.Client/src/core/controller-host/controller-host-test.test.ts index df0e1f3c42..322bb7246c 100644 --- a/src/Umbraco.Web.UI.Client/src/core/controller-host/controller-host-test.test.ts +++ b/src/Umbraco.Web.UI.Client/src/core/controller-host/controller-host-test.test.ts @@ -6,7 +6,7 @@ import { UmbContextProviderController } from '@umbraco-cms/backoffice/context-ap import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; @customElement('umb-controller-host-test-consumer') -export class ControllerHostTestConsumerElement extends UmbLitElement { +export class UmbControllerHostTestConsumerElement extends UmbLitElement { public value: string | null = null; constructor() { super(); @@ -18,7 +18,7 @@ export class ControllerHostTestConsumerElement extends UmbLitElement { describe('UmbControllerHostTestElement', () => { let element: UmbControllerHostTestElement; - let consumer: ControllerHostTestConsumerElement; + let consumer: UmbControllerHostTestConsumerElement; const contextValue = 'test-value'; beforeEach(async () => { @@ -31,7 +31,7 @@ describe('UmbControllerHostTestElement', () => { ); consumer = element.getElementsByTagName( 'umb-controller-host-test-consumer' - )[0] as ControllerHostTestConsumerElement; + )[0] as UmbControllerHostTestConsumerElement; }); it('element is defined with its own instance', () => { diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/data/data-type.data.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/data/data-type.data.ts index 9196041145..ec75e3aa08 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/data/data-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/data/data-type.data.ts @@ -4,12 +4,13 @@ import type { FolderTreeItemResponseModel, DataTypeResponseModel, CreateFolderRequestModel, + DataTypeItemResponseModel, } from '@umbraco-cms/backoffice/backend-api'; // TODO: investigate why we don't get an type as part of the DataTypeModel -export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | FolderTreeItemResponseModel> = [ +export const data: Array = [ { - $type: 'data-type', + $type: '', type: 'data-type', name: 'Folder 1', id: 'dt-folder1', @@ -17,7 +18,7 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde isFolder: true, }, { - $type: 'data-type', + $type: '', type: 'data-type', id: '0cc0eba1-9960-42c9-bf9b-60e150b429ae', parentId: null, @@ -27,7 +28,7 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde values: [], }, { - $type: 'data-type', + $type: '', type: 'data-type', name: 'Text', id: 'dt-textBox', @@ -42,7 +43,7 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde ], }, { - $type: 'data-type', + $type: '', type: 'data-type', name: 'Text Area', id: 'dt-textArea', @@ -52,7 +53,7 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde values: [], }, { - $type: 'data-type', + $type: '', type: 'data-type', name: 'My JS Property Editor', id: 'dt-custom', @@ -62,7 +63,7 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde values: [], }, { - $type: 'data-type', + $type: '', type: 'data-type', name: 'Color Picker', id: 'dt-colorPicker', @@ -118,7 +119,7 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde ], }, { - $type: 'data-type', + $type: '', type: 'data-type', name: 'Content Picker', id: 'dt-contentPicker', @@ -133,7 +134,7 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde ], }, { - $type: 'data-type', + $type: '', type: 'data-type', name: 'Eye Dropper', id: 'dt-eyeDropper', @@ -170,7 +171,7 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde ], }, { - $type: 'data-type', + $type: '', type: 'data-type', name: 'Multi URL Picker', id: 'dt-multiUrlPicker', @@ -201,7 +202,7 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde ], }, { - $type: 'data-type', + $type: '', type: 'data-type', name: 'Multi Node Tree Picker', id: 'dt-multiNodeTreePicker', @@ -211,7 +212,7 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde values: [], }, { - $type: 'data-type', + $type: '', type: 'data-type', name: 'Date Picker', id: 'dt-datePicker', @@ -230,9 +231,9 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde ], }, { - $type: 'data-type', - name: 'Date Picker With Time', + $type: '', type: 'data-type', + name: 'Date Picker With Time', id: 'dt-datePicker-time', parentId: null, propertyEditorAlias: 'Umbraco.DateTime', @@ -249,9 +250,9 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde ], }, { - $type: 'data-type', - name: 'Time', + $type: '', type: 'data-type', + name: 'Time', id: 'dt-time', parentId: null, propertyEditorAlias: 'Umbraco.DateTime', @@ -268,7 +269,7 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde ], }, { - $type: 'data-type', + $type: '', type: 'data-type', name: 'Email', id: 'dt-email', @@ -278,7 +279,7 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde values: [], }, { - $type: 'data-type', + $type: '', type: 'data-type', name: 'Multiple Text String', id: 'dt-multipleTextString', @@ -297,7 +298,7 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde ], }, { - $type: 'data-type', + $type: '', type: 'data-type', name: 'Dropdown', id: 'dt-dropdown', @@ -307,7 +308,7 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde values: [], }, { - $type: 'data-type', + $type: '', type: 'data-type', name: 'Slider', id: 'dt-slider', @@ -342,7 +343,7 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde ], }, { - $type: 'data-type', + $type: '', type: 'data-type', name: 'Toggle', id: 'dt-toggle', @@ -369,7 +370,7 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde ], }, { - $type: 'data-type', + $type: '', type: 'data-type', name: 'Tags', id: 'dt-tags', @@ -379,7 +380,7 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde values: [], }, { - $type: 'data-type', + $type: '', type: 'data-type', name: 'Markdown Editor', id: 'dt-markdownEditor', @@ -389,7 +390,7 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde values: [], }, { - $type: 'data-type', + $type: '', type: 'data-type', name: 'Radio Button List', id: 'dt-radioButtonList', @@ -408,7 +409,7 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde ], }, { - $type: 'data-type', + $type: '', type: 'data-type', name: 'Checkbox List', id: 'dt-checkboxList', @@ -427,7 +428,7 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde ], }, { - $type: 'data-type', + $type: '', type: 'data-type', name: 'Block List', id: 'dt-blockList', @@ -437,7 +438,7 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde values: [], }, { - $type: 'data-type', + $type: '', type: 'data-type', name: 'Media Picker', id: 'dt-mediaPicker', @@ -447,7 +448,7 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde values: [], }, { - $type: 'data-type', + $type: '', type: 'data-type', name: 'Image Cropper', id: 'dt-imageCropper', @@ -457,7 +458,7 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde values: [], }, { - $type: 'data-type', + $type: '', type: 'data-type', name: 'Upload Field', id: 'dt-uploadField', @@ -472,7 +473,7 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde ], }, { - $type: 'data-type', + $type: '', type: 'data-type', name: 'Block Grid', id: 'dt-blockGrid', @@ -482,7 +483,7 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde values: [], }, { - $type: 'data-type', + $type: '', type: 'data-type', name: 'Collection View', id: 'dt-collectionView', @@ -492,7 +493,7 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde values: [], }, { - $type: 'data-type', + $type: '', type: 'data-type', name: 'Icon Picker', id: 'dt-iconPicker', @@ -502,7 +503,7 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde values: [], }, { - $type: 'data-type', + $type: '', type: 'data-type', name: 'Number Range', id: 'dt-numberRange', @@ -512,7 +513,7 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde values: [], }, { - $type: 'data-type', + $type: '', type: 'data-type', name: 'Order Direction', id: 'dt-orderDirection', @@ -522,7 +523,7 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde values: [], }, { - $type: 'data-type', + $type: '', type: 'data-type', name: 'Overlay Size', id: 'dt-overlaySize', @@ -532,7 +533,7 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde values: [], }, { - $type: 'data-type', + $type: '', type: 'data-type', name: 'Rich Text Editor', id: 'dt-richTextEditor', @@ -611,7 +612,7 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde ], }, { - $type: 'data-type', + $type: '', type: 'data-type', name: 'Label', id: 'dt-label', @@ -621,7 +622,7 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde values: [], }, { - $type: 'data-type', + $type: '', type: 'data-type', name: 'Integer', id: 'dt-integer', @@ -631,7 +632,7 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde values: [], }, { - $type: 'data-type', + $type: '', type: 'data-type', name: 'Decimal', id: 'dt-decimal', @@ -641,7 +642,7 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde values: [], }, { - $type: 'data-type', + $type: '', type: 'data-type', name: 'User Picker', id: 'dt-userPicker', @@ -651,7 +652,7 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde values: [], }, { - $type: 'data-type', + $type: '', type: 'data-type', name: 'Member Picker', id: 'dt-memberPicker', @@ -661,7 +662,7 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde values: [], }, { - $type: 'data-type', + $type: '', type: 'data-type', name: 'Member Group Picker', id: 'dt-memberGroupPicker', @@ -672,6 +673,13 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde }, ]; +const createDataTypeItem = (item: DataTypeResponseModel | FolderTreeItemResponseModel): DataTypeItemResponseModel => { + return { + id: item.id, + name: item.name, + }; +}; + // Temp mocked database // TODO: all properties are optional in the server schema. I don't think this is correct. // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -691,9 +699,9 @@ class UmbDataTypeData extends UmbEntityData createFolderTreeItem(item)); } - getTreeItem(ids: Array): Array { + getItems(ids: Array): Array { const items = this.data.filter((item) => ids.includes(item.id ?? '')); - return items.map((item) => createFolderTreeItem(item)); + return items.map((item) => createDataTypeItem(item)); } createFolder(folder: CreateFolderRequestModel & { id: string | undefined }) { @@ -701,8 +709,7 @@ class UmbDataTypeData extends UmbEntityData = [ defaultTemplateId: null, id: 'all-property-editors-document-type-id', alias: 'blogPost', - name: 'Blog Post', + name: 'All property editors document type', description: null, icon: 'umb:item-arrangement', allowedAsRoot: true, @@ -651,9 +651,9 @@ export const data: Array = [ defaultTemplateId: null, id: '29643452-cff9-47f2-98cd-7de4b6807681', alias: 'blogPost', - name: 'Blog Post', + name: 'Page Document Type', description: null, - icon: 'umb:item-arrangement', + icon: 'umb:document', allowedAsRoot: true, variesByCulture: true, variesBySegment: false, diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/data/entity.data.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/data/entity.data.ts index d3a7356430..9eb9ba8be8 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/data/entity.data.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/data/entity.data.ts @@ -1,3 +1,4 @@ +import { v4 as uuid } from 'uuid'; import { UmbData } from './data'; import type { Entity } from '@umbraco-cms/backoffice/models'; @@ -44,6 +45,9 @@ export class UmbEntityData extends UmbData { } move(ids: Array, destinationKey: string) { + const destinationItem = this.getById(destinationKey); + if (!destinationItem) throw new Error(`Destination item with key ${destinationKey} not found`); + const items = this.getByIds(ids); const movedItems = items.map((item) => { return { @@ -53,7 +57,32 @@ export class UmbEntityData extends UmbData { }); movedItems.forEach((movedItem) => this.updateData(movedItem)); - return movedItems; + destinationItem.hasChildren = true; + this.updateData(destinationItem); + } + + copy(ids: Array, destinationKey: string) { + const destinationItem = this.getById(destinationKey); + if (!destinationItem) throw new Error(`Destination item with key ${destinationKey} not found`); + + // TODO: Notice we don't add numbers to the 'copy' name. + const items = this.getByIds(ids); + const copyItems = items.map((item) => { + return { + ...item, + name: item.name + ' Copy', + id: uuid(), + parentId: destinationKey, + }; + }); + + copyItems.forEach((copyItem) => this.insert(copyItem)); + const newIds = copyItems.map((item) => item.id); + + destinationItem.hasChildren = true; + this.updateData(destinationItem); + + return newIds; } trash(ids: Array) { diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/data/languages.data.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/data/languages.data.ts index 142e8b426d..5d0be1a5f6 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/data/languages.data.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/data/languages.data.ts @@ -16,6 +16,10 @@ class UmbLanguagesData extends UmbData { return this.data.find((item) => item.isoCode === isoCode); } + getItems(isoCodes: Array) { + return this.data.filter((item) => isoCodes.indexOf(item.isoCode || '') !== -1); + } + insert(language: LanguageResponseModel) { const foundIndex = this.data.findIndex((item) => item.isoCode === language.isoCode); diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/data/utils.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/data/utils.ts index 5c847f47e8..527d5c6304 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/data/utils.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/data/utils.ts @@ -25,6 +25,7 @@ export const createEntityTreeItem = (item: any): EntityTreeItemResponseModel => export const createFolderTreeItem = (item: any): FolderTreeItemResponseModel => { return { ...createEntityTreeItem(item), + $type: 'FolderTreeItemResponseModel', isFolder: item.isFolder, }; }; diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/domains/data-type/copy.handlers.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/data-type/copy.handlers.ts new file mode 100644 index 0000000000..1d5b764699 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/data-type/copy.handlers.ts @@ -0,0 +1,18 @@ +import { rest } from 'msw'; +import { umbDataTypeData } from '../../data/data-type.data'; +import { slug } from './slug'; +import { umbracoPath } from '@umbraco-cms/backoffice/utils'; + +export const copyHandlers = [ + rest.post(umbracoPath(`${slug}/:id/copy`), async (req, res, ctx) => { + const id = req.params.id as string; + if (!id) return; + + const data = await req.json(); + if (!data) return; + + const newIds = umbDataTypeData.copy([id], data.targetId); + + return res(ctx.status(200), ctx.set({ Location: newIds[0] })); + }), +]; diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/domains/data-type/index.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/data-type/index.ts index fb46e23ffd..706757b33e 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/domains/data-type/index.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/data-type/index.ts @@ -2,5 +2,14 @@ import { folderHandlers } from './folder.handlers'; import { treeHandlers } from './tree.handlers'; import { detailHandlers } from './detail.handlers'; import { itemHandlers } from './item.handlers'; +import { moveHandlers } from './move.handlers'; +import { copyHandlers } from './copy.handlers'; -export const handlers = [...treeHandlers, ...itemHandlers, ...folderHandlers, ...detailHandlers]; +export const handlers = [ + ...treeHandlers, + ...itemHandlers, + ...folderHandlers, + ...moveHandlers, + ...copyHandlers, + ...detailHandlers, +]; diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/domains/data-type/item.handlers.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/data-type/item.handlers.ts index 24d17c79e0..5601aae644 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/domains/data-type/item.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/data-type/item.handlers.ts @@ -7,7 +7,7 @@ export const itemHandlers = [ rest.get(umbracoPath(`${slug}/item`), (req, res, ctx) => { const ids = req.url.searchParams.getAll('id'); if (!ids) return; - const items = umbDataTypeData.getTreeItem(ids); + const items = umbDataTypeData.getItems(ids); return res(ctx.status(200), ctx.json(items)); }), ]; diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/domains/data-type/move.handlers.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/data-type/move.handlers.ts new file mode 100644 index 0000000000..daf980630f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/data-type/move.handlers.ts @@ -0,0 +1,18 @@ +import { rest } from 'msw'; +import { umbDataTypeData } from '../../data/data-type.data'; +import { slug } from './slug'; +import { umbracoPath } from '@umbraco-cms/backoffice/utils'; + +export const moveHandlers = [ + rest.post(umbracoPath(`${slug}/:id/move`), async (req, res, ctx) => { + const id = req.params.id as string; + if (!id) return; + + const data = await req.json(); + if (!data) return; + + umbDataTypeData.move([id], data.targetId); + + return res(ctx.status(200)); + }), +]; diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/domains/document-type.handlers.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/document-type.handlers.ts index 1daea0ba0f..75f44b4a9a 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/domains/document-type.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/document-type.handlers.ts @@ -1,11 +1,21 @@ import { rest } from 'msw'; import { umbDocumentTypeData } from '../data/document-type.data'; import type { DocumentTypeResponseModel } from '@umbraco-cms/backoffice/backend-api'; +import { umbracoPath } from '@umbraco-cms/backoffice/utils'; // TODO: add schema export const handlers = [ - rest.post('/umbraco/management/api/v1/document-type/:id', (req, res, ctx) => { - const data = req.body; + rest.post(umbracoPath(`/document-type`), async (req, res, ctx) => { + const data = await req.json(); + if (!data) return; + + umbDocumentTypeData.insert(data); + + return res(ctx.status(200)); + }), + + rest.put(umbracoPath(`/document-type/:id`), async (req, res, ctx) => { + const data = await req.json(); if (!data) return; const saved = umbDocumentTypeData.save(data); diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/domains/document.handlers.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/document.handlers.ts index 44c3eb41da..487ccbcb0f 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/domains/document.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/document.handlers.ts @@ -1,6 +1,5 @@ import { rest } from 'msw'; import { umbDocumentData } from '../data/document.data'; -import type { DocumentResponseModel } from '@umbraco-cms/backoffice/backend-api'; import { umbracoPath } from '@umbraco-cms/backoffice/utils'; // TODO: add schema @@ -35,7 +34,16 @@ export const handlers = [ return res(ctx.status(200), ctx.json(items)); }), - rest.post('/umbraco/management/api/v1/document/:id', async (req, res, ctx) => { + rest.post(umbracoPath(`/document`), async (req, res, ctx) => { + const data = await req.json(); + if (!data) return; + + umbDocumentData.insert(data); + + return res(ctx.status(200)); + }), + + rest.put(umbracoPath(`/document/:id`), async (req, res, ctx) => { const data = await req.json(); if (!data) return; diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/domains/language.handlers.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/language.handlers.ts index 41158abe5b..b6e4557351 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/domains/language.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/language.handlers.ts @@ -5,6 +5,13 @@ import { umbracoPath } from '@umbraco-cms/backoffice/utils'; // TODO: add schema export const handlers = [ + rest.get(umbracoPath('/language/item'), (req, res, ctx) => { + const isoCodes = req.url.searchParams.getAll('isoCode'); + if (!isoCodes) return; + const items = umbLanguagesData.getItems(isoCodes); + return res(ctx.status(200), ctx.json(items)); + }), + rest.get(umbracoPath('/language'), (req, res, ctx) => { const skip = req.url.searchParams.get('skip'); const skipNumber = skip ? Number.parseInt(skip) : undefined; diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/domains/media.handlers.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/media.handlers.ts index efb5a91845..dc28dbd021 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/domains/media.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/media.handlers.ts @@ -26,8 +26,8 @@ export const handlers = [ rest.post('/umbraco/management/api/v1/media/move', async (req, res, ctx) => { const data = await req.json(); if (!data) return; - const moved = umbMediaData.move(data.ids, data.destination); - return res(ctx.status(200), ctx.json(moved)); + umbMediaData.move(data.ids, data.destination); + return res(ctx.status(200)); }), rest.post('/umbraco/management/api/v1/media/trash', async (req, res, ctx) => { diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/stories/modal.stories.ts b/src/Umbraco.Web.UI.Client/src/core/modal/stories/modal.stories.ts index e1c4bb2daa..b80cd203b5 100644 --- a/src/Umbraco.Web.UI.Client/src/core/modal/stories/modal.stories.ts +++ b/src/Umbraco.Web.UI.Client/src/core/modal/stories/modal.stories.ts @@ -15,7 +15,7 @@ export default { const Template: Story = (props) => { return html` Under construction - + `; }; diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/stories/story-modal-service-example.element.ts b/src/Umbraco.Web.UI.Client/src/core/modal/stories/story-modal-service-example.element.ts index 42f97388b3..4e0580a122 100644 --- a/src/Umbraco.Web.UI.Client/src/core/modal/stories/story-modal-service-example.element.ts +++ b/src/Umbraco.Web.UI.Client/src/core/modal/stories/story-modal-service-example.element.ts @@ -3,8 +3,8 @@ import { customElement, property, state } from 'lit/decorators.js'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; import { UMB_MODAL_CONTEXT_TOKEN, UmbModalContext } from '@umbraco-cms/backoffice/modal'; -@customElement('story-modal-context-example') -export class StoryModalContextExampleElement extends UmbLitElement { +@customElement('umb-story-modal-context-example') +export class UmbStoryModalContextExampleElement extends UmbLitElement { @property() modalLayout = 'confirm'; diff --git a/src/Umbraco.Web.UI.Client/src/core/notification/stories/notification.stories.ts b/src/Umbraco.Web.UI.Client/src/core/notification/stories/notification.stories.ts index 3c8e401107..6928ff210a 100644 --- a/src/Umbraco.Web.UI.Client/src/core/notification/stories/notification.stories.ts +++ b/src/Umbraco.Web.UI.Client/src/core/notification/stories/notification.stories.ts @@ -15,7 +15,7 @@ export default { ], } as Meta; -const Template: Story = () => html``; +const Template: Story = () => html``; export const Default = Template.bind({}); Default.parameters = { diff --git a/src/Umbraco.Web.UI.Client/src/core/notification/stories/story-notification-default-example.element.ts b/src/Umbraco.Web.UI.Client/src/core/notification/stories/story-notification-default-example.element.ts index de78281ffb..fc29c90356 100644 --- a/src/Umbraco.Web.UI.Client/src/core/notification/stories/story-notification-default-example.element.ts +++ b/src/Umbraco.Web.UI.Client/src/core/notification/stories/story-notification-default-example.element.ts @@ -8,8 +8,8 @@ import { } from '@umbraco-cms/backoffice/notification'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; -@customElement('story-notification-default-example') -export class StoryNotificationDefaultExampleElement extends UmbLitElement { +@customElement('umb-story-notification-default-example') +export class UmbStoryNotificationDefaultExampleElement extends UmbLitElement { private _notificationContext?: UmbNotificationContext; connectedCallback(): void { @@ -56,6 +56,6 @@ export class StoryNotificationDefaultExampleElement extends UmbLitElement { declare global { interface HTMLElementTagNameMap { - 'story-notification-default-example': StoryNotificationDefaultExampleElement; + 'umb-story-notification-default-example': UmbStoryNotificationDefaultExampleElement; } } diff --git a/src/Umbraco.Web.UI.Client/src/installer/installer.context.ts b/src/Umbraco.Web.UI.Client/src/installer/installer.context.ts index 9e6b26b8cf..30b4c7350a 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/installer.context.ts +++ b/src/Umbraco.Web.UI.Client/src/installer/installer.context.ts @@ -8,7 +8,7 @@ import { } from '@umbraco-cms/backoffice/backend-api'; import { tryExecute } from '@umbraco-cms/backoffice/resources'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import { ObjectState, NumberState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbObjectState, UmbNumberState } from '@umbraco-cms/backoffice/observable-api'; /** * Context API for the installer @@ -16,20 +16,20 @@ import { ObjectState, NumberState } from '@umbraco-cms/backoffice/observable-api * @class UmbInstallerContext */ export class UmbInstallerContext { - private _data = new ObjectState({ + private _data = new UmbObjectState({ user: { name: '', email: '', password: '', subscribeToNewsletter: false }, database: { id: '', providerName: '' }, telemetryLevel: TelemetryLevelModel.BASIC, }); public readonly data = this._data.asObservable(); - private _currentStep = new NumberState(1); + private _currentStep = new UmbNumberState(1); public readonly currentStep = this._currentStep.asObservable(); - private _settings = new ObjectState(undefined); + private _settings = new UmbObjectState(undefined); public readonly settings = this._settings.asObservable(); - private _installStatus = new ObjectState(null); + private _installStatus = new UmbObjectState(null); public readonly installStatus = this._installStatus.asObservable(); constructor() { diff --git a/src/Umbraco.Web.UI.Client/src/stories/store.mdx b/src/Umbraco.Web.UI.Client/src/stories/store.mdx index 1140ee8e40..9622967a0c 100644 --- a/src/Umbraco.Web.UI.Client/src/stories/store.mdx +++ b/src/Umbraco.Web.UI.Client/src/stories/store.mdx @@ -11,7 +11,7 @@ Generally a Store will be holding one or more RxJS Subjects, each Subject is mad ```typescript class MyProductStore { - #products = new ArrayState(>[], (product) => product.id); + #products = new UmbArrayState(>[], (product) => product.id); public readonly products = this.#products.asObservable(); @@ -118,7 +118,7 @@ This example give some inspiration to how fine grained this can become: ```typescript class MyProductStore { - #products = new ArrayState(>[]); + #products = new UmbArrayState(>[]); public readonly products = this.#products.asObservable(); public readonly amountOfProducts = this.#products.getObservablePart((products) => products.length); @@ -138,7 +138,7 @@ In the examples of this guide each product has a id, and we have clarified this ```typescript class MyProductStore { - #products = new ArrayState(>[], (product) => product.id); + #products = new UmbArrayState(>[], (product) => product.id); ... } ``` diff --git a/src/Umbraco.Web.UI.Client/tsconfig.json b/src/Umbraco.Web.UI.Client/tsconfig.json index b9ed9f3fce..6aeb5fbd4f 100644 --- a/src/Umbraco.Web.UI.Client/tsconfig.json +++ b/src/Umbraco.Web.UI.Client/tsconfig.json @@ -37,9 +37,11 @@ "@umbraco-cms/backoffice/repository": ["libs/repository"], "@umbraco-cms/backoffice/resources": ["libs/resources"], "@umbraco-cms/backoffice/router": ["libs/router"], + "@umbraco-cms/backoffice/sorter": ["libs/sorter"], "@umbraco-cms/backoffice/store": ["libs/store"], "@umbraco-cms/backoffice/utils": ["libs/utils"], "@umbraco-cms/backoffice/workspace": ["libs/workspace"], + "@umbraco-cms/backoffice/picker-input": ["libs/picker-input"], "@umbraco-cms/internal/lit-element": ["src/core/lit-element"], "@umbraco-cms/internal/modal": ["src/core/modal"], "@umbraco-cms/internal/router": ["src/core/router"], diff --git a/src/Umbraco.Web.UI.Client/utils/json-schema/test-package.json b/src/Umbraco.Web.UI.Client/utils/json-schema/test-package.json new file mode 100644 index 0000000000..34edf0f9e9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/utils/json-schema/test-package.json @@ -0,0 +1,22 @@ +{ + "$schema": "../../types/umbraco-package-schema.json", + "name": "My Package", + "version": "1.0.0", + "extensions": [ + { + "name": "My Dashboard", + "alias": "myDashboard", + + "weight": -10, + "elementName": "my-dashboard", + "js": "js/my-dashboard.js", + + "type": "dashboard", + "meta": { + "label": "My Dashboard", + "pathname": "my-dashboard", + "sections": ["Umb.Section.Content"] + } + } + ] +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/utils/move-libs.js b/src/Umbraco.Web.UI.Client/utils/move-libs.js index 59c3f7d044..76c29dade7 100644 --- a/src/Umbraco.Web.UI.Client/utils/move-libs.js +++ b/src/Umbraco.Web.UI.Client/utils/move-libs.js @@ -3,28 +3,27 @@ // Example: import { Foo } from '@umbraco-cms/backoffice/element' -> import { Foo } from './element' // This is needed because the d.ts files are not in the same folder as the source files // and the absolute paths are not valid when the d.ts files are copied to the dist folder -// This is only used when building the d.ts files +// This is only used when building the d.ts files. // -// Usage: node utils/transform-dts.js +// This script also copies the package.json and README.md files to the dist/libs folder +// and the umbraco-package-schema.json file to the Umbraco.Web.UI.New folder // -// Note: This script is not used in the build process, it is only used to transform the d.ts files -// when the d.ts files are copied to the dist folder - -// Note: Updated to help copy the two JSON files generated from webcomponant analyzer tool -// One is specific to VSCode HTMLCutomData for intellisense and the other is a more broad format used in storybook etc +// Usage: node utils/move-libs.js import { readdirSync, readFileSync, writeFileSync, cpSync, mkdirSync } from 'fs'; -const rootDir = './'; const srcDir = './libs'; const inputDir = './dist/libs'; const outputDir = '../Umbraco.Cms.StaticAssets/wwwroot/umbraco/backoffice/libs'; +const executableDir = '../Umbraco.Web.UI.New'; // Copy package files cpSync(`${srcDir}/package.json`, `${inputDir}/package.json`, { recursive: true }); +console.log(`Copied ${srcDir}/package.json to ${inputDir}/package.json`); cpSync(`${srcDir}/README.md`, `${inputDir}/README.md`, { recursive: true }); -cpSync(`${rootDir}/custom-elements.json`, `${inputDir}/custom-elements.json`, { recursive: true }); -cpSync(`${rootDir}/vscode-html-custom-data.json`, `${inputDir}/vscode-html-custom-data.json`, { recursive: true }); +console.log(`Copied ${srcDir}/README.md to ${inputDir}/README.md`); +cpSync(`${inputDir}/umbraco-package-schema.json`, `${executableDir}/umbraco-json-schema.json`, { recursive: true }); +console.log(`Copied ${inputDir}/umbraco-package-schema.json to ${executableDir}/umbraco-package-schema.json`); const libs = readdirSync(inputDir); @@ -38,6 +37,8 @@ try { // Transform all .d.ts files and copy all other files to the output folder libs.forEach(lib => { + if (lib.endsWith('.js') === false && lib.endsWith('.js.map') === false) return; + console.log(`Transforming ${lib}`); const dtsFile = `${inputDir}/${lib}`; diff --git a/src/Umbraco.Web.UI.Client/vite.cms.config.ts b/src/Umbraco.Web.UI.Client/vite.cms.config.ts index 2d0006a27b..a2ac17fd86 100644 --- a/src/Umbraco.Web.UI.Client/vite.cms.config.ts +++ b/src/Umbraco.Web.UI.Client/vite.cms.config.ts @@ -1,9 +1,9 @@ import { defineConfig } from 'vite'; +import { viteStaticCopy } from 'vite-plugin-static-copy'; -import config from './vite.config'; +import { plugins } from './vite.config'; export default defineConfig({ - ...config, build: { lib: { entry: 'src/app.ts', @@ -11,12 +11,13 @@ export default defineConfig({ fileName: 'main', }, rollupOptions: { - external: [/^@umbraco-cms\/backoffice\//] + external: [/^@umbraco-cms\/backoffice\//], }, outDir: '../Umbraco.Cms.StaticAssets/wwwroot/umbraco/backoffice', emptyOutDir: false, sourcemap: true, }, base: '/umbraco/backoffice/', - mode: 'production' + mode: 'production', + plugins: [...plugins], }); diff --git a/src/Umbraco.Web.UI.Client/vite.config.ts b/src/Umbraco.Web.UI.Client/vite.config.ts index fb67314d73..28f834cc03 100644 --- a/src/Umbraco.Web.UI.Client/vite.config.ts +++ b/src/Umbraco.Web.UI.Client/vite.config.ts @@ -1,29 +1,23 @@ -import { defineConfig } from 'vite'; +import { defineConfig, PluginOption } from 'vite'; import { viteStaticCopy } from 'vite-plugin-static-copy'; import viteTSConfigPaths from 'vite-tsconfig-paths'; +export const plugins: PluginOption[] = [ + viteStaticCopy({ + targets: [ + { + src: 'public-assets/icons/*.js', + dest: 'icons', + }, + ], + }), + viteTSConfigPaths(), +]; + // https://vitejs.dev/config/ export default defineConfig({ build: { sourcemap: true, }, - plugins: [ - viteStaticCopy({ - targets: [ - { - src: 'public-assets/icons/*.js', - dest: 'icons', - }, - { - src: 'public-assets/App_Plugins/*.js', - dest: 'App_Plugins', - }, - { - src: 'public-assets/css/*.css', - dest: 'css', - } - ], - }), - viteTSConfigPaths(), - ], + plugins, }); diff --git a/src/Umbraco.Web.UI.Client/web-test-runner.config.mjs b/src/Umbraco.Web.UI.Client/web-test-runner.config.mjs index eacc5a70f2..e54d8272e5 100644 --- a/src/Umbraco.Web.UI.Client/web-test-runner.config.mjs +++ b/src/Umbraco.Web.UI.Client/web-test-runner.config.mjs @@ -57,10 +57,12 @@ export default { '@umbraco-cms/backoffice/store': './libs/store/index.ts', '@umbraco-cms/backoffice/utils': './libs/utils/index.ts', '@umbraco-cms/backoffice/workspace': './libs/workspace/index.ts', + '@umbraco-cms/backoffice/picker-input': './libs/picker-input/index.ts', '@umbraco-cms/internal/lit-element': './src/core/lit-element/index.ts', '@umbraco-cms/internal/modal': './src/core/modal/index.ts', '@umbraco-cms/internal/router': './src/core/router/index.ts', - '@umbraco-cms/internal/test-utils': './utils/test-utils.ts' + '@umbraco-cms/internal/sorter': './src/core/sorter/index.ts', + '@umbraco-cms/internal/test-utils': './utils/test-utils.ts', }, }, },