diff --git a/src/Umbraco.Web.UI.Client/devops/build/check-path-length.js b/src/Umbraco.Web.UI.Client/devops/build/check-path-length.js new file mode 100644 index 0000000000..d21097639b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/devops/build/check-path-length.js @@ -0,0 +1,44 @@ +import { readdirSync, statSync } from 'fs'; +import { join } from 'path'; + +const PROJECT_DIR = process.argv[2] ?? '.'; +const MAX_PATH_LENGTH = process.argv[3] ?? 140; +const IS_CI = process.env.CI === 'true'; +const IS_AZURE_PIPELINES = process.env.TF_BUILD === 'true'; +const IS_GITHUB_ACTIONS = process.env.GITHUB_ACTIONS === 'true'; +const FILE_PATH_COLOR = '\x1b[36m%s\x1b[0m'; + +console.log(`Checking path length in ${PROJECT_DIR} for paths exceeding ${MAX_PATH_LENGTH}...`); +console.log('CI detected:', IS_CI); + +console.log('\n-----------------------------------'); +console.log('Results:'); +console.log('-----------------------------------\n'); + +function checkPathLength(dir) { + const files = readdirSync(dir); + + files.forEach(file => { + const filePath = join(dir, file); + if (filePath.length > MAX_PATH_LENGTH) { + + if (IS_CI) { + //process.exitCode = 1; // TODO: Uncomment this line to fail the build + } + + if (IS_AZURE_PIPELINES) { + console.error(`##vso[task.logissue type=warning;sourcepath=${filePath};]Path exceeds maximum length of ${MAX_PATH_LENGTH} characters: ${filePath} with ${filePath.length} characters`); + } else if (IS_GITHUB_ACTIONS) { + console.error(`::warning file=${filePath},title=Path exceeds ${MAX_PATH_LENGTH} characters::Paths should not be longer than ${MAX_PATH_LENGTH} characters to support WIN32 systems. The file ${filePath} exceeds that with ${filePath.length - MAX_PATH_LENGTH} characters.`); + } else { + console.error(`Path exceeds maximum length of ${MAX_PATH_LENGTH} characters: ${FILE_PATH_COLOR}`, filePath, filePath.length - MAX_PATH_LENGTH); + } + } + + if (statSync(filePath).isDirectory()) { + checkPathLength(filePath, MAX_PATH_LENGTH); + } + }); +} + +checkPathLength(PROJECT_DIR, MAX_PATH_LENGTH); diff --git a/src/Umbraco.Web.UI.Client/examples/dashboard-with-property-dataset/dataset-dashboard.ts b/src/Umbraco.Web.UI.Client/examples/dashboard-with-property-dataset/dataset-dashboard.ts index 4d4c0a0cb7..3c77005c71 100644 --- a/src/Umbraco.Web.UI.Client/examples/dashboard-with-property-dataset/dataset-dashboard.ts +++ b/src/Umbraco.Web.UI.Client/examples/dashboard-with-property-dataset/dataset-dashboard.ts @@ -39,11 +39,7 @@ export class ExampleDatasetDashboard extends UmbElementMixin(LitElement) { }, { alias: 'items', - value: { - 0: { sortOrder: 1, value: 'First Option' }, - 1: { sortOrder: 2, value: 'Second Option' }, - 2: { sortOrder: 3, value: 'Third Option' }, - }, + value: [ 'First Option' , 'Second Option', 'Third Option' ], }, ]} property-editor-ui-alias="Umb.PropertyEditorUi.Dropdown"> diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 760ddaa09c..dec2f440a5 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -83,6 +83,7 @@ "./user-permission": "./dist-cms/packages/user/user-permission/index.js", "./user": "./dist-cms/packages/user/user/index.js", "./utils": "./dist-cms/packages/core/utils/index.js", + "./validation": "./dist-cms/packages/core/validation/index.js", "./variant": "./dist-cms/packages/core/variant/index.js", "./webhook": "./dist-cms/packages/webhook/index.js", "./workspace": "./dist-cms/packages/core/workspace/index.js", @@ -122,8 +123,9 @@ "build:for:cms": "npm run build && node ./devops/build/copy-to-cms.js", "build:for:static": "vite build", "build:vite": "tsc && vite build --mode staging", - "build": "tsc --project ./src/tsconfig.build.json && rollup -c ./src/rollup.config.js && npm run package:validate && npm run generate:manifest", + "build": "tsc --project ./src/tsconfig.build.json && rollup -c ./src/rollup.config.js && npm run package:validate && npm run generate:manifest && npm run check:paths", "check": "npm run lint:errors && npm run compile && npm run build-storybook && npm run generate:jsonschema:dist", + "check:paths": "node ./devops/build/check-path-length.js src 140", "compile": "tsc", "dev": "vite", "dev:server": "VITE_UMBRACO_USE_MSW=off vite", diff --git a/src/Umbraco.Web.UI.Client/src/apps/backoffice/backoffice.element.ts b/src/Umbraco.Web.UI.Client/src/apps/backoffice/backoffice.element.ts index 51c2146779..4d77370af7 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/backoffice/backoffice.element.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/backoffice/backoffice.element.ts @@ -1,10 +1,10 @@ import { UmbBackofficeContext } from './backoffice.context.js'; -import { UmbServerExtensionRegistrator } from './server-extension-registrator.controller.js'; import { css, html, customElement } from '@umbraco-cms/backoffice/external/lit'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; import { UmbBundleExtensionInitializer, UmbEntryPointExtensionInitializer, + UmbServerExtensionRegistrator, } from '@umbraco-cms/backoffice/extension-api'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; @@ -55,7 +55,7 @@ export class UmbBackofficeElement extends UmbLitElement { new UmbBackofficeContext(this); new UmbBundleExtensionInitializer(this, umbExtensionsRegistry); new UmbEntryPointExtensionInitializer(this, umbExtensionsRegistry); - new UmbServerExtensionRegistrator(this, umbExtensionsRegistry); + new UmbServerExtensionRegistrator(this, umbExtensionsRegistry).registerAllExtensions(); // So far local packages are this simple to registerer, so no need for a manager to do that: CORE_PACKAGES.forEach(async (packageImport) => { diff --git a/src/Umbraco.Web.UI.Client/src/apps/backoffice/server-extension-registrator.controller.ts b/src/Umbraco.Web.UI.Client/src/apps/backoffice/server-extension-registrator.controller.ts deleted file mode 100644 index 0f13864082..0000000000 --- a/src/Umbraco.Web.UI.Client/src/apps/backoffice/server-extension-registrator.controller.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { PackageResource, OpenAPI } from '@umbraco-cms/backoffice/external/backend-api'; -import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; -import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import type { UmbBackofficeExtensionRegistry } from '@umbraco-cms/backoffice/extension-registry'; -import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; -import type { ManifestBase } from '@umbraco-cms/backoffice/extension-api'; -import { isManifestBaseType } from '@umbraco-cms/backoffice/extension-api'; - -// TODO: consider if this can be replaced by the new extension controllers -export class UmbServerExtensionRegistrator extends UmbControllerBase { - #extensionRegistry: UmbBackofficeExtensionRegistry; - #apiBaseUrl = OpenAPI.BASE; - - constructor(host: UmbControllerHost, extensionRegistry: UmbBackofficeExtensionRegistry) { - super(host, UmbServerExtensionRegistrator.name); - this.#extensionRegistry = extensionRegistry; - this.#loadServerPackages(); - } - - async #loadServerPackages() { - /* TODO: we need a new endpoint here, to remove the dependency on the package repository, to get the modules available for the backoffice scope - / we will need a similar endpoint for the login, installer etc at some point. - We should expose more information about the packages when not authorized so the end point should only return a list of modules from the manifest with - with the correct scope. - - This code is copy pasted from the package repository. We probably don't need this is the package repository anymore. - */ - const { data: packages } = await tryExecuteAndNotify(this, PackageResource.getPackageManifest()); - - if (packages) { - // Append packages to the store but only if they have a name - //store.appendItems(packages.filter((p) => p.name?.length)); - const extensions: ManifestBase[] = []; - - packages.forEach((p) => { - p.extensions?.forEach((e) => { - // Crudely validate that the extension at least follows a basic manifest structure - // Idea: Use `Zod` to validate the manifest - if (isManifestBaseType(e)) { - /** - * Crude check to see if extension is of type "js" since it is safe to assume we do not - * need to load any other types of extensions in the backoffice (we need a js file to load) - */ - - // TODO: add helper to check for relative paths - // Add base url if the js path is relative - if ('js' in e && typeof e.js === 'string' && !e.js.startsWith('http')) { - e.js = `${this.#apiBaseUrl}${e.js}`; - } - - // Add base url if the element path is relative - if ('element' in e && typeof e.element === 'string' && !e.element.startsWith('http')) { - e.element = `${this.#apiBaseUrl}${e.element}`; - } - - // Add base url if the element path api relative - if ('api' in e && typeof e.api === 'string' && !e.api.startsWith('http')) { - e.api = `${this.#apiBaseUrl}${e.api}`; - } - - extensions.push(e); - } - }); - }); - - this.#extensionRegistry.registerMany(extensions); - } - } -} diff --git a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/models/RecycleBinItemResponseModelBaseModel.ts b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/models/RecycleBinItemResponseModelBaseModel.ts index 2666184305..33493007b2 100644 --- a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/models/RecycleBinItemResponseModelBaseModel.ts +++ b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/models/RecycleBinItemResponseModelBaseModel.ts @@ -7,7 +7,6 @@ import type { ItemReferenceByIdResponseModel } from './ItemReferenceByIdResponse export type RecycleBinItemResponseModelBaseModel = { id: string; - type: string; hasChildren: boolean; parent?: ItemReferenceByIdResponseModel | null; }; diff --git a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/DataTypeResource.ts b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/DataTypeResource.ts index b430515cb7..7c5daa8327 100644 --- a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/DataTypeResource.ts +++ b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/DataTypeResource.ts @@ -9,6 +9,7 @@ import type { DatatypeConfigurationResponseModel } from '../models/DatatypeConfi import type { DataTypeItemResponseModel } from '../models/DataTypeItemResponseModel'; import type { DataTypeReferenceResponseModel } from '../models/DataTypeReferenceResponseModel'; import type { DataTypeResponseModel } from '../models/DataTypeResponseModel'; +import type { DataTypeTreeItemResponseModel } from '../models/DataTypeTreeItemResponseModel'; import type { FolderResponseModel } from '../models/FolderResponseModel'; import type { MoveDataTypeRequestModel } from '../models/MoveDataTypeRequestModel'; import type { PagedDataTypeItemResponseModel } from '../models/PagedDataTypeItemResponseModel'; @@ -382,6 +383,27 @@ export class DataTypeResource { }); } + /** + * @returns any Success + * @throws ApiError + */ + public static getTreeDataTypeAncestors({ + descendantId, + }: { + descendantId?: string, + }): CancelablePromise> { + return __request(OpenAPI, { + method: 'GET', + url: '/umbraco/management/api/v1/tree/data-type/ancestors', + query: { + 'descendantId': descendantId, + }, + errors: { + 401: `The resource is protected and requires an authentication token`, + }, + }); + } + /** * @returns PagedDataTypeTreeItemResponseModel Success * @throws ApiError diff --git a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/DictionaryResource.ts b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/DictionaryResource.ts index 6ee347c44e..6c4b632ba7 100644 --- a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/DictionaryResource.ts +++ b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/DictionaryResource.ts @@ -3,12 +3,19 @@ /* tslint:disable */ /* eslint-disable */ import type { CreateDictionaryItemRequestModel } from '../models/CreateDictionaryItemRequestModel'; +import type { DataTypeTreeItemResponseModel } from '../models/DataTypeTreeItemResponseModel'; import type { DictionaryItemItemResponseModel } from '../models/DictionaryItemItemResponseModel'; import type { DictionaryItemResponseModel } from '../models/DictionaryItemResponseModel'; +import type { DocumentBlueprintTreeItemResponseModel } from '../models/DocumentBlueprintTreeItemResponseModel'; +import type { DocumentTypeTreeItemResponseModel } from '../models/DocumentTypeTreeItemResponseModel'; +import type { FolderTreeItemResponseModel } from '../models/FolderTreeItemResponseModel'; import type { ImportDictionaryRequestModel } from '../models/ImportDictionaryRequestModel'; +import type { MediaTypeTreeItemResponseModel } from '../models/MediaTypeTreeItemResponseModel'; import type { MoveDictionaryRequestModel } from '../models/MoveDictionaryRequestModel'; +import type { NamedEntityTreeItemResponseModel } from '../models/NamedEntityTreeItemResponseModel'; import type { PagedDictionaryOverviewResponseModel } from '../models/PagedDictionaryOverviewResponseModel'; import type { PagedNamedEntityTreeItemResponseModel } from '../models/PagedNamedEntityTreeItemResponseModel'; +import type { RelationTypeTreeItemResponseModel } from '../models/RelationTypeTreeItemResponseModel'; import type { UpdateDictionaryItemRequestModel } from '../models/UpdateDictionaryItemRequestModel'; import type { CancelablePromise } from '../core/CancelablePromise'; @@ -243,6 +250,27 @@ export class DictionaryResource { }); } + /** + * @returns any Success + * @throws ApiError + */ + public static getTreeDictionaryAncestors({ + descendantId, + }: { + descendantId?: string, + }): CancelablePromise> { + return __request(OpenAPI, { + method: 'GET', + url: '/umbraco/management/api/v1/tree/dictionary/ancestors', + query: { + 'descendantId': descendantId, + }, + errors: { + 401: `The resource is protected and requires an authentication token`, + }, + }); + } + /** * @returns PagedNamedEntityTreeItemResponseModel Success * @throws ApiError diff --git a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/DocumentResource.ts b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/DocumentResource.ts index 6cad9f1dde..8536bb1c3b 100644 --- a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/DocumentResource.ts +++ b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/DocumentResource.ts @@ -9,6 +9,7 @@ import type { DocumentConfigurationResponseModel } from '../models/DocumentConfi import type { DocumentItemResponseModel } from '../models/DocumentItemResponseModel'; import type { DocumentNotificationResponseModel } from '../models/DocumentNotificationResponseModel'; import type { DocumentResponseModel } from '../models/DocumentResponseModel'; +import type { DocumentTreeItemResponseModel } from '../models/DocumentTreeItemResponseModel'; import type { DomainsResponseModel } from '../models/DomainsResponseModel'; import type { MoveDocumentRequestModel } from '../models/MoveDocumentRequestModel'; import type { MoveMediaRequestModel } from '../models/MoveMediaRequestModel'; @@ -807,6 +808,27 @@ export class DocumentResource { }); } + /** + * @returns any Success + * @throws ApiError + */ + public static getTreeDocumentAncestors({ + descendantId, + }: { + descendantId?: string, + }): CancelablePromise> { + return __request(OpenAPI, { + method: 'GET', + url: '/umbraco/management/api/v1/tree/document/ancestors', + query: { + 'descendantId': descendantId, + }, + errors: { + 401: `The resource is protected and requires an authentication token`, + }, + }); + } + /** * @returns PagedDocumentTreeItemResponseModel Success * @throws ApiError diff --git a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/DocumentTypeResource.ts b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/DocumentTypeResource.ts index 363e5c343a..07bf1f7292 100644 --- a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/DocumentTypeResource.ts +++ b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/DocumentTypeResource.ts @@ -11,6 +11,7 @@ import type { DocumentTypeCompositionResponseModel } from '../models/DocumentTyp import type { DocumentTypeConfigurationResponseModel } from '../models/DocumentTypeConfigurationResponseModel'; import type { DocumentTypeItemResponseModel } from '../models/DocumentTypeItemResponseModel'; import type { DocumentTypeResponseModel } from '../models/DocumentTypeResponseModel'; +import type { DocumentTypeTreeItemResponseModel } from '../models/DocumentTypeTreeItemResponseModel'; import type { FolderResponseModel } from '../models/FolderResponseModel'; import type { MoveDocumentTypeRequestModel } from '../models/MoveDocumentTypeRequestModel'; import type { PagedAllowedDocumentTypeModel } from '../models/PagedAllowedDocumentTypeModel'; @@ -405,6 +406,27 @@ export class DocumentTypeResource { }); } + /** + * @returns any Success + * @throws ApiError + */ + public static getTreeDocumentTypeAncestors({ + descendantId, + }: { + descendantId?: string, + }): CancelablePromise> { + return __request(OpenAPI, { + method: 'GET', + url: '/umbraco/management/api/v1/tree/document-type/ancestors', + query: { + 'descendantId': descendantId, + }, + errors: { + 401: `The resource is protected and requires an authentication token`, + }, + }); + } + /** * @returns PagedDocumentTypeTreeItemResponseModel Success * @throws ApiError diff --git a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/MediaResource.ts b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/MediaResource.ts index ce2f8c2f0a..4f4a015050 100644 --- a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/MediaResource.ts +++ b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/MediaResource.ts @@ -7,6 +7,7 @@ import type { DirectionModel } from '../models/DirectionModel'; import type { MediaConfigurationResponseModel } from '../models/MediaConfigurationResponseModel'; import type { MediaItemResponseModel } from '../models/MediaItemResponseModel'; import type { MediaResponseModel } from '../models/MediaResponseModel'; +import type { MediaTreeItemResponseModel } from '../models/MediaTreeItemResponseModel'; import type { MoveMediaRequestModel } from '../models/MoveMediaRequestModel'; import type { PagedMediaCollectionResponseModel } from '../models/PagedMediaCollectionResponseModel'; import type { PagedMediaRecycleBinItemResponseModel } from '../models/PagedMediaRecycleBinItemResponseModel'; @@ -474,6 +475,27 @@ export class MediaResource { }); } + /** + * @returns any Success + * @throws ApiError + */ + public static getTreeMediaAncestors({ + descendantId, + }: { + descendantId?: string, + }): CancelablePromise> { + return __request(OpenAPI, { + method: 'GET', + url: '/umbraco/management/api/v1/tree/media/ancestors', + query: { + 'descendantId': descendantId, + }, + errors: { + 401: `The resource is protected and requires an authentication token`, + }, + }); + } + /** * @returns PagedMediaTreeItemResponseModel Success * @throws ApiError diff --git a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/MediaTypeResource.ts b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/MediaTypeResource.ts index 0174e4c972..e627e14f43 100644 --- a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/MediaTypeResource.ts +++ b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/MediaTypeResource.ts @@ -11,6 +11,7 @@ import type { MediaTypeCompositionRequestModel } from '../models/MediaTypeCompos import type { MediaTypeCompositionResponseModel } from '../models/MediaTypeCompositionResponseModel'; import type { MediaTypeItemResponseModel } from '../models/MediaTypeItemResponseModel'; import type { MediaTypeResponseModel } from '../models/MediaTypeResponseModel'; +import type { MediaTypeTreeItemResponseModel } from '../models/MediaTypeTreeItemResponseModel'; import type { MoveMediaTypeRequestModel } from '../models/MoveMediaTypeRequestModel'; import type { PagedAllowedMediaTypeModel } from '../models/PagedAllowedMediaTypeModel'; import type { PagedMediaTypeTreeItemResponseModel } from '../models/PagedMediaTypeTreeItemResponseModel'; @@ -390,6 +391,27 @@ export class MediaTypeResource { }); } + /** + * @returns any Success + * @throws ApiError + */ + public static getTreeMediaTypeAncestors({ + descendantId, + }: { + descendantId?: string, + }): CancelablePromise> { + return __request(OpenAPI, { + method: 'GET', + url: '/umbraco/management/api/v1/tree/media-type/ancestors', + query: { + 'descendantId': descendantId, + }, + errors: { + 401: `The resource is protected and requires an authentication token`, + }, + }); + } + /** * @returns PagedMediaTypeTreeItemResponseModel Success * @throws ApiError diff --git a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/PartialViewResource.ts b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/PartialViewResource.ts index 6c61f51aee..7504bb951e 100644 --- a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/PartialViewResource.ts +++ b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/PartialViewResource.ts @@ -4,6 +4,7 @@ /* eslint-disable */ import type { CreatePartialViewFolderRequestModel } from '../models/CreatePartialViewFolderRequestModel'; import type { CreatePartialViewRequestModel } from '../models/CreatePartialViewRequestModel'; +import type { FileSystemTreeItemPresentationModel } from '../models/FileSystemTreeItemPresentationModel'; import type { PagedFileSystemTreeItemPresentationModel } from '../models/PagedFileSystemTreeItemPresentationModel'; import type { PagedPartialViewSnippetItemResponseModel } from '../models/PagedPartialViewSnippetItemResponseModel'; import type { PartialViewFolderResponseModel } from '../models/PartialViewFolderResponseModel'; @@ -280,6 +281,27 @@ export class PartialViewResource { }); } + /** + * @returns any Success + * @throws ApiError + */ + public static getTreePartialViewAncestors({ + descendantPath, + }: { + descendantPath?: string, + }): CancelablePromise> { + return __request(OpenAPI, { + method: 'GET', + url: '/umbraco/management/api/v1/tree/partial-view/ancestors', + query: { + 'descendantPath': descendantPath, + }, + errors: { + 401: `The resource is protected and requires an authentication token`, + }, + }); + } + /** * @returns PagedFileSystemTreeItemPresentationModel Success * @throws ApiError diff --git a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/ScriptResource.ts b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/ScriptResource.ts index 4f05cb0525..6e0495ec7d 100644 --- a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/ScriptResource.ts +++ b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/ScriptResource.ts @@ -4,6 +4,7 @@ /* eslint-disable */ import type { CreateScriptFolderRequestModel } from '../models/CreateScriptFolderRequestModel'; import type { CreateScriptRequestModel } from '../models/CreateScriptRequestModel'; +import type { FileSystemTreeItemPresentationModel } from '../models/FileSystemTreeItemPresentationModel'; import type { PagedFileSystemTreeItemPresentationModel } from '../models/PagedFileSystemTreeItemPresentationModel'; import type { RenameScriptRequestModel } from '../models/RenameScriptRequestModel'; import type { ScriptFolderResponseModel } from '../models/ScriptFolderResponseModel'; @@ -232,6 +233,27 @@ export class ScriptResource { }); } + /** + * @returns any Success + * @throws ApiError + */ + public static getTreeScriptAncestors({ + descendantPath, + }: { + descendantPath?: string, + }): CancelablePromise> { + return __request(OpenAPI, { + method: 'GET', + url: '/umbraco/management/api/v1/tree/script/ancestors', + query: { + 'descendantPath': descendantPath, + }, + errors: { + 401: `The resource is protected and requires an authentication token`, + }, + }); + } + /** * @returns PagedFileSystemTreeItemPresentationModel Success * @throws ApiError diff --git a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/StaticFileResource.ts b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/StaticFileResource.ts index fb9e31c72e..95d59e2241 100644 --- a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/StaticFileResource.ts +++ b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/StaticFileResource.ts @@ -2,6 +2,7 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ +import type { FileSystemTreeItemPresentationModel } from '../models/FileSystemTreeItemPresentationModel'; import type { PagedFileSystemTreeItemPresentationModel } from '../models/PagedFileSystemTreeItemPresentationModel'; import type { StaticFileItemResponseModel } from '../models/StaticFileItemResponseModel'; @@ -32,6 +33,27 @@ export class StaticFileResource { }); } + /** + * @returns any Success + * @throws ApiError + */ + public static getTreeStaticFileAncestors({ + descendantPath, + }: { + descendantPath?: string, + }): CancelablePromise> { + return __request(OpenAPI, { + method: 'GET', + url: '/umbraco/management/api/v1/tree/static-file/ancestors', + query: { + 'descendantPath': descendantPath, + }, + errors: { + 401: `The resource is protected and requires an authentication token`, + }, + }); + } + /** * @returns PagedFileSystemTreeItemPresentationModel Success * @throws ApiError diff --git a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/StylesheetResource.ts b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/StylesheetResource.ts index 635f209c5d..636ee5780d 100644 --- a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/StylesheetResource.ts +++ b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/StylesheetResource.ts @@ -4,6 +4,7 @@ /* eslint-disable */ import type { CreateStylesheetFolderRequestModel } from '../models/CreateStylesheetFolderRequestModel'; import type { CreateStylesheetRequestModel } from '../models/CreateStylesheetRequestModel'; +import type { FileSystemTreeItemPresentationModel } from '../models/FileSystemTreeItemPresentationModel'; import type { PagedFileSystemTreeItemPresentationModel } from '../models/PagedFileSystemTreeItemPresentationModel'; import type { RenameStylesheetRequestModel } from '../models/RenameStylesheetRequestModel'; import type { StylesheetFolderResponseModel } from '../models/StylesheetFolderResponseModel'; @@ -232,6 +233,27 @@ export class StylesheetResource { }); } + /** + * @returns any Success + * @throws ApiError + */ + public static getTreeStylesheetAncestors({ + descendantPath, + }: { + descendantPath?: string, + }): CancelablePromise> { + return __request(OpenAPI, { + method: 'GET', + url: '/umbraco/management/api/v1/tree/stylesheet/ancestors', + query: { + 'descendantPath': descendantPath, + }, + errors: { + 401: `The resource is protected and requires an authentication token`, + }, + }); + } + /** * @returns PagedFileSystemTreeItemPresentationModel Success * @throws ApiError diff --git a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/TemplateResource.ts b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/TemplateResource.ts index 269c145b8c..13c6dfec77 100644 --- a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/TemplateResource.ts +++ b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/TemplateResource.ts @@ -3,7 +3,14 @@ /* tslint:disable */ /* eslint-disable */ import type { CreateTemplateRequestModel } from '../models/CreateTemplateRequestModel'; +import type { DataTypeTreeItemResponseModel } from '../models/DataTypeTreeItemResponseModel'; +import type { DocumentBlueprintTreeItemResponseModel } from '../models/DocumentBlueprintTreeItemResponseModel'; +import type { DocumentTypeTreeItemResponseModel } from '../models/DocumentTypeTreeItemResponseModel'; +import type { FolderTreeItemResponseModel } from '../models/FolderTreeItemResponseModel'; +import type { MediaTypeTreeItemResponseModel } from '../models/MediaTypeTreeItemResponseModel'; +import type { NamedEntityTreeItemResponseModel } from '../models/NamedEntityTreeItemResponseModel'; import type { PagedNamedEntityTreeItemResponseModel } from '../models/PagedNamedEntityTreeItemResponseModel'; +import type { RelationTypeTreeItemResponseModel } from '../models/RelationTypeTreeItemResponseModel'; import type { TemplateConfigurationResponseModel } from '../models/TemplateConfigurationResponseModel'; import type { TemplateItemResponseModel } from '../models/TemplateItemResponseModel'; import type { TemplateQueryExecuteModel } from '../models/TemplateQueryExecuteModel'; @@ -184,6 +191,27 @@ export class TemplateResource { }); } + /** + * @returns any Success + * @throws ApiError + */ + public static getTreeTemplateAncestors({ + descendantId, + }: { + descendantId?: string, + }): CancelablePromise> { + return __request(OpenAPI, { + method: 'GET', + url: '/umbraco/management/api/v1/tree/template/ancestors', + query: { + 'descendantId': descendantId, + }, + errors: { + 401: `The resource is protected and requires an authentication token`, + }, + }); + } + /** * @returns PagedNamedEntityTreeItemResponseModel Success * @throws ApiError diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/index.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/index.ts index a49d53fb99..0b1a7fed65 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/index.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/index.ts @@ -8,3 +8,4 @@ export * from './extension-manifest-initializer.controller.js'; export * from './extensions-manifest-initializer.controller.js'; export * from './extension-element-and-api-initializer.controller.js'; export * from './extensions-element-and-api-initializer.controller.js'; +export * from './server-extension-registrator.controller.js'; diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/server-extension-registrator.controller.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/server-extension-registrator.controller.ts new file mode 100644 index 0000000000..c663868854 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/server-extension-registrator.controller.ts @@ -0,0 +1,83 @@ +import type { ManifestBase } from '../types/index.js'; +import { isManifestBaseType } from '../type-guards/index.js'; +import { + PackageResource, + OpenAPI, + type PackageManifestResponseModel, +} from '@umbraco-cms/backoffice/external/backend-api'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { UmbBackofficeExtensionRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; + +// TODO: consider if this can be replaced by the new extension controllers +export class UmbServerExtensionRegistrator extends UmbControllerBase { + #extensionRegistry: UmbBackofficeExtensionRegistry; + #apiBaseUrl = OpenAPI.BASE; + + constructor(host: UmbControllerHost, extensionRegistry: UmbBackofficeExtensionRegistry) { + super(host, UmbServerExtensionRegistrator.name); + this.#extensionRegistry = extensionRegistry; + } + + /** + * Registers all extensions from the server. + * This is used to register all extensions that are available to the user (including private extensions). + * @remark Users must have the BACKOFFICE_ACCESS permission to access this method. + */ + public async registerAllExtensions() { + const { data: packages } = await tryExecuteAndNotify(this, PackageResource.getPackageManifest()); + if (packages) { + await this.#loadServerPackages(packages); + } + } + + /** + * Registers all public extensions from the server. + * This is used to register all extensions that are available to the user (excluding private extensions) such as login extensions. + * @remark Any user can access this method without any permissions. + */ + public async registerPublicExtensions() { + const { data: packages } = await tryExecuteAndNotify(this, PackageResource.getPackageManifestPublic()); + if (packages) { + await this.#loadServerPackages(packages); + } + } + + async #loadServerPackages(packages: PackageManifestResponseModel[]) { + const extensions: ManifestBase[] = []; + + packages.forEach((p) => { + p.extensions?.forEach((e) => { + // Crudely validate that the extension at least follows a basic manifest structure + // Idea: Use `Zod` to validate the manifest + if (isManifestBaseType(e)) { + /** + * Crude check to see if extension is of type "js" since it is safe to assume we do not + * need to load any other types of extensions in the backoffice (we need a js file to load) + */ + + // TODO: add helper to check for relative paths + // Add base url if the js path is relative + if ('js' in e && typeof e.js === 'string' && !e.js.startsWith('http')) { + e.js = `${this.#apiBaseUrl}${e.js}`; + } + + // Add base url if the element path is relative + if ('element' in e && typeof e.element === 'string' && !e.element.startsWith('http')) { + e.element = `${this.#apiBaseUrl}${e.element}`; + } + + // Add base url if the element path api relative + if ('api' in e && typeof e.api === 'string' && !e.api.startsWith('http')) { + e.api = `${this.#apiBaseUrl}${e.api}`; + } + + extensions.push(e); + } + }); + }); + + this.#extensionRegistry.registerMany(extensions); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts index b7f23ccf80..35e2e49edf 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts @@ -399,11 +399,7 @@ export const data: Array = [ }, { alias: 'items', - value: { - 0: { sortOrder: 1, value: 'First Option' }, - 1: { sortOrder: 2, value: 'Second Option' }, - 2: { sortOrder: 3, value: 'I Am the third Option' }, - }, + value: ['First Option', 'Second Option', 'I Am the third Option'], }, ], }, @@ -519,11 +515,7 @@ export const data: Array = [ values: [ { alias: 'items', - value: { - 0: { sortOrder: 1, value: 'First Option' }, - 1: { sortOrder: 2, value: 'Second Option' }, - 2: { sortOrder: 3, value: 'I Am the third Option' }, - }, + value: ['First Option', 'Second Option', 'I Am the third Option'], }, ], }, @@ -540,11 +532,7 @@ export const data: Array = [ values: [ { alias: 'items', - value: { - 0: { sortOrder: 1, value: 'First Option' }, - 1: { sortOrder: 2, value: 'Second Option' }, - 2: { sortOrder: 3, value: 'I Am the third Option' }, - }, + value: ['First Option', 'Second Option', 'I Am the third Option'], }, ], }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area-config-entry/workspace/block-grid-area-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area-config-entry/workspace/block-grid-area-type-workspace.context.ts index 3dfbff5f95..7354fc4d2a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area-config-entry/workspace/block-grid-area-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area-config-entry/workspace/block-grid-area-type-workspace.context.ts @@ -1,12 +1,9 @@ import type { UmbBlockGridTypeAreaType } from '../../../types.js'; import type { UmbPropertyDatasetContext } from '@umbraco-cms/backoffice/property'; import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property'; -import type { - UmbInvariantableWorkspaceContextInterface, - UmbWorkspaceContextInterface, -} from '@umbraco-cms/backoffice/workspace'; +import type { UmbInvariantDatasetWorkspaceContext, UmbWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; import { - UmbEditableWorkspaceContextBase, + UmbSaveableWorkspaceContextBase, UmbInvariantWorkspacePropertyDatasetContext, } from '@umbraco-cms/backoffice/workspace'; import { UmbArrayState, UmbObjectState, appendToFrozenArray } from '@umbraco-cms/backoffice/observable-api'; @@ -15,8 +12,8 @@ import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; import type { ManifestWorkspace, PropertyEditorSettingsProperty } from '@umbraco-cms/backoffice/extension-registry'; export class UmbBlockGridAreaTypeWorkspaceContext - extends UmbEditableWorkspaceContextBase - implements UmbInvariantableWorkspaceContextInterface + extends UmbSaveableWorkspaceContextBase + implements UmbInvariantDatasetWorkspaceContext { // Just for context token safety: public readonly IS_BLOCK_GRID_AREA_TYPE_WORKSPACE_CONTEXT = true; @@ -132,7 +129,7 @@ export class UmbBlockGridAreaTypeWorkspaceContext export default UmbBlockGridAreaTypeWorkspaceContext; export const UMB_BLOCK_GRID_AREA_TYPE_WORKSPACE_CONTEXT = new UmbContextToken< - UmbWorkspaceContextInterface, + UmbWorkspaceContext, UmbBlockGridAreaTypeWorkspaceContext >( 'UmbWorkspaceContext', diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-type/workspace/block-type-workspace.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-type/workspace/block-type-workspace.context-token.ts index 6817e2d795..01b8191323 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-type/workspace/block-type-workspace.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-type/workspace/block-type-workspace.context-token.ts @@ -1,11 +1,8 @@ import type { UmbBlockTypeWorkspaceContext } from './block-type-workspace.context.js'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import type { UmbWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; +import type { UmbWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; -export const UMB_BLOCK_TYPE_WORKSPACE_CONTEXT = new UmbContextToken< - UmbWorkspaceContextInterface, - UmbBlockTypeWorkspaceContext ->( +export const UMB_BLOCK_TYPE_WORKSPACE_CONTEXT = new UmbContextToken( 'UmbWorkspaceContext', undefined, (context): context is UmbBlockTypeWorkspaceContext => (context as any).IS_BLOCK_TYPE_WORKSPACE_CONTEXT, diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-type/workspace/block-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-type/workspace/block-type-workspace.context.ts index 74b691f62e..5c74f82147 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-type/workspace/block-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-type/workspace/block-type-workspace.context.ts @@ -3,11 +3,11 @@ import { UmbBlockTypeWorkspaceEditorElement } from './block-type-workspace-edito import type { UmbPropertyDatasetContext } from '@umbraco-cms/backoffice/property'; import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property'; import type { - UmbInvariantableWorkspaceContextInterface, + UmbInvariantDatasetWorkspaceContext, UmbRoutableWorkspaceContext, } from '@umbraco-cms/backoffice/workspace'; import { - UmbEditableWorkspaceContextBase, + UmbSaveableWorkspaceContextBase, UmbInvariantWorkspacePropertyDatasetContext, UmbWorkspaceIsNewRedirectController, UmbWorkspaceRouteManager, @@ -17,8 +17,8 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { ManifestWorkspace, PropertyEditorSettingsProperty } from '@umbraco-cms/backoffice/extension-registry'; export class UmbBlockTypeWorkspaceContext - extends UmbEditableWorkspaceContextBase - implements UmbInvariantableWorkspaceContextInterface, UmbRoutableWorkspaceContext + extends UmbSaveableWorkspaceContextBase + implements UmbInvariantDatasetWorkspaceContext, UmbRoutableWorkspaceContext { // Just for context token safety: public readonly IS_BLOCK_TYPE_WORKSPACE_CONTEXT = true; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context-token.ts index 696d6c70a4..a2147870e1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context-token.ts @@ -1,8 +1,8 @@ import type { UmbBlockWorkspaceContext } from './block-workspace.context.js'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import type { UmbWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; +import type { UmbWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; -export const UMB_BLOCK_WORKSPACE_CONTEXT = new UmbContextToken( +export const UMB_BLOCK_WORKSPACE_CONTEXT = new UmbContextToken( 'UmbWorkspaceContext', undefined, (context): context is UmbBlockWorkspaceContext => (context as any).IS_BLOCK_WORKSPACE_CONTEXT, diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts index b9ebead2b4..c379a4ce51 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts @@ -2,7 +2,7 @@ import type { UmbBlockDataType, UmbBlockLayoutBaseModel } from '../types.js'; import { UmbBlockElementManager } from './block-element-manager.js'; import { UmbBlockWorkspaceEditorElement } from './block-workspace-editor.element.js'; import { - UmbEditableWorkspaceContextBase, + UmbSaveableWorkspaceContextBase, UmbWorkspaceRouteManager, type UmbRoutableWorkspaceContext, UmbWorkspaceIsNewRedirectController, @@ -20,7 +20,7 @@ import { decodeFilePath } from '@umbraco-cms/backoffice/utils'; export type UmbBlockWorkspaceElementManagerNames = 'content' | 'settings'; export class UmbBlockWorkspaceContext - extends UmbEditableWorkspaceContextBase + extends UmbSaveableWorkspaceContextBase implements UmbRoutableWorkspaceContext { // Just for context token safety: @@ -48,7 +48,7 @@ export class UmbBlockWorkspaceContext(undefined); readonly layout = this.#layout.asObservable(); - //readonly unique = this.#layout.asObservablePart((x) => x?.contentUdi); + readonly unique = this.#layout.asObservablePart((x) => x?.contentUdi); readonly contentUdi = this.#layout.asObservablePart((x) => x?.contentUdi); readonly content = new UmbBlockElementManager(this); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-actions-bundle/entity-actions-bundle.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-actions-bundle/entity-actions-bundle.element.ts index b856c443da..c696d25743 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-actions-bundle/entity-actions-bundle.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-actions-bundle/entity-actions-bundle.element.ts @@ -1,25 +1,17 @@ -import { html, nothing, customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; -import { map } from '@umbraco-cms/backoffice/external/rxjs'; +import type { UmbEntityAction } from '@umbraco-cms/backoffice/entity-action'; +import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; +import { html, nothing, customElement, property, state, ifDefined } from '@umbraco-cms/backoffice/external/lit'; import type { UmbSectionSidebarContext } from '@umbraco-cms/backoffice/section'; import { UMB_SECTION_SIDEBAR_CONTEXT } from '@umbraco-cms/backoffice/section'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { ManifestEntityActionDefaultKind } from '@umbraco-cms/backoffice/extension-registry'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { createExtensionApi } from '@umbraco-cms/backoffice/extension-api'; @customElement('umb-entity-actions-bundle') export class UmbEntityActionsBundleElement extends UmbLitElement { - private _entityType?: string; @property({ type: String, attribute: 'entity-type' }) - public get entityType() { - return this._entityType; - } - public set entityType(value: string | undefined) { - const oldValue = this._entityType; - if (oldValue === value) return; - - this._entityType = value; - this.#observeEntityActions(); - this.requestUpdate('entityType', oldValue); - } + entityType?: string; @property({ type: String }) unique?: string | null; @@ -28,7 +20,13 @@ export class UmbEntityActionsBundleElement extends UmbLitElement { public label?: string; @state() - private _hasActions = false; + private _numberOfActions = 0; + + @state() + private _firstActionManifest?: ManifestEntityActionDefaultKind; + + @state() + private _firstActionApi?: UmbEntityAction; #sectionSidebarContext?: UmbSectionSidebarContext; @@ -40,36 +38,58 @@ export class UmbEntityActionsBundleElement extends UmbLitElement { }); } + protected updated(_changedProperties: PropertyValueMap | Map): void { + if (_changedProperties.has('entityType') && _changedProperties.has('unique')) { + this.#observeEntityActions(); + } + } + #observeEntityActions() { this.observe( - umbExtensionsRegistry - .byType('entityAction') - .pipe(map((actions) => actions.some((action) => action.forEntityTypes.includes(this.entityType!)))), - (hasActions) => { - this._hasActions = hasActions; + umbExtensionsRegistry.byTypeAndFilter('entityAction', (ext) => ext.forEntityTypes.includes(this.entityType!)), + async (actions) => { + this._numberOfActions = actions.length; + this._firstActionManifest = + this._numberOfActions > 0 ? (actions[0] as ManifestEntityActionDefaultKind) : undefined; + if (!this._firstActionManifest) return; + this._firstActionApi = await createExtensionApi(this, this._firstActionManifest, [ + { unique: this.unique, entityType: this.entityType, meta: this._firstActionManifest.meta }, + ]); }, 'umbEntityActionsObserver', ); } - private _openActions() { + #openContextMenu() { if (!this.entityType) throw new Error('Entity type is not defined'); if (this.unique === undefined) throw new Error('Unique is not defined'); this.#sectionSidebarContext?.toggleContextMenu(this.entityType, this.unique, this.label); } + async #onFirstActionClick(event: PointerEvent) { + event.stopPropagation(); + await this._firstActionApi?.execute(); + } + render() { - return html` - ${this._hasActions - ? html` - - - - - - ` - : nothing} - `; + if (this._numberOfActions === 0) return nothing; + return html` ${this.#renderFirstAction()} ${this.#renderMore()} `; + } + + #renderMore() { + if (this._numberOfActions === 1) return nothing; + return html` + + `; + } + + #renderFirstAction() { + if (!this._firstActionApi) return nothing; + return html` + + `; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-number-range/input-number-range.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-number-range/input-number-range.element.ts index ca4f086e89..9b2c008fb3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-number-range/input-number-range.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-number-range/input-number-range.element.ts @@ -1,5 +1,5 @@ -import { html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; -import { FormControlMixin } from '@umbraco-cms/backoffice/external/uui'; +import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; +import { html, customElement, property, state, type PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; function getNumberOrUndefined(value: string) { @@ -8,7 +8,7 @@ function getNumberOrUndefined(value: string) { } @customElement('umb-input-number-range') -export class UmbInputNumberRangeElement extends FormControlMixin(UmbLitElement) { +export class UmbInputNumberRangeElement extends UmbFormControlMixin(UmbLitElement, undefined) { @property({ type: String, attribute: 'min-label' }) minLabel = 'Low value'; @@ -40,7 +40,8 @@ export class UmbInputNumberRangeElement extends FormControlMixin(UmbLitElement) } private updateValue() { - const newValue = this._minValue || this._maxValue ? (this._minValue || '') + ',' + (this._maxValue || '') : ''; + const newValue = + this._minValue || this._maxValue ? (this._minValue ?? '') + ',' + (this._maxValue ?? '') : undefined; if (super.value !== newValue) { super.value = newValue; } @@ -48,7 +49,7 @@ export class UmbInputNumberRangeElement extends FormControlMixin(UmbLitElement) @property() public set value(valueString: string) { - if (valueString !== this._value) { + if (valueString !== this.value) { const splittedValue = valueString.split(/[ ,]+/); this.minValue = getNumberOrUndefined(splittedValue[0]); this.maxValue = getNumberOrUndefined(splittedValue[1]); @@ -62,19 +63,27 @@ export class UmbInputNumberRangeElement extends FormControlMixin(UmbLitElement) return this; } + protected firstUpdated(_changedProperties: PropertyValueMap | Map): void { + super.firstUpdated(_changedProperties); + this.shadowRoot + ?.querySelectorAll('uui-input') + .forEach((x) => this.addFormControlElement(x as unknown as HTMLInputElement)); + } + private _onMinInput(e: InputEvent) { this.minValue = Number((e.target as HTMLInputElement).value); - this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true })); + this.dispatchEvent(new CustomEvent('change', { bubbles: true })); } private _onMaxInput(e: InputEvent) { this.maxValue = Number((e.target as HTMLInputElement).value); - this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true })); + this.dispatchEvent(new CustomEvent('change', { bubbles: true })); } render() { return html` diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/modals/property-type-settings/property-type-settings-modal.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/modals/property-type-settings/property-type-settings-modal.context.ts index e22f3f0aa2..76c94f8483 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/modals/property-type-settings/property-type-settings-modal.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/modals/property-type-settings/property-type-settings-modal.context.ts @@ -1,4 +1,4 @@ -import type { UmbWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; +import type { UmbWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; @@ -10,7 +10,7 @@ export const UMB_PROPERTY_TYPE_WORKSPACE_ALIAS = 'Umb.Workspace.PropertyType'; */ export class UmbPropertyTypeWorkspaceContext extends UmbContextBase - implements UmbWorkspaceContextInterface + implements UmbWorkspaceContext { constructor(host: UmbControllerHost) { super(host, UMB_PROPERTY_TYPE_WORKSPACE_CONTEXT); @@ -32,7 +32,7 @@ export class UmbPropertyTypeWorkspaceContext export default UmbPropertyTypeWorkspaceContext; export const UMB_PROPERTY_TYPE_WORKSPACE_CONTEXT = new UmbContextToken< - UmbWorkspaceContextInterface, + UmbWorkspaceContext, UmbPropertyTypeWorkspaceContext >( 'UmbWorkspaceContext', diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/workspace/content-type-workspace-context.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/workspace/content-type-workspace-context.interface.ts index 743abd5e8e..959d8a438b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/workspace/content-type-workspace-context.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/workspace/content-type-workspace-context.interface.ts @@ -1,10 +1,10 @@ import type { UmbContentTypeCompositionModel, UmbContentTypeModel, UmbContentTypeSortModel } from '../types.js'; import type { UmbContentTypeStructureManager } from '../structure/index.js'; import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; -import type { UmbSaveableWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; +import type { UmbSaveableWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; export interface UmbContentTypeWorkspaceContext - extends UmbSaveableWorkspaceContextInterface { + extends UmbSaveableWorkspaceContext { readonly IS_CONTENT_TYPE_WORKSPACE_CONTEXT: true; readonly name: Observable; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/menu-item/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/menu-item/manifests.ts index 300913745e..6604cf0ea2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/menu-item/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/menu-item/manifests.ts @@ -3,13 +3,13 @@ import type { ManifestMenuItem } from '@umbraco-cms/backoffice/extension-registr const menuItem: ManifestMenuItem = { type: 'menuItem', alias: 'Umb.MenuItem.Extensions', - name: 'Extensions Menu Item', - weight: 0, + name: 'Extension Insights Menu Item', + weight: 200, meta: { - label: 'Extensions', + label: 'Extension Insights', icon: 'icon-wand', entityType: 'extension-root', - menus: ['Umb.Menu.Settings'], + menus: ['Umb.Menu.AdvancedSettings'], }, }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/index.ts index f513625afd..fac0614d51 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/index.ts @@ -43,7 +43,11 @@ import type { ManifestWorkspace, ManifestWorkspaceRoutableKind } from './workspa import type { ManifestWorkspaceAction, ManifestWorkspaceActionDefaultKind } from './workspace-action.model.js'; import type { ManifestWorkspaceActionMenuItem } from './workspace-action-menu-item.model.js'; import type { ManifestWorkspaceContext } from './workspace-context.model.js'; -import type { ManifestWorkspaceFooterApp } from './workspace-footer-app.model.js'; +import type { + ManifestWorkspaceFooterApp, + ManifestWorkspaceFooterAppMenuBreadcrumbKind, + ManifestWorkspaceFooterAppVariantMenuBreadcrumbKind, +} from './workspace-footer-app.model.js'; import type { ManifestWorkspaceView, ManifestWorkspaceViewContentTypeDesignEditorKind, @@ -110,6 +114,11 @@ export type ManifestEntityActions = | ManifestEntityActionDeleteFolderKind | ManifestEntityActionTrashKind; +export type ManifestWorkspaceFooterApps = + | ManifestWorkspaceFooterApp + | ManifestWorkspaceFooterAppMenuBreadcrumbKind + | ManifestWorkspaceFooterAppVariantMenuBreadcrumbKind; + export type ManifestPropertyActions = ManifestPropertyAction | ManifestPropertyActionDefaultKind; export type ManifestWorkspaceActions = ManifestWorkspaceAction | ManifestWorkspaceActionDefaultKind; @@ -158,11 +167,11 @@ export type ManifestTypes = | ManifestTreeItem | ManifestTreeStore | ManifestUserProfileApp - | ManifestWorkspaces - | ManifestWorkspaceActions | ManifestWorkspaceActionMenuItem + | ManifestWorkspaceActions | ManifestWorkspaceContext - | ManifestWorkspaceFooterApp + | ManifestWorkspaceFooterApps + | ManifestWorkspaces | ManifestWorkspaceViews | ManifestEntityUserPermission | ManifestGranularUserPermission diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/workspace-footer-app.model.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/workspace-footer-app.model.ts index 02ec1d080b..b1d3972895 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/workspace-footer-app.model.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/workspace-footer-app.model.ts @@ -7,3 +7,13 @@ export interface ManifestWorkspaceFooterApp ManifestWithDynamicConditions { type: 'workspaceFooterApp'; } + +export interface ManifestWorkspaceFooterAppMenuBreadcrumbKind extends ManifestWorkspaceFooterApp { + type: 'workspaceFooterApp'; + kind: 'menuBreadcrumb'; +} + +export interface ManifestWorkspaceFooterAppVariantMenuBreadcrumbKind extends ManifestWorkspaceFooterApp { + type: 'workspaceFooterApp'; + kind: 'variantMenuBreadcrumb'; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/workspace.model.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/workspace.model.ts index e4d674f53f..d53e829441 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/workspace.model.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/workspace.model.ts @@ -1,5 +1,5 @@ -import type { UmbRoutableWorkspaceContext } from '../../workspace/contexts/routable-workspace-context.interface.js'; -import type { UmbWorkspaceContextInterface } from '../../workspace/contexts/workspace-context.interface.js'; +import type { UmbRoutableWorkspaceContext } from '../../workspace/contexts/tokens/routable-workspace-context.interface.js'; +import type { UmbWorkspaceContext } from '../../workspace/contexts/tokens/workspace-context.interface.js'; import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; import type { ManifestElementAndApi } from '@umbraco-cms/backoffice/extension-api'; @@ -7,7 +7,7 @@ import type { ManifestElementAndApi } from '@umbraco-cms/backoffice/extension-ap export interface ManifestWorkspace< MetaType extends MetaWorkspace = MetaWorkspace, ElementType extends UmbControllerHostElement = UmbControllerHostElement, - ApiType extends UmbWorkspaceContextInterface = UmbWorkspaceContextInterface, + ApiType extends UmbWorkspaceContext = UmbWorkspaceContext, > extends ManifestElementAndApi { type: 'workspace'; meta: MetaType; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/umbraco-package.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/umbraco-package.ts index 7663b53c2c..3a05a2d7f0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/umbraco-package.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/umbraco-package.ts @@ -27,6 +27,12 @@ export interface UmbracoPackage { */ allowTelemetry?: boolean; + /** + * @title Decides if the package is allowed to be accessed by the public, e.g. on the login screen + * @default false + */ + allowPublicAccess?: boolean; + /** * @title An array of Umbraco package manifest types that will be installed * @required diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/workspace/extension-root-workspace.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/workspace/extension-root-workspace.element.ts index 198f495da2..9c701cf6e7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/workspace/extension-root-workspace.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/workspace/extension-root-workspace.element.ts @@ -7,7 +7,10 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; export class UmbExtensionRootWorkspaceElement extends UmbLitElement { render() { return html` - + `; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize-date.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize-date.element.ts index fbddbaeccb..9d8238aec2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize-date.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize-date.element.ts @@ -13,20 +13,20 @@ export class UmbLocalizeDateElement extends UmbLitElement { * @attr * @example date="Sep 22 2023" */ - @property() - date!: string | Date; + @property({ type: String }) + date?: string | Date; /** * Formatting options * @attr * @example options={ dateStyle: 'full', timeStyle: 'long', timeZone: 'Australia/Sydney' } */ - @property() + @property({ type: Object }) options?: Intl.DateTimeFormatOptions; @state() protected get text(): string { - return this.localize.date(this.date, this.options); + return this.localize.date(this.date!, this.options); } protected render() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/menu/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/menu/index.ts index 00decb2f86..e128f53e2f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/menu/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/menu/index.ts @@ -2,3 +2,8 @@ export * from './menu-item/index.js'; export * from './menu-item-layout/index.js'; export * from './menu.element.js'; export * from './menu.context.js'; +export * from './menu-tree-structure-workspace-context-base.js'; +export * from './menu-variant-tree-structure-workspace-context-base.js'; +export * from './types.js'; + +export type { UmbMenuStructureWorkspaceContext } from './menu-structure-workspace-context.interface.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-structure-workspace-context.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-structure-workspace-context.interface.ts new file mode 100644 index 0000000000..6b0827d2b6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-structure-workspace-context.interface.ts @@ -0,0 +1,6 @@ +import type { UmbStructureItemModel } from './types.js'; +import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; + +export interface UmbMenuStructureWorkspaceContext { + structure: Observable; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-tree-structure-workspace-context-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-tree-structure-workspace-context-base.ts new file mode 100644 index 0000000000..cf97cd2c10 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-tree-structure-workspace-context-base.ts @@ -0,0 +1,78 @@ +import type { UmbStructureItemModel } from './types.js'; +import type { UmbTreeRepository, UmbUniqueTreeItemModel, UmbUniqueTreeRootModel } from '@umbraco-cms/backoffice/tree'; +import { createExtensionApiByAlias } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; +import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; + +interface UmbMenuTreeStructureWorkspaceContextBaseArgs { + treeRepositoryAlias: string; +} + +export abstract class UmbMenuTreeStructureWorkspaceContextBase extends UmbContextBase { + #workspaceContext?: any; + #args: UmbMenuTreeStructureWorkspaceContextBaseArgs; + + #structure = new UmbArrayState([], (x) => x.unique); + public readonly structure = this.#structure.asObservable(); + + constructor(host: UmbControllerHost, args: UmbMenuTreeStructureWorkspaceContextBaseArgs) { + // TODO: set up context token + super(host, 'UmbMenuStructureWorkspaceContext'); + this.#args = args; + + this.consumeContext(UMB_WORKSPACE_CONTEXT, (instance) => { + this.#workspaceContext = instance; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + this.#workspaceContext.observe(this.#workspaceContext.unique, (value) => { + if (!value) return; + this.#requestStructure(); + }); + }); + } + + async #requestStructure() { + let structureItems: Array = []; + + const treeRepository = await createExtensionApiByAlias< + UmbTreeRepository + >(this, this.#args.treeRepositoryAlias); + + const { data: root } = await treeRepository.requestTreeRoot(); + + if (root) { + structureItems = [ + { + unique: root.unique, + entityType: root.entityType, + name: root.name, + isFolder: root.isFolder, + }, + ]; + } + + const isNew = this.#workspaceContext?.getIsNew(); + const uniqueObservable = isNew ? this.#workspaceContext?.parentUnique : this.#workspaceContext?.unique; + + const unique = (await this.observe(uniqueObservable, () => {})?.asPromise()) as string; + if (!unique) throw new Error('Unique is not available'); + + const { data } = await treeRepository.requestTreeItemAncestors({ descendantUnique: unique }); + + if (data) { + const ancestorItems = data.map((treeItem) => { + return { + unique: treeItem.unique, + entityType: treeItem.entityType, + name: treeItem.name, + isFolder: treeItem.isFolder, + }; + }); + structureItems.push(...ancestorItems); + } + + this.#structure.setValue(structureItems); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-variant-tree-structure-workspace-context-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-variant-tree-structure-workspace-context-base.ts new file mode 100644 index 0000000000..bc53f28278 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-variant-tree-structure-workspace-context-base.ts @@ -0,0 +1,68 @@ +import type { UmbVariantStructureItemModel } from './types.js'; +import type { UmbTreeRepository } from '@umbraco-cms/backoffice/tree'; +import { createExtensionApiByAlias } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; +import { UMB_VARIANT_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; + +interface UmbMenuVariantTreeStructureWorkspaceContextBaseArgs { + treeRepositoryAlias: string; +} + +export abstract class UmbMenuVariantTreeStructureWorkspaceContextBase extends UmbContextBase { + // TODO: add correct interface + #workspaceContext?: any; + #args: UmbMenuVariantTreeStructureWorkspaceContextBaseArgs; + + #structure = new UmbArrayState([], (x) => x.unique); + public readonly structure = this.#structure.asObservable(); + + constructor(host: UmbControllerHost, args: UmbMenuVariantTreeStructureWorkspaceContextBaseArgs) { + // TODO: set up context token + super(host, 'UmbMenuStructureWorkspaceContext'); + this.#args = args; + + this.consumeContext(UMB_VARIANT_WORKSPACE_CONTEXT, (instance) => { + this.#workspaceContext = instance; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + this.#workspaceContext.observe(this.#workspaceContext.unique, (value) => { + if (!value) return; + this.#requestStructure(); + }); + }); + } + + async #requestStructure() { + const isNew = this.#workspaceContext?.getIsNew(); + const uniqueObservable = isNew ? this.#workspaceContext?.parentUnique : this.#workspaceContext?.unique; + + const unique = (await this.observe(uniqueObservable, () => {})?.asPromise()) as string; + if (!unique) throw new Error('Unique is not available'); + + const treeRepository = await createExtensionApiByAlias>( + this, + this.#args.treeRepositoryAlias, + ); + const { data } = await treeRepository.requestTreeItemAncestors({ descendantUnique: unique }); + + if (data) { + const structureItems = data.map((treeItem) => { + return { + unique: treeItem.unique, + entityType: treeItem.entityType, + variants: treeItem.variants.map((variant: any) => { + return { + name: variant.name, + culture: variant.culture, + segment: variant.segment, + }; + }), + }; + }); + + this.#structure.setValue(structureItems); + } + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/menu/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/menu/types.ts new file mode 100644 index 0000000000..97ed2f677b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/menu/types.ts @@ -0,0 +1,13 @@ +export interface UmbStructureItemModelBase { + unique: string | null; + entityType: string; +} + +export interface UmbStructureItemModel extends UmbStructureItemModelBase { + name: string; + isFolder: boolean; +} + +export interface UmbVariantStructureItemModel extends UmbStructureItemModelBase { + variants: Array<{ name: string; culture: string | null; segment: string | null }>; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/models/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/models/index.ts index 443a993919..e546f7df28 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/models/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/models/index.ts @@ -1,3 +1,5 @@ +export type UmbEntityUnique = string | null; + /** Tried to find a common base of our entities — used by Entity Workspace Context */ export type UmbEntityBase = { id?: string; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/property-editor-ui-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/property-editor-ui-collection-view.element.ts index b423bf9ebb..3d7838c4ef 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/property-editor-ui-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/collection-view/property-editor-ui-collection-view.element.ts @@ -3,7 +3,7 @@ import { html, customElement, property, state } from '@umbraco-cms/backoffice/ex import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UMB_DOCUMENT_COLLECTION_ALIAS } from '@umbraco-cms/backoffice/document'; import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property'; -import { UMB_WORKSPACE_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/workspace'; +import { UMB_COLLECTION_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry'; import type { UmbCollectionBulkActionPermissions, @@ -31,7 +31,7 @@ export class UmbPropertyEditorUICollectionViewElement extends UmbLitElement impl constructor() { super(); - this.consumeContext(UMB_WORKSPACE_COLLECTION_CONTEXT, (workspaceContext) => { + this.consumeContext(UMB_COLLECTION_WORKSPACE_CONTEXT, (workspaceContext) => { this._collectionAlias = workspaceContext.getCollectionAlias(); this.consumeContext(UMB_PROPERTY_CONTEXT, (propertyContext) => { @@ -39,8 +39,9 @@ export class UmbPropertyEditorUICollectionViewElement extends UmbLitElement impl if (propertyAlias) { // Gets the Data Type ID for the current property. const property = await workspaceContext.structure.getPropertyStructureByAlias(propertyAlias); - if (property && this._config) { - this._config.unique = workspaceContext.getUnique(); + const unique = workspaceContext.getUnique(); + if (unique && property && this._config) { + this._config.unique = unique; this._config.dataTypeId = property.dataType.unique; this.requestUpdate('_config'); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/tree-picker/property-editor-ui-tree-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/tree-picker/property-editor-ui-tree-picker.element.ts index a8e13e3fbd..490b0e6a1f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/tree-picker/property-editor-ui-tree-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/tree-picker/property-editor-ui-tree-picker.element.ts @@ -2,7 +2,7 @@ import { html, customElement, property, state } from '@umbraco-cms/backoffice/ex import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor'; import { UmbDynamicRootRepository } from '@umbraco-cms/backoffice/dynamic-root'; -import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; +import { UMB_ENTITY_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry'; import type { UmbInputTreeElement } from '@umbraco-cms/backoffice/tree'; @@ -69,7 +69,7 @@ export class UmbPropertyEditorUITreePickerElement extends UmbLitElement implemen // TODO: Awaiting the workspace context to have a parent entity ID value. [LK] // e.g. const parentEntityId = this.#workspaceContext?.getParentEntityId(); - const workspaceContext = await this.getContext(UMB_WORKSPACE_CONTEXT); + const workspaceContext = await this.getContext(UMB_ENTITY_WORKSPACE_CONTEXT); const unique = workspaceContext.getUnique(); if (unique && this.#dynamicRoot) { const result = await this.#dynamicRootRepository.postDynamicRootQuery(this.#dynamicRoot, unique); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/property-dataset/property-dataset-context.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-dataset/property-dataset-context.interface.ts index e6523ce269..f4f41bda12 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property/property-dataset/property-dataset-context.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-dataset/property-dataset-context.interface.ts @@ -1,6 +1,7 @@ import type { UmbVariantId } from '../../variant/variant-id.class.js'; import type { UmbContext } from '@umbraco-cms/backoffice/class-api'; import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; +import type { UmbEntityUnique } from '@umbraco-cms/backoffice/models'; /** * A property dataset context, represents the data of a set of properties. @@ -18,7 +19,7 @@ import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; */ export interface UmbPropertyDatasetContext extends UmbContext { getEntityType(): string; - getUnique(): string | undefined; + getUnique(): UmbEntityUnique | undefined; getVariantId: () => UmbVariantId; getName(): string | undefined; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/section/section.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/section/section.context.ts index 436675bac0..799ca5e0af 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/section/section.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/section/section.context.ts @@ -19,6 +19,10 @@ export class UmbSectionContext { this.#manifestPathname.setValue(manifest?.meta?.pathname); this.#manifestLabel.setValue(manifest ? manifest.meta?.label || manifest.name : undefined); } + + getPathname() { + return this.#manifestPathname.getValue(); + } } export const UMB_SECTION_CONTEXT = new UmbContextToken('UmbSectionContext'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/settings/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/settings/manifests.ts index 0f84871955..73f0a117b6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/settings/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/settings/manifests.ts @@ -21,21 +21,40 @@ export const manifests = [ }, { type: 'menu', - alias: 'Umb.Menu.Settings', + alias: 'Umb.Menu.StructureSettings', name: 'Settings Menu', - meta: { - label: 'Settings', - }, }, { type: 'sectionSidebarApp', kind: 'menu', alias: 'Umb.SectionSidebarMenu.Settings', - name: 'Settings Section Sidebar Menu', - weight: 200, + name: 'Structure Settings Sidebar Menu', + weight: 300, meta: { - label: 'Settings', - menu: 'Umb.Menu.Settings', + label: 'Structure', + menu: 'Umb.Menu.StructureSettings', + }, + conditions: [ + { + alias: 'Umb.Condition.SectionAlias', + match: UMB_SETTINGS_SECTION_ALIAS, + }, + ], + }, + { + type: 'menu', + alias: 'Umb.Menu.AdvancedSettings', + name: 'Advanced Settings Menu', + }, + { + type: 'sectionSidebarApp', + kind: 'menu', + alias: 'Umb.SectionSidebarMenu.AdvancedSettings', + name: 'Advanced Settings Sidebar Menu', + weight: 100, + meta: { + label: 'Advanced', + menu: 'Umb.Menu.AdvancedSettings', }, conditions: [ { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/index.ts index c2ab9f12b2..a5980487ef 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/index.ts @@ -5,6 +5,10 @@ export type { UmbTreeDataSource } from './tree-data-source.interface.js'; export type { UmbTreeRepository } from './tree-repository.interface.js'; export type { UmbTreeStore } from './tree-store.interface.js'; -export type { UmbTreeRootItemsRequestArgs, UmbTreeChildrenOfRequestArgs } from './types.js'; +export type { + UmbTreeRootItemsRequestArgs, + UmbTreeChildrenOfRequestArgs, + UmbTreeAncestorsOfRequestArgs, +} from './types.js'; export { UmbUniqueTreeStore } from './unique-tree-store.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-data-source.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-data-source.interface.ts index 120d398903..231fab39b7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-data-source.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-data-source.interface.ts @@ -1,5 +1,9 @@ import type { UmbTreeItemModelBase } from '../types.js'; -import type { UmbTreeChildrenOfRequestArgs, UmbTreeRootItemsRequestArgs } from './types.js'; +import type { + UmbTreeAncestorsOfRequestArgs, + UmbTreeChildrenOfRequestArgs, + UmbTreeRootItemsRequestArgs, +} from './types.js'; import type { UmbPagedModel, UmbDataSourceResponse } from '@umbraco-cms/backoffice/repository'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; @@ -33,4 +37,11 @@ export interface UmbTreeDataSource { * @memberof UmbTreeDataSource */ getChildrenOf(args: UmbTreeChildrenOfRequestArgs): Promise>>; + + /** + * Gets the ancestors of the given item. + * @return {*} {Promise>} + * @memberof UmbTreeDataSource + */ + getAncestorsOf(args: UmbTreeAncestorsOfRequestArgs): Promise>>; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-repository-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-repository-base.ts index 0ea9f637b0..2409617d42 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-repository-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-repository-base.ts @@ -2,6 +2,7 @@ import type { UmbUniqueTreeItemModel, UmbUniqueTreeRootModel } from '../types.js import type { UmbTreeStore } from './tree-store.interface.js'; import type { UmbTreeRepository } from './tree-repository.interface.js'; import type { UmbTreeDataSource, UmbTreeDataSourceConstructor } from './tree-data-source.interface.js'; +import type { UmbTreeAncestorsOfRequestArgs } from './types.js'; import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; @@ -92,6 +93,22 @@ export abstract class UmbTreeRepositoryBase< return { data, error, asObservable: () => this._treeStore!.childrenOf(args.parentUnique) }; } + /** + * Requests ancestors of a given item + * @param {UmbTreeAncestorsOfRequestArgs} args + * @return {*} + * @memberof UmbTreeRepositoryBase + */ + async requestTreeItemAncestors(args: UmbTreeAncestorsOfRequestArgs) { + if (args.descendantUnique === undefined) throw new Error('Descendant unique is missing'); + await this._init; + + const { data, error } = await this.#treeSource.getAncestorsOf(args); + + // TODO: implement observable for ancestor items in the store + return { data, error }; + } + /** * Returns a promise with an observable of tree root items * @return {*} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-repository.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-repository.interface.ts index e7cbbf4223..852ea0d38d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-repository.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-repository.interface.ts @@ -1,5 +1,9 @@ import type { UmbTreeItemModelBase } from '../types.js'; -import type { UmbTreeChildrenOfRequestArgs, UmbTreeRootItemsRequestArgs } from './types.js'; +import type { + UmbTreeChildrenOfRequestArgs, + UmbTreeAncestorsOfRequestArgs, + UmbTreeRootItemsRequestArgs, +} from './types.js'; import type { UmbPagedModel } from '@umbraco-cms/backoffice/repository'; import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; import type { ProblemDetails } from '@umbraco-cms/backoffice/external/backend-api'; @@ -48,6 +52,15 @@ export interface UmbTreeRepository< asObservable?: () => Observable; }>; + /** + * Requests the ancestors of the given item. + * @param {UmbTreeAncestorsOfRequestArgs} args + * @memberof UmbTreeRepository + */ + requestTreeItemAncestors: ( + args: UmbTreeAncestorsOfRequestArgs, + ) => Promise<{ data?: TreeItemType[]; error?: ProblemDetails; asObservable?: () => Observable }>; + /** * Returns an observable of the root items of the tree. * @memberof UmbTreeRepository diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-server-data-source-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-server-data-source-base.ts index 457ad915f5..844c91d1cf 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-server-data-source-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-server-data-source-base.ts @@ -1,6 +1,10 @@ import type { UmbTreeItemModelBase } from '../types.js'; import type { UmbTreeDataSource } from './tree-data-source.interface.js'; -import type { UmbTreeChildrenOfRequestArgs, UmbTreeRootItemsRequestArgs } from './types.js'; +import type { + UmbTreeAncestorsOfRequestArgs, + UmbTreeChildrenOfRequestArgs, + UmbTreeRootItemsRequestArgs, +} from './types.js'; import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { TreeItemPresentationModel } from '@umbraco-cms/backoffice/external/backend-api'; @@ -12,6 +16,7 @@ export interface UmbTreeServerDataSourceBaseArgs< > { getRootItems: (args: UmbTreeRootItemsRequestArgs) => Promise>; getChildrenOf: (args: UmbTreeChildrenOfRequestArgs) => Promise>; + getAncestorsOf: (args: UmbTreeAncestorsOfRequestArgs) => Promise>; mapper: (item: ServerTreeItemType) => ClientTreeItemType; } @@ -29,6 +34,7 @@ export abstract class UmbTreeServerDataSourceBase< #host; #getRootItems; #getChildrenOf; + #getAncestorsOf; #mapper; /** @@ -40,6 +46,7 @@ export abstract class UmbTreeServerDataSourceBase< this.#host = host; this.#getRootItems = args.getRootItems; this.#getChildrenOf = args.getChildrenOf; + this.#getAncestorsOf = args.getAncestorsOf; this.#mapper = args.mapper; } @@ -78,4 +85,23 @@ export abstract class UmbTreeServerDataSourceBase< return { error }; } + + /** + * Fetches the ancestors of a given item from the server + * @param {UmbTreeAncestorsOfRequestArgs} args + * @return {*} + * @memberof UmbTreeServerDataSourceBase + */ + async getAncestorsOf(args: UmbTreeAncestorsOfRequestArgs) { + if (!args.descendantUnique) throw new Error('Parent unique is missing'); + + const { data, error } = await tryExecuteAndNotify(this.#host, this.#getAncestorsOf(args)); + + if (data) { + const items = data?.map((item: ServerTreeItemType) => this.#mapper(item)); + return { data: items }; + } + + return { error }; + } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/types.ts index 3a74d850d7..d94863a55d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/types.ts @@ -8,3 +8,7 @@ export interface UmbTreeChildrenOfRequestArgs { skip: number; take: number; } + +export interface UmbTreeAncestorsOfRequestArgs { + descendantUnique: string; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/index.ts new file mode 100644 index 0000000000..99311cb3cf --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/index.ts @@ -0,0 +1,2 @@ +export * from './validation.context.js'; +export * from './validation.context-token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/validation.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/validation.context-token.ts new file mode 100644 index 0000000000..3223a775dc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/validation.context-token.ts @@ -0,0 +1,4 @@ +import type { UmbValidationContext } from './validation.context.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export const UMB_VALIDATION_CONTEXT = new UmbContextToken('UmbValidationContext'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/validation.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/validation.context.ts new file mode 100644 index 0000000000..e5d1eaeaca --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/validation.context.ts @@ -0,0 +1,9 @@ +import { UMB_VALIDATION_CONTEXT } from './validation.context-token.js'; +import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; + +export class UmbValidationContext extends UmbContextBase { + constructor(host: UmbControllerHost) { + super(host, UMB_VALIDATION_CONTEXT); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/events/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/events/index.ts new file mode 100644 index 0000000000..b3f00f0bd3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/events/index.ts @@ -0,0 +1,2 @@ +export * from './validation-valid.event.js'; +export * from './validation-invalid.event.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/events/validation-invalid.event.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/events/validation-invalid.event.ts new file mode 100644 index 0000000000..2fe6ad0c81 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/events/validation-invalid.event.ts @@ -0,0 +1,9 @@ +import { UmbValidationEvent } from './validation.event.js'; + +export class UmbValidationInvalidEvent extends UmbValidationEvent { + static readonly TYPE = 'invalid'; + + public constructor() { + super(UmbValidationInvalidEvent.TYPE); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/events/validation-valid.event.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/events/validation-valid.event.ts new file mode 100644 index 0000000000..5d4d29a031 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/events/validation-valid.event.ts @@ -0,0 +1,9 @@ +import { UmbValidationEvent } from './validation.event.js'; + +export class UmbValidationValidEvent extends UmbValidationEvent { + static readonly TYPE = 'valid'; + + constructor() { + super(UmbValidationValidEvent.TYPE); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/events/validation.event.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/events/validation.event.ts new file mode 100644 index 0000000000..62d03f34da --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/events/validation.event.ts @@ -0,0 +1,5 @@ +export class UmbValidationEvent extends Event { + public constructor(type: string) { + super(type, { bubbles: true, composed: true, cancelable: false }); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/index.ts new file mode 100644 index 0000000000..807bd98e24 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/index.ts @@ -0,0 +1,3 @@ +export * from './events/index.js'; +export * from './mixins/index.js'; +export * from './context/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/mixins/form-control.mixin.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/mixins/form-control.mixin.ts new file mode 100644 index 0000000000..dd9368a7f8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/mixins/form-control.mixin.ts @@ -0,0 +1,306 @@ +import { UmbValidationInvalidEvent } from '../events/validation-invalid.event.js'; +import { UmbValidationValidEvent } from '../events/validation-valid.event.js'; +import { UmbValidityState } from './validity-state.class.js'; +import { property, type LitElement } from '@umbraco-cms/backoffice/external/lit'; +import type { HTMLElementConstructor } from '@umbraco-cms/backoffice/extension-api'; + +type NativeFormControlElement = HTMLInputElement | HTMLTextAreaElement; // Eventually use a specific interface or list multiple options like appending these types: ... | HTMLTextAreaElement | HTMLSelectElement + +/* FlagTypes type options originate from: + * https://developer.mozilla.org/en-US/docs/Web/API/ValidityState + * */ +type FlagTypes = + | 'badInput' + | 'customError' + | 'patternMismatch' + | 'rangeOverflow' + | 'rangeUnderflow' + | 'stepMismatch' + | 'tooLong' + | 'tooShort' + | 'typeMismatch' + | 'valueMissing' + | 'badInput' + | 'valid'; + +// Acceptable as an internal interface/type, BUT if exposed externally this should be turned into a public class in a separate file. +interface Validator { + flagKey: FlagTypes; + getMessageMethod: () => string; + checkMethod: () => boolean; +} + +export interface UmbFormControlMixinInterface extends HTMLElement { + formAssociated: boolean; + get value(): ValueType | DefaultValueType; + set value(newValue: ValueType | DefaultValueType); + formResetCallback(): void; + checkValidity(): boolean; + get validationMessage(): string; + get validity(): ValidityState; + setCustomValidity(error: string): void; + submit(): void; + pristine: boolean; + required: boolean; + requiredMessage: string; + error: boolean; + errorMessage: string; +} + +export declare abstract class UmbFormControlMixinElement + extends LitElement + implements UmbFormControlMixinInterface +{ + protected _internals: ElementInternals; + protected _runValidators(): void; + protected abstract getFormElement(): HTMLElement | undefined; + protected addValidator: (flagKey: FlagTypes, getMessageMethod: () => string, checkMethod: () => boolean) => void; + protected addFormControlElement(element: NativeFormControlElement): void; + + formAssociated: boolean; + get value(): ValueType | DefaultValueType; + set value(newValue: ValueType | DefaultValueType); + formResetCallback(): void; + checkValidity(): boolean; + get validationMessage(): string; + get validity(): ValidityState; + setCustomValidity(error: string): void; + submit(): void; + pristine: boolean; + required: boolean; + requiredMessage: string; + error: boolean; + errorMessage: string; +} + +/** + * The mixin allows a custom element to participate in HTML forms. + * + * @param {Object} superClass - superclass to be extended. + * @mixin + */ +export const UmbFormControlMixin = < + ValueType = FormDataEntryValue | FormData, + T extends HTMLElementConstructor = HTMLElementConstructor, + DefaultValueType = unknown, +>( + superClass: T, + defaultValue: DefaultValueType, +) => { + abstract class UmbFormControlMixinClass extends superClass { + /** + * This is a static class field indicating that the element is can be used inside a native form and participate in its events. + * It may require a polyfill, check support here https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/attachInternals. + * Read more about form controls here https://web.dev/more-capable-form-controls/ + * @type {boolean} + */ + static readonly formAssociated = true; + + /** + * Value of this form control. + * If you dont want the setFormValue to be called on the ElementInternals, then prevent calling this method, by not calling super.value = newValue in your implementation of the value setter method. + * @type {string} + * @attr value + * @default '' + */ + @property({ reflect: false }) // Do not 'reflect' as the attribute is used as fallback. + get value(): ValueType | DefaultValueType { + return this.#value; + } + set value(newValue: ValueType | DefaultValueType) { + this.#value = newValue; + this._runValidators(); + } + + // Validation + private _validityState = new UmbValidityState(); + + /** + * Determines wether the form control has been touched or interacted with, this determines wether the validation-status of this form control should be made visible. + * @type {boolean} + * @attr + * @default false + */ + @property({ type: Boolean, reflect: true }) + pristine: boolean = true; + + #value: ValueType | DefaultValueType = defaultValue; + protected _internals: ElementInternals; + #form: HTMLFormElement | null = null; + #validators: Validator[] = []; + #formCtrlElements: NativeFormControlElement[] = []; + + constructor(...args: any[]) { + super(...args); + this._internals = this.attachInternals(); + + this.addEventListener('blur', () => { + this.pristine = false; + this.checkValidity(); + }); + } + + /** + * Get internal form element. + * This has to be implemented to provide a FormControl Element of choice for the given context. The element is used as anchor for validation-messages. + * @abstract + * @method getFormElement + * @returns {HTMLElement | undefined} + */ + protected abstract getFormElement(): HTMLElement | undefined; + + disconnectedCallback(): void { + super.disconnectedCallback(); + this.#removeFormListeners(); + } + #removeFormListeners() { + if (this.#form) { + this.#form.removeEventListener('submit', this.#onFormSubmit); + } + } + + /** + * Add validator, to validate this Form Control. + * See https://developer.mozilla.org/en-US/docs/Web/API/ValidityState for available Validator FlagTypes. + * + * @example + * this.addValidator( + * 'tooLong', + * () => 'This input contains too many characters', + * () => this._value.length > 10 + * ); + * @method addValidator + * @param {FlagTypes} flagKey the type of validation. + * @param {method} getMessageMethod method to retrieve relevant message. Is executed every time the validator is re-executed. + * @param {method} checkMethod method to determine if this validator should invalidate this form control. Return true if this should prevent submission. + */ + protected addValidator(flagKey: FlagTypes, getMessageMethod: () => string, checkMethod: () => boolean): Validator { + const obj = { + flagKey: flagKey, + getMessageMethod: getMessageMethod, + checkMethod: checkMethod, + }; + this.#validators.push(obj); + return obj; + } + + protected removeValidator(validator: Validator) { + const index = this.#validators.indexOf(validator); + if (index !== -1) { + this.#validators.splice(index, 1); + } + } + + /** + * @method addFormControlElement + * @description Important notice if adding a native form control then ensure that its value and thereby validity is updated when value is changed from the outside. + * @param element {NativeFormControlElement} - element to validate and include as part of this form association. + */ + protected addFormControlElement(element: NativeFormControlElement) { + this.#formCtrlElements.push(element); + element.addEventListener(UmbValidationInvalidEvent.TYPE, () => { + this._runValidators(); + }); + element.addEventListener(UmbValidationValidEvent.TYPE, () => { + this._runValidators(); + }); + } + + /** + * @method _runValidators + * @description Run all validators and set the validityState of this form control. + * Run this method when you want to re-run all validators. + * This can be relevant if you have a validators that is using values that is not triggering the Lit Updated Callback. + * Such are mainly properties that are not declared as a Lit state and or Lit property. + */ + protected _runValidators() { + this._validityState = new UmbValidityState(); + + // Loop through inner native form controls to adapt their validityState. + this.#formCtrlElements.forEach((formCtrlEl) => { + let key: keyof ValidityState; + for (key in formCtrlEl.validity) { + if (key !== 'valid' && formCtrlEl.validity[key]) { + this._validityState[key] = true; + this._internals.setValidity(this._validityState, formCtrlEl.validationMessage, formCtrlEl); + } + } + }); + + // Loop through custom validators, currently its intentional to have them overwritten native validity. but might need to be reconsidered (This current way enables to overwrite with custom messages) [NL] + this.#validators.forEach((validator) => { + if (validator.checkMethod()) { + this._validityState[validator.flagKey] = true; + this._internals.setValidity(this._validityState, validator.getMessageMethod(), this.getFormElement()); + } + }); + + const hasError = Object.values(this._validityState).includes(true); + + // https://developer.mozilla.org/en-US/docs/Web/API/ValidityState#valid + this._validityState.valid = !hasError; + + if (hasError) { + this.dispatchEvent(new UmbValidationInvalidEvent()); + } else { + this._internals.setValidity({}); + this.dispatchEvent(new UmbValidationValidEvent()); + } + } + + updated(changedProperties: Map) { + super.updated(changedProperties); + this._runValidators(); + } + + #onFormSubmit = () => { + this.pristine = false; + }; + + public formAssociatedCallback() { + this.#removeFormListeners(); + this.#form = this._internals.form; + if (this.#form) { + // This relies on the form begin a 'uui-form': [NL] + if (this.#form.hasAttribute('submit-invalid')) { + this.pristine = false; + } + this.#form.addEventListener('submit', this.#onFormSubmit); + } + } + public formResetCallback() { + this.pristine = true; + this.value = this.getInitialValue() ?? this.getDefaultValue(); + } + + protected getDefaultValue(): DefaultValueType { + return defaultValue; + } + protected getInitialValue(): ValueType | DefaultValueType { + return this.getAttribute('value') as ValueType | DefaultValueType; + } + + public checkValidity() { + for (const key in this.#formCtrlElements) { + if (this.#formCtrlElements[key].checkValidity() === false) { + return false; + } + } + + return this._internals?.checkValidity(); + } + + // https://developer.mozilla.org/en-US/docs/Web/API/HTMLObjectElement/validity + public get validity(): ValidityState { + return this._validityState; + } + + get validationMessage() { + return this._internals?.validationMessage; + } + } + return UmbFormControlMixinClass as unknown as HTMLElementConstructor< + UmbFormControlMixinElement + > & + T; +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/mixins/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/mixins/index.ts new file mode 100644 index 0000000000..0635d528ca --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/mixins/index.ts @@ -0,0 +1 @@ +export * from './form-control.mixin.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/mixins/validity-state.class.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/mixins/validity-state.class.ts new file mode 100644 index 0000000000..d62b087a0a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/mixins/validity-state.class.ts @@ -0,0 +1,15 @@ +type Writeable = { -readonly [P in keyof T]: T[P] }; + +export class UmbValidityState implements Writeable { + badInput: boolean = false; + customError: boolean = false; + patternMismatch: boolean = false; + rangeOverflow: boolean = false; + rangeUnderflow: boolean = false; + stepMismatch: boolean = false; + tooLong: boolean = false; + tooShort: boolean = false; + typeMismatch: boolean = false; + valid: boolean = false; + valueMissing: boolean = false; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/manifests.ts index 42fa69d567..db2a31585c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/manifests.ts @@ -1,4 +1,9 @@ import { manifests as workspaceActionManifests } from './workspace-action/manifests.js'; import { manifests as workspaceActionMenuItemManifests } from './workspace-action-menu-item/manifests.js'; +import { manifests as workspaceBreadcrumbManifests } from './workspace-breadcrumb/manifests.js'; -export const manifests = [...workspaceActionManifests, ...workspaceActionMenuItemManifests]; +export const manifests = [ + ...workspaceActionManifests, + ...workspaceActionMenuItemManifests, + ...workspaceBreadcrumbManifests, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/variant-selector/variant-selector.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/variant-selector/variant-selector.element.ts index ce0148af9d..6342015531 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/variant-selector/variant-selector.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/variant-selector/variant-selector.element.ts @@ -99,7 +99,7 @@ export class UmbVariantSelectorElement extends UmbLitElement { async #observeActiveVariants() { if (!this.#splitViewContext) return; - const workspaceContext = this.#splitViewContext.getWorkspaceContext(); + const workspaceContext = this.#splitViewContext.getWorkspaceContext() as UmbDocumentWorkspaceContext; if (workspaceContext) { this.observe( workspaceContext.splitView.activeVariantsInfo, @@ -212,6 +212,7 @@ export class UmbVariantSelectorElement extends UmbLitElement { ? html` @@ -223,9 +224,9 @@ export class UmbVariantSelectorElement extends UmbLitElement { - ` + ` : ''} - ` + ` : nothing } @@ -264,7 +265,7 @@ export class UmbVariantSelectorElement extends UmbLitElement { @click=${() => this.#openSplitView(variant)}> Split view - `} + `} `, )} @@ -272,7 +273,7 @@ export class UmbVariantSelectorElement extends UmbLitElement { - ` + ` : nothing } @@ -284,7 +285,6 @@ export class UmbVariantSelectorElement extends UmbLitElement { css` #name-input { width: 100%; - height: 100%; /** I really don't know why this fixes the border colliding with variant-selector-toggle, but lets this solution for now */ } #variant-selector-toggle { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/common/save/save.action.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/common/save/save.action.ts index d106cae3e5..c7f4c1f985 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/common/save/save.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/common/save/save.action.ts @@ -1,10 +1,10 @@ -import type { UmbSaveableWorkspaceContextInterface } from '../../../../contexts/saveable-workspace-context.interface.js'; +import type { UmbSaveableWorkspaceContext } from '../../../../contexts/tokens/saveable-workspace-context.interface.js'; import { UmbWorkspaceActionBase } from '../../workspace-action-base.controller.js'; import { UMB_SAVEABLE_WORKSPACE_CONTEXT, type UmbWorkspaceActionArgs } from '@umbraco-cms/backoffice/workspace'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -export class UmbSaveWorkspaceAction extends UmbWorkspaceActionBase { - constructor(host: UmbControllerHost, args: UmbWorkspaceActionArgs) { +export class UmbSaveWorkspaceAction extends UmbWorkspaceActionBase { + constructor(host: UmbControllerHost, args: UmbWorkspaceActionArgs) { super(host, args); // TODO: Could we make change label depending on the state? diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/manifests.ts new file mode 100644 index 0000000000..20d338c127 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/manifests.ts @@ -0,0 +1,4 @@ +import { manifest as workspaceBreadcrumbKind } from './workspace-menu-breadcrumb/workspace-menu-breadcrumb.kind.js'; +import { manifest as variantBreadcrumbKind } from './workspace-variant-menu-breadcrumb/workspace-variant-menu-breadcrumb.kind.js'; + +export const manifests = [workspaceBreadcrumbKind, variantBreadcrumbKind]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/workspace-menu-breadcrumb/workspace-menu-breadcrumb.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/workspace-menu-breadcrumb/workspace-menu-breadcrumb.element.ts new file mode 100644 index 0000000000..bef6a6a284 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/workspace-menu-breadcrumb/workspace-menu-breadcrumb.element.ts @@ -0,0 +1,95 @@ +import { html, customElement, state, ifDefined } from '@umbraco-cms/backoffice/external/lit'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; +import { UMB_SECTION_CONTEXT } from '@umbraco-cms/backoffice/section'; +import type { UmbMenuStructureWorkspaceContext, UmbStructureItemModel } from '@umbraco-cms/backoffice/menu'; + +@customElement('umb-workspace-breadcrumb') +export class UmbWorkspaceBreadcrumbElement extends UmbLitElement { + @state() + _name: string = ''; + + @state() + _structure: UmbStructureItemModel[] = []; + + // TODO: figure out the correct context type + #workspaceContext?: any; + #sectionContext?: typeof UMB_SECTION_CONTEXT.TYPE; + #structureContext?: UmbMenuStructureWorkspaceContext; + + constructor() { + super(); + + this.consumeContext(UMB_SECTION_CONTEXT, (instance) => { + this.#sectionContext = instance; + }); + + this.consumeContext(UMB_WORKSPACE_CONTEXT, (instance) => { + this.#workspaceContext = instance as any; + this.#observeStructure(); + this.#observeName(); + }); + + // TODO: set up context token + this.consumeContext('UmbMenuStructureWorkspaceContext', (instance) => { + // TODO: get the correct interface from the context token + this.#structureContext = instance as UmbMenuStructureWorkspaceContext; + this.#observeStructure(); + }); + } + + #observeStructure() { + if (!this.#structureContext || !this.#workspaceContext) return; + const isNew = this.#workspaceContext.getIsNew(); + + this.observe( + this.#structureContext.structure, + (value) => { + // TODO: get the type from the context + const structure = value as Array; + this._structure = isNew ? structure : structure.slice(0, -1); + }, + 'menuStructureObserver', + ); + } + + #observeName() { + this.observe( + this.#workspaceContext?.name, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + (value) => (this._name = value || ''), + 'breadcrumbWorkspaceNameObserver', + ); + } + + #getHref(structureItem: UmbStructureItemModel) { + const workspaceBasePath = `section/${this.#sectionContext?.getPathname()}/workspace/${structureItem.entityType}/edit`; + return structureItem.isFolder ? undefined : `${workspaceBasePath}/${structureItem.unique}`; + } + + render() { + return html` + + ${this._structure?.map( + (structureItem) => + html`${structureItem.name}`, + )} + ${this._name} + + `; + } + + static styles = [UmbTextStyles]; +} + +export default UmbWorkspaceBreadcrumbElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-workspace-breadcrumb': UmbWorkspaceBreadcrumbElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/workspace-menu-breadcrumb/workspace-menu-breadcrumb.kind.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/workspace-menu-breadcrumb/workspace-menu-breadcrumb.kind.ts new file mode 100644 index 0000000000..32c3475018 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/workspace-menu-breadcrumb/workspace-menu-breadcrumb.kind.ts @@ -0,0 +1,14 @@ +import type { UmbBackofficeManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifest: UmbBackofficeManifestKind = { + type: 'kind', + alias: 'Umb.Kind.WorkspaceFooterApp.MenuBreadcrumb', + matchKind: 'menuBreadcrumb', + matchType: 'workspaceFooterApp', + manifest: { + type: 'workspaceFooterApp', + kind: 'menuBreadcrumb', + element: () => import('./workspace-menu-breadcrumb.element.js'), + weight: 1000, + }, +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/workspace-variant-menu-breadcrumb/workspace-variant-menu-breadcrumb.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/workspace-variant-menu-breadcrumb/workspace-variant-menu-breadcrumb.element.ts new file mode 100644 index 0000000000..4015162467 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/workspace-variant-menu-breadcrumb/workspace-variant-menu-breadcrumb.element.ts @@ -0,0 +1,136 @@ +import { html, customElement, state, ifDefined } from '@umbraco-cms/backoffice/external/lit'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { UmbVariantDatasetWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; +import { UMB_VARIANT_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; +import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; +import type { UmbAppLanguageContext } from '@umbraco-cms/backoffice/language'; +import { UMB_APP_LANGUAGE_CONTEXT } from '@umbraco-cms/backoffice/language'; +import { UMB_SECTION_CONTEXT } from '@umbraco-cms/backoffice/section'; +import type { UmbVariantStructureItemModel } from '@umbraco-cms/backoffice/menu'; + +@customElement('umb-workspace-variant-menu-breadcrumb') +export class UmbWorkspaceVariantMenuBreadcrumbElement extends UmbLitElement { + @state() + _name: string = ''; + + @state() + _structure: Array = []; + + @state() + _workspaceActiveVariantId?: UmbVariantId; + + @state() + _appDefaultCulture?: string; + + #sectionContext?: typeof UMB_SECTION_CONTEXT.TYPE; + #workspaceContext?: UmbVariantDatasetWorkspaceContext; + #appLanguageContext?: UmbAppLanguageContext; + #structureContext?: any; + + constructor() { + super(); + + this.consumeContext(UMB_APP_LANGUAGE_CONTEXT, (instance) => { + this.#appLanguageContext = instance; + this.#observeDefaultCulture(); + }); + + this.consumeContext(UMB_SECTION_CONTEXT, (instance) => { + this.#sectionContext = instance; + }); + + this.consumeContext(UMB_VARIANT_WORKSPACE_CONTEXT, (instance) => { + if (!instance) return; + this.#workspaceContext = instance; + this.#observeWorkspaceActiveVariant(); + this.#observeStructure(); + }); + + // TODO: set up context token + this.consumeContext('UmbMenuStructureWorkspaceContext', (instance) => { + if (!instance) return; + this.#structureContext = instance; + this.#observeStructure(); + }); + } + + #observeStructure() { + if (!this.#structureContext || !this.#workspaceContext) return; + const isNew = this.#workspaceContext.getIsNew(); + + this.observe(this.#structureContext.structure, (value) => { + // TODO: get the type from the context + const structure = value as Array; + this._structure = isNew ? structure : structure.slice(0, -1); + }); + } + + #observeDefaultCulture() { + this.observe(this.#appLanguageContext!.appDefaultLanguage, (value) => { + this._appDefaultCulture = value?.unique; + }); + } + + #observeWorkspaceActiveVariant() { + this.observe( + this.#workspaceContext?.splitView.activeVariantsInfo, + (value) => { + if (!value) return; + this._workspaceActiveVariantId = UmbVariantId.Create(value[0]); + this.#observeActiveVariantName(); + }, + + 'breadcrumbWorkspaceActiveVariantObserver', + ); + } + + #observeActiveVariantName() { + this.observe( + this.#workspaceContext?.name(this._workspaceActiveVariantId), + (value) => (this._name = value || ''), + 'breadcrumbWorkspaceNameObserver', + ); + } + + // TODO: we should move the fallback name logic to a helper class. It will be used in multiple places + #getItemVariantName(structureItem: UmbVariantStructureItemModel) { + const fallbackName = + structureItem.variants.find((variant) => variant.culture === this._appDefaultCulture)?.name ?? + structureItem.variants[0].name ?? + 'Unknown'; + const name = structureItem.variants.find((variant) => this._workspaceActiveVariantId?.compare(variant))?.name; + return name ?? `(${fallbackName})`; + } + + #getHref(structureItem: any) { + const workspaceBasePath = `section/${this.#sectionContext?.getPathname()}/workspace/${structureItem.entityType}/edit`; + return structureItem.isFolder + ? undefined + : `${workspaceBasePath}/${structureItem.unique}/${this._workspaceActiveVariantId?.culture}`; + } + + render() { + return html` + + ${this._structure.map( + (structureItem) => + html`${this.#getItemVariantName(structureItem)}`, + )} + ${this._name} + + `; + } + + static styles = [UmbTextStyles]; +} + +export default UmbWorkspaceVariantMenuBreadcrumbElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-workspace-variant-menu-breadcrumb': UmbWorkspaceVariantMenuBreadcrumbElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/workspace-variant-menu-breadcrumb/workspace-variant-menu-breadcrumb.kind.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/workspace-variant-menu-breadcrumb/workspace-variant-menu-breadcrumb.kind.ts new file mode 100644 index 0000000000..6c37392e39 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/workspace-variant-menu-breadcrumb/workspace-variant-menu-breadcrumb.kind.ts @@ -0,0 +1,14 @@ +import type { UmbBackofficeManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifest: UmbBackofficeManifestKind = { + type: 'kind', + alias: 'Umb.Kind.WorkspaceFooterApp.VariantMenuBreadcrumb', + matchKind: 'variantMenuBreadcrumb', + matchType: 'workspaceFooterApp', + manifest: { + type: 'workspaceFooterApp', + kind: 'variantMenuBreadcrumb', + element: () => import('./workspace-variant-menu-breadcrumb.element.js'), + weight: 1000, + }, +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-entity-action-menu/workspace-entity-action-menu.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-entity-action-menu/workspace-entity-action-menu.element.ts index 3cab2661db..c0d6c00d1c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-entity-action-menu/workspace-entity-action-menu.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-entity-action-menu/workspace-entity-action-menu.element.ts @@ -2,14 +2,14 @@ import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { css, html, customElement, state, nothing, query } from '@umbraco-cms/backoffice/external/lit'; import type { UmbActionExecutedEvent } from '@umbraco-cms/backoffice/event'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; +import { UMB_ENTITY_WORKSPACE_CONTEXT, type UmbWorkspaceUniqueType } from '@umbraco-cms/backoffice/workspace'; import type { UUIPopoverContainerElement } from '@umbraco-cms/backoffice/external/uui'; @customElement('umb-workspace-entity-action-menu') export class UmbWorkspaceEntityActionMenuElement extends UmbLitElement { - private _workspaceContext?: typeof UMB_WORKSPACE_CONTEXT.TYPE; + private _workspaceContext?: typeof UMB_ENTITY_WORKSPACE_CONTEXT.TYPE; @state() - private _unique?: string; + private _unique?: UmbWorkspaceUniqueType; @state() private _entityType?: string; @@ -23,7 +23,7 @@ export class UmbWorkspaceEntityActionMenuElement extends UmbLitElement { constructor() { super(); - this.consumeContext(UMB_WORKSPACE_CONTEXT, (context) => { + this.consumeContext(UMB_ENTITY_WORKSPACE_CONTEXT, (context) => { this._workspaceContext = context; this._observeInfo(); }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-footer/workspace-footer.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-footer/workspace-footer.element.ts index f91a2858f4..a97ab40327 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-footer/workspace-footer.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-footer/workspace-footer.element.ts @@ -1,4 +1,4 @@ -import { UMB_SAVEABLE_WORKSPACE_CONTEXT } from '../../contexts/saveable-workspace.context-token.js'; +import { UMB_SAVEABLE_WORKSPACE_CONTEXT } from '../../contexts/tokens/saveable-workspace.context-token.js'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/conditions/workspace-alias.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/conditions/workspace-alias.condition.ts index 46b33dc39d..db6308554b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/conditions/workspace-alias.condition.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/conditions/workspace-alias.condition.ts @@ -1,4 +1,4 @@ -import { UMB_WORKSPACE_CONTEXT, type UmbWorkspaceContextInterface } from '../contexts/index.js'; +import { UMB_WORKSPACE_CONTEXT, type UmbWorkspaceContext } from '../contexts/index.js'; import { UmbConditionBase } from '@umbraco-cms/backoffice/extension-registry'; import type { ManifestCondition, @@ -15,12 +15,11 @@ export class UmbWorkspaceAliasCondition constructor(host: UmbControllerHost, args: UmbConditionControllerArguments) { super(host, args); - let permissionCheck: ((context: UmbWorkspaceContextInterface) => boolean) | undefined = undefined; + let permissionCheck: ((context: UmbWorkspaceContext) => boolean) | undefined = undefined; if (this.config.match) { - permissionCheck = (context: UmbWorkspaceContextInterface) => context.workspaceAlias === this.config.match; + permissionCheck = (context: UmbWorkspaceContext) => context.workspaceAlias === this.config.match; } else if (this.config.oneOf) { - permissionCheck = (context: UmbWorkspaceContextInterface) => - this.config.oneOf!.indexOf(context.workspaceAlias) !== -1; + permissionCheck = (context: UmbWorkspaceContext) => this.config.oneOf!.indexOf(context.workspaceAlias) !== -1; } if (permissionCheck !== undefined) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/conditions/workspace-has-collection.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/conditions/workspace-has-collection.condition.ts index d30c8ea65e..ef1ef8c203 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/conditions/workspace-has-collection.condition.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/conditions/workspace-has-collection.condition.ts @@ -1,4 +1,4 @@ -import { UMB_WORKSPACE_COLLECTION_CONTEXT } from '../contexts/workspace-collection-context.token.js'; +import { UMB_COLLECTION_WORKSPACE_CONTEXT } from '../contexts/tokens/collection-workspace.context-token.js'; import { UmbConditionBase } from '@umbraco-cms/backoffice/extension-registry'; import type { ManifestCondition, @@ -15,7 +15,7 @@ export class UmbWorkspaceHasCollectionCondition constructor(host: UmbControllerHost, args: UmbConditionControllerArguments) { super(host, args); - this.consumeContext(UMB_WORKSPACE_COLLECTION_CONTEXT, (context) => { + this.consumeContext(UMB_COLLECTION_WORKSPACE_CONTEXT, (context) => { this.observe( context.contentTypeHasCollection, (hasCollection) => { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/default-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/default-workspace.context.ts index d6b3439584..04c90380b3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/default-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/default-workspace.context.ts @@ -1,12 +1,12 @@ -import { UMB_WORKSPACE_CONTEXT } from './workspace-context.token.js'; -import type { UmbWorkspaceContextInterface } from './workspace-context.interface.js'; +import { UMB_WORKSPACE_CONTEXT } from './tokens/workspace.context-token.js'; +import type { UmbWorkspaceContext } from './tokens/workspace-context.interface.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; import type { ManifestWorkspace } from '@umbraco-cms/backoffice/extension-registry'; export abstract class UmbDefaultWorkspaceContext extends UmbContextBase - implements UmbWorkspaceContextInterface + implements UmbWorkspaceContext { public workspaceAlias!: string; #entityType!: string; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/index.ts index 6c3697d84e..ebf0204607 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/index.ts @@ -1,15 +1,3 @@ export * from './default-workspace.context.js'; -export * from './editable-workspace-context-base.js'; -export * from './property-structure-workspace-context.interface.js'; -export * from './publishable-workspace-context.interface.js'; -export * from './publishable-workspace.context-token.js'; -export * from './routable-workspace-context.interface.js'; -export * from './saveable-workspace-context.interface.js'; -export * from './saveable-workspace.context-token.js'; -export * from './variant-workspace-context.token.js'; -export * from './workspace-collection-context.interface.js'; -export * from './workspace-collection-context.token.js'; -export * from './workspace-context.interface.js'; -export * from './workspace-context.token.js'; -export * from './workspace-invariantable-context.interface.js'; -export * from './workspace-variantable-context.interface.js'; +export * from './saveable-workspace-context-base.js'; +export * from './tokens/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/publishable-workspace-context.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/publishable-workspace-context.interface.ts deleted file mode 100644 index d3dc964eec..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/publishable-workspace-context.interface.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { UmbSaveableWorkspaceContextInterface } from './saveable-workspace-context.interface.js'; - -export interface UmbPublishableWorkspaceContextInterface extends UmbSaveableWorkspaceContextInterface { - saveAndPublish(): Promise; - publish(): Promise; - unpublish(): Promise; -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/publishable-workspace.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/publishable-workspace.context-token.ts deleted file mode 100644 index be1cd00336..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/publishable-workspace.context-token.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import type { - UmbPublishableWorkspaceContextInterface, - UmbSaveableWorkspaceContextInterface, -} from '@umbraco-cms/backoffice/workspace'; - -export const UMB_PUBLISHABLE_WORKSPACE_CONTEXT = new UmbContextToken< - UmbSaveableWorkspaceContextInterface, - UmbPublishableWorkspaceContextInterface ->( - 'UmbWorkspaceContext', - undefined, - (context): context is UmbPublishableWorkspaceContextInterface => (context as any).publish !== undefined, -); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/routable-workspace-context.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/routable-workspace-context.interface.ts deleted file mode 100644 index c6a0a16e36..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/routable-workspace-context.interface.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { UmbWorkspaceRouteManager } from '../index.js'; -import type { UmbWorkspaceContextInterface } from './workspace-context.interface.js'; - -export interface UmbRoutableWorkspaceContext extends UmbWorkspaceContextInterface { - readonly routes: UmbWorkspaceRouteManager; -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/editable-workspace-context-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/saveable-workspace-context-base.ts similarity index 66% rename from src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/editable-workspace-context-base.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/saveable-workspace-context-base.ts index 3aab9c5b0d..021b72f055 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/editable-workspace-context-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/saveable-workspace-context-base.ts @@ -1,14 +1,16 @@ -import { UMB_WORKSPACE_CONTEXT } from './workspace-context.token.js'; -import type { UmbSaveableWorkspaceContextInterface } from './saveable-workspace-context.interface.js'; +import { UmbWorkspaceRouteManager } from '../controllers/workspace-route-manager.controller.js'; +import { UMB_WORKSPACE_CONTEXT } from './tokens/workspace.context-token.js'; +import type { UmbSaveableWorkspaceContext } from './tokens/saveable-workspace-context.interface.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; import { UmbBooleanState } from '@umbraco-cms/backoffice/observable-api'; import type { UmbModalContext } from '@umbraco-cms/backoffice/modal'; import { UMB_MODAL_CONTEXT } from '@umbraco-cms/backoffice/modal'; +import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; -export abstract class UmbEditableWorkspaceContextBase - extends UmbContextBase> - implements UmbSaveableWorkspaceContextInterface +export abstract class UmbSaveableWorkspaceContextBase + extends UmbContextBase> + implements UmbSaveableWorkspaceContext { public readonly workspaceAlias: string; @@ -18,6 +20,8 @@ export abstract class UmbEditableWorkspaceContextBase #isNew = new UmbBooleanState(undefined); isNew = this.#isNew.asObservable(); + readonly routes = new UmbWorkspaceRouteManager(this); + /* Concept notes: [NL] Considerations are, if we bring a dirty state (observable) we need to maintain it all the time. @@ -56,8 +60,17 @@ export abstract class UmbEditableWorkspaceContextBase } //abstract getIsDirty(): Promise; - abstract getUnique(): string | undefined; // TODO: Consider if this should go away/be renamed? now that we have getUnique() + abstract getUnique(): string | undefined; abstract getEntityType(): string; abstract getData(): WorkspaceDataModelType | undefined; abstract save(): Promise; + abstract readonly unique: Observable; } + +/* + * @deprecated Use UmbSaveableWorkspaceContextBase instead — Will be removed before RC. + * TODO: Delete before RC. + */ +export abstract class UmbEditableWorkspaceContextBase< + WorkspaceDataModelType, +> extends UmbSaveableWorkspaceContextBase {} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/saveable-workspace-context.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/saveable-workspace-context.interface.ts deleted file mode 100644 index 1c3d095a31..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/saveable-workspace-context.interface.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { UmbWorkspaceContextInterface } from './workspace-context.interface.js'; -import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; - -export interface UmbSaveableWorkspaceContextInterface extends UmbWorkspaceContextInterface { - isNew: Observable; - getIsNew(): boolean | undefined; - save(): Promise; - setValidationErrors?(errorMap: any): void; // TODO: temp solution to bubble validation errors to the UI - destroy(): void; -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/saveable-workspace.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/saveable-workspace.context-token.ts deleted file mode 100644 index 3daf803e7b..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/saveable-workspace.context-token.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import type { UmbWorkspaceContextInterface, UmbSaveableWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; - -export const UMB_SAVEABLE_WORKSPACE_CONTEXT = new UmbContextToken< - UmbWorkspaceContextInterface, - UmbSaveableWorkspaceContextInterface ->( - 'UmbWorkspaceContext', - undefined, - (context): context is UmbSaveableWorkspaceContextInterface => - (context as UmbSaveableWorkspaceContextInterface).getIsNew !== undefined, -); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/workspace-collection-context.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/collection-workspace-context.interface.ts similarity index 51% rename from src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/workspace-collection-context.interface.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/collection-workspace-context.interface.ts index e60933172a..d4d4e54a63 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/workspace-collection-context.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/collection-workspace-context.interface.ts @@ -1,10 +1,16 @@ -import type { UmbWorkspaceContextInterface } from './workspace-context.interface.js'; +import type { UmbEntityWorkspaceContext } from './entity-workspace-context.interface.js'; import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; import type { UmbContentTypeModel, UmbContentTypeStructureManager } from '@umbraco-cms/backoffice/content-type'; -export interface UmbWorkspaceCollectionContextInterface - extends UmbWorkspaceContextInterface { +export interface UmbCollectionWorkspaceContext extends UmbEntityWorkspaceContext { contentTypeHasCollection: Observable; getCollectionAlias(): string; structure: UmbContentTypeStructureManager; } + +/** + * @deprecated Use UmbCollectionWorkspaceContextInterface instead — Will be removed before RC. + * TODO: Delete before RC. + */ +export interface UmbWorkspaceCollectionContextInterface + extends UmbCollectionWorkspaceContext {} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/collection-workspace.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/collection-workspace.context-token.ts new file mode 100644 index 0000000000..c39ef48779 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/collection-workspace.context-token.ts @@ -0,0 +1,19 @@ +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; +import type { UmbContentTypeModel } from '@umbraco-cms/backoffice/content-type'; +import type { UmbWorkspaceContext, UmbCollectionWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; + +export const UMB_COLLECTION_WORKSPACE_CONTEXT = new UmbContextToken< + UmbWorkspaceContext, + UmbCollectionWorkspaceContext +>( + 'UmbWorkspaceContext', + undefined, + (context): context is UmbCollectionWorkspaceContext => + (context as UmbCollectionWorkspaceContext).contentTypeHasCollection !== undefined, +); + +/** + * @deprecated Use `UMB_COLLECTION_WORKSPACE_CONTEXT` instead. This token will be removed in the RC version. + * TODO: Remove in RC + */ +export const UMB_WORKSPACE_COLLECTION_CONTEXT = UMB_COLLECTION_WORKSPACE_CONTEXT; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/entity-workspace-context.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/entity-workspace-context.interface.ts new file mode 100644 index 0000000000..16f335e73e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/entity-workspace-context.interface.ts @@ -0,0 +1,8 @@ +import type { UmbWorkspaceContext } from './workspace-context.interface.js'; +import type { UmbWorkspaceUniqueType } from './../../types.js'; +import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; + +export interface UmbEntityWorkspaceContext extends UmbWorkspaceContext { + unique: Observable; + getUnique(): UmbWorkspaceUniqueType | undefined; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/entity-workspace.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/entity-workspace.context-token.ts new file mode 100644 index 0000000000..6305c04de2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/entity-workspace.context-token.ts @@ -0,0 +1,9 @@ +import type { UmbWorkspaceContext } from './workspace-context.interface.js'; +import type { UmbEntityWorkspaceContext } from './entity-workspace-context.interface.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export const UMB_ENTITY_WORKSPACE_CONTEXT = new UmbContextToken( + 'UmbWorkspaceContext', + undefined, + (context): context is UmbEntityWorkspaceContext => (context as any).getUnique !== undefined, +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/index.ts new file mode 100644 index 0000000000..d68f5d9945 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/index.ts @@ -0,0 +1,16 @@ +export * from './collection-workspace.context-token.js'; +export * from './entity-workspace.context-token.js'; +export * from './publishable-workspace.context-token.js'; +export * from './routable-workspace.context-token.js'; +export * from './saveable-workspace.context-token.js'; +export * from './variant-workspace.context-token.js'; +export * from './workspace.context-token.js'; +export type * from './collection-workspace-context.interface.js'; +export type * from './entity-workspace-context.interface.js'; +export type * from './invariant-dataset-workspace-context.interface.js'; +export type * from './property-structure-workspace-context.interface.js'; +export type * from './publishable-workspace-context.interface.js'; +export type * from './routable-workspace-context.interface.js'; +export type * from './saveable-workspace-context.interface.js'; +export type * from './variant-dataset-workspace-context.interface.js'; +export type * from './workspace-context.interface.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/workspace-invariantable-context.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/invariant-dataset-workspace-context.interface.ts similarity index 55% rename from src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/workspace-invariantable-context.interface.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/invariant-dataset-workspace-context.interface.ts index b9585fd07c..1fdf54d75c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/workspace-invariantable-context.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/invariant-dataset-workspace-context.interface.ts @@ -1,10 +1,10 @@ -import type { UmbVariantId } from '../../variant/variant-id.class.js'; -import type { UmbPropertyDatasetContext } from '../../property/property-dataset/property-dataset-context.interface.js'; -import type { UmbSaveableWorkspaceContextInterface } from './saveable-workspace-context.interface.js'; +import type { UmbVariantId } from '../../../variant/variant-id.class.js'; +import type { UmbPropertyDatasetContext } from '../../../property/property-dataset/property-dataset-context.interface.js'; +import type { UmbSaveableWorkspaceContext } from './saveable-workspace-context.interface.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; -export interface UmbInvariantableWorkspaceContextInterface extends UmbSaveableWorkspaceContextInterface { +export interface UmbInvariantDatasetWorkspaceContext extends UmbSaveableWorkspaceContext { // Name: name: Observable; getName(): string | undefined; @@ -17,3 +17,9 @@ export interface UmbInvariantableWorkspaceContextInterface extends UmbSaveableWo createPropertyDatasetContext(host: UmbControllerHost, variantId?: UmbVariantId): UmbPropertyDatasetContext; } + +/** + * @deprecated Use UmbInvariantWorkspaceContextInterface instead — Will be removed before RC. + * TODO: Delete before RC. + */ +export interface UmbInvariantableWorkspaceContextInterface extends UmbInvariantDatasetWorkspaceContext {} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/property-structure-workspace-context.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/property-structure-workspace-context.interface.ts similarity index 57% rename from src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/property-structure-workspace-context.interface.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/property-structure-workspace-context.interface.ts index 7e8722c7cb..da9d3467e2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/property-structure-workspace-context.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/property-structure-workspace-context.interface.ts @@ -1,7 +1,7 @@ -import type { UmbWorkspaceContextInterface } from './workspace-context.interface.js'; +import type { UmbEntityWorkspaceContext } from './entity-workspace-context.interface.js'; import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; import type { ValueModelBaseModel } from '@umbraco-cms/backoffice/external/backend-api'; -export interface UmbPropertyStructureWorkspaceContextInterface extends UmbWorkspaceContextInterface { +export interface UmbPropertyStructureWorkspaceContext extends UmbEntityWorkspaceContext { propertyStructureById(id: string): Promise>; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/property-structure-workspace.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/property-structure-workspace.context-token.ts new file mode 100644 index 0000000000..6b4f95d8c3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/property-structure-workspace.context-token.ts @@ -0,0 +1,12 @@ +import type { UmbWorkspaceContext } from './workspace-context.interface.js'; +import type { UmbPropertyStructureWorkspaceContext } from './property-structure-workspace-context.interface.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export const UMB_PROPERTY_STRUCTURE_WORKSPACE_CONTEXT = new UmbContextToken< + UmbWorkspaceContext, + UmbPropertyStructureWorkspaceContext +>( + 'UmbWorkspaceContext', + undefined, + (context): context is UmbPropertyStructureWorkspaceContext => 'propertyStructureById' in context, +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/publishable-workspace-context.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/publishable-workspace-context.interface.ts new file mode 100644 index 0000000000..c5cf6ca39c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/publishable-workspace-context.interface.ts @@ -0,0 +1,7 @@ +import type { UmbSaveableWorkspaceContext } from './saveable-workspace-context.interface.js'; + +export interface UmbPublishableWorkspaceContext extends UmbSaveableWorkspaceContext { + saveAndPublish(): Promise; + publish(): Promise; + unpublish(): Promise; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/publishable-workspace.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/publishable-workspace.context-token.ts new file mode 100644 index 0000000000..616b772a8f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/publishable-workspace.context-token.ts @@ -0,0 +1,7 @@ +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; +import type { UmbPublishableWorkspaceContext, UmbWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; + +export const UMB_PUBLISHABLE_WORKSPACE_CONTEXT = new UmbContextToken< + UmbWorkspaceContext, + UmbPublishableWorkspaceContext +>('UmbWorkspaceContext', undefined, (context): context is UmbPublishableWorkspaceContext => 'publish' in context); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/routable-workspace-context.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/routable-workspace-context.interface.ts new file mode 100644 index 0000000000..c268b6c4d7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/routable-workspace-context.interface.ts @@ -0,0 +1,6 @@ +import type { UmbWorkspaceRouteManager } from '../../index.js'; +import type { UmbWorkspaceContext } from './workspace-context.interface.js'; + +export interface UmbRoutableWorkspaceContext extends UmbWorkspaceContext { + readonly routes: UmbWorkspaceRouteManager; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/routable-workspace.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/routable-workspace.context-token.ts new file mode 100644 index 0000000000..36deeebcfe --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/routable-workspace.context-token.ts @@ -0,0 +1,9 @@ +import type { UmbRoutableWorkspaceContext } from './routable-workspace-context.interface.js'; +import type { UmbWorkspaceContext } from './workspace-context.interface.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export const UMB_ROUTABLE_WORKSPACE_CONTEXT = new UmbContextToken( + 'UmbWorkspaceContext', + undefined, + (context): context is UmbRoutableWorkspaceContext => 'routes' in context, +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/saveable-workspace-context.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/saveable-workspace-context.interface.ts new file mode 100644 index 0000000000..b9a50d8673 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/saveable-workspace-context.interface.ts @@ -0,0 +1,15 @@ +import type { UmbEntityWorkspaceContext } from './entity-workspace-context.interface.js'; +import type { UmbRoutableWorkspaceContext } from './routable-workspace-context.interface.js'; +import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; + +export interface UmbSaveableWorkspaceContext extends UmbEntityWorkspaceContext, UmbRoutableWorkspaceContext { + isNew: Observable; + getIsNew(): boolean | undefined; + save(): Promise; + destroy(): void; +} +/** + * @deprecated Use `UmbSaveableWorkspaceContext` instead. This token will be removed in the RC version. + * TODO: Remove in RC + */ +export interface UmbSaveableWorkspaceContextInterface extends UmbSaveableWorkspaceContext {} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/saveable-workspace.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/saveable-workspace.context-token.ts new file mode 100644 index 0000000000..7e537194ca --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/saveable-workspace.context-token.ts @@ -0,0 +1,8 @@ +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; +import type { UmbWorkspaceContext, UmbSaveableWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; + +export const UMB_SAVEABLE_WORKSPACE_CONTEXT = new UmbContextToken( + 'UmbWorkspaceContext', + undefined, + (context): context is UmbSaveableWorkspaceContext => 'getIsNew' in context, +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/workspace-variantable-context.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/variant-dataset-workspace-context.interface.ts similarity index 60% rename from src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/workspace-variantable-context.interface.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/variant-dataset-workspace-context.interface.ts index 4f488e9d22..81cb64b366 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/workspace-variantable-context.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/variant-dataset-workspace-context.interface.ts @@ -1,15 +1,16 @@ -import type { UmbWorkspaceSplitViewManager } from '../controllers/workspace-split-view-manager.controller.js'; -import type { UmbPropertyDatasetContext } from '../../property/property-dataset/property-dataset-context.interface.js'; -import type { UmbSaveableWorkspaceContextInterface } from './saveable-workspace-context.interface.js'; +import type { UmbWorkspaceSplitViewManager } from '../../controllers/workspace-split-view-manager.controller.js'; +import type { UmbPropertyDatasetContext } from '../../../property/property-dataset/property-dataset-context.interface.js'; +import type { UmbSaveableWorkspaceContext } from './saveable-workspace-context.interface.js'; import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; import type { UmbVariantId, UmbVariantModel, UmbVariantOptionModel } from '@umbraco-cms/backoffice/variant'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -export interface UmbVariantableWorkspaceContextInterface - extends UmbSaveableWorkspaceContextInterface { +export interface UmbVariantDatasetWorkspaceContext + extends UmbSaveableWorkspaceContext { // Name: getName(variantId?: UmbVariantId): string | undefined; setName(name: string, variantId?: UmbVariantId): void; + name(variantId?: UmbVariantId): Observable; // Variant: variants: Observable>; @@ -18,14 +19,19 @@ export interface UmbVariantableWorkspaceContextInterface( alias: string, variantId?: UmbVariantId, ): Promise | undefined>; getPropertyValue(alias: string, variantId?: UmbVariantId): ReturnValue | undefined; setPropertyValue(alias: string, value: unknown, variantId?: UmbVariantId): Promise; - //propertyDataByAlias(alias: string, variantId?: UmbVariantId): Observable; createPropertyDatasetContext(host: UmbControllerHost, variantId?: UmbVariantId): UmbPropertyDatasetContext; } + +/** + * @deprecated Use UmbVariantWorkspaceContextInterface instead — Will be removed before RC. + * TODO: Delete before RC. + */ +export interface UmbVariantableWorkspaceContextInterface extends UmbVariantDatasetWorkspaceContext {} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/variant-workspace.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/variant-workspace.context-token.ts new file mode 100644 index 0000000000..0141a6c978 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/variant-workspace.context-token.ts @@ -0,0 +1,8 @@ +import type { UmbWorkspaceContext } from './workspace-context.interface.js'; +import type { UmbVariantDatasetWorkspaceContext } from './variant-dataset-workspace-context.interface.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export const UMB_VARIANT_WORKSPACE_CONTEXT = new UmbContextToken< + UmbWorkspaceContext, + UmbVariantDatasetWorkspaceContext +>('UmbWorkspaceContext', undefined, (context): context is UmbVariantDatasetWorkspaceContext => 'variants' in context); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/workspace-context.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/workspace-context.interface.ts new file mode 100644 index 0000000000..de4c7e1535 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/workspace-context.interface.ts @@ -0,0 +1,13 @@ +import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; + +export interface UmbWorkspaceContext extends UmbApi { + readonly workspaceAlias: string; + getEntityType(): string; +} + +/** + * @deprecated Use UmbWorkspaceContext instead — Will be removed before RC. + * Notice if you have complains on missing `getUnique()` then you need to use UmbEntityWorkspaceContext instead. + * TODO: Delete before RC. + */ +export interface UmbWorkspaceContextInterface extends UmbWorkspaceContext {} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/workspace-context.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/workspace.context-token.ts similarity index 50% rename from src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/workspace-context.token.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/workspace.context-token.ts index e1a8855cee..2b393c54b3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/workspace-context.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/workspace.context-token.ts @@ -1,4 +1,4 @@ -import type { UmbWorkspaceContextInterface } from './workspace-context.interface.js'; +import type { UmbWorkspaceContext } from './workspace-context.interface.js'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -export const UMB_WORKSPACE_CONTEXT = new UmbContextToken('UmbWorkspaceContext'); +export const UMB_WORKSPACE_CONTEXT = new UmbContextToken('UmbWorkspaceContext'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/variant-workspace-context.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/variant-workspace-context.token.ts deleted file mode 100644 index 562eabe48f..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/variant-workspace-context.token.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { UmbWorkspaceContextInterface } from './workspace-context.interface.js'; -import type { UmbVariantableWorkspaceContextInterface } from './workspace-variantable-context.interface.js'; -import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; - -export const UMB_VARIANT_WORKSPACE_CONTEXT = new UmbContextToken< - UmbWorkspaceContextInterface, - UmbVariantableWorkspaceContextInterface ->( - 'UmbWorkspaceContext', - undefined, - (context): context is UmbVariantableWorkspaceContextInterface => 'variants' in context, -); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/workspace-collection-context.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/workspace-collection-context.token.ts deleted file mode 100644 index 4cc4ba1460..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/workspace-collection-context.token.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import type { UmbContentTypeModel } from '@umbraco-cms/backoffice/content-type'; -import type { - UmbWorkspaceContextInterface, - UmbWorkspaceCollectionContextInterface, -} from '@umbraco-cms/backoffice/workspace'; - -export const UMB_WORKSPACE_COLLECTION_CONTEXT = new UmbContextToken< - UmbWorkspaceContextInterface, - UmbWorkspaceCollectionContextInterface ->( - 'UmbWorkspaceContext', - undefined, - (context): context is UmbWorkspaceCollectionContextInterface => - (context as UmbWorkspaceCollectionContextInterface).contentTypeHasCollection !== undefined, -); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/workspace-context.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/workspace-context.interface.ts deleted file mode 100644 index 272e945062..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/workspace-context.interface.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; - -export interface UmbWorkspaceContextInterface extends UmbApi { - readonly workspaceAlias: string; - getEntityType(): string; - getUnique(): string | undefined; -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/controllers/workspace-is-new-redirect.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/controllers/workspace-is-new-redirect.controller.ts index 1157df8c8e..5bb0d8b2f3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/controllers/workspace-is-new-redirect.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/controllers/workspace-is-new-redirect.controller.ts @@ -1,4 +1,4 @@ -import type { UmbEditableWorkspaceContextBase } from '../contexts/index.js'; +import type { UmbSaveableWorkspaceContextBase } from '../contexts/index.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; import { createRoutePathBuilder, type UmbRouterSlotElement } from '@umbraco-cms/backoffice/router'; @@ -16,7 +16,7 @@ import { ensurePathEndsWithSlash } from '@umbraco-cms/backoffice/utils'; export class UmbWorkspaceIsNewRedirectController extends UmbControllerBase { constructor( host: UmbControllerHost, - workspaceContext: UmbEditableWorkspaceContextBase, + workspaceContext: UmbSaveableWorkspaceContextBase, router: UmbRouterSlotElement, ) { super(host, 'isNewRedirectController'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/index.ts index e31957d8fb..1babbbae1f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/index.ts @@ -1,6 +1,7 @@ export * from './components/index.js'; export * from './contexts/index.js'; export * from './controllers/index.js'; -export type * from './conditions/index.js'; export * from './modals/index.js'; export * from './workspace-property-dataset/index.js'; +export type * from './conditions/index.js'; +export type * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/kinds/editable/editable-workspace.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/kinds/editable/editable-workspace.element.ts new file mode 100644 index 0000000000..85b8dbf8ab --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/kinds/editable/editable-workspace.element.ts @@ -0,0 +1,34 @@ +import type { UmbSaveableWorkspaceContext } from '../../contexts/tokens/saveable-workspace-context.interface.js'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { UmbRoute } from '@umbraco-cms/backoffice/router'; +import { UmbExtensionsApiInitializer } from '@umbraco-cms/backoffice/extension-api'; + +@customElement('umb-editable-workspace') +export class UmbEditableWorkspaceElement extends UmbLitElement { + @state() + _routes: UmbRoute[] = []; + + public set api(api: UmbSaveableWorkspaceContext) { + this.observe(api.routes.routes, (routes) => (this._routes = routes)); + + new UmbExtensionsApiInitializer(this, umbExtensionsRegistry, 'workspaceContext', [api]); + } + + render() { + return html` +
+ +
+
`; + } +} + +export default UmbEditableWorkspaceElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-editable-workspace': UmbEditableWorkspaceElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/kinds/editable/editable-workspace.kind.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/kinds/editable/editable-workspace.kind.ts new file mode 100644 index 0000000000..81ddb39fda --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/kinds/editable/editable-workspace.kind.ts @@ -0,0 +1,13 @@ +import type { UmbBackofficeManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifest: UmbBackofficeManifestKind = { + type: 'kind', + alias: 'Umb.Kind.Workspace.Editable', + matchKind: 'editable', + matchType: 'workspace', + manifest: { + type: 'workspace', + kind: 'editable', + element: () => import('./editable-workspace.element.js'), + }, +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/kinds/editable/editable-workspace.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/kinds/editable/editable-workspace.test.ts new file mode 100644 index 0000000000..e0f2d541ca --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/kinds/editable/editable-workspace.test.ts @@ -0,0 +1,22 @@ +import { expect, fixture, html } from '@open-wc/testing'; +import { UmbEditableWorkspaceElement } from './editable-workspace.element.js'; +import { type UmbTestRunnerWindow, defaultA11yConfig } from '@umbraco-cms/internal/test-utils'; + +describe('UmbEditableWorkspaceElement', () => { + let element: UmbEditableWorkspaceElement; + + beforeEach(async () => { + element = await fixture(html``); + }); + + it('is defined with its own instance', () => { + expect(element).to.be.instanceOf(UmbEditableWorkspaceElement); + }); + + if ((window as UmbTestRunnerWindow).__UMBRACO_TEST_RUN_A11Y_TEST) { + it('passes the a11y audit', async () => { + // TODO: should we use shadowDom here? + await expect(element).to.be.accessible(defaultA11yConfig); + }); + } +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/kinds/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/kinds/manifests.ts index 0c129d12fc..fb50387ddd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/kinds/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/kinds/manifests.ts @@ -1,3 +1,3 @@ -import { manifest as routableKindManifest } from './routable-workspace.kind.js'; +import { manifest as routableKindManifest } from './routable/routable-workspace.kind.js'; export const manifests = [routableKindManifest]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/kinds/routable-workspace.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/kinds/routable/routable-workspace.element.ts similarity index 89% rename from src/Umbraco.Web.UI.Client/src/packages/core/workspace/kinds/routable-workspace.element.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/workspace/kinds/routable/routable-workspace.element.ts index b108b86b50..f87e29a9d4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/kinds/routable-workspace.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/kinds/routable/routable-workspace.element.ts @@ -1,4 +1,4 @@ -import type { UmbRoutableWorkspaceContext } from '../contexts/routable-workspace-context.interface.js'; +import type { UmbRoutableWorkspaceContext } from '../../contexts/tokens/routable-workspace-context.interface.js'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; import { html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/kinds/routable-workspace.kind.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/kinds/routable/routable-workspace.kind.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/core/workspace/kinds/routable-workspace.kind.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/workspace/kinds/routable/routable-workspace.kind.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/kinds/routable-workspace.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/kinds/routable/routable-workspace.test.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/core/workspace/kinds/routable-workspace.test.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/workspace/kinds/routable/routable-workspace.test.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/types.ts new file mode 100644 index 0000000000..738a9fdb2d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/types.ts @@ -0,0 +1 @@ +export type UmbWorkspaceUniqueType = string | null; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-property-dataset/invariant-workspace-property-dataset-context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-property-dataset/invariant-workspace-property-dataset-context.ts index 44ca0b1bc4..5187570626 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-property-dataset/invariant-workspace-property-dataset-context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-property-dataset/invariant-workspace-property-dataset-context.ts @@ -3,13 +3,13 @@ import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; -import type { UmbInvariantableWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; +import type { UmbInvariantDatasetWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; /** * A property dataset context that hooks directly into the workspace context. */ export class UmbInvariantWorkspacePropertyDatasetContext< - WorkspaceType extends UmbInvariantableWorkspaceContextInterface = UmbInvariantableWorkspaceContextInterface, + WorkspaceType extends UmbInvariantDatasetWorkspaceContext = UmbInvariantDatasetWorkspaceContext, > extends UmbContextBase implements UmbPropertyDatasetContext, UmbNameablePropertyDatasetContext diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/collection/repository/data-type-collection.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/collection/repository/data-type-collection.server.data-source.ts index c6dd9ce6f5..558131b3e8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/collection/repository/data-type-collection.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/collection/repository/data-type-collection.server.data-source.ts @@ -5,6 +5,7 @@ import { DataTypeResource } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbCollectionDataSource } from '@umbraco-cms/backoffice/collection'; import type { DataTypeItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { type ManifestPropertyEditorUi, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; /** * A data source that fetches the data-type collection data from the server. @@ -14,6 +15,7 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; */ export class UmbDataTypeCollectionServerDataSource implements UmbCollectionDataSource { #host: UmbControllerHost; + #manifestPropertyEditorUis: Array = []; /** * Creates an instance of UmbDataTypeCollectionServerDataSource. @@ -22,6 +24,12 @@ export class UmbDataTypeCollectionServerDataSource implements UmbCollectionDataS */ constructor(host: UmbControllerHost) { this.#host = host; + umbExtensionsRegistry + .byType('propertyEditorUi') + .subscribe((manifestPropertyEditorUIs) => { + this.#manifestPropertyEditorUis = manifestPropertyEditorUIs; + }) + .unsubscribe(); } /** @@ -48,6 +56,7 @@ export class UmbDataTypeCollectionServerDataSource implements UmbCollectionDataS unique: item.id, name: item.name, propertyEditorUiAlias: item.editorUiAlias!, + icon: this.#manifestPropertyEditorUis.find((ui) => ui.alias === item.editorUiAlias!)?.meta.icon, }; return dataTypeDetail; diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/components/data-type-flow-input/data-type-flow-input.element.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/components/data-type-flow-input/data-type-flow-input.element.ts index 2f888c4a5b..d89712a6da 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/components/data-type-flow-input/data-type-flow-input.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/components/data-type-flow-input/data-type-flow-input.element.ts @@ -78,8 +78,6 @@ export class UmbInputDataTypeElement extends FormControlMixin(UmbLitElement) { this.#editDataTypeModal?.open({}, 'edit/' + this._ids![0]); }} standalone> - - diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/components/ref-data-type/ref-data-type.element.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/components/ref-data-type/ref-data-type.element.ts index 4af4380ec5..e144ad4b2e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/components/ref-data-type/ref-data-type.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/components/ref-data-type/ref-data-type.element.ts @@ -1,7 +1,8 @@ import { UmbDataTypeDetailRepository } from '../../repository/detail/data-type-detail.repository.js'; -import { UUIRefNodeElement } from '@umbraco-cms/backoffice/external/uui'; +import { UUIIconRequestEvent, UUIRefNodeElement } from '@umbraco-cms/backoffice/external/uui'; import { html, customElement, property, state, css } from '@umbraco-cms/backoffice/external/lit'; import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; /** * @element umb-ref-data-type @@ -10,8 +11,10 @@ import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api'; */ @customElement('umb-ref-data-type') export class UmbRefDataTypeElement extends UmbElementMixin(UUIRefNodeElement) { + @state() protected fallbackIcon = - ''; + ``; + //icon-circle-dotted.svg @property({ type: String, attribute: 'data-type-id' }) public get dataTypeId(): string | undefined { @@ -28,8 +31,11 @@ export class UmbRefDataTypeElement extends UmbElementMixin(UUIRefNodeElement) { (dataType) => { if (dataType) { this.name = dataType.name ?? ''; - this.propertyEditorUiAlias = dataType.editorUiAlias ?? ''; this.propertyEditorSchemaAlias = dataType.editorAlias ?? ''; + if (dataType.editorUiAlias ?? '' !== this.propertyEditorUiAlias) { + this.propertyEditorUiAlias = dataType.editorUiAlias ?? ''; + this.#getIconFromUiAlias(); + } } }, 'dataType', @@ -53,6 +59,37 @@ export class UmbRefDataTypeElement extends UmbElementMixin(UUIRefNodeElement) { @state() propertyEditorSchemaAlias = ''; + async #getIconFromUiAlias() { + if (!this.propertyEditorUiAlias) return; + this.observe( + umbExtensionsRegistry.byTypeAndAlias('propertyEditorUi', this.propertyEditorUiAlias), + async (manifestPropertyEditorUi) => { + const icon = manifestPropertyEditorUi?.meta.icon; + /** [LI] We have the icon name now, but because this element extends from uui-ref-node, it wants the icon via the icon slot. + * From what I can see, this is not possible via this file, but this is the file that have the datatype data.... + * Instead, overwriting the fallbackIcon property which requires a SVG... */ + if (icon) { + this.#requestIconSVG(icon); + } + }, + ), + '_observeIcon'; + } + + #requestIconSVG(iconName: string) { + if (iconName !== '' && iconName !== null) { + const event = new UUIIconRequestEvent(UUIIconRequestEvent.ICON_REQUEST, { + detail: { iconName: iconName }, + }); + this.dispatchEvent(event); + if (event.icon !== null) { + event.icon.then((iconSvg: string) => { + this.fallbackIcon = iconSvg; + }); + } + } + } + protected renderDetail() { const details: string[] = []; diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/entity-actions/create/modal/data-type-create-options-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/entity-actions/create/modal/data-type-create-options-modal.element.ts index e914585176..7956cd6e75 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/entity-actions/create/modal/data-type-create-options-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/entity-actions/create/modal/data-type-create-options-modal.element.ts @@ -1,24 +1,27 @@ import { UMB_DATA_TYPE_FOLDER_REPOSITORY_ALIAS } from '../../../tree/index.js'; import type { UmbDataTypeCreateOptionsModalData } from './index.js'; import { html, customElement } from '@umbraco-cms/backoffice/external/lit'; -import { UmbModalBaseElement, UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; -import { UMB_FOLDER_CREATE_MODAL } from '@umbraco-cms/backoffice/tree'; +import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; +import { UmbCreateFolderEntityAction } from '@umbraco-cms/backoffice/tree'; @customElement('umb-data-type-create-options-modal') export class UmbDataTypeCreateOptionsModalElement extends UmbModalBaseElement { - async #onClick(event: PointerEvent) { - event.stopPropagation(); + #createFolderAction?: UmbCreateFolderEntityAction; + + connectedCallback(): void { + super.connectedCallback(); if (!this.data?.parent) throw new Error('A parent is required to create a folder'); - const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); - const folderModalHandler = modalManager.open(this, UMB_FOLDER_CREATE_MODAL, { - data: { + // TODO: render the info from this instance in the list of actions + this.#createFolderAction = new UmbCreateFolderEntityAction(this, { + unique: this.data.parent.unique, + entityType: this.data.parent.entityType, + meta: { + icon: 'icon-folder', + label: 'New Folder...', folderRepositoryAlias: UMB_DATA_TYPE_FOLDER_REPOSITORY_ALIAS, - parent: this.data.parent, }, }); - - folderModalHandler?.onSubmit().then(() => this._submitModal()); } #getCreateHref() { @@ -27,6 +30,17 @@ export class UmbDataTypeCreateOptionsModalElement extends UmbModalBaseElement @@ -35,8 +49,8 @@ export class UmbDataTypeCreateOptionsModalElement extends UmbModalBaseElement - - + + } Cancel diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/manifests.ts index 867300c07f..907c67310c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/manifests.ts @@ -1,6 +1,6 @@ import { manifests as entityActions } from './entity-actions/manifests.js'; import { manifests as repositoryManifests } from './repository/manifests.js'; -import { manifests as menuItemManifests } from './menu-item/manifests.js'; +import { manifests as menuManifests } from './menu/manifests.js'; import { manifests as treeManifests } from './tree/manifests.js'; import { manifests as workspaceManifests } from './workspace/manifests.js'; import { manifests as modalManifests } from './modals/manifests.js'; @@ -9,7 +9,7 @@ import { manifests as collectionManifests } from './collection/manifests.js'; export const manifests = [ ...entityActions, ...repositoryManifests, - ...menuItemManifests, + ...menuManifests, ...treeManifests, ...workspaceManifests, ...modalManifests, diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/menu-item/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/menu-item/manifests.ts deleted file mode 100644 index ee50bd8fa2..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/menu-item/manifests.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { ManifestMenuItemTreeKind } from '@umbraco-cms/backoffice/extension-registry'; - -const menuItem: ManifestMenuItemTreeKind = { - type: 'menuItem', - kind: 'tree', - alias: 'Umb.MenuItem.DataTypes', - name: 'Data Types Menu Item', - weight: 600, - meta: { - label: 'Data Types', - entityType: 'data-type', - treeAlias: 'Umb.Tree.DataType', - menus: ['Umb.Menu.Settings'], - }, -}; - -export const manifests = [menuItem]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/menu/data-type-menu-structure.context.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/menu/data-type-menu-structure.context.ts new file mode 100644 index 0000000000..f0a0c77e82 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/menu/data-type-menu-structure.context.ts @@ -0,0 +1,12 @@ +import { UMB_DATA_TYPE_TREE_REPOSITORY_ALIAS } from '../tree/index.js'; +import { UmbMenuTreeStructureWorkspaceContextBase } from '@umbraco-cms/backoffice/menu'; + +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; + +export class UmbDataTypeMenuStructureWorkspaceContext extends UmbMenuTreeStructureWorkspaceContextBase { + constructor(host: UmbControllerHost) { + super(host, { treeRepositoryAlias: UMB_DATA_TYPE_TREE_REPOSITORY_ALIAS }); + } +} + +export default UmbDataTypeMenuStructureWorkspaceContext; diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/menu/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/menu/manifests.ts new file mode 100644 index 0000000000..810bf7da91 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/menu/manifests.ts @@ -0,0 +1,39 @@ +export const manifests = [ + { + type: 'menuItem', + kind: 'tree', + alias: 'Umb.MenuItem.DataTypes', + name: 'Data Types Menu Item', + weight: 600, + meta: { + label: 'Data Types', + entityType: 'data-type', + treeAlias: 'Umb.Tree.DataType', + menus: ['Umb.Menu.StructureSettings'], + }, + }, + { + type: 'workspaceContext', + name: 'Data Type Menu Structure Workspace Context', + alias: 'Umb.Context.DataType.Menu.Structure', + api: () => import('./data-type-menu-structure.context.js'), + conditions: [ + { + alias: 'Umb.Condition.WorkspaceAlias', + match: 'Umb.Workspace.DataType', + }, + ], + }, + { + type: 'workspaceFooterApp', + kind: 'menuBreadcrumb', + alias: 'Umb.WorkspaceFooterApp.DataType.Breadcrumb', + name: 'Data Type Breadcrumb Workspace Footer App', + conditions: [ + { + alias: 'Umb.Condition.WorkspaceAlias', + match: 'Umb.Workspace.DataType', + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/modals/data-type-picker-flow/data-type-picker-flow-data-type-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/modals/data-type-picker-flow/data-type-picker-flow-data-type-picker-modal.element.ts index 9df268dfcf..23d0012b14 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/modals/data-type-picker-flow/data-type-picker-flow-data-type-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/modals/data-type-picker-flow/data-type-picker-flow-data-type-picker-modal.element.ts @@ -86,7 +86,7 @@ export class UmbDataTypePickerFlowDataTypePickerModalElement extends UmbModalBas ? html`
  • - + ${dataType.name}
    diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/modals/data-type-picker-flow/data-type-picker-flow-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/modals/data-type-picker-flow/data-type-picker-flow-modal.element.ts index af03a97473..d0d634e4e0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/modals/data-type-picker-flow/data-type-picker-flow-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/modals/data-type-picker-flow/data-type-picker-flow-modal.element.ts @@ -1,4 +1,3 @@ -import { UmbDataTypeTreeRepository } from '../../tree/data-type-tree.repository.js'; import { UMB_DATATYPE_WORKSPACE_MODAL } from '../../workspace/data-type-workspace.modal-token.js'; import { UMB_DATA_TYPE_ENTITY_TYPE } from '../../entity.js'; import { UmbDataTypeCollectionRepository } from '../../collection/index.js'; @@ -275,10 +274,10 @@ export class UmbDataTypePickerFlowModalElement extends UmbModalBaseElement< dataTypes, (dataType) => dataType.unique, (dataType) => - html`
  • + html`
  • - + ${dataType.name}
    diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/item/data-type-item.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/item/data-type-item.server.data-source.ts index d2963a59c1..6e5faabc94 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/item/data-type-item.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/item/data-type-item.server.data-source.ts @@ -3,6 +3,9 @@ import { UmbItemServerDataSourceBase } from '@umbraco-cms/backoffice/repository' import type { DataTypeItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; import { DataTypeResource } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { type ManifestPropertyEditorUi, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; + +let manifestPropertyEditorUis: Array = []; /** * A server data source for Data Type items @@ -19,11 +22,19 @@ export class UmbDataTypeItemServerDataSource extends UmbItemServerDataSourceBase * @param {UmbControllerHost} host * @memberof UmbDataTypeItemServerDataSource */ + constructor(host: UmbControllerHost) { super(host, { getItems, mapper, }); + + umbExtensionsRegistry + .byType('propertyEditorUi') + .subscribe((manifestPropertyEditorUIs) => { + manifestPropertyEditorUis = manifestPropertyEditorUIs; + }) + .unsubscribe(); } } @@ -35,5 +46,6 @@ const mapper = (item: DataTypeItemResponseModel): UmbDataTypeItemModel => { unique: item.id, name: item.name, propertyEditorUiAlias: item.editorUiAlias || '', // TODO: why can this be undefined or null on the server? + icon: manifestPropertyEditorUis.find((ui) => ui.alias === item.editorUiAlias)?.meta.icon, }; }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/item/types.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/item/types.ts index d798ac80cc..185aa5cba4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/item/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/item/types.ts @@ -2,4 +2,5 @@ export interface UmbDataTypeItemModel { unique: string; name: string; propertyEditorUiAlias: string; + icon?: string; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/tree/data-type-tree.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/tree/data-type-tree.server.data-source.ts index d4fda7664a..719ef42024 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/tree/data-type-tree.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/tree/data-type-tree.server.data-source.ts @@ -1,9 +1,16 @@ import type { UmbDataTypeTreeItemModel } from './types.js'; -import type { UmbTreeChildrenOfRequestArgs, UmbTreeRootItemsRequestArgs } from '@umbraco-cms/backoffice/tree'; +import type { + UmbTreeChildrenOfRequestArgs, + UmbTreeRootItemsRequestArgs, + UmbTreeAncestorsOfRequestArgs, +} from '@umbraco-cms/backoffice/tree'; import { UmbTreeServerDataSourceBase } from '@umbraco-cms/backoffice/tree'; import type { DataTypeTreeItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; import { DataTypeResource } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { type ManifestPropertyEditorUi, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; + +let manifestPropertyEditorUis: Array = []; /** * A data source for a data type tree that fetches data from the server @@ -24,14 +31,22 @@ export class UmbDataTypeTreeServerDataSource extends UmbTreeServerDataSourceBase super(host, { getRootItems, getChildrenOf, + getAncestorsOf, mapper, }); + umbExtensionsRegistry + .byType('propertyEditorUi') + .subscribe((manifestPropertyEditorUIs) => { + manifestPropertyEditorUis = manifestPropertyEditorUIs; + }) + .unsubscribe(); } } -const getRootItems = (args: UmbTreeRootItemsRequestArgs) => +const getRootItems = async (args: UmbTreeRootItemsRequestArgs) => { // eslint-disable-next-line local-rules/no-direct-api-import - DataTypeResource.getTreeDataTypeRoot({ skip: args.skip, take: args.take }); + return DataTypeResource.getTreeDataTypeRoot({ skip: args.skip, take: args.take }); +}; const getChildrenOf = (args: UmbTreeChildrenOfRequestArgs) => { if (args.parentUnique === null) { @@ -44,10 +59,17 @@ const getChildrenOf = (args: UmbTreeChildrenOfRequestArgs) => { } }; +const getAncestorsOf = (args: UmbTreeAncestorsOfRequestArgs) => + // eslint-disable-next-line local-rules/no-direct-api-import + DataTypeResource.getTreeDataTypeAncestors({ + descendantId: args.descendantUnique, + }); + const mapper = (item: DataTypeTreeItemResponseModel): UmbDataTypeTreeItemModel => { return { unique: item.id, parentUnique: item.parent?.id || null, + icon: manifestPropertyEditorUis.find((ui) => ui.alias === item.editorUiAlias)?.meta.icon, name: item.name, entityType: item.isFolder ? 'data-type-folder' : 'data-type', isFolder: item.isFolder, diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/tree/index.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/tree/index.ts index 268957ad6e..09e37032a3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/tree/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/tree/index.ts @@ -1,2 +1,3 @@ export { UMB_DATA_TYPE_TREE_STORE_CONTEXT } from './data-type-tree.store.js'; +export { UMB_DATA_TYPE_TREE_REPOSITORY_ALIAS } from './manifests.js'; export * from './folder/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/data-type-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/data-type-workspace-editor.element.ts index 62896e5616..49e488c69f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/data-type-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/data-type-workspace-editor.element.ts @@ -23,7 +23,7 @@ export class UmbDataTypeWorkspaceEditorElement extends UmbLitElement { this.consumeContext(UMB_DATA_TYPE_WORKSPACE_CONTEXT, (workspaceContext) => { this.#workspaceContext = workspaceContext; - this.#workspaceContext?.createPropertyDatasetContext(this); + this.#workspaceContext.createPropertyDatasetContext(this); this.#observeIsNew(); this.#observeName(); }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/data-type-workspace.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/data-type-workspace.context-token.ts index 0604febd3f..f3f71750d2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/data-type-workspace.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/data-type-workspace.context-token.ts @@ -1,11 +1,8 @@ import type { UmbDataTypeWorkspaceContext } from './data-type-workspace.context.js'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import type { UmbWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; +import type { UmbWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; -export const UMB_DATA_TYPE_WORKSPACE_CONTEXT = new UmbContextToken< - UmbWorkspaceContextInterface, - UmbDataTypeWorkspaceContext ->( +export const UMB_DATA_TYPE_WORKSPACE_CONTEXT = new UmbContextToken( 'UmbWorkspaceContext', undefined, (context): context is UmbDataTypeWorkspaceContext => context.getEntityType?.() === 'data-type', diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/data-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/data-type-workspace.context.ts index 63c6a5f1a2..ed497956ea 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/data-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/data-type-workspace.context.ts @@ -3,11 +3,11 @@ import type { UmbDataTypeDetailModel } from '../types.js'; import { UmbDataTypeWorkspaceEditorElement } from './data-type-workspace-editor.element.js'; import type { UmbPropertyDatasetContext } from '@umbraco-cms/backoffice/property'; import type { - UmbInvariantableWorkspaceContextInterface, + UmbInvariantDatasetWorkspaceContext, UmbRoutableWorkspaceContext, } from '@umbraco-cms/backoffice/workspace'; import { - UmbEditableWorkspaceContextBase, + UmbSaveableWorkspaceContextBase, UmbInvariantWorkspacePropertyDatasetContext, UmbWorkspaceIsNewRedirectController, UmbWorkspaceRouteManager, @@ -32,13 +32,15 @@ import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice type EntityType = UmbDataTypeDetailModel; export class UmbDataTypeWorkspaceContext - extends UmbEditableWorkspaceContextBase - implements UmbInvariantableWorkspaceContextInterface, UmbRoutableWorkspaceContext + extends UmbSaveableWorkspaceContextBase + implements UmbInvariantDatasetWorkspaceContext, UmbRoutableWorkspaceContext { // public readonly repository: UmbDataTypeDetailRepository = new UmbDataTypeDetailRepository(this); - #parent?: { entityType: string; unique: string | null }; + #parent = new UmbObjectState<{ entityType: string; unique: string | null } | undefined>(undefined); + readonly parentUnique = this.#parent.asObservablePart((parent) => (parent ? parent.unique : undefined)); + #persistedData = new UmbObjectState(undefined); #currentData = new UmbObjectState(undefined); @@ -237,7 +239,7 @@ export class UmbDataTypeWorkspaceContext async create(parent: { entityType: string; unique: string | null }) { this.resetState(); - this.#parent = parent; + this.#parent.setValue(parent); const request = this.repository.createScaffold(); this.#getDataPromise = request; let { data } = await request; @@ -319,14 +321,15 @@ export class UmbDataTypeWorkspaceContext if (!this.#currentData.value.unique) return; if (this.getIsNew()) { - if (!this.#parent) throw new Error('Parent is not set'); - await this.repository.create(this.#currentData.value, this.#parent.unique); + const parent = this.#parent.getValue(); + if (!parent) throw new Error('Parent is not set'); + await this.repository.create(this.#currentData.value, parent.unique); // TODO: this might not be the right place to alert the tree, but it works for now const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); const event = new UmbReloadTreeItemChildrenRequestEntityActionEvent({ - entityType: this.#parent.entityType, - unique: this.#parent.unique, + entityType: parent.entityType, + unique: parent.unique, }); eventContext.dispatchEvent(event); } else { @@ -345,6 +348,11 @@ export class UmbDataTypeWorkspaceContext this.workspaceComplete(this.#currentData.value); } + protected workspaceComplete(data: EntityType | undefined) { + this.dispatchEvent(new CustomEvent('workspace-complete')); + super.workspaceComplete(data); + } + async delete(unique: string) { await this.repository.delete(unique); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/menu/dictionary-menu-structure.context.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/menu/dictionary-menu-structure.context.ts new file mode 100644 index 0000000000..69e158ca15 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/menu/dictionary-menu-structure.context.ts @@ -0,0 +1,12 @@ +import { UMB_DICTIONARY_TREE_REPOSITORY_ALIAS } from '../tree/index.js'; +import { UmbMenuTreeStructureWorkspaceContextBase } from '@umbraco-cms/backoffice/menu'; + +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; + +export class UmbDictionaryMenuStructureWorkspaceContext extends UmbMenuTreeStructureWorkspaceContextBase { + constructor(host: UmbControllerHost) { + super(host, { treeRepositoryAlias: UMB_DICTIONARY_TREE_REPOSITORY_ALIAS }); + } +} + +export default UmbDictionaryMenuStructureWorkspaceContext; diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/menu/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/menu/manifests.ts index bd909ef43c..a8cd4f5f9f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/dictionary/menu/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/menu/manifests.ts @@ -1,32 +1,51 @@ import { UMB_DICTIONARY_ENTITY_TYPE } from '../entity.js'; import { UMB_DICTIONARY_TREE_ALIAS } from '../tree/index.js'; -import type { ManifestMenu, ManifestMenuItemTreeKind } from '@umbraco-cms/backoffice/extension-registry'; export const UMB_DICTIONARY_MENU_ALIAS = 'Umb.Menu.Dictionary'; -const menu: ManifestMenu = { - type: 'menu', - alias: UMB_DICTIONARY_MENU_ALIAS, - name: 'Dictionary Menu', - meta: { - label: 'Dictionary', +export const manifests = [ + { + type: 'menu', + alias: UMB_DICTIONARY_MENU_ALIAS, + name: 'Dictionary Menu', }, -}; - -const menuItem: ManifestMenuItemTreeKind = { - type: 'menuItem', - kind: 'tree', - alias: 'Umb.MenuItem.Dictionary', - name: 'Dictionary Menu Item', - weight: 400, - meta: { - label: 'Dictionary', - icon: 'icon-book-alt', - entityType: UMB_DICTIONARY_ENTITY_TYPE, - menus: [UMB_DICTIONARY_MENU_ALIAS], - treeAlias: UMB_DICTIONARY_TREE_ALIAS, - hideTreeRoot: true, + { + type: 'menuItem', + kind: 'tree', + alias: 'Umb.MenuItem.Dictionary', + name: 'Dictionary Menu Item', + weight: 400, + meta: { + label: 'Dictionary', + icon: 'icon-book-alt', + entityType: UMB_DICTIONARY_ENTITY_TYPE, + menus: [UMB_DICTIONARY_MENU_ALIAS], + treeAlias: UMB_DICTIONARY_TREE_ALIAS, + hideTreeRoot: true, + }, }, -}; - -export const manifests = [menu, menuItem]; + { + type: 'workspaceContext', + name: 'Dictionary Menu Structure Workspace Context', + alias: 'Umb.Context.Dictionary.Menu.Structure', + api: () => import('./dictionary-menu-structure.context.js'), + conditions: [ + { + alias: 'Umb.Condition.WorkspaceAlias', + match: 'Umb.Workspace.Dictionary', + }, + ], + }, + { + type: 'workspaceFooterApp', + kind: 'menuBreadcrumb', + alias: 'Umb.WorkspaceFooterApp.Dictionary.Breadcrumb', + name: 'Data Type Breadcrumb Workspace Footer App', + conditions: [ + { + alias: 'Umb.Condition.WorkspaceAlias', + match: 'Umb.Workspace.Dictionary', + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/tree/dictionary-tree.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/tree/dictionary-tree.server.data-source.ts index 843cfcd8d2..af5b62174f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/dictionary/tree/dictionary-tree.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/tree/dictionary-tree.server.data-source.ts @@ -1,6 +1,10 @@ import { UMB_DICTIONARY_ENTITY_TYPE } from '../entity.js'; import type { UmbDictionaryTreeItemModel } from './types.js'; -import type { UmbTreeChildrenOfRequestArgs, UmbTreeRootItemsRequestArgs } from '@umbraco-cms/backoffice/tree'; +import type { + UmbTreeAncestorsOfRequestArgs, + UmbTreeChildrenOfRequestArgs, + UmbTreeRootItemsRequestArgs, +} from '@umbraco-cms/backoffice/tree'; import { UmbTreeServerDataSourceBase } from '@umbraco-cms/backoffice/tree'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { NamedEntityTreeItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; @@ -25,6 +29,7 @@ export class UmbDictionaryTreeServerDataSource extends UmbTreeServerDataSourceBa super(host, { getRootItems, getChildrenOf, + getAncestorsOf, mapper, }); } @@ -45,6 +50,12 @@ const getChildrenOf = (args: UmbTreeChildrenOfRequestArgs) => { } }; +const getAncestorsOf = (args: UmbTreeAncestorsOfRequestArgs) => + // eslint-disable-next-line local-rules/no-direct-api-import + DictionaryResource.getTreeDictionaryAncestors({ + descendantId: args.descendantUnique, + }); + const mapper = (item: NamedEntityTreeItemResponseModel): UmbDictionaryTreeItemModel => { return { unique: item.id, diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/workspace/dictionary-workspace.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/workspace/dictionary-workspace.context-token.ts index 06413a52f6..d5597f3763 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/dictionary/workspace/dictionary-workspace.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/workspace/dictionary-workspace.context-token.ts @@ -1,9 +1,9 @@ import type { UmbDictionaryWorkspaceContext } from './dictionary-workspace.context.js'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import type { UmbSaveableWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; +import type { UmbSaveableWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; export const UMB_DICTIONARY_WORKSPACE_CONTEXT = new UmbContextToken< - UmbSaveableWorkspaceContextInterface, + UmbSaveableWorkspaceContext, UmbDictionaryWorkspaceContext >( 'UmbWorkspaceContext', diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/workspace/dictionary-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/workspace/dictionary-workspace.context.ts index 83440198b3..f06019c605 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/dictionary/workspace/dictionary-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/workspace/dictionary-workspace.context.ts @@ -2,8 +2,8 @@ import { UmbDictionaryDetailRepository } from '../repository/index.js'; import type { UmbDictionaryDetailModel } from '../types.js'; import { UmbDictionaryWorkspaceEditorElement } from './dictionary-workspace-editor.element.js'; import { - type UmbSaveableWorkspaceContextInterface, - UmbEditableWorkspaceContextBase, + type UmbSaveableWorkspaceContext, + UmbSaveableWorkspaceContextBase, UmbWorkspaceRouteManager, UmbWorkspaceIsNewRedirectController, type UmbRoutableWorkspaceContext, @@ -15,17 +15,19 @@ import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/event'; export class UmbDictionaryWorkspaceContext - extends UmbEditableWorkspaceContextBase - implements UmbSaveableWorkspaceContextInterface, UmbRoutableWorkspaceContext + extends UmbSaveableWorkspaceContextBase + implements UmbSaveableWorkspaceContext, UmbRoutableWorkspaceContext { // public readonly detailRepository = new UmbDictionaryDetailRepository(this); - #parent?: { entityType: string; unique: string | null }; + #parent = new UmbObjectState<{ entityType: string; unique: string | null } | undefined>(undefined); + readonly parentUnique = this.#parent.asObservablePart((parent) => (parent ? parent.unique : undefined)); #data = new UmbObjectState(undefined); readonly data = this.#data.asObservable(); + readonly unique = this.#data.asObservablePart((data) => data?.unique); readonly name = this.#data.asObservablePart((data) => data?.name); readonly dictionary = this.#data.asObservablePart((data) => data); @@ -114,7 +116,7 @@ export class UmbDictionaryWorkspaceContext async create(parent: { entityType: string; unique: string | null }) { this.resetState(); - this.#parent = parent; + this.#parent.setValue(parent); const { data } = await this.detailRepository.createScaffold(); if (!data) return; this.setIsNew(true); @@ -126,8 +128,9 @@ export class UmbDictionaryWorkspaceContext if (!this.#data.value.unique) return; if (this.getIsNew()) { - if (!this.#parent) throw new Error('Parent is not set'); - const { error } = await this.detailRepository.create(this.#data.value, this.#parent.unique); + const parent = this.#parent.getValue(); + if (!parent) throw new Error('Parent is not set'); + const { error } = await this.detailRepository.create(this.#data.value, parent.unique); if (error) { return; } @@ -135,8 +138,8 @@ export class UmbDictionaryWorkspaceContext // TODO: this might not be the right place to alert the tree, but it works for now const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); const event = new UmbReloadTreeItemChildrenRequestEntityActionEvent({ - entityType: this.#parent.entityType, - unique: this.#parent.unique, + entityType: parent.entityType, + unique: parent.unique, }); eventContext.dispatchEvent(event); diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/menu-item/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/menu-item/manifests.ts index 6c936aa77c..71b18dec07 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/menu-item/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/menu-item/manifests.ts @@ -9,7 +9,7 @@ const menuItem: ManifestMenuItem = { label: 'Document Blueprints', icon: 'icon-blueprint', entityType: 'document-blueprint-root', - menus: ['Umb.Menu.Settings'], + menus: ['Umb.Menu.StructureSettings'], }, }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/manifests.ts index f1a81fd7d1..e582d0a40e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/manifests.ts @@ -1,5 +1,5 @@ import { manifests as entityActionsManifests } from './entity-actions/manifests.js'; -import { manifests as menuItemManifests } from './menu-item/manifests.js'; +import { manifests as menuManifests } from './menu/manifests.js'; import { manifests as propertyEditorManifests } from './property-editors/manifests.js'; import { manifests as repositoryManifests } from './repository/manifests.js'; import { manifests as treeManifests } from './tree/manifests.js'; @@ -7,7 +7,7 @@ import { manifests as workspaceManifests } from './workspace/manifests.js'; export const manifests = [ ...entityActionsManifests, - ...menuItemManifests, + ...menuManifests, ...propertyEditorManifests, ...repositoryManifests, ...treeManifests, diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/menu-item/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/menu-item/manifests.ts deleted file mode 100644 index c048b1075f..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/menu-item/manifests.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { UMB_DOCUMENT_TYPE_TREE_ALIAS } from '../tree/manifests.js'; -import type { ManifestMenuItemTreeKind } from '@umbraco-cms/backoffice/extension-registry'; - -const menuItem: ManifestMenuItemTreeKind = { - type: 'menuItem', - kind: 'tree', - alias: 'Umb.MenuItem.DocumentTypes', - name: 'Document Types Menu Item', - weight: 900, - meta: { - treeAlias: UMB_DOCUMENT_TYPE_TREE_ALIAS, - label: 'Document Types', - menus: ['Umb.Menu.Settings'], - }, -}; - -export const manifests = [menuItem]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/menu/document-type-menu-structure.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/menu/document-type-menu-structure.context.ts new file mode 100644 index 0000000000..1b88dc2bc4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/menu/document-type-menu-structure.context.ts @@ -0,0 +1,12 @@ +import { UMB_DOCUMENT_TYPE_TREE_REPOSITORY_ALIAS } from '../tree/index.js'; +import { UmbMenuTreeStructureWorkspaceContextBase } from '@umbraco-cms/backoffice/menu'; + +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; + +export class UmbDocumentTypeMenuStructureWorkspaceContext extends UmbMenuTreeStructureWorkspaceContextBase { + constructor(host: UmbControllerHost) { + super(host, { treeRepositoryAlias: UMB_DOCUMENT_TYPE_TREE_REPOSITORY_ALIAS }); + } +} + +export default UmbDocumentTypeMenuStructureWorkspaceContext; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/menu/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/menu/manifests.ts new file mode 100644 index 0000000000..042e49cc96 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/menu/manifests.ts @@ -0,0 +1,40 @@ +import { UMB_DOCUMENT_TYPE_TREE_ALIAS } from '../tree/manifests.js'; + +export const manifests = [ + { + type: 'menuItem', + kind: 'tree', + alias: 'Umb.MenuItem.DocumentTypes', + name: 'Document Types Menu Item', + weight: 900, + meta: { + treeAlias: UMB_DOCUMENT_TYPE_TREE_ALIAS, + label: 'Document Types', + menus: ['Umb.Menu.StructureSettings'], + }, + }, + { + type: 'workspaceContext', + name: 'Document Type Menu Structure Workspace Context', + alias: 'Umb.Context.DocumentType.Menu.Structure', + api: () => import('./document-type-menu-structure.context.js'), + conditions: [ + { + alias: 'Umb.Condition.WorkspaceAlias', + match: 'Umb.Workspace.DocumentType', + }, + ], + }, + { + type: 'workspaceFooterApp', + kind: 'menuBreadcrumb', + alias: 'Umb.WorkspaceFooterApp.DocumentType.Breadcrumb', + name: 'Document Type Breadcrumb Workspace Footer App', + conditions: [ + { + alias: 'Umb.Condition.WorkspaceAlias', + match: 'Umb.Workspace.DocumentType', + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/tree/document-type.tree.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/tree/document-type.tree.server.data-source.ts index d0e170aaed..6988ebde67 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/tree/document-type.tree.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/tree/document-type.tree.server.data-source.ts @@ -1,6 +1,10 @@ import { UMB_DOCUMENT_TYPE_ENTITY_TYPE, UMB_DOCUMENT_TYPE_FOLDER_ENTITY_TYPE } from '../entity.js'; import type { UmbDocumentTypeTreeItemModel } from './types.js'; -import type { UmbTreeChildrenOfRequestArgs, UmbTreeRootItemsRequestArgs } from '@umbraco-cms/backoffice/tree'; +import type { + UmbTreeAncestorsOfRequestArgs, + UmbTreeChildrenOfRequestArgs, + UmbTreeRootItemsRequestArgs, +} from '@umbraco-cms/backoffice/tree'; import { UmbTreeServerDataSourceBase } from '@umbraco-cms/backoffice/tree'; import type { DocumentTypeTreeItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; import { DocumentTypeResource } from '@umbraco-cms/backoffice/external/backend-api'; @@ -25,6 +29,7 @@ export class UmbDocumentTypeTreeServerDataSource extends UmbTreeServerDataSource super(host, { getRootItems, getChildrenOf, + getAncestorsOf, mapper, }); } @@ -47,6 +52,12 @@ const getChildrenOf = (args: UmbTreeChildrenOfRequestArgs) => { } }; +const getAncestorsOf = (args: UmbTreeAncestorsOfRequestArgs) => + // eslint-disable-next-line local-rules/no-direct-api-import + DocumentTypeResource.getTreeDocumentTypeAncestors({ + descendantId: args.descendantUnique, + }); + const mapper = (item: DocumentTypeTreeItemResponseModel): UmbDocumentTypeTreeItemModel => { return { unique: item.id, diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/tree/index.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/tree/index.ts index 3c8096f8a5..96ea19523c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/tree/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/tree/index.ts @@ -1,2 +1,3 @@ export { UMB_DOCUMENT_TYPE_TREE_STORE_CONTEXT } from './document-type.tree.store.js'; +export { UMB_DOCUMENT_TYPE_TREE_REPOSITORY_ALIAS } from './manifests.js'; export * from './folder/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type-workspace-editor.element.ts index a8af3e3a76..dc9a714f33 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type-workspace-editor.element.ts @@ -130,20 +130,6 @@ export class UmbDocumentTypeWorkspaceEditorElement extends UmbLitElement { - -
    - - - Keyboard Shortcuts - - ALT - + - shift - + - k - - -
    `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type-workspace.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type-workspace.context-token.ts index 6fc579e6e0..712e436906 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type-workspace.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type-workspace.context-token.ts @@ -1,9 +1,9 @@ import type { UmbDocumentTypeWorkspaceContext } from './document-type-workspace.context.js'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import type { UmbSaveableWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; +import type { UmbSaveableWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; export const UMB_DOCUMENT_TYPE_WORKSPACE_CONTEXT = new UmbContextToken< - UmbSaveableWorkspaceContextInterface, + UmbSaveableWorkspaceContext, UmbDocumentTypeWorkspaceContext >( 'UmbWorkspaceContext', diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type-workspace.context.ts index fc3265ddd1..b4e3eed8f2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type-workspace.context.ts @@ -4,7 +4,7 @@ import type { UmbDocumentTypeDetailModel } from '../types.js'; import { UmbDocumentTypeWorkspaceEditorElement } from './document-type-workspace-editor.element.js'; import { UmbContentTypeStructureManager } from '@umbraco-cms/backoffice/content-type'; import { - UmbEditableWorkspaceContextBase, + UmbSaveableWorkspaceContextBase, type UmbRoutableWorkspaceContext, UmbWorkspaceIsNewRedirectController, UmbWorkspaceRouteManager, @@ -23,7 +23,7 @@ import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice type EntityType = UmbDocumentTypeDetailModel; export class UmbDocumentTypeWorkspaceContext - extends UmbEditableWorkspaceContextBase + extends UmbSaveableWorkspaceContextBase implements UmbContentTypeWorkspaceContext, UmbRoutableWorkspaceContext { readonly IS_CONTENT_TYPE_WORKSPACE_CONTEXT = true; @@ -31,11 +31,14 @@ export class UmbDocumentTypeWorkspaceContext readonly repository = new UmbDocumentTypeDetailRepository(this); // Data/Draft is located in structure manager - #parent?: { entityType: string; unique: string | null }; + #parent = new UmbObjectState<{ entityType: string; unique: string | null } | undefined>(undefined); + readonly parentUnique = this.#parent.asObservablePart((parent) => (parent ? parent.unique : undefined)); + #persistedData = new UmbObjectState(undefined); // General for content types: //readonly data; + readonly unique; readonly name; readonly alias; readonly description; @@ -62,6 +65,8 @@ export class UmbDocumentTypeWorkspaceContext // General for content types: //this.data = this.structure.ownerContentType; + + this.unique = this.structure.ownerContentTypeObservablePart((data) => data?.unique); this.name = this.structure.ownerContentTypeObservablePart((data) => data?.name); this.alias = this.structure.ownerContentTypeObservablePart((data) => data?.alias); this.description = this.structure.ownerContentTypeObservablePart((data) => data?.description); @@ -180,7 +185,7 @@ export class UmbDocumentTypeWorkspaceContext async create(parent: { entityType: string; unique: string | null }) { this.resetState(); - this.#parent = parent; + this.#parent.setValue(parent); const { data } = await this.structure.createScaffold(); if (!data) return undefined; @@ -218,14 +223,15 @@ export class UmbDocumentTypeWorkspaceContext if (data === undefined) throw new Error('Cannot save, no data'); if (this.getIsNew()) { - if (!this.#parent) throw new Error('Parent is not set'); + const parent = this.#parent.getValue(); + if (!parent) throw new Error('Parent is not set'); - if ((await this.structure.create(this.#parent.unique)) === true) { + if ((await this.structure.create(parent.unique)) === true) { // TODO: this might not be the right place to alert the tree, but it works for now const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); const event = new UmbReloadTreeItemChildrenRequestEntityActionEvent({ - entityType: this.#parent.entityType, - unique: this.#parent.unique, + entityType: parent.entityType, + unique: parent.unique, }); eventContext.dispatchEvent(event); diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/views/settings/document-type-workspace-view-settings.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/views/settings/document-type-workspace-view-settings.element.ts index 5e5e55f6be..58aef22912 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/views/settings/document-type-workspace-view-settings.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/views/settings/document-type-workspace-view-settings.element.ts @@ -1,14 +1,13 @@ -import type { UmbDocumentTypeWorkspaceContext } from '../../document-type-workspace.context.js'; +import { UMB_DOCUMENT_TYPE_WORKSPACE_CONTEXT } from '../../document-type-workspace.context-token.js'; import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import type { UUIToggleElement } from '@umbraco-cms/backoffice/external/uui'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/extension-registry'; @customElement('umb-document-type-workspace-view-settings') export class UmbDocumentTypeWorkspaceViewSettingsElement extends UmbLitElement implements UmbWorkspaceViewElement { - #workspaceContext?: UmbDocumentTypeWorkspaceContext; + #workspaceContext?: typeof UMB_DOCUMENT_TYPE_WORKSPACE_CONTEXT.TYPE; @state() private _variesByCulture?: boolean; @@ -21,8 +20,8 @@ export class UmbDocumentTypeWorkspaceViewSettingsElement extends UmbLitElement i 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_WORKSPACE_CONTEXT, (documentTypeContext) => { - this.#workspaceContext = documentTypeContext as UmbDocumentTypeWorkspaceContext; + this.consumeContext(UMB_DOCUMENT_TYPE_WORKSPACE_CONTEXT, (documentTypeContext) => { + this.#workspaceContext = documentTypeContext; this._observeDocumentType(); }); } @@ -47,7 +46,7 @@ export class UmbDocumentTypeWorkspaceViewSettingsElement extends UmbLitElement i
    Allow editors to create content of different languages.
    { this.#workspaceContext?.setVariesByCulture((e.target as UUIToggleElement).checked); }} @@ -58,7 +57,7 @@ export class UmbDocumentTypeWorkspaceViewSettingsElement extends UmbLitElement i
    Allow editors to segment their content.
    { this.#workspaceContext?.setVariesBySegment((e.target as UUIToggleElement).checked); }} @@ -71,7 +70,7 @@ export class UmbDocumentTypeWorkspaceViewSettingsElement extends UmbLitElement i
    { this.#workspaceContext?.setIsElement((e.target as UUIToggleElement).checked); }} diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/views/structure/document-type-workspace-view-structure.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/views/structure/document-type-workspace-view-structure.element.ts index 62a4719935..71a89c2ed2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/views/structure/document-type-workspace-view-structure.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/views/structure/document-type-workspace-view-structure.element.ts @@ -1,9 +1,9 @@ import type { UmbDocumentTypeWorkspaceContext } from '../../document-type-workspace.context.js'; import type { UmbInputDocumentTypeElement } from '../../../components/input-document-type/input-document-type.element.js'; +import { UMB_DOCUMENT_TYPE_WORKSPACE_CONTEXT } from '../../document-type-workspace.context-token.js'; import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; import type { UmbContentTypeSortModel } from '@umbraco-cms/backoffice/content-type'; import type { UmbInputCollectionConfigurationElement } from '@umbraco-cms/backoffice/components'; import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/extension-registry'; @@ -26,7 +26,7 @@ export class UmbDocumentTypeWorkspaceViewStructureElement extends UmbLitElement 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_WORKSPACE_CONTEXT, (documentTypeContext) => { + this.consumeContext(UMB_DOCUMENT_TYPE_WORKSPACE_CONTEXT, (documentTypeContext) => { this.#workspaceContext = documentTypeContext as UmbDocumentTypeWorkspaceContext; this._observeDocumentType(); }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/views/templates/document-type-workspace-view-templates.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/views/templates/document-type-workspace-view-templates.element.ts index e66d2cb0a4..081ba0069d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/views/templates/document-type-workspace-view-templates.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/views/templates/document-type-workspace-view-templates.element.ts @@ -1,9 +1,9 @@ import type { UmbDocumentTypeWorkspaceContext } from '../../document-type-workspace.context.js'; +import { UMB_DOCUMENT_TYPE_WORKSPACE_CONTEXT } from '../../document-type-workspace.context-token.js'; import type { UmbInputTemplateElement } from '@umbraco-cms/backoffice/template'; import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/extension-registry'; import '@umbraco-cms/backoffice/template'; // TODO: This is needed to register the element, but it should be done in a better way without importing the whole module. @@ -20,7 +20,7 @@ export class UmbDocumentTypeWorkspaceViewTemplatesElement extends UmbLitElement constructor() { super(); - this.consumeContext(UMB_WORKSPACE_CONTEXT, (documentTypeContext) => { + this.consumeContext(UMB_DOCUMENT_TYPE_WORKSPACE_CONTEXT, (documentTypeContext) => { this.#workspaceContext = documentTypeContext as UmbDocumentTypeWorkspaceContext; this._observeDocumentType(); }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/breadcrumb/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/breadcrumb/manifests.ts deleted file mode 100644 index c9ce7a117b..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/breadcrumb/manifests.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { ManifestWorkspaceFooterApp } from '@umbraco-cms/backoffice/extension-registry'; - -export const manifests: Array = [ - { - type: 'workspaceFooterApp', - alias: 'Umb.WorkspaceFooterApp.Document.Breadcrumb', - name: 'Document Breadcrumb Workspace Footer App', - weight: 10, - element: () => import('./workspace-breadcrumb.element.js'), - conditions: [ - { - alias: 'Umb.Condition.WorkspaceAlias', - match: 'Umb.Workspace.Document', - }, - ], - }, -]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/breadcrumb/workspace-breadcrumb.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/breadcrumb/workspace-breadcrumb.element.ts deleted file mode 100644 index 9141067419..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/breadcrumb/workspace-breadcrumb.element.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { html, customElement } from '@umbraco-cms/backoffice/external/lit'; -import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; - -// TODO: POC element. This will be updated when the final implementation is done. -@customElement('umb-workspace-breadcrumb') -export class UmbWorkspaceBreadcrumbElement extends UmbLitElement { - render() { - return html`Document Breadcrumbs here`; - } - - static styles = [UmbTextStyles]; -} - -export default UmbWorkspaceBreadcrumbElement; - -declare global { - interface HTMLElementTagNameMap { - 'umb-workspace-breadcrumb': UmbWorkspaceBreadcrumbElement; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/index.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/index.ts index bc89935e82..862805500c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/index.ts @@ -12,7 +12,7 @@ export * from './modals/index.js'; export * from './global-contexts/index.js'; export * from './tree/index.js'; -export { UMB_CONTENT_MENU_ALIAS } from './menu.manifests.js'; +export { UMB_CONTENT_MENU_ALIAS } from './menu/manifests.js'; export { UMB_DOCUMENT_COLLECTION_ALIAS } from './collection/index.js'; export type * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/manifests.ts index 795d22178f..56fe3f53bb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/manifests.ts @@ -1,29 +1,27 @@ -import { manifests as breadcrumbManifests } from './breadcrumb/manifests.js'; import { manifests as collectionManifests } from './collection/manifests.js'; import { manifests as entityActionManifests } from './entity-actions/manifests.js'; import { manifests as entityBulkActionManifests } from './entity-bulk-actions/manifests.js'; -import { manifests as menuItemManifests } from './menu-item/manifests.js'; +import { manifests as modalManifests } from './modals/manifests.js'; +import { manifests as menuManifests } from './menu/manifests.js'; import { manifests as propertyEditorManifests } from './property-editors/manifests.js'; import { manifests as recycleBinManifests } from './recycle-bin/manifests.js'; import { manifests as repositoryManifests } from './repository/manifests.js'; import { manifests as trackedReferenceManifests } from './tracked-reference/manifests.js'; -import { manifests as modalManifests } from './modals/manifests.js'; import { manifests as treeManifests } from './tree/manifests.js'; import { manifests as userPermissionManifests } from './user-permissions/manifests.js'; import { manifests as workspaceManifests } from './workspace/manifests.js'; import { manifests as globalContextManifests } from './global-contexts/manifests.js'; export const manifests = [ - ...breadcrumbManifests, ...collectionManifests, ...entityActionManifests, ...entityBulkActionManifests, - ...menuItemManifests, + ...modalManifests, + ...menuManifests, ...propertyEditorManifests, ...recycleBinManifests, ...repositoryManifests, ...trackedReferenceManifests, - ...modalManifests, ...treeManifests, ...userPermissionManifests, ...workspaceManifests, diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/menu-item/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/menu-item/manifests.ts deleted file mode 100644 index b71965c753..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/menu-item/manifests.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { UMB_CONTENT_MENU_ALIAS } from '../menu.manifests.js'; -import { UMB_DOCUMENT_TREE_ALIAS } from '../tree/index.js'; -import type { ManifestMenuItemTreeKind } from '@umbraco-cms/backoffice/extension-registry'; - -const menuItem: ManifestMenuItemTreeKind = { - type: 'menuItem', - kind: 'tree', - alias: 'Umb.MenuItem.Document', - name: 'Document Menu Item', - weight: 200, - meta: { - label: 'Documents', - menus: [UMB_CONTENT_MENU_ALIAS], - treeAlias: UMB_DOCUMENT_TREE_ALIAS, - hideTreeRoot: true, - }, -}; - -export const manifests = [menuItem]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/menu.manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/menu.manifests.ts deleted file mode 100644 index 6bb7fe2726..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/menu.manifests.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { ManifestMenu } from '@umbraco-cms/backoffice/extension-registry'; - -export const UMB_CONTENT_MENU_ALIAS = 'Umb.Menu.Content'; - -const menu: ManifestMenu = { - type: 'menu', - alias: UMB_CONTENT_MENU_ALIAS, - name: 'Content Menu', - meta: { - label: 'Content', - }, -}; - -export const manifests = [menu]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/menu/document-menu-structure.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/menu/document-menu-structure.context.ts new file mode 100644 index 0000000000..55caf0e070 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/menu/document-menu-structure.context.ts @@ -0,0 +1,11 @@ +import { UMB_DOCUMENT_TREE_REPOSITORY_ALIAS } from '../tree/index.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbMenuVariantTreeStructureWorkspaceContextBase } from '@umbraco-cms/backoffice/menu'; + +export class UmbDocumentMenuStructureContext extends UmbMenuVariantTreeStructureWorkspaceContextBase { + constructor(host: UmbControllerHost) { + super(host, { treeRepositoryAlias: UMB_DOCUMENT_TREE_REPOSITORY_ALIAS }); + } +} + +export default UmbDocumentMenuStructureContext; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/menu/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/menu/manifests.ts new file mode 100644 index 0000000000..ada6c457c3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/menu/manifests.ts @@ -0,0 +1,48 @@ +import { UMB_DOCUMENT_TREE_ALIAS } from '../tree/index.js'; + +export const UMB_CONTENT_MENU_ALIAS = 'Umb.Menu.Content'; + +export const manifests = [ + { + type: 'menu', + alias: UMB_CONTENT_MENU_ALIAS, + name: 'Content Menu', + }, + { + type: 'menuItem', + kind: 'tree', + alias: 'Umb.MenuItem.Document', + name: 'Document Menu Item', + weight: 200, + meta: { + label: 'Documents', + menus: [UMB_CONTENT_MENU_ALIAS], + treeAlias: UMB_DOCUMENT_TREE_ALIAS, + hideTreeRoot: true, + }, + }, + { + type: 'workspaceContext', + name: 'Document Menu Structure Workspace Context', + alias: 'Umb.Context.Document.Menu.Structure', + api: () => import('./document-menu-structure.context.js'), + conditions: [ + { + alias: 'Umb.Condition.WorkspaceAlias', + match: 'Umb.Workspace.Document', + }, + ], + }, + { + type: 'workspaceFooterApp', + kind: 'variantMenuBreadcrumb', + alias: 'Umb.WorkspaceFooterApp.Document.Breadcrumb', + name: 'Document Breadcrumb Workspace Footer App', + conditions: [ + { + alias: 'Umb.Condition.WorkspaceAlias', + match: 'Umb.Workspace.Document', + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/menu-item/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/menu-item/manifests.ts index 23c912975b..60699a97fa 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/menu-item/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/menu-item/manifests.ts @@ -1,4 +1,4 @@ -import { UMB_CONTENT_MENU_ALIAS } from '../../menu.manifests.js'; +import { UMB_CONTENT_MENU_ALIAS } from '../../menu/manifests.js'; import type { ManifestMenuItemTreeKind } from '@umbraco-cms/backoffice/extension-registry'; const menuItem: ManifestMenuItemTreeKind = { diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/document-recycle-bin-tree.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/document-recycle-bin-tree.server.data-source.ts index a3466f28b6..4497a30476 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/document-recycle-bin-tree.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/document-recycle-bin-tree.server.data-source.ts @@ -2,7 +2,11 @@ import type { UmbDocumentRecycleBinTreeItemModel } from './types.js'; import type { DocumentRecycleBinItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; import { DocumentResource } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import type { UmbTreeChildrenOfRequestArgs, UmbTreeRootItemsRequestArgs } from '@umbraco-cms/backoffice/tree'; +import type { + UmbTreeAncestorsOfRequestArgs, + UmbTreeChildrenOfRequestArgs, + UmbTreeRootItemsRequestArgs, +} from '@umbraco-cms/backoffice/tree'; import { UmbTreeServerDataSourceBase } from '@umbraco-cms/backoffice/tree'; /** @@ -24,6 +28,7 @@ export class UmbDocumentRecycleBinTreeServerDataSource extends UmbTreeServerData super(host, { getRootItems, getChildrenOf, + getAncestorsOf, mapper, }); } @@ -44,6 +49,12 @@ const getChildrenOf = (args: UmbTreeChildrenOfRequestArgs) => { } }; +const getAncestorsOf = (args: UmbTreeAncestorsOfRequestArgs) => + // eslint-disable-next-line local-rules/no-direct-api-import + DocumentResource.getTreeDocumentAncestors({ + descendantId: args.descendantUnique, + }); + const mapper = (item: DocumentRecycleBinItemResponseModel): UmbDocumentRecycleBinTreeItemModel => { return { unique: item.id, diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/document-tree.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/document-tree.server.data-source.ts index 238a334d1b..5d9c486b44 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/document-tree.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/document-tree.server.data-source.ts @@ -1,6 +1,10 @@ import { UMB_DOCUMENT_ENTITY_TYPE } from '../entity.js'; import type { UmbDocumentTreeItemModel } from './types.js'; -import type { UmbTreeChildrenOfRequestArgs, UmbTreeRootItemsRequestArgs } from '@umbraco-cms/backoffice/tree'; +import type { + UmbTreeAncestorsOfRequestArgs, + UmbTreeChildrenOfRequestArgs, + UmbTreeRootItemsRequestArgs, +} from '@umbraco-cms/backoffice/tree'; import { UmbTreeServerDataSourceBase } from '@umbraco-cms/backoffice/tree'; import type { DocumentTreeItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; import { DocumentResource } from '@umbraco-cms/backoffice/external/backend-api'; @@ -25,6 +29,7 @@ export class UmbDocumentTreeServerDataSource extends UmbTreeServerDataSourceBase super(host, { getRootItems, getChildrenOf, + getAncestorsOf, mapper, }); } @@ -45,6 +50,12 @@ const getChildrenOf = (args: UmbTreeChildrenOfRequestArgs) => { } }; +const getAncestorsOf = (args: UmbTreeAncestorsOfRequestArgs) => + // eslint-disable-next-line local-rules/no-direct-api-import + DocumentResource.getTreeDocumentAncestors({ + descendantId: args.descendantUnique, + }); + const mapper = (item: DocumentTreeItemResponseModel): UmbDocumentTreeItemModel => { return { unique: item.id, @@ -63,6 +74,7 @@ const mapper = (item: DocumentTreeItemResponseModel): UmbDocumentTreeItemModel = return { name: variant.name, culture: variant.culture || null, + segment: null, // TODO: add segment to the backend API? state: variant.state, }; }), diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/document-tree.store.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/document-tree.store.ts index d81c675034..a7b830577c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/document-tree.store.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/document-tree.store.ts @@ -34,6 +34,7 @@ export class UmbDocumentTreeStore extends UmbUniqueTreeStore { return { name: variant.name, culture: variant.culture, + segment: variant.segment, state: variant.state, }; }), diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/manifests.ts index 1ffb9ab703..281c0d85a7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/manifests.ts @@ -45,4 +45,4 @@ const treeItem: ManifestTreeItem = { forEntityTypes: [UMB_DOCUMENT_ROOT_ENTITY_TYPE, UMB_DOCUMENT_ENTITY_TYPE], }; -export const manifests = [treeRepository, treeStore, tree, treeItem, ...reloadTreeItemChildrenManifests]; +export const manifests = [...reloadTreeItemChildrenManifests, tree, treeItem, treeRepository, treeStore]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/tree-item/document-tree-item.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/tree-item/document-tree-item.element.ts index b1c5d99cba..513371d537 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/tree-item/document-tree-item.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/tree-item/document-tree-item.element.ts @@ -45,6 +45,7 @@ export class UmbDocumentTreeItemElement extends UmbTreeItemElementBase x.culture === culture); } + // TODO: we should move the fallback name logic to a helper class. It will be used in multiple places #getLabel() { const fallbackName = this.#getVariant(this._defaultCulture)?.name ?? this._item?.variants[0].name ?? 'Unknown'; return this._variant?.name ?? `(${fallbackName})`; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/types.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/types.ts index b15f0487d4..e31730d723 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/types.ts @@ -23,5 +23,6 @@ export interface UmbDocumentTreeRootModel extends UmbUniqueTreeRootModel { export interface UmbDocumentTreeItemVariantModel { name: string; culture: string | null; + segment: string | null; state: DocumentVariantStateModel | null; // TODO: make our own enum for this. We might have states for "unsaved changes" etc. } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context-token.ts index 65cb0d1ec3..47f40626cb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context-token.ts @@ -1,10 +1,10 @@ import { UMB_DOCUMENT_ENTITY_TYPE } from '../entity.js'; import type { UmbDocumentWorkspaceContext } from './document-workspace.context.js'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import type { UmbSaveableWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; +import type { UmbSaveableWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; export const UMB_DOCUMENT_WORKSPACE_CONTEXT = new UmbContextToken< - UmbSaveableWorkspaceContextInterface, + UmbSaveableWorkspaceContext, UmbDocumentWorkspaceContext >( 'UmbWorkspaceContext', diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts index 50abd6196a..9a63f00ffc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts @@ -22,15 +22,15 @@ import { UmbDocumentWorkspaceEditorElement } from './document-workspace-editor.e import { UMB_INVARIANT_CULTURE, UmbVariantId } from '@umbraco-cms/backoffice/variant'; import { UmbContentTypeStructureManager } from '@umbraco-cms/backoffice/content-type'; import { - UmbEditableWorkspaceContextBase, + UmbSaveableWorkspaceContextBase, UmbWorkspaceIsNewRedirectController, UmbWorkspaceRouteManager, UmbWorkspaceSplitViewManager, } from '@umbraco-cms/backoffice/workspace'; import type { - UmbWorkspaceCollectionContextInterface, - UmbVariantableWorkspaceContextInterface, - UmbPublishableWorkspaceContextInterface, + UmbCollectionWorkspaceContext, + UmbVariantDatasetWorkspaceContext, + UmbPublishableWorkspaceContext, UmbRoutableWorkspaceContext, } from '@umbraco-cms/backoffice/workspace'; import { @@ -51,17 +51,18 @@ import type { UmbDocumentTypeDetailModel } from '@umbraco-cms/backoffice/documen type EntityType = UmbDocumentDetailModel; export class UmbDocumentWorkspaceContext - extends UmbEditableWorkspaceContextBase + extends UmbSaveableWorkspaceContextBase implements UmbRoutableWorkspaceContext, - UmbVariantableWorkspaceContextInterface, - UmbPublishableWorkspaceContextInterface, - UmbWorkspaceCollectionContextInterface + UmbVariantDatasetWorkspaceContext, + UmbPublishableWorkspaceContext, + UmbCollectionWorkspaceContext { public readonly repository = new UmbDocumentDetailRepository(this); public readonly publishingRepository = new UmbDocumentPublishingRepository(this); - #parent?: { entityType: string; unique: string | null }; + #parent = new UmbObjectState<{ entityType: string; unique: string | null } | undefined>(undefined); + readonly parentUnique = this.#parent.asObservablePart((parent) => (parent ? parent.unique : undefined)); /** * The document is the current state/draft version of the document. @@ -204,7 +205,7 @@ export class UmbDocumentWorkspaceContext async create(parent: { entityType: string; unique: string | null }, documentTypeUnique: string) { this.resetState(); - this.#parent = parent; + this.#parent.setValue(parent); this.#getDataPromise = this.repository.createScaffold({ documentType: { unique: documentTypeUnique, @@ -284,6 +285,10 @@ export class UmbDocumentWorkspaceContext this.#updateVariantData(variantId ?? UmbVariantId.CreateInvariant(), { name }); } + name(variantId?: UmbVariantId) { + return this.#currentData.asObservablePart((data) => data?.variants?.find((x) => variantId?.compare(x))?.name ?? ''); + } + setTemplate(templateUnique: string) { this.#currentData.update({ template: { unique: templateUnique } }); } @@ -485,9 +490,10 @@ export class UmbDocumentWorkspaceContext const saveData = this.#buildSaveData(selectedVariants); if (this.getIsNew()) { - if (!this.#parent) throw new Error('Parent is not set'); + const parent = this.#parent.getValue(); + if (!parent) throw new Error('Parent is not set'); - const { data: create, error } = await this.repository.create(saveData, this.#parent.unique); + const { data: create, error } = await this.repository.create(saveData, parent.unique); if (!create || error) { console.error('Error creating document', error); throw new Error('Error creating document'); @@ -498,8 +504,8 @@ export class UmbDocumentWorkspaceContext // TODO: this might not be the right place to alert the tree, but it works for now const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); const event = new UmbReloadTreeItemChildrenRequestEntityActionEvent({ - entityType: this.#parent.entityType, - unique: this.#parent.unique, + entityType: parent.entityType, + unique: parent.unique, }); eventContext.dispatchEvent(event); } else { diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info.element.ts index 031aed1397..851b75d8cb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info.element.ts @@ -2,7 +2,6 @@ import { TimeOptions } from './utils.js'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { css, html, customElement, state, repeat, ifDefined } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; import { UMB_MODAL_MANAGER_CONTEXT, UMB_WORKSPACE_MODAL, @@ -10,7 +9,11 @@ import { } from '@umbraco-cms/backoffice/modal'; import './document-workspace-view-info-history.element.js'; import './document-workspace-view-info-reference.element.js'; -import type { UmbDocumentVariantModel, UmbDocumentWorkspaceContext } from '@umbraco-cms/backoffice/document'; +import { + UMB_DOCUMENT_WORKSPACE_CONTEXT, + type UmbDocumentVariantModel, + type UmbDocumentWorkspaceContext, +} from '@umbraco-cms/backoffice/document'; import { DocumentVariantStateModel, type DocumentUrlInfoModel } from '@umbraco-cms/backoffice/external/backend-api'; import { type UmbDocumentTypeDetailModel, @@ -78,8 +81,8 @@ export class UmbDocumentWorkspaceViewInfoElement extends UmbLitElement { this._editDocumentTypePath = routeBuilder({}); }); - this.consumeContext(UMB_WORKSPACE_CONTEXT, (nodeContext) => { - this.#workspaceContext = nodeContext as UmbDocumentWorkspaceContext; + this.consumeContext(UMB_DOCUMENT_WORKSPACE_CONTEXT, (context) => { + this.#workspaceContext = context; this._documentTypeUnique = this.#workspaceContext.getContentTypeId()!; this.#getData(); this._observeContent(); @@ -258,7 +261,7 @@ export class UmbDocumentWorkspaceViewInfoElement extends UmbLitElement { - ` + ` : html``} diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/manifests.ts index cff410034a..eb049a21ef 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/manifests.ts @@ -1,15 +1,13 @@ import { manifests as dashboardManifests } from './dashboards/manifests.js'; -import { manifests as contentSectionManifests } from './section.manifests.js'; -import { manifests as contentMenuManifest } from './documents/menu.manifests.js'; import { manifests as documentBlueprintManifests } from './document-blueprints/manifests.js'; -import { manifests as documentTypeManifests } from './document-types/manifests.js'; import { manifests as documentManifests } from './documents/manifests.js'; +import { manifests as documentTypeManifests } from './document-types/manifests.js'; +import { manifests as sectionManifests } from './section.manifests.js'; export const manifests = [ ...dashboardManifests, - ...contentSectionManifests, - ...contentMenuManifest, ...documentBlueprintManifests, - ...documentTypeManifests, ...documentManifests, + ...documentTypeManifests, + ...sectionManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/language/manifests.ts index 28ddb19d95..bcae092c6e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/language/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/language/manifests.ts @@ -1,5 +1,5 @@ import { manifests as repositoryManifests } from './repository/manifests.js'; -import { manifests as treeManifests } from './menu-item/manifests.js'; +import { manifests as menuManifests } from './menu/manifests.js'; import { manifests as entityActions } from './entity-actions/manifests.js'; import { manifests as workspaceManifests } from './workspace/manifests.js'; import { manifests as appLanguageSelect } from './app-language-select/manifests.js'; @@ -10,7 +10,7 @@ import { manifests as globalContextManifests } from './global-contexts/manifests export const manifests = [ ...repositoryManifests, ...entityActions, - ...treeManifests, + ...menuManifests, ...workspaceManifests, ...appLanguageSelect, ...modalManifests, diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/menu-item/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/language/menu-item/manifests.ts deleted file mode 100644 index 76380d73ea..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/language/menu-item/manifests.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { ManifestMenuItem } from '@umbraco-cms/backoffice/extension-registry'; - -const menuItem: ManifestMenuItem = { - type: 'menuItem', - alias: 'Umb.MenuItem.Languages', - name: 'Languages Menu Item', - weight: 100, - meta: { - label: 'Languages', - icon: 'icon-globe', - entityType: 'language-root', - menus: ['Umb.Menu.Settings'], - }, -}; - -export const manifests = [menuItem]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/menu/language-menu-structure.context.ts b/src/Umbraco.Web.UI.Client/src/packages/language/menu/language-menu-structure.context.ts new file mode 100644 index 0000000000..3a149c6d09 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/language/menu/language-menu-structure.context.ts @@ -0,0 +1,51 @@ +import type { UmbLanguageDetailModel } from '../types.js'; +import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; + +import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { UmbStructureItemModel } from '@umbraco-cms/backoffice/menu'; + +export class UmbLanguageNavigationStructureWorkspaceContext extends UmbContextBase { + // TODO: figure out the correct type where we have "data" available + #workspaceContext?: any; + + #structure = new UmbArrayState([], (x) => x.unique); + public readonly structure = this.#structure.asObservable(); + + constructor(host: UmbControllerHost) { + // TODO: set up context token + super(host, 'UmbMenuStructureWorkspaceContext'); + + this.consumeContext(UMB_WORKSPACE_CONTEXT, (instance) => { + this.#workspaceContext = instance; + this.#requestStructure(); + }); + } + + async #requestStructure() { + const data = (await this.observe(this.#workspaceContext?.data, () => {})?.asPromise()) as UmbLanguageDetailModel; + if (!data) throw new Error('Data is not available'); + + const items = [ + // TODO: figure out if we can get the root from somewhere + // so we don't have to hardcode it + { + unique: null, + entityType: 'language-root', + name: 'Languages', + isFolder: false, + }, + { + unique: data.unique, + entityType: data.entityType, + name: data.name, + isFolder: false, + }, + ]; + + this.#structure.setValue(items); + } +} + +export default UmbLanguageNavigationStructureWorkspaceContext; diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/menu/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/language/menu/manifests.ts new file mode 100644 index 0000000000..198584f08d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/language/menu/manifests.ts @@ -0,0 +1,38 @@ +export const manifests = [ + { + type: 'menuItem', + alias: 'Umb.MenuItem.Languages', + name: 'Languages Menu Item', + weight: 100, + meta: { + label: 'Languages', + icon: 'icon-globe', + entityType: 'language-root', + menus: ['Umb.Menu.StructureSettings'], + }, + }, + { + type: 'workspaceContext', + name: 'Language Menu Structure Workspace Context', + alias: 'Umb.Context.Language.Menu.Structure', + api: () => import('./language-menu-structure.context.js'), + conditions: [ + { + alias: 'Umb.Condition.WorkspaceAlias', + match: 'Umb.Workspace.Language', + }, + ], + }, + { + type: 'workspaceFooterApp', + kind: 'menuBreadcrumb', + alias: 'Umb.WorkspaceFooterApp.Language.Breadcrumb', + name: 'Language Breadcrumb Workspace Footer App', + conditions: [ + { + alias: 'Umb.Condition.WorkspaceAlias', + match: 'Umb.Workspace.Language', + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/workspace/language/language-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/language/workspace/language/language-workspace-editor.element.ts index 7cd76f13bd..2648fd0d73 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/language/workspace/language/language-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/language/workspace/language/language-workspace-editor.element.ts @@ -57,10 +57,6 @@ export class UmbLanguageWorkspaceEditorElement extends UmbLitElement { value=${ifDefined(this._language?.name)} @input="${this.#handleInput}">`}
    - `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/workspace/language/language-workspace.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/language/workspace/language/language-workspace.context-token.ts index dccc8de0cf..fd6f1c74df 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/language/workspace/language/language-workspace.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/language/workspace/language/language-workspace.context-token.ts @@ -1,9 +1,9 @@ import type { UmbLanguageWorkspaceContext } from './language-workspace.context.js'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import type { UmbSaveableWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; +import type { UmbSaveableWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; export const UMB_LANGUAGE_WORKSPACE_CONTEXT = new UmbContextToken< - UmbSaveableWorkspaceContextInterface, + UmbSaveableWorkspaceContext, UmbLanguageWorkspaceContext >( 'UmbWorkspaceContext', diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/workspace/language/language-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/language/workspace/language/language-workspace.context.ts index c25d16beba..451a14fb69 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/language/workspace/language/language-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/language/workspace/language/language-workspace.context.ts @@ -2,25 +2,27 @@ import { UmbLanguageDetailRepository } from '../../repository/index.js'; import type { UmbLanguageDetailModel } from '../../types.js'; import { UmbLanguageWorkspaceEditorElement } from './language-workspace-editor.element.js'; import { - type UmbSaveableWorkspaceContextInterface, - UmbEditableWorkspaceContextBase, + type UmbSaveableWorkspaceContext, + UmbSaveableWorkspaceContextBase, UmbWorkspaceRouteManager, UmbWorkspaceIsNewRedirectController, type UmbRoutableWorkspaceContext, } from '@umbraco-cms/backoffice/workspace'; -import { ApiError } from '@umbraco-cms/backoffice/external/backend-api'; import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; export class UmbLanguageWorkspaceContext - extends UmbEditableWorkspaceContextBase - implements UmbSaveableWorkspaceContextInterface, UmbRoutableWorkspaceContext + extends UmbSaveableWorkspaceContextBase + implements UmbSaveableWorkspaceContext, UmbRoutableWorkspaceContext { public readonly repository: UmbLanguageDetailRepository = new UmbLanguageDetailRepository(this); #data = new UmbObjectState(undefined); readonly data = this.#data.asObservable(); + readonly unique = this.#data.asObservablePart((data) => data?.unique); + readonly name = this.#data.asObservablePart((data) => data?.name); + // TODO: this is a temp solution to bubble validation errors to the UI #validationErrors = new UmbObjectState(undefined); readonly validationErrors = this.#validationErrors.asObservable(); @@ -111,30 +113,17 @@ export class UmbLanguageWorkspaceContext this.#data.update({ fallbackIsoCode: unique }); } - // TODO: this is a temp solution to bubble validation errors to the UI - setValidationErrors(errorMap: any) { - // TODO: I can't use the update method to set the value to undefined - this.#validationErrors.setValue(errorMap); - } - async save() { - const data = this.getData(); - if (!data) return; + const newData = this.getData(); + if (!newData) return; if (this.getIsNew()) { - const { error } = await this.repository.create(data); - // TODO: this is temp solution to bubble validation errors to the UI - if (error) { - if (error instanceof ApiError && error.body.type === 'validation') { - this.setValidationErrors?.(error.body.errors); - } - } else { - this.setValidationErrors?.(undefined); - // TODO: do not make it the buttons responsibility to set the workspace to not new. + const { data } = await this.repository.create(newData); + if (data) { this.setIsNew(false); } } else { - await this.repository.save(data); + await this.repository.save(newData); // TODO: Show validation errors as warnings? } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/workspace/language/views/language-details-workspace-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/language/workspace/language/views/language-details-workspace-view.element.ts index 1645e22d54..259a7751b4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/language/workspace/language/views/language-details-workspace-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/language/workspace/language/views/language-details-workspace-view.element.ts @@ -22,9 +22,6 @@ export class UmbLanguageDetailsWorkspaceViewElement extends UmbLitElement implem @state() _isNew?: boolean; - @state() - _validationErrors?: { [key: string]: Array }; - #languageWorkspaceContext?: typeof UMB_LANGUAGE_WORKSPACE_CONTEXT.TYPE; constructor() { @@ -52,11 +49,6 @@ export class UmbLanguageDetailsWorkspaceViewElement extends UmbLitElement implem this.observe(this.#languageWorkspaceContext.isNew, (isNew) => { this._isNew = isNew; }); - - this.observe(this.#languageWorkspaceContext.validationErrors, (value) => { - this._validationErrors = value; - this.requestUpdate('_validationErrors'); - }); }); } @@ -123,9 +115,6 @@ export class UmbLanguageDetailsWorkspaceViewElement extends UmbLitElement implem value=${ifDefined(this._language?.unique)} @change=${this.#handleCultureChange} ?readonly=${this._isNew === false}> - - - ${this._validationErrors?.isoCode.map((error) => html`
    ${error}
    `)}
    diff --git a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/menu-item/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/menu-item/manifests.ts index 90e1fb7c0b..34a5fff422 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/menu-item/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/menu-item/manifests.ts @@ -3,13 +3,13 @@ import type { ManifestMenuItem } from '@umbraco-cms/backoffice/extension-registr const menuItem: ManifestMenuItem = { type: 'menuItem', alias: 'Umb.MenuItem.LogViewer', - name: 'LogViewer Menu Item', + name: 'Log Viewer Menu Item', weight: 300, meta: { label: 'Log Viewer', icon: 'icon-box-alt', entityType: 'logviewer', - menus: ['Umb.Menu.Settings'], + menus: ['Umb.Menu.AdvancedSettings'], }, }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/logviewer-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/logviewer-workspace.context.ts index d37ee3f8b1..73af896920 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/logviewer-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/logviewer-workspace.context.ts @@ -13,7 +13,7 @@ import { DirectionModel, LogLevelModel } from '@umbraco-cms/backoffice/external/ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; import { query } from '@umbraco-cms/backoffice/router'; -import type { UmbWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; +import type { UmbWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; export type PoolingInterval = 0 | 2000 | 5000 | 10000 | 20000 | 30000; @@ -27,7 +27,7 @@ export interface LogViewerDateRange { } // TODO: Revisit usage of workspace for this case... -export class UmbLogViewerWorkspaceContext extends UmbControllerBase implements UmbWorkspaceContextInterface { +export class UmbLogViewerWorkspaceContext extends UmbControllerBase implements UmbWorkspaceContext { public readonly workspaceAlias: string = 'Umb.Workspace.LogViewer'; #repository: UmbLogViewerRepository; @@ -39,10 +39,6 @@ export class UmbLogViewerWorkspaceContext extends UmbControllerBase implements U return 'Log Viewer'; } - getUnique() { - return undefined; - } - get today() { const today = new Date(); const dd = String(today.getDate()).padStart(2, '0'); @@ -109,7 +105,6 @@ export class UmbLogViewerWorkspaceContext extends UmbControllerBase implements U constructor(host: UmbControllerHost) { super(host); - console.log('log-viewer context'); this.provideContext(UMB_WORKSPACE_CONTEXT, this); // TODO: Revisit usage of workspace for this case... currently no other workspace context provides them self with their own token. this.provideContext(UMB_APP_LOG_VIEWER_CONTEXT, this); diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/manifests.ts index 940d82b7ca..488ffacb71 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/manifests.ts @@ -1,8 +1,7 @@ import { manifests as mediaSectionManifests } from './section.manifests.js'; -import { manifests as mediaMenuManifests } from './menu.manifests.js'; import { manifests as mediaManifests } from './media/manifests.js'; import { manifests as mediaTypesManifests } from './media-types/manifests.js'; import './media/components/index.js'; -export const manifests = [...mediaSectionManifests, ...mediaMenuManifests, ...mediaManifests, ...mediaTypesManifests]; +export const manifests = [...mediaSectionManifests, ...mediaManifests, ...mediaTypesManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/create/modal/media-type-create-options-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/create/modal/media-type-create-options-modal.element.ts index 50c43b78f1..3dc2b8563d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/create/modal/media-type-create-options-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/create/modal/media-type-create-options-modal.element.ts @@ -1,30 +1,38 @@ import { UMB_MEDIA_TYPE_FOLDER_REPOSITORY_ALIAS } from '../../../tree/index.js'; import type { UmbMediaTypeCreateOptionsModalData } from './index.js'; -import { html, customElement, property } from '@umbraco-cms/backoffice/external/lit'; -import { type UmbModalContext, UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; -import { UMB_FOLDER_CREATE_MODAL } from '@umbraco-cms/backoffice/tree'; -import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { html, customElement } from '@umbraco-cms/backoffice/external/lit'; +import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; +import { UmbCreateFolderEntityAction } from '@umbraco-cms/backoffice/tree'; @customElement('umb-media-type-create-options-modal') -export class UmbDataTypeCreateOptionsModalElement extends UmbLitElement { - @property({ attribute: false }) - modalContext?: UmbModalContext; +export class UmbDataTypeCreateOptionsModalElement extends UmbModalBaseElement { + #createFolderAction?: UmbCreateFolderEntityAction; - @property({ type: Object }) - data?: UmbMediaTypeCreateOptionsModalData; - - async #onClick(event: PointerEvent) { - event.stopPropagation(); + connectedCallback(): void { + super.connectedCallback(); if (!this.data?.parent) throw new Error('A parent is required to create a folder'); - const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); - const folderModalHandler = modalManager.open(this, UMB_FOLDER_CREATE_MODAL, { - data: { + // TODO: render the info from this instance in the list of actions + this.#createFolderAction = new UmbCreateFolderEntityAction(this, { + unique: this.data.parent.unique, + entityType: this.data.parent.entityType, + meta: { + icon: 'icon-folder', + label: 'New Folder...', folderRepositoryAlias: UMB_MEDIA_TYPE_FOLDER_REPOSITORY_ALIAS, - parent: this.data?.parent, }, }); - folderModalHandler?.onSubmit().then(() => this.modalContext?.submit()); + } + + async #onCreateFolderClick(event: PointerEvent) { + event.stopPropagation(); + + try { + await this.#createFolderAction?.execute(); + this._submitModal(); + } catch (error) { + console.error(error); + } } // close the modal when navigating to data type @@ -50,8 +58,8 @@ export class UmbDataTypeCreateOptionsModalElement extends UmbLitElement { - - + + } Cancel diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/manifests.ts index 208a10d90d..a868fd5b1e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/manifests.ts @@ -1,5 +1,5 @@ import { manifests as entityActionsManifests } from './entity-actions/manifests.js'; -import { manifests as menuItemManifests } from './menu-item/manifests.js'; +import { manifests as menuManifests } from './menu/manifests.js'; import { manifests as repositoryManifests } from './repository/manifests.js'; import { manifests as treeManifests } from './tree/manifests.js'; import { manifests as workspaceManifests } from './workspace/manifests.js'; @@ -7,7 +7,7 @@ import { manifests as propertyEditorUiManifests } from './property-editors/manif export const manifests = [ ...entityActionsManifests, - ...menuItemManifests, + ...menuManifests, ...repositoryManifests, ...treeManifests, ...workspaceManifests, diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/menu-item/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/menu-item/manifests.ts deleted file mode 100644 index 04f3b4d4df..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/menu-item/manifests.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { UMB_MEDIA_TYPE_TREE_ALIAS } from '../tree/manifests.js'; -import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; - -const menuItem: ManifestTypes = { - type: 'menuItem', - kind: 'tree', - alias: 'Umb.MenuItem.MediaTypes', - name: 'Media Types Menu Item', - weight: 800, - meta: { - label: 'Media Types', - treeAlias: UMB_MEDIA_TYPE_TREE_ALIAS, - menus: ['Umb.Menu.Settings'], - }, -}; - -export const manifests = [menuItem]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/menu/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/menu/manifests.ts new file mode 100644 index 0000000000..2158b4abaf --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/menu/manifests.ts @@ -0,0 +1,40 @@ +import { UMB_MEDIA_TYPE_TREE_ALIAS } from '../tree/index.js'; + +export const manifests = [ + { + type: 'menuItem', + kind: 'tree', + alias: 'Umb.MenuItem.MediaTypes', + name: 'Media Types Menu Item', + weight: 800, + meta: { + label: 'Media Types', + treeAlias: UMB_MEDIA_TYPE_TREE_ALIAS, + menus: ['Umb.Menu.StructureSettings'], + }, + }, + { + type: 'workspaceContext', + name: 'Media Type Menu Structure Workspace Context', + alias: 'Umb.Context.MediaType.Menu.Structure', + api: () => import('./media-type-menu-structure.context.js'), + conditions: [ + { + alias: 'Umb.Condition.WorkspaceAlias', + match: 'Umb.Workspace.MediaType', + }, + ], + }, + { + type: 'workspaceFooterApp', + kind: 'menuBreadcrumb', + alias: 'Umb.WorkspaceFooterApp.MediaType.Breadcrumb', + name: 'Media Type Breadcrumb Workspace Footer App', + conditions: [ + { + alias: 'Umb.Condition.WorkspaceAlias', + match: 'Umb.Workspace.MediaType', + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/menu/media-type-menu-structure.context.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/menu/media-type-menu-structure.context.ts new file mode 100644 index 0000000000..c6d9de9ea2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/menu/media-type-menu-structure.context.ts @@ -0,0 +1,11 @@ +import { UMB_MEDIA_TYPE_TREE_REPOSITORY_ALIAS } from '../tree/index.js'; +import { UmbMenuTreeStructureWorkspaceContextBase } from '@umbraco-cms/backoffice/menu'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; + +export class UmbMediaTypeMenuStructureWorkspaceContext extends UmbMenuTreeStructureWorkspaceContextBase { + constructor(host: UmbControllerHost) { + super(host, { treeRepositoryAlias: UMB_MEDIA_TYPE_TREE_REPOSITORY_ALIAS }); + } +} + +export default UmbMediaTypeMenuStructureWorkspaceContext; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/folder/media-type-folder.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/folder/media-type-folder.server.data-source.ts index 5976006ded..080558d0f0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/folder/media-type-folder.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/folder/media-type-folder.server.data-source.ts @@ -61,7 +61,7 @@ export class UmbMediaTypeFolderServerDataSource implements UmbFolderDataSource { const requestBody = { id: args.unique, - parentId: args.parentUnique ? { id: args.parentUnique } : null, + parent: args.parentUnique ? { id: args.parentUnique } : null, name: args.name, }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/media-type-tree.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/media-type-tree.server.data-source.ts index 7ca5d45402..d5be706a5b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/media-type-tree.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/media-type-tree.server.data-source.ts @@ -3,7 +3,11 @@ import type { UmbMediaTypeTreeItemModel } from './types.js'; import type { MediaTypeTreeItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; import { MediaTypeResource } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import type { UmbTreeChildrenOfRequestArgs, UmbTreeRootItemsRequestArgs } from '@umbraco-cms/backoffice/tree'; +import type { + UmbTreeAncestorsOfRequestArgs, + UmbTreeChildrenOfRequestArgs, + UmbTreeRootItemsRequestArgs, +} from '@umbraco-cms/backoffice/tree'; import { UmbTreeServerDataSourceBase } from '@umbraco-cms/backoffice/tree'; /** @@ -25,6 +29,7 @@ export class UmbMediaTypeTreeServerDataSource extends UmbTreeServerDataSourceBas super(host, { getRootItems, getChildrenOf, + getAncestorsOf, mapper, }); } @@ -45,6 +50,12 @@ const getChildrenOf = (args: UmbTreeChildrenOfRequestArgs) => { } }; +const getAncestorsOf = (args: UmbTreeAncestorsOfRequestArgs) => + // eslint-disable-next-line local-rules/no-direct-api-import + MediaTypeResource.getTreeMediaTypeAncestors({ + descendantId: args.descendantUnique, + }); + const mapper = (item: MediaTypeTreeItemResponseModel): UmbMediaTypeTreeItemModel => { return { unique: item.id, diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/reload-tree-item-children/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/reload-tree-item-children/manifests.ts index 37cb482a42..a5bbd2ac9e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/reload-tree-item-children/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/reload-tree-item-children/manifests.ts @@ -1,4 +1,8 @@ -import { UMB_MEDIA_TYPE_ENTITY_TYPE, UMB_MEDIA_TYPE_ROOT_ENTITY_TYPE } from '../../entity.js'; +import { + UMB_MEDIA_TYPE_ENTITY_TYPE, + UMB_MEDIA_TYPE_FOLDER_ENTITY_TYPE, + UMB_MEDIA_TYPE_ROOT_ENTITY_TYPE, +} from '../../entity.js'; import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; export const manifests: Array = [ @@ -7,6 +11,6 @@ export const manifests: Array = [ kind: 'reloadTreeItemChildren', alias: 'Umb.EntityAction.MediaType.Tree.ReloadChildrenOf', name: 'Reload Media Type Tree Item Children Entity Action', - forEntityTypes: [UMB_MEDIA_TYPE_ENTITY_TYPE, UMB_MEDIA_TYPE_ROOT_ENTITY_TYPE], + forEntityTypes: [UMB_MEDIA_TYPE_ENTITY_TYPE, UMB_MEDIA_TYPE_ROOT_ENTITY_TYPE, UMB_MEDIA_TYPE_FOLDER_ENTITY_TYPE], }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/media-type-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/media-type-workspace-editor.element.ts index 93abbbb22b..ca73191e23 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/media-type-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/media-type-workspace-editor.element.ts @@ -1,10 +1,10 @@ import type { UmbMediaTypeWorkspaceContext } from './media-type-workspace.context.js'; +import { UMB_MEDIA_TYPE_WORKSPACE_CONTEXT } from './media-type-workspace.context-token.js'; import type { UUIInputElement } from '@umbraco-cms/backoffice/external/uui'; import { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; import { css, html, customElement, state, ifDefined } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UMB_ICON_PICKER_MODAL, UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; -import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; import { generateAlias } from '@umbraco-cms/backoffice/utils'; @customElement('umb-media-type-workspace-editor') @@ -26,8 +26,8 @@ export class UmbMediaTypeWorkspaceEditorElement extends UmbLitElement { constructor() { super(); - this.consumeContext(UMB_WORKSPACE_CONTEXT, (instance) => { - this.#workspaceContext = instance as UmbMediaTypeWorkspaceContext; + this.consumeContext(UMB_MEDIA_TYPE_WORKSPACE_CONTEXT, (context) => { + this.#workspaceContext = context; this.#observeMediaType(); }); } @@ -132,20 +132,6 @@ export class UmbMediaTypeWorkspaceEditorElement extends UmbLitElement { - -
    - - - Keyboard Shortcuts - - ALT - + - shift - + - k - - -
    `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/media-type-workspace.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/media-type-workspace.context-token.ts index c0e20a1550..74e55cb85e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/media-type-workspace.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/media-type-workspace.context-token.ts @@ -1,9 +1,9 @@ import type { UmbMediaTypeWorkspaceContext } from './media-type-workspace.context.js'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import type { UmbSaveableWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; +import type { UmbSaveableWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; export const UMB_MEDIA_TYPE_WORKSPACE_CONTEXT = new UmbContextToken< - UmbSaveableWorkspaceContextInterface, + UmbSaveableWorkspaceContext, UmbMediaTypeWorkspaceContext >( 'UmbWorkspaceContext', diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/media-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/media-type-workspace.context.ts index f28eb8b625..798deece94 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/media-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/media-type-workspace.context.ts @@ -3,7 +3,7 @@ import { UMB_MEDIA_TYPE_ENTITY_TYPE } from '../entity.js'; import type { UmbMediaTypeDetailModel } from '../types.js'; import { UmbMediaTypeWorkspaceEditorElement } from './media-type-workspace-editor.element.js'; import { - UmbEditableWorkspaceContextBase, + UmbSaveableWorkspaceContextBase, type UmbRoutableWorkspaceContext, UmbWorkspaceIsNewRedirectController, UmbWorkspaceRouteManager, @@ -23,7 +23,7 @@ import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice type EntityType = UmbMediaTypeDetailModel; export class UmbMediaTypeWorkspaceContext - extends UmbEditableWorkspaceContextBase + extends UmbSaveableWorkspaceContextBase implements UmbContentTypeWorkspaceContext, UmbRoutableWorkspaceContext { readonly IS_CONTENT_TYPE_WORKSPACE_CONTEXT = true; @@ -31,11 +31,14 @@ export class UmbMediaTypeWorkspaceContext public readonly repository: UmbMediaTypeDetailRepository = new UmbMediaTypeDetailRepository(this); // Draft is located in structure manager - #parent?: { entityType: string; unique: string | null }; + #parent = new UmbObjectState<{ entityType: string; unique: string | null } | undefined>(undefined); + readonly parentUnique = this.#parent.asObservablePart((parent) => (parent ? parent.unique : undefined)); + #persistedData = new UmbObjectState(undefined); // General for content types: readonly data; + readonly unique; readonly name; readonly alias; readonly description; @@ -56,6 +59,7 @@ export class UmbMediaTypeWorkspaceContext // General for content types: this.data = this.structure.ownerContentType; + this.unique = this.structure.ownerContentTypeObservablePart((data) => data?.unique); this.name = this.structure.ownerContentTypeObservablePart((data) => data?.name); this.alias = this.structure.ownerContentTypeObservablePart((data) => data?.alias); this.description = this.structure.ownerContentTypeObservablePart((data) => data?.description); @@ -158,7 +162,7 @@ export class UmbMediaTypeWorkspaceContext async create(parent: { entityType: string; unique: string | null }) { this.resetState(); - this.#parent = parent; + this.#parent.setValue(parent); const { data } = await this.structure.createScaffold(); if (!data) return undefined; @@ -199,13 +203,14 @@ export class UmbMediaTypeWorkspaceContext } if (this.getIsNew()) { - if (!this.#parent) throw new Error('Parent is not set'); - if ((await this.structure.create(this.#parent.unique)) === true) { - if (!this.#parent) throw new Error('Parent is not set'); + const parent = this.#parent.getValue(); + if (!parent) throw new Error('Parent is not set'); + if ((await this.structure.create(parent.unique)) === true) { + if (!parent) throw new Error('Parent is not set'); const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); const event = new UmbReloadTreeItemChildrenRequestEntityActionEvent({ - entityType: this.#parent.entityType, - unique: this.#parent.unique, + entityType: parent.entityType, + unique: parent.unique, }); eventContext.dispatchEvent(event); this.setIsNew(false); diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/views/structure/media-type-workspace-view-structure.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/views/structure/media-type-workspace-view-structure.element.ts index 02db30bb48..04a8e22e52 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/views/structure/media-type-workspace-view-structure.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/views/structure/media-type-workspace-view-structure.element.ts @@ -1,9 +1,9 @@ import type { UmbMediaTypeWorkspaceContext } from '../../media-type-workspace.context.js'; import type { UmbInputMediaTypeElement } from '../../../components/input-media-type/input-media-type.element.js'; +import { UMB_MEDIA_TYPE_WORKSPACE_CONTEXT } from '../../media-type-workspace.context-token.js'; import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; import type { UmbContentTypeSortModel } from '@umbraco-cms/backoffice/content-type'; import type { UmbInputCollectionConfigurationElement } from '@umbraco-cms/backoffice/components'; import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/extension-registry'; @@ -26,8 +26,8 @@ export class UmbMediaTypeWorkspaceViewStructureElement extends UmbLitElement imp 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_WORKSPACE_CONTEXT, (mediaTypeContext) => { - this.#workspaceContext = mediaTypeContext as UmbMediaTypeWorkspaceContext; + this.consumeContext(UMB_MEDIA_TYPE_WORKSPACE_CONTEXT, (context) => { + this.#workspaceContext = context; this._observeMediaType(); }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/index.ts index 2629a02d48..226014f2a5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/index.ts @@ -9,4 +9,4 @@ export * from './utils/index.js'; export { UMB_MEDIA_TREE_ALIAS } from './tree/index.js'; export { UMB_MEDIA_COLLECTION_ALIAS } from './collection/index.js'; -export { UMB_MEDIA_MENU_ALIAS } from './menu.manifests.js'; +export { UMB_MEDIA_MENU_ALIAS } from './menu/manifests.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/manifests.ts index fcff2b6510..dba85d8ce2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/manifests.ts @@ -1,7 +1,7 @@ import { manifests as collectionManifests } from './collection/manifests.js'; import { manifests as entityActionsManifests } from './entity-actions/manifests.js'; import { manifests as entityBulkActionsManifests } from './entity-bulk-actions/manifests.js'; -import { manifests as menuItemManifests } from './menu-item/manifests.js'; +import { manifests as menuManifests } from './menu/manifests.js'; import { manifests as propertyEditorsManifests } from './property-editors/manifests.js'; import { manifests as repositoryManifests } from './repository/manifests.js'; import { manifests as sectionViewManifests } from './section-view/manifests.js'; @@ -12,7 +12,7 @@ export const manifests = [ ...collectionManifests, ...entityActionsManifests, ...entityBulkActionsManifests, - ...menuItemManifests, + ...menuManifests, ...propertyEditorsManifests, ...repositoryManifests, ...sectionViewManifests, diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/menu-item/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/menu-item/manifests.ts deleted file mode 100644 index e5e3aa4c2c..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/menu-item/manifests.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { UMB_MEDIA_MENU_ALIAS } from '../menu.manifests.js'; -import { UMB_MEDIA_TREE_ALIAS } from '../tree/index.js'; -import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; - -const menuItem: ManifestTypes = { - type: 'menuItem', - kind: 'tree', - alias: 'Umb.MenuItem.Media', - name: 'Media Menu Item', - weight: 100, - meta: { - label: 'Media', - menus: [UMB_MEDIA_MENU_ALIAS], - treeAlias: UMB_MEDIA_TREE_ALIAS, - hideTreeRoot: true, - }, -}; - -export const manifests = [menuItem]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/menu.manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/menu.manifests.ts deleted file mode 100644 index 7f7f2a0444..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/menu.manifests.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { ManifestMenu } from '@umbraco-cms/backoffice/extension-registry'; - -export const UMB_MEDIA_MENU_ALIAS = 'Umb.Menu.Media'; - -const menu: ManifestMenu = { - type: 'menu', - alias: UMB_MEDIA_MENU_ALIAS, - name: 'Media Menu', - meta: { - label: 'Media', - }, -}; - -export const manifests = [menu]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/menu/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/menu/manifests.ts new file mode 100644 index 0000000000..34ad988a1c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/menu/manifests.ts @@ -0,0 +1,48 @@ +import { UMB_MEDIA_TREE_ALIAS } from '../tree/index.js'; + +export const UMB_MEDIA_MENU_ALIAS = 'Umb.Menu.Media'; + +export const manifests = [ + { + type: 'menu', + alias: UMB_MEDIA_MENU_ALIAS, + name: 'Media Menu', + }, + { + type: 'menuItem', + kind: 'tree', + alias: 'Umb.MenuItem.Media', + name: 'Media Menu Item', + weight: 100, + meta: { + label: 'Media', + menus: [UMB_MEDIA_MENU_ALIAS], + treeAlias: UMB_MEDIA_TREE_ALIAS, + hideTreeRoot: true, + }, + }, + { + type: 'workspaceContext', + name: 'Media Menu Structure Workspace Context', + alias: 'Umb.Context.Media.Menu.Structure', + api: () => import('./media-menu-structure.context.js'), + conditions: [ + { + alias: 'Umb.Condition.WorkspaceAlias', + match: 'Umb.Workspace.Media', + }, + ], + }, + { + type: 'workspaceFooterApp', + kind: 'variantMenuBreadcrumb', + alias: 'Umb.WorkspaceFooterApp.Media.Breadcrumb', + name: 'Media Breadcrumb Workspace Footer App', + conditions: [ + { + alias: 'Umb.Condition.WorkspaceAlias', + match: 'Umb.Workspace.Media', + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/menu/media-menu-structure.context.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/menu/media-menu-structure.context.ts new file mode 100644 index 0000000000..87efaafbb1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/menu/media-menu-structure.context.ts @@ -0,0 +1,11 @@ +import { UMB_MEDIA_TREE_REPOSITORY_ALIAS } from '../tree/index.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbMenuVariantTreeStructureWorkspaceContextBase } from '@umbraco-cms/backoffice/menu'; + +export class UmbMediaMenuStructureContext extends UmbMenuVariantTreeStructureWorkspaceContextBase { + constructor(host: UmbControllerHost) { + super(host, { treeRepositoryAlias: UMB_MEDIA_TREE_REPOSITORY_ALIAS }); + } +} + +export default UmbMediaMenuStructureContext; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/media-tree.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/media-tree.server.data-source.ts index dd491954d4..ab445331ae 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/media-tree.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/media-tree.server.data-source.ts @@ -1,6 +1,10 @@ import { UMB_MEDIA_ENTITY_TYPE } from '../entity.js'; import type { UmbMediaTreeItemModel } from './types.js'; -import type { UmbTreeChildrenOfRequestArgs, UmbTreeRootItemsRequestArgs } from '@umbraco-cms/backoffice/tree'; +import type { + UmbTreeAncestorsOfRequestArgs, + UmbTreeChildrenOfRequestArgs, + UmbTreeRootItemsRequestArgs, +} from '@umbraco-cms/backoffice/tree'; import { UmbTreeServerDataSourceBase } from '@umbraco-cms/backoffice/tree'; import { MediaResource, type MediaTreeItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; @@ -24,6 +28,7 @@ export class UmbMediaTreeServerDataSource extends UmbTreeServerDataSourceBase< super(host, { getRootItems, getChildrenOf, + getAncestorsOf, mapper, }); } @@ -44,6 +49,12 @@ const getChildrenOf = (args: UmbTreeChildrenOfRequestArgs) => { } }; +const getAncestorsOf = (args: UmbTreeAncestorsOfRequestArgs) => + // eslint-disable-next-line local-rules/no-direct-api-import + MediaResource.getTreeMediaAncestors({ + descendantId: args.descendantUnique, + }); + const mapper = (item: MediaTreeItemResponseModel): UmbMediaTreeItemModel => { return { unique: item.id, diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace-split-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace-split-view.element.ts index 87d2d724ca..c289b0f87f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace-split-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace-split-view.element.ts @@ -49,9 +49,7 @@ export class UmbMediaWorkspaceSplitViewElement extends UmbLitElement { )} - - - ` + ` : nothing; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context-token.ts index a467048308..24ffa52a23 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context-token.ts @@ -1,12 +1,9 @@ import { UMB_MEDIA_ENTITY_TYPE } from '../entity.js'; import type { UmbMediaWorkspaceContext } from './media-workspace.context.js'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import type { UmbSaveableWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; +import type { UmbSaveableWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; -export const UMB_MEDIA_WORKSPACE_CONTEXT = new UmbContextToken< - UmbSaveableWorkspaceContextInterface, - UmbMediaWorkspaceContext ->( +export const UMB_MEDIA_WORKSPACE_CONTEXT = new UmbContextToken( 'UmbWorkspaceContext', undefined, (context): context is UmbMediaWorkspaceContext => context.getEntityType?.() === UMB_MEDIA_ENTITY_TYPE, diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts index b2ec225c93..b49c0679f8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts @@ -6,20 +6,18 @@ import type { UmbMediaDetailModel, UmbMediaVariantModel, UmbMediaVariantOptionMo import { UMB_INVARIANT_CULTURE, UmbVariantId } from '@umbraco-cms/backoffice/variant'; import { UmbContentTypeStructureManager } from '@umbraco-cms/backoffice/content-type'; import type { - UmbWorkspaceCollectionContextInterface, - UmbVariantableWorkspaceContextInterface, + UmbCollectionWorkspaceContext, + UmbVariantDatasetWorkspaceContext, } from '@umbraco-cms/backoffice/workspace'; import { - UmbEditableWorkspaceContextBase, + UmbSaveableWorkspaceContextBase, UmbWorkspaceIsNewRedirectController, UmbWorkspaceRouteManager, UmbWorkspaceSplitViewManager, } from '@umbraco-cms/backoffice/workspace'; import { appendToFrozenArray, - jsonStringComparison, mergeObservables, - partialUpdateFrozenArray, UmbArrayState, UmbObjectState, } from '@umbraco-cms/backoffice/observable-api'; @@ -33,13 +31,14 @@ import UmbMediaWorkspaceEditorElement from './media-workspace-editor.element.js' type EntityType = UmbMediaDetailModel; export class UmbMediaWorkspaceContext - extends UmbEditableWorkspaceContextBase - implements UmbVariantableWorkspaceContextInterface, UmbWorkspaceCollectionContextInterface + extends UmbSaveableWorkspaceContextBase + implements UmbVariantDatasetWorkspaceContext, UmbCollectionWorkspaceContext { // public readonly repository = new UmbMediaDetailRepository(this); - #parent?: { entityType: string; unique: string | null }; + #parent = new UmbObjectState<{ entityType: string; unique: string | null } | undefined>(undefined); + readonly parentUnique = this.#parent.asObservablePart((parent) => (parent ? parent.unique : undefined)); /** * The media is the current state/draft version of the media. @@ -180,7 +179,7 @@ export class UmbMediaWorkspaceContext async create(parent: { entityType: string; unique: string | null }, mediaTypeUnique: string) { this.resetState(); - this.#parent = parent; + this.#parent.setValue(parent); this.#getDataPromise = this.repository.createScaffold({ mediaType: { unique: mediaTypeUnique, collection: null } }); const { data } = await this.#getDataPromise; if (!data) return undefined; @@ -246,6 +245,10 @@ export class UmbMediaWorkspaceContext this.#updateVariantData(variantId ?? UmbVariantId.CreateInvariant(), { name }); } + name(variantId?: UmbVariantId) { + return this.#currentData.asObservablePart((data) => data?.variants?.find((x) => variantId?.compare(x))?.name ?? ''); + } + async propertyStructureById(propertyId: string) { return this.structure.propertyStructureById(propertyId); } @@ -373,17 +376,18 @@ export class UmbMediaWorkspaceContext if (!this.#currentData.value?.unique) throw new Error('Unique is missing'); if (this.getIsNew()) { - if (!this.#parent) throw new Error('Parent is not set'); + const parent = this.#parent.getValue(); + if (!parent) throw new Error('Parent is not set'); const value = this.#currentData.value; - if ((await this.repository.create(value, this.#parent.unique)).data !== undefined) { + if ((await this.repository.create(value, parent.unique)).data !== undefined) { this.setIsNew(false); // TODO: this might not be the right place to alert the tree, but it works for now const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); const event = new UmbReloadTreeItemChildrenRequestEntityActionEvent({ - entityType: this.#parent.entityType, - unique: this.#parent.unique, + entityType: parent.entityType, + unique: parent.unique, }); eventContext.dispatchEvent(event); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/views/info/media-workspace-view-info.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/views/info/media-workspace-view-info.element.ts index 7f2c56d9bd..d6c8eaf1da 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/views/info/media-workspace-view-info.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/views/info/media-workspace-view-info.element.ts @@ -1,12 +1,11 @@ import { TimeOptions } from './utils.js'; import { css, html, customElement, state, repeat } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; import { UMB_WORKSPACE_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import './media-workspace-view-info-history.element.js'; import './media-workspace-view-info-reference.element.js'; -import type { UmbMediaWorkspaceContext } from '@umbraco-cms/backoffice/media'; +import { UMB_MEDIA_WORKSPACE_CONTEXT, type UmbMediaWorkspaceContext } from '@umbraco-cms/backoffice/media'; import type { MediaUrlInfoModel } from '@umbraco-cms/backoffice/external/backend-api'; @customElement('umb-media-workspace-view-info') @@ -20,7 +19,7 @@ export class UmbMediaWorkspaceViewInfoElement extends UmbLitElement { @state() private _mediaUnique = ''; - private _workspaceContext?: typeof UMB_WORKSPACE_CONTEXT.TYPE; + private _workspaceContext?: typeof UMB_MEDIA_WORKSPACE_CONTEXT.TYPE; @state() private _editMediaTypePath = ''; @@ -43,8 +42,8 @@ export class UmbMediaWorkspaceViewInfoElement extends UmbLitElement { this._editMediaTypePath = routeBuilder({}); }); - this.consumeContext(UMB_WORKSPACE_CONTEXT, (nodeContext) => { - this._workspaceContext = nodeContext; + this.consumeContext(UMB_MEDIA_WORKSPACE_CONTEXT, (context) => { + this._workspaceContext = context; this._observeContent(); }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/menu.manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/menu.manifests.ts deleted file mode 100644 index cdefc905ff..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/media/menu.manifests.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { ManifestMenu } from '@umbraco-cms/backoffice/extension-registry'; - -const menu: ManifestMenu = { - type: 'menu', - alias: 'Umb.Menu.Media', - name: 'Media Menu', - meta: { - label: 'Media', - }, -}; - -export const manifests = [menu]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/workspace/member-group-workspace.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/workspace/member-group-workspace.context-token.ts index e8dc975186..45835e08e7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/workspace/member-group-workspace.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/workspace/member-group-workspace.context-token.ts @@ -1,9 +1,9 @@ import type { UmbMemberGroupWorkspaceContext } from './member-group-workspace.context.js'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import type { UmbSaveableWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; +import type { UmbSaveableWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; export const UMB_MEMBER_GROUP_WORKSPACE_CONTEXT = new UmbContextToken< - UmbSaveableWorkspaceContextInterface, + UmbSaveableWorkspaceContext, UmbMemberGroupWorkspaceContext >( 'UmbWorkspaceContext', diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/workspace/member-group-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/workspace/member-group-workspace.context.ts index 1d69d6a74b..493df687f7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/workspace/member-group-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/workspace/member-group-workspace.context.ts @@ -3,8 +3,8 @@ import type { UmbMemberGroupDetailModel } from '../types.js'; import { UMB_MEMBER_GROUP_WORKSPACE_ALIAS } from './manifests.js'; import { UmbMemberGroupWorkspaceEditorElement } from './member-group-workspace-editor.element.js'; import { - type UmbSaveableWorkspaceContextInterface, - UmbEditableWorkspaceContextBase, + type UmbSaveableWorkspaceContext, + UmbSaveableWorkspaceContextBase, UmbWorkspaceRouteManager, UmbWorkspaceIsNewRedirectController, type UmbRoutableWorkspaceContext, @@ -13,8 +13,8 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; export class UmbMemberGroupWorkspaceContext - extends UmbEditableWorkspaceContextBase - implements UmbSaveableWorkspaceContextInterface, UmbRoutableWorkspaceContext + extends UmbSaveableWorkspaceContextBase + implements UmbSaveableWorkspaceContext, UmbRoutableWorkspaceContext { public readonly repository = new UmbMemberGroupDetailRepository(this); #getDataPromise?: Promise; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/workspace/views/info/member-type-workspace-view-info.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/workspace/views/info/member-type-workspace-view-info.element.ts index fe7267c904..d38e509e02 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/workspace/views/info/member-type-workspace-view-info.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/workspace/views/info/member-type-workspace-view-info.element.ts @@ -1,14 +1,13 @@ // import { UMB_COMPOSITION_PICKER_MODAL, type UmbCompositionPickerModalData } from '../../../modals/index.js'; -import type { UmbMemberGroupWorkspaceContext } from '../../member-group-workspace.context.js'; +import { UMB_MEMBER_GROUP_WORKSPACE_CONTEXT } from '../../member-group-workspace.context-token.js'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/extension-registry'; -import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; @customElement('umb-member-type-workspace-view-member-info') export class UmbMemberTypeWorkspaceViewMemberInfoElement extends UmbLitElement implements UmbWorkspaceViewElement { - private _workspaceContext?: UmbMemberGroupWorkspaceContext; + private _workspaceContext?: typeof UMB_MEMBER_GROUP_WORKSPACE_CONTEXT.TYPE; @state() private _unique = ''; @@ -16,8 +15,8 @@ export class UmbMemberTypeWorkspaceViewMemberInfoElement extends UmbLitElement i constructor() { super(); - this.consumeContext(UMB_WORKSPACE_CONTEXT, async (context) => { - this._workspaceContext = context as UmbMemberGroupWorkspaceContext; + this.consumeContext(UMB_MEMBER_GROUP_WORKSPACE_CONTEXT, async (context) => { + this._workspaceContext = context; this._unique = this._workspaceContext.getUnique() ?? ''; }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/manifests.ts index 47e4ffa227..baad2b5948 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/manifests.ts @@ -1,5 +1,5 @@ import { manifests as entityActionsManifests } from './entity-actions/manifests.js'; -import { manifests as menuItemManifests } from './menu-item/manifests.js'; +import { manifests as menuManifests } from './menu/manifests.js'; import { manifests as repositoryManifests } from './repository/manifests.js'; import { manifests as treeManifests } from './tree/manifests.js'; import { manifests as workspaceManifests } from './workspace/manifests.js'; @@ -8,7 +8,7 @@ import './components/index.js'; export const manifests = [ ...entityActionsManifests, - ...menuItemManifests, + ...menuManifests, ...repositoryManifests, ...treeManifests, ...workspaceManifests, diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/menu-item/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/menu-item/manifests.ts deleted file mode 100644 index ddf86d8d68..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/menu-item/manifests.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { UMB_MEMBER_TYPE_TREE_ALIAS } from '../tree/index.js'; -import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; - -const menuItem: ManifestTypes = { - type: 'menuItem', - kind: 'tree', - alias: 'Umb.MenuItem.MemberTypes', - name: 'Member Type Menu Item', - weight: 700, - meta: { - label: 'Member Types', - treeAlias: UMB_MEMBER_TYPE_TREE_ALIAS, - menus: ['Umb.Menu.Settings'], - }, -}; - -export const manifests = [menuItem]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/menu/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/menu/manifests.ts new file mode 100644 index 0000000000..53819ffc24 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/menu/manifests.ts @@ -0,0 +1,40 @@ +import { UMB_MEMBER_TYPE_TREE_ALIAS } from '../tree/index.js'; + +export const manifests = [ + { + type: 'menuItem', + kind: 'tree', + alias: 'Umb.MenuItem.MemberTypes', + name: 'Member Type Menu Item', + weight: 700, + meta: { + label: 'Member Types', + treeAlias: UMB_MEMBER_TYPE_TREE_ALIAS, + menus: ['Umb.Menu.StructureSettings'], + }, + }, + { + type: 'workspaceContext', + name: 'Member Type Menu Structure Workspace Context', + alias: 'Umb.Context.MemberType.Menu.Structure', + api: () => import('./member-type-menu-structure.context.js'), + conditions: [ + { + alias: 'Umb.Condition.WorkspaceAlias', + match: 'Umb.Workspace.MemberType', + }, + ], + }, + { + type: 'workspaceFooterApp', + kind: 'menuBreadcrumb', + alias: 'Umb.WorkspaceFooterApp.MemberType.Breadcrumb', + name: 'Member Type Breadcrumb Workspace Footer App', + conditions: [ + { + alias: 'Umb.Condition.WorkspaceAlias', + match: 'Umb.Workspace.MemberType', + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/menu/member-type-menu-structure.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/menu/member-type-menu-structure.context.ts new file mode 100644 index 0000000000..b81cf8682f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/menu/member-type-menu-structure.context.ts @@ -0,0 +1,12 @@ +import { UMB_MEMBER_TYPE_TREE_REPOSITORY_ALIAS } from '../tree/index.js'; +import { UmbMenuTreeStructureWorkspaceContextBase } from '@umbraco-cms/backoffice/menu'; + +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; + +export class UmbMemberTypeMenuStructureWorkspaceContext extends UmbMenuTreeStructureWorkspaceContextBase { + constructor(host: UmbControllerHost) { + super(host, { treeRepositoryAlias: UMB_MEMBER_TYPE_TREE_REPOSITORY_ALIAS }); + } +} + +export default UmbMemberTypeMenuStructureWorkspaceContext; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/tree/member-type-tree.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/tree/member-type-tree.server.data-source.ts index aa8948ed8c..c593f6fa68 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/tree/member-type-tree.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/tree/member-type-tree.server.data-source.ts @@ -25,6 +25,7 @@ export class UmbMemberTypeTreeServerDataSource extends UmbTreeServerDataSourceBa super(host, { getRootItems, getChildrenOf, + getAncestorsOf, mapper, }); } @@ -42,6 +43,10 @@ const getChildrenOf = (args: UmbTreeChildrenOfRequestArgs) => { } }; +const getAncestorsOf = () => { + throw new Error('Not supported for the member type tree'); +}; + const mapper = (item: NamedEntityTreeItemResponseModel): UmbMemberTypeTreeItemModel => { return { unique: item.id, diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace-editor.element.ts index 08364b3615..71206a3cc8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace-editor.element.ts @@ -135,20 +135,6 @@ export class UmbMemberTypeWorkspaceEditorElement extends UmbLitElement { - -
    - - - Keyboard Shortcuts - - ALT - + - shift - + - k - - -
    `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context-token.ts index 3307c71b19..fe736c7d78 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context-token.ts @@ -1,9 +1,9 @@ import type { UmbMemberTypeWorkspaceContext } from './member-type-workspace.context.js'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import type { UmbSaveableWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; +import type { UmbSaveableWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; export const UMB_MEMBER_TYPE_WORKSPACE_CONTEXT = new UmbContextToken< - UmbSaveableWorkspaceContextInterface, + UmbSaveableWorkspaceContext, UmbMemberTypeWorkspaceContext >( 'UmbWorkspaceContext', diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context.ts index cc01f5917a..f96cff3dde 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context.ts @@ -3,7 +3,7 @@ import type { UmbMemberTypeDetailModel } from '../types.js'; import { UMB_MEMBER_TYPE_ENTITY_TYPE } from '../index.js'; import { UmbMemberTypeWorkspaceEditorElement } from './member-type-workspace-editor.element.js'; import { - UmbEditableWorkspaceContextBase, + UmbSaveableWorkspaceContextBase, type UmbRoutableWorkspaceContext, UmbWorkspaceRouteManager, UmbWorkspaceIsNewRedirectController, @@ -21,17 +21,21 @@ import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice type EntityType = UmbMemberTypeDetailModel; export class UmbMemberTypeWorkspaceContext - extends UmbEditableWorkspaceContextBase + extends UmbSaveableWorkspaceContextBase implements UmbContentTypeWorkspaceContext, UmbRoutableWorkspaceContext { readonly IS_CONTENT_TYPE_WORKSPACE_CONTEXT = true; public readonly repository = new UmbMemberTypeDetailRepository(this); - #parent?: { entityType: string; unique: string | null }; + + #parent = new UmbObjectState<{ entityType: string; unique: string | null } | undefined>(undefined); + readonly parentUnique = this.#parent.asObservablePart((parent) => (parent ? parent.unique : undefined)); + #persistedData = new UmbObjectState(undefined); // General for content types: readonly data; + readonly unique; readonly name; readonly alias; readonly description; @@ -52,6 +56,8 @@ export class UmbMemberTypeWorkspaceContext // General for content types: this.data = this.structure.ownerContentType; + + this.unique = this.structure.ownerContentTypeObservablePart((data) => data?.unique); this.name = this.structure.ownerContentTypeObservablePart((data) => data?.name); this.alias = this.structure.ownerContentTypeObservablePart((data) => data?.alias); this.description = this.structure.ownerContentTypeObservablePart((data) => data?.description); @@ -134,7 +140,7 @@ export class UmbMemberTypeWorkspaceContext async create(parent: { entityType: string; unique: string | null }) { this.resetState(); - this.#parent = parent; + this.#parent.setValue(parent); const { data } = await this.structure.createScaffold(); if (!data) return undefined; @@ -169,14 +175,15 @@ export class UmbMemberTypeWorkspaceContext if (data === undefined) throw new Error('Cannot save, no data'); if (this.getIsNew()) { - if (!this.#parent) throw new Error('Parent is not set'); - await this.repository.create(data, this.#parent.unique); + const parent = this.#parent.getValue(); + if (!parent) throw new Error('Parent is not set'); + await this.repository.create(data, parent.unique); // TODO: this might not be the right place to alert the tree, but it works for now const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); const event = new UmbReloadTreeItemChildrenRequestEntityActionEvent({ - entityType: this.#parent.entityType, - unique: this.#parent.unique, + entityType: parent.entityType, + unique: parent.unique, }); eventContext.dispatchEvent(event); } else { diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context-token.ts index a70bdcd274..4cbc630994 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context-token.ts @@ -1,11 +1,8 @@ import type { UmbMemberWorkspaceContext } from './member-workspace.context.js'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import type { UmbSaveableWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; +import type { UmbSaveableWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; -export const UMB_MEMBER_WORKSPACE_CONTEXT = new UmbContextToken< - UmbSaveableWorkspaceContextInterface, - UmbMemberWorkspaceContext ->( +export const UMB_MEMBER_WORKSPACE_CONTEXT = new UmbContextToken( 'UmbWorkspaceContext', undefined, (context): context is UmbMemberWorkspaceContext => context.getEntityType?.() === 'member', diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts index 5afc5fff60..ca3e423594 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts @@ -5,16 +5,12 @@ import { UMB_MEMBER_WORKSPACE_ALIAS } from './manifests.js'; import { UmbMemberWorkspaceEditorElement } from './member-workspace-editor.element.js'; import { UmbMemberTypeDetailRepository } from '@umbraco-cms/backoffice/member-type'; import { - UmbEditableWorkspaceContextBase, + UmbSaveableWorkspaceContextBase, UmbWorkspaceIsNewRedirectController, UmbWorkspaceRouteManager, UmbWorkspaceSplitViewManager, } from '@umbraco-cms/backoffice/workspace'; -import type { - UmbRoutableWorkspaceContext, - UmbVariantableWorkspaceContextInterface, - UmbWorkspaceContextInterface, -} from '@umbraco-cms/backoffice/workspace'; +import type { UmbRoutableWorkspaceContext, UmbVariantDatasetWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbArrayState, @@ -30,11 +26,8 @@ import type { UmbDataSourceResponse } from '@umbraco-cms/backoffice/repository'; type EntityType = UmbMemberDetailModel; export class UmbMemberWorkspaceContext - extends UmbEditableWorkspaceContextBase - implements - UmbVariantableWorkspaceContextInterface, - UmbRoutableWorkspaceContext, - UmbWorkspaceContextInterface + extends UmbSaveableWorkspaceContextBase + implements UmbVariantDatasetWorkspaceContext, UmbRoutableWorkspaceContext { public readonly repository = new UmbMemberDetailRepository(this); @@ -52,7 +45,7 @@ export class UmbMemberWorkspaceContext } readonly data = this.#currentData.asObservable(); - readonly name = this.#currentData.asObservablePart((data) => data?.variants[0].name); + readonly unique = this.#currentData.asObservablePart((data) => data?.unique); readonly createDate = this.#currentData.asObservablePart((data) => data?.variants[0].createDate); readonly updateDate = this.#currentData.asObservablePart((data) => data?.variants[0].updateDate); readonly contentTypeUnique = this.#currentData.asObservablePart((data) => data?.memberType.unique); @@ -65,8 +58,6 @@ export class UmbMemberWorkspaceContext readonly variants = this.#currentData.asObservablePart((data) => data?.variants ?? []); - readonly unique = this.#currentData.asObservablePart((data) => data?.unique); - readonly routes = new UmbWorkspaceRouteManager(this); readonly splitView = new UmbWorkspaceSplitViewManager(); @@ -238,6 +229,10 @@ export class UmbMemberWorkspaceContext this.#updateVariantData(variantId ?? UmbVariantId.CreateInvariant(), { name }); } + name(variantId?: UmbVariantId) { + return this.#currentData.asObservablePart((data) => data?.variants?.find((x) => variantId?.compare(x))?.name ?? ''); + } + async propertyStructureById(propertyId: string) { return this.structure.propertyStructureById(propertyId); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member-info.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member-info.element.ts index bd1c6d3fde..48025bc6a3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member-info.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member-info.element.ts @@ -1,10 +1,9 @@ // import { UMB_COMPOSITION_PICKER_MODAL, type UmbCompositionPickerModalData } from '../../../modals/index.js'; -import type { UmbMemberWorkspaceContext } from '../../member-workspace.context.js'; +import { UMB_MEMBER_WORKSPACE_CONTEXT } from '../../member-workspace.context-token.js'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/extension-registry'; -import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; import { UMB_WORKSPACE_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal'; import { UmbMemberTypeItemRepository } from '@umbraco-cms/backoffice/member-type'; @@ -17,7 +16,7 @@ export class UmbMemberWorkspaceViewMemberInfoElement extends UmbLitElement imple @state() private _memberTypeIcon = ''; - private _workspaceContext?: UmbMemberWorkspaceContext; + private _workspaceContext?: typeof UMB_MEMBER_WORKSPACE_CONTEXT.TYPE; private _memberTypeItemRepository: UmbMemberTypeItemRepository = new UmbMemberTypeItemRepository(this); @state() @@ -40,8 +39,8 @@ export class UmbMemberWorkspaceViewMemberInfoElement extends UmbLitElement imple this._editMemberTypePath = routeBuilder({}); }); - this.consumeContext(UMB_WORKSPACE_CONTEXT, async (nodeContext) => { - this._workspaceContext = nodeContext as UmbMemberWorkspaceContext; + this.consumeContext(UMB_MEMBER_WORKSPACE_CONTEXT, async (context) => { + this._workspaceContext = context; this.observe(this._workspaceContext.contentTypeUnique, (unique) => (this._memberTypeUnique = unique || '')); this.observe(this._workspaceContext.createDate, (date) => (this._createDate = date || 'Unknown')); this.observe(this._workspaceContext.updateDate, (date) => (this._updateDate = date || 'Unknown')); diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relation-types/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relation-types/manifests.ts index 12f1863639..3a315095df 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/relations/relation-types/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relation-types/manifests.ts @@ -1,12 +1,12 @@ import { manifests as repositoryManifests } from './repository/manifests.js'; -import { manifests as menuItemManifests } from './menu-item/manifests.js'; +import { manifests as menuManifests } from './menu/manifests.js'; import { manifests as treeManifests } from './tree/manifests.js'; import { manifests as workspaceManifests } from './workspace/manifests.js'; import { manifests as entityActionManifests } from './entity-actions/manifests.js'; export const manifests = [ ...repositoryManifests, - ...menuItemManifests, + ...menuManifests, ...treeManifests, ...workspaceManifests, ...entityActionManifests, diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relation-types/menu-item/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relation-types/menu-item/manifests.ts deleted file mode 100644 index ef111632a2..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/relations/relation-types/menu-item/manifests.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { UMB_RELATION_TYPE_TREE_ALIAS } from '../tree/index.js'; -import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; - -const menuItem: ManifestTypes = { - type: 'menuItem', - kind: 'tree', - alias: 'Umb.MenuItem.RelationTypes', - name: 'Relation Types Menu Item', - weight: 500, - meta: { - treeAlias: UMB_RELATION_TYPE_TREE_ALIAS, - label: 'Relation Types', - menus: ['Umb.Menu.Settings'], - }, -}; - -export const manifests = [menuItem]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relation-types/menu/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relation-types/menu/manifests.ts new file mode 100644 index 0000000000..0caf5e535d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relation-types/menu/manifests.ts @@ -0,0 +1,40 @@ +import { UMB_RELATION_TYPE_TREE_ALIAS } from '../tree/index.js'; + +export const manifests = [ + { + type: 'menuItem', + kind: 'tree', + alias: 'Umb.MenuItem.RelationTypes', + name: 'Relation Types Menu Item', + weight: 800, + meta: { + treeAlias: UMB_RELATION_TYPE_TREE_ALIAS, + label: 'Relation Types', + menus: ['Umb.Menu.AdvancedSettings'], + }, + }, + { + type: 'workspaceContext', + name: 'Relation Type Menu Structure Workspace Context', + alias: 'Umb.Context.RelationType.Menu.Structure', + api: () => import('./relation-type-menu-structure.context.js'), + conditions: [ + { + alias: 'Umb.Condition.WorkspaceAlias', + match: 'Umb.Workspace.RelationType', + }, + ], + }, + { + type: 'workspaceFooterApp', + kind: 'menbuBreadcrumb', + alias: 'Umb.WorkspaceFooterApp.RelationType.Breadcrumb', + name: 'Relation Type Breadcrumb Workspace Footer App', + conditions: [ + { + alias: 'Umb.Condition.WorkspaceAlias', + match: 'Umb.Workspace.RelationType', + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relation-types/menu/relation-type-menu-structure.context.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relation-types/menu/relation-type-menu-structure.context.ts new file mode 100644 index 0000000000..4a97a4bdcf --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relation-types/menu/relation-type-menu-structure.context.ts @@ -0,0 +1,11 @@ +import { UMB_RELATION_TYPE_TREE_REPOSITORY_ALIAS } from '../tree/index.js'; +import { UmbMenuTreeStructureWorkspaceContextBase } from '@umbraco-cms/backoffice/menu'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; + +export class UmbRelationTypeMenuStructureWorkspaceContext extends UmbMenuTreeStructureWorkspaceContextBase { + constructor(host: UmbControllerHost) { + super(host, { treeRepositoryAlias: UMB_RELATION_TYPE_TREE_REPOSITORY_ALIAS }); + } +} + +export default UmbRelationTypeMenuStructureWorkspaceContext; diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relation-types/tree/relation-type-tree.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relation-types/tree/relation-type-tree.server.data-source.ts index d513e4ccb3..4a176d8092 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/relations/relation-types/tree/relation-type-tree.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relation-types/tree/relation-type-tree.server.data-source.ts @@ -24,6 +24,7 @@ export class UmbRelationTypeTreeServerDataSource extends UmbTreeServerDataSource super(host, { getRootItems, getChildrenOf, + getAncestorsOf, mapper, }); } @@ -41,6 +42,10 @@ const getChildrenOf = (args: UmbTreeChildrenOfRequestArgs) => { } }; +const getAncestorsOf = () => { + throw new Error('Not supported for the relation type tree'); +}; + const mapper = (item: NamedEntityTreeItemResponseModel): UmbRelationTypeTreeItemModel => { return { unique: item.id, diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relation-types/workspace/relation-type-workspace.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relation-types/workspace/relation-type-workspace.context-token.ts index a9c560302b..2fcb61a826 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/relations/relation-types/workspace/relation-type-workspace.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relation-types/workspace/relation-type-workspace.context-token.ts @@ -1,9 +1,9 @@ import type { UmbRelationTypeWorkspaceContext } from './relation-type-workspace.context.js'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import type { UmbSaveableWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; +import type { UmbSaveableWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; export const UMB_RELATION_TYPE_WORKSPACE_CONTEXT = new UmbContextToken< - UmbSaveableWorkspaceContextInterface, + UmbSaveableWorkspaceContext, UmbRelationTypeWorkspaceContext >( 'UmbWorkspaceContext', diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relation-types/workspace/relation-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relation-types/workspace/relation-type-workspace.context.ts index 93784537b1..0319266191 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/relations/relation-types/workspace/relation-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relation-types/workspace/relation-type-workspace.context.ts @@ -1,8 +1,8 @@ import { UmbRelationTypeRepository } from '../repository/relation-type.repository.js'; import { UmbRelationTypeWorkspaceEditorElement } from './relation-type-workspace-editor.element.js'; import { - type UmbSaveableWorkspaceContextInterface, - UmbEditableWorkspaceContextBase, + type UmbSaveableWorkspaceContext, + UmbSaveableWorkspaceContextBase, type UmbRoutableWorkspaceContext, UmbWorkspaceRouteManager, UmbWorkspaceIsNewRedirectController, @@ -12,16 +12,18 @@ import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; export class UmbRelationTypeWorkspaceContext - extends UmbEditableWorkspaceContextBase - implements UmbSaveableWorkspaceContextInterface, UmbRoutableWorkspaceContext + extends UmbSaveableWorkspaceContextBase + implements UmbSaveableWorkspaceContext, UmbRoutableWorkspaceContext { // public readonly repository: UmbRelationTypeRepository = new UmbRelationTypeRepository(this); - #parent?: { entityType: string; unique: string | null }; + #parent = new UmbObjectState<{ entityType: string; unique: string | null } | undefined>(undefined); + readonly parentUnique = this.#parent.asObservablePart((parent) => (parent ? parent.unique : undefined)); #data = new UmbObjectState(undefined); readonly data = this.#data.asObservable(); + readonly unique = this.#data.asObservablePart((data) => data?.id); readonly name = this.#data.asObservablePart((data) => data?.name); readonly id = this.#data.asObservablePart((data) => data?.id); @@ -74,7 +76,7 @@ export class UmbRelationTypeWorkspaceContext async create(parent: { entityType: string; unique: string | null }) { this.resetState(); - this.#parent = parent; + this.#parent.setValue(parent); const { data } = await this.repository.createScaffold(); if (!data) return; this.setIsNew(true); diff --git a/src/Umbraco.Web.UI.Client/src/packages/static-file/tree/static-file-tree.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/static-file/tree/static-file-tree.server.data-source.ts index bb153b6af8..c88bf71e9d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/static-file/tree/static-file-tree.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/static-file/tree/static-file-tree.server.data-source.ts @@ -1,7 +1,11 @@ import { UMB_STATIC_FILE_ENTITY_TYPE, UMB_STATIC_FILE_FOLDER_ENTITY_TYPE } from '../entity.js'; import type { UmbStaticFileTreeItemModel } from './types.js'; import { UmbServerFilePathUniqueSerializer } from '@umbraco-cms/backoffice/server-file-system'; -import type { UmbTreeChildrenOfRequestArgs, UmbTreeRootItemsRequestArgs } from '@umbraco-cms/backoffice/tree'; +import type { + UmbTreeAncestorsOfRequestArgs, + UmbTreeChildrenOfRequestArgs, + UmbTreeRootItemsRequestArgs, +} from '@umbraco-cms/backoffice/tree'; import { UmbTreeServerDataSourceBase } from '@umbraco-cms/backoffice/tree'; import { StaticFileResource, @@ -28,6 +32,7 @@ export class UmbStaticFileTreeServerDataSource extends UmbTreeServerDataSourceBa super(host, { getRootItems, getChildrenOf, + getAncestorsOf, mapper, }); } @@ -50,6 +55,12 @@ const getChildrenOf = (args: UmbTreeChildrenOfRequestArgs) => { } }; +const getAncestorsOf = (args: UmbTreeAncestorsOfRequestArgs) => + // eslint-disable-next-line local-rules/no-direct-api-import + StaticFileResource.getTreeStaticFileAncestors({ + descendantPath: args.descendantUnique, + }); + const mapper = (item: FileSystemTreeItemPresentationModel): UmbStaticFileTreeItemModel => { const serializer = new UmbServerFilePathUniqueSerializer(); diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/code-editor/code-editor.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/code-editor/code-editor.stories.ts index cb94bb5bfc..df07650c41 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/code-editor/code-editor.stories.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/code-editor/code-editor.stories.ts @@ -113,7 +113,7 @@ const codeSnippets: Record = { "Smartypants, double quotes" and 'single quotes'`, typescript: `import { UmbTemplateRepository } from '../repository/template.repository.js'; - import { UmbWorkspaceContext } from '../../../shared/components/workspace/workspace-context/workspace-context.js'; + import { UmbWorkspaceContextBase } from '../../../shared/components/workspace/workspace-context/workspace-context.js'; import { UmbObjectState } from '@umbraco-cms/observable-api'; import { TemplateModel } from '@umbraco-cms/backend-api'; import { UmbControllerHostElement } from '@umbraco-cms/controller'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/menu.manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/menu.manifests.ts index 0b1437c2b5..a353119dea 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/menu.manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/menu.manifests.ts @@ -6,9 +6,6 @@ const menu: ManifestMenu = { type: 'menu', alias: 'Umb.Menu.Templating', name: 'Templating Menu', - meta: { - label: 'Templating', - }, }; const menuSectionSidebarApp: ManifestTypes = { @@ -16,7 +13,7 @@ const menuSectionSidebarApp: ManifestTypes = { kind: 'menu', alias: 'Umb.SectionSidebarMenu.Templating', name: 'Templating Section Sidebar Menu', - weight: 100, + weight: 200, meta: { label: 'Templating', menu: 'Umb.Menu.Templating', diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/manifests.ts index 1f440270ba..fda692d2f3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/manifests.ts @@ -1,12 +1,12 @@ import { manifests as repositoryManifests } from './repository/manifests.js'; -import { manifests as menuItemManifests } from './menu-item/manifests.js'; +import { manifests as menuManifests } from './menu/manifests.js'; import { manifests as treeManifests } from './tree/manifests.js'; import { manifests as entityActionsManifests } from './entity-actions/manifests.js'; import { manifests as workspaceManifests } from './workspace/manifests.js'; export const manifests = [ ...repositoryManifests, - ...menuItemManifests, + ...menuManifests, ...treeManifests, ...entityActionsManifests, ...workspaceManifests, diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/menu-item/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/menu-item/manifests.ts deleted file mode 100644 index 00b7384a60..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/menu-item/manifests.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { UMB_PARTIAL_VIEW_TREE_ALIAS } from '../tree/index.js'; -import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; - -const menuItem: ManifestTypes = { - type: 'menuItem', - kind: 'tree', - alias: 'Umb.MenuItem.PartialView', - name: 'Partial View Menu Item', - weight: 40, - meta: { - label: 'Partial Views', - treeAlias: UMB_PARTIAL_VIEW_TREE_ALIAS, - menus: ['Umb.Menu.Templating'], - }, -}; - -export const manifests = [menuItem]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/menu/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/menu/manifests.ts new file mode 100644 index 0000000000..005929ad4d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/menu/manifests.ts @@ -0,0 +1,40 @@ +import { UMB_PARTIAL_VIEW_TREE_ALIAS } from '../tree/index.js'; + +export const manifests = [ + { + type: 'menuItem', + kind: 'tree', + alias: 'Umb.MenuItem.PartialView', + name: 'Partial View Menu Item', + weight: 40, + meta: { + label: 'Partial Views', + treeAlias: UMB_PARTIAL_VIEW_TREE_ALIAS, + menus: ['Umb.Menu.Templating'], + }, + }, + { + type: 'workspaceContext', + name: 'Partial View Menu Structure Workspace Context', + alias: 'Umb.Context.PartialView.Menu.Structure', + api: () => import('./partial-view-menu-structure.context.js'), + conditions: [ + { + alias: 'Umb.Condition.WorkspaceAlias', + match: 'Umb.Workspace.PartialView', + }, + ], + }, + { + type: 'workspaceFooterApp', + kind: 'menuBreadcrumb', + alias: 'Umb.WorkspaceFooterApp.PartialView.Breadcrumb', + name: 'Partial View Breadcrumb Workspace Footer App', + conditions: [ + { + alias: 'Umb.Condition.WorkspaceAlias', + match: 'Umb.Workspace.PartialView', + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/menu/partial-view-menu-structure.context.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/menu/partial-view-menu-structure.context.ts new file mode 100644 index 0000000000..5357fe1ad1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/menu/partial-view-menu-structure.context.ts @@ -0,0 +1,11 @@ +import { UMB_PARTIAL_VIEW_TREE_REPOSITORY_ALIAS } from '../tree/index.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbMenuTreeStructureWorkspaceContextBase } from '@umbraco-cms/backoffice/menu'; + +export class UmbPartialViewMenuStructureWorkspaceContext extends UmbMenuTreeStructureWorkspaceContextBase { + constructor(host: UmbControllerHost) { + super(host, { treeRepositoryAlias: UMB_PARTIAL_VIEW_TREE_REPOSITORY_ALIAS }); + } +} + +export default UmbPartialViewMenuStructureWorkspaceContext; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/tree/partial-view-tree.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/tree/partial-view-tree.server.data-source.ts index 0780960c72..d718e6e5e1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/tree/partial-view-tree.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/tree/partial-view-tree.server.data-source.ts @@ -1,7 +1,11 @@ import { UMB_PARTIAL_VIEW_ENTITY_TYPE, UMB_PARTIAL_VIEW_FOLDER_ENTITY_TYPE } from '../entity.js'; import type { UmbPartialViewTreeItemModel } from './types.js'; import { UmbServerFilePathUniqueSerializer } from '@umbraco-cms/backoffice/server-file-system'; -import type { UmbTreeChildrenOfRequestArgs, UmbTreeRootItemsRequestArgs } from '@umbraco-cms/backoffice/tree'; +import type { + UmbTreeAncestorsOfRequestArgs, + UmbTreeChildrenOfRequestArgs, + UmbTreeRootItemsRequestArgs, +} from '@umbraco-cms/backoffice/tree'; import { UmbTreeServerDataSourceBase } from '@umbraco-cms/backoffice/tree'; import type { FileSystemTreeItemPresentationModel } from '@umbraco-cms/backoffice/external/backend-api'; import { PartialViewResource } from '@umbraco-cms/backoffice/external/backend-api'; @@ -26,6 +30,7 @@ export class UmbPartialViewTreeServerDataSource extends UmbTreeServerDataSourceB super(host, { getRootItems, getChildrenOf, + getAncestorsOf, mapper, }); } @@ -48,6 +53,16 @@ const getChildrenOf = (args: UmbTreeChildrenOfRequestArgs) => { } }; +const getAncestorsOf = (args: UmbTreeAncestorsOfRequestArgs) => { + const descendantPath = new UmbServerFilePathUniqueSerializer().toServerPath(args.descendantUnique); + if (!descendantPath) throw new Error('Descendant path is not available'); + + // eslint-disable-next-line local-rules/no-direct-api-import + return PartialViewResource.getTreePartialViewAncestors({ + descendantPath, + }); +}; + const mapper = (item: FileSystemTreeItemPresentationModel): UmbPartialViewTreeItemModel => { const serializer = new UmbServerFilePathUniqueSerializer(); diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/partial-view-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/partial-view-workspace-editor.element.ts index e334376665..1b1ca191e7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/partial-view-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/partial-view-workspace-editor.element.ts @@ -16,9 +16,6 @@ export class UmbPartialViewWorkspaceEditorElement extends UmbLitElement { @state() private _content?: string | null = ''; - @state() - private _path?: string | null = ''; - @state() private _ready: boolean = false; @@ -43,10 +40,6 @@ export class UmbPartialViewWorkspaceEditorElement extends UmbLitElement { this._content = content; }); - this.observe(this.#workspaceContext.path, (path) => { - this._path = path; - }); - this.observe(this.#workspaceContext.isCodeEditorReady, (isReady) => { this._ready = isReady; }); @@ -104,7 +97,6 @@ export class UmbPartialViewWorkspaceEditorElement extends UmbLitElement { @input=${this.#onNameInput} label="Partial view name" ?readonly=${this._isNew === false}> - Views/Partials${this._path}
    diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/partial-view-workspace.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/partial-view-workspace.context-token.ts index cd524599bf..d17a5ae6f0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/partial-view-workspace.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/partial-view-workspace.context-token.ts @@ -1,10 +1,10 @@ import { UMB_PARTIAL_VIEW_ENTITY_TYPE } from '../entity.js'; import type { UmbPartialViewWorkspaceContext } from './partial-view-workspace.context.js'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import type { UmbSaveableWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; +import type { UmbSaveableWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; export const UMB_PARTIAL_VIEW_WORKSPACE_CONTEXT = new UmbContextToken< - UmbSaveableWorkspaceContextInterface, + UmbSaveableWorkspaceContext, UmbPartialViewWorkspaceContext >( 'UmbWorkspaceContext', diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/partial-view-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/partial-view-workspace.context.ts index 764f7623d9..fdbe8b2d11 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/partial-view-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/partial-view-workspace.context.ts @@ -4,12 +4,9 @@ import { UMB_PARTIAL_VIEW_ENTITY_TYPE } from '../entity.js'; import { UmbPartialViewWorkspaceEditorElement } from './partial-view-workspace-editor.element.js'; import { UmbBooleanState, UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import type { - UmbRoutableWorkspaceContext, - UmbSaveableWorkspaceContextInterface, -} from '@umbraco-cms/backoffice/workspace'; +import type { UmbRoutableWorkspaceContext, UmbSaveableWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; import { - UmbEditableWorkspaceContextBase, + UmbSaveableWorkspaceContextBase, UmbWorkspaceIsNewRedirectController, UmbWorkspaceRouteManager, } from '@umbraco-cms/backoffice/workspace'; @@ -22,15 +19,17 @@ import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice import type { IRoutingInfo, PageComponent } from '@umbraco-cms/backoffice/router'; export class UmbPartialViewWorkspaceContext - extends UmbEditableWorkspaceContextBase - implements UmbSaveableWorkspaceContextInterface, UmbRoutableWorkspaceContext + extends UmbSaveableWorkspaceContextBase + implements UmbSaveableWorkspaceContext, UmbRoutableWorkspaceContext { public readonly repository = new UmbPartialViewDetailRepository(this); - #parent?: { entityType: string; unique: string | null }; + #parent = new UmbObjectState<{ entityType: string; unique: string | null } | undefined>(undefined); + readonly parentUnique = this.#parent.asObservablePart((parent) => (parent ? parent.unique : undefined)); #data = new UmbObjectState(undefined); readonly data = this.#data.asObservable(); + readonly unique = this.#data.asObservablePart((data) => data?.unique); readonly name = this.#data.asObservablePart((data) => data?.name); readonly content = this.#data.asObservablePart((data) => data?.content); readonly path = this.#data.asObservablePart((data) => data?.path); @@ -132,7 +131,7 @@ export class UmbPartialViewWorkspaceContext async create(parent: { entityType: string; unique: string | null }, snippetId?: string) { this.resetState(); - this.#parent = parent; + this.#parent.setValue(parent); let snippetContent = ''; if (snippetId) { @@ -154,14 +153,15 @@ export class UmbPartialViewWorkspaceContext let newData = undefined; if (this.getIsNew()) { - if (!this.#parent) throw new Error('Parent is not set'); - const { data } = await this.repository.create(this.#data.value, this.#parent.unique); + const parent = this.#parent.getValue(); + if (!parent) throw new Error('Parent is not set'); + const { data } = await this.repository.create(this.#data.value, parent.unique); // TODO: this might not be the right place to alert the tree, but it works for now const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); const event = new UmbReloadTreeItemChildrenRequestEntityActionEvent({ - entityType: this.#parent.entityType, - unique: this.#parent.unique, + entityType: parent.entityType, + unique: parent.unique, }); eventContext.dispatchEvent(event); diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/partial-view-workspace.element.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/partial-view-workspace.element.ts index 3dcaccdf8a..39d0d71736 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/partial-view-workspace.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/partial-view-workspace.element.ts @@ -6,6 +6,8 @@ import type { UmbRoute, IRoutingInfo, PageComponent } from '@umbraco-cms/backoff import { UmbWorkspaceIsNewRedirectController } from '@umbraco-cms/backoffice/workspace'; import '../../components/templating-item-menu/templating-item-menu.element.js'; +import { UmbExtensionsApiInitializer } from '@umbraco-cms/backoffice/extension-api'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; @customElement('umb-partial-view-workspace') export class UmbPartialViewWorkspaceElement extends UmbLitElement { @@ -44,6 +46,12 @@ export class UmbPartialViewWorkspaceElement extends UmbLitElement { }, ]; + constructor() { + super(); + // TODO: We need to recreate when ID changed? + new UmbExtensionsApiInitializer(this, umbExtensionsRegistry, 'workspaceContext', [this, this.#workspaceContext]); + } + #onCreate = async (parent: { entityType: string; unique: string | null }, snippetId?: string) => { await this.#workspaceContext.create(parent, snippetId); diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/index.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/index.ts index a545c0ae31..bc053ba3af 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/index.ts @@ -1,4 +1,5 @@ export * from './repository/index.js'; +export * from './workspace/script-workspace.context-token.js'; export * from './tree/index.js'; export * from './types.js'; export * from './entity.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/manifests.ts index 1f440270ba..97aaa29889 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/manifests.ts @@ -1,13 +1,13 @@ -import { manifests as repositoryManifests } from './repository/manifests.js'; -import { manifests as menuItemManifests } from './menu-item/manifests.js'; -import { manifests as treeManifests } from './tree/manifests.js'; import { manifests as entityActionsManifests } from './entity-actions/manifests.js'; +import { manifests as menuManifests } from './menu/manifests.js'; +import { manifests as repositoryManifests } from './repository/manifests.js'; +import { manifests as treeManifests } from './tree/manifests.js'; import { manifests as workspaceManifests } from './workspace/manifests.js'; export const manifests = [ - ...repositoryManifests, - ...menuItemManifests, - ...treeManifests, ...entityActionsManifests, + ...menuManifests, + ...repositoryManifests, + ...treeManifests, ...workspaceManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/menu-item/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/menu-item/manifests.ts deleted file mode 100644 index bf12d94dc7..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/menu-item/manifests.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { UMB_SCRIPT_TREE_ALIAS } from '../tree/index.js'; -import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; - -export const UMB_SCRIPT_MENU_ITEM_ALIAS = 'Umb.MenuItem.Script'; - -const menuItem: ManifestTypes = { - type: 'menuItem', - kind: 'tree', - alias: UMB_SCRIPT_MENU_ITEM_ALIAS, - name: 'Scripts Menu Item', - weight: 10, - meta: { - label: 'Scripts', - treeAlias: UMB_SCRIPT_TREE_ALIAS, - menus: ['Umb.Menu.Templating'], - }, -}; - -export const manifests = [menuItem]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/menu/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/menu/manifests.ts new file mode 100644 index 0000000000..ab0304a7dd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/menu/manifests.ts @@ -0,0 +1,42 @@ +import { UMB_SCRIPT_TREE_ALIAS } from '../tree/index.js'; + +export const UMB_SCRIPT_MENU_ITEM_ALIAS = 'Umb.MenuItem.Script'; + +export const manifests = [ + { + type: 'menuItem', + kind: 'tree', + alias: UMB_SCRIPT_MENU_ITEM_ALIAS, + name: 'Scripts Menu Item', + weight: 10, + meta: { + label: 'Scripts', + treeAlias: UMB_SCRIPT_TREE_ALIAS, + menus: ['Umb.Menu.Templating'], + }, + }, + { + type: 'workspaceContext', + name: 'Script Menu Structure Workspace Context', + alias: 'Umb.Context.Script.Menu.Structure', + api: () => import('./script-menu-structure.context.js'), + conditions: [ + { + alias: 'Umb.Condition.WorkspaceAlias', + match: 'Umb.Workspace.Script', + }, + ], + }, + { + type: 'workspaceFooterApp', + kind: 'menuBreadcrumb', + alias: 'Umb.WorkspaceFooterApp.Script.Breadcrumb', + name: 'Script Breadcrumb Workspace Footer App', + conditions: [ + { + alias: 'Umb.Condition.WorkspaceAlias', + match: 'Umb.Workspace.Script', + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/menu/script-menu-structure.context.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/menu/script-menu-structure.context.ts new file mode 100644 index 0000000000..458f13317f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/menu/script-menu-structure.context.ts @@ -0,0 +1,12 @@ +import { UMB_SCRIPT_TREE_REPOSITORY_ALIAS } from '../tree/index.js'; +import { UmbMenuTreeStructureWorkspaceContextBase } from '@umbraco-cms/backoffice/menu'; + +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; + +export class UmbScriptMenuStructureWorkspaceContext extends UmbMenuTreeStructureWorkspaceContextBase { + constructor(host: UmbControllerHost) { + super(host, { treeRepositoryAlias: UMB_SCRIPT_TREE_REPOSITORY_ALIAS }); + } +} + +export default UmbScriptMenuStructureWorkspaceContext; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/tree/script-tree.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/tree/script-tree.server.data-source.ts index af3b7c3f5c..0288114f2d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/tree/script-tree.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/tree/script-tree.server.data-source.ts @@ -1,7 +1,11 @@ import { UMB_SCRIPT_ENTITY_TYPE, UMB_SCRIPT_FOLDER_ENTITY_TYPE } from '../entity.js'; import type { UmbScriptTreeItemModel } from './types.js'; import { UmbServerFilePathUniqueSerializer } from '@umbraco-cms/backoffice/server-file-system'; -import type { UmbTreeChildrenOfRequestArgs, UmbTreeRootItemsRequestArgs } from '@umbraco-cms/backoffice/tree'; +import type { + UmbTreeAncestorsOfRequestArgs, + UmbTreeChildrenOfRequestArgs, + UmbTreeRootItemsRequestArgs, +} from '@umbraco-cms/backoffice/tree'; import { UmbTreeServerDataSourceBase } from '@umbraco-cms/backoffice/tree'; import type { FileSystemTreeItemPresentationModel } from '@umbraco-cms/backoffice/external/backend-api'; import { ScriptResource } from '@umbraco-cms/backoffice/external/backend-api'; @@ -26,6 +30,7 @@ export class UmbScriptTreeServerDataSource extends UmbTreeServerDataSourceBase< super(host, { getRootItems, getChildrenOf, + getAncestorsOf, mapper, }); } @@ -48,6 +53,16 @@ const getChildrenOf = (args: UmbTreeChildrenOfRequestArgs) => { } }; +const getAncestorsOf = (args: UmbTreeAncestorsOfRequestArgs) => { + const descendantPath = new UmbServerFilePathUniqueSerializer().toServerPath(args.descendantUnique); + if (!descendantPath) throw new Error('Descendant path is not available'); + + // eslint-disable-next-line local-rules/no-direct-api-import + return ScriptResource.getTreeScriptAncestors({ + descendantPath, + }); +}; + const mapper = (item: FileSystemTreeItemPresentationModel): UmbScriptTreeItemModel => { const serializer = new UmbServerFilePathUniqueSerializer(); diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/workspace/script-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/workspace/script-workspace-editor.element.ts index 9142f47d06..a8ba2ef651 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/workspace/script-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/workspace/script-workspace-editor.element.ts @@ -1,9 +1,8 @@ -import type { UmbScriptWorkspaceContext } from './script-workspace.context.js'; +import { UMB_SCRIPT_WORKSPACE_CONTEXT } from './script-workspace.context-token.js'; import type { UmbCodeEditorElement } from '@umbraco-cms/backoffice/code-editor'; import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; import type { UUIInputElement } from '@umbraco-cms/backoffice/external/uui'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; @customElement('umb-script-workspace-editor') @@ -14,40 +13,33 @@ export class UmbScriptWorkspaceEditorElement extends UmbLitElement { @state() private _content?: string | null = ''; - @state() - private _path?: string | null = ''; - @state() private _ready?: boolean = false; @state() private _isNew?: boolean = false; - #scriptsWorkspaceContext?: UmbScriptWorkspaceContext; + #context?: typeof UMB_SCRIPT_WORKSPACE_CONTEXT.TYPE; constructor() { super(); - this.consumeContext(UMB_WORKSPACE_CONTEXT, (workspaceContext) => { - this.#scriptsWorkspaceContext = workspaceContext as UmbScriptWorkspaceContext; + this.consumeContext(UMB_SCRIPT_WORKSPACE_CONTEXT, (context) => { + this.#context = context; - this.observe(this.#scriptsWorkspaceContext.name, (name) => { + this.observe(this.#context.name, (name) => { this._name = name; }); - this.observe(this.#scriptsWorkspaceContext.content, (content) => { + this.observe(this.#context.content, (content) => { this._content = content; }); - this.observe(this.#scriptsWorkspaceContext.path, (path) => { - this._path = path; - }); - - this.observe(this.#scriptsWorkspaceContext.isCodeEditorReady, (isReady) => { + this.observe(this.#context.isCodeEditorReady, (isReady) => { this._ready = isReady; }); - this.observe(this.#scriptsWorkspaceContext.isNew, (isNew) => { + this.observe(this.#context.isNew, (isNew) => { this._isNew = isNew; }); }); @@ -56,13 +48,13 @@ export class UmbScriptWorkspaceEditorElement extends UmbLitElement { #onNameInput(event: Event) { const target = event.target as UUIInputElement; const value = target.value as string; - this.#scriptsWorkspaceContext?.setName(value); + this.#context?.setName(value); } #onCodeEditorInput(event: Event) { const target = event.target as UmbCodeEditorElement; const value = target.code as string; - this.#scriptsWorkspaceContext?.setContent(value); + this.#context?.setContent(value); } #renderCodeEditor() { @@ -82,7 +74,6 @@ export class UmbScriptWorkspaceEditorElement extends UmbLitElement { @input=${this.#onNameInput} label="Script name" ?readonly=${this._isNew === false}> - /scripts${this._path}
    @@ -91,7 +82,7 @@ export class UmbScriptWorkspaceEditorElement extends UmbLitElement { ? this.#renderCodeEditor() : html`
    -
    `} + `}
    `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/workspace/script-workspace.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/workspace/script-workspace.context-token.ts new file mode 100644 index 0000000000..c5dd0234b5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/workspace/script-workspace.context-token.ts @@ -0,0 +1,9 @@ +import type { UmbScriptWorkspaceContext } from './script-workspace.context.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; +import type { UmbWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; + +export const UMB_SCRIPT_WORKSPACE_CONTEXT = new UmbContextToken( + 'UmbWorkspaceContext', + undefined, + (context): context is UmbScriptWorkspaceContext => context.getEntityType?.() === 'script', +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/workspace/script-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/workspace/script-workspace.context.ts index ea0c25b340..3500eef443 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/workspace/script-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/workspace/script-workspace.context.ts @@ -6,9 +6,9 @@ import { UmbScriptWorkspaceEditorElement } from './script-workspace-editor.eleme import { UmbBooleanState, UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { - UmbEditableWorkspaceContextBase, + UmbSaveableWorkspaceContextBase, type UmbRoutableWorkspaceContext, - type UmbSaveableWorkspaceContextInterface, + type UmbSaveableWorkspaceContext, UmbWorkspaceIsNewRedirectController, UmbWorkspaceRouteManager, } from '@umbraco-cms/backoffice/workspace'; @@ -19,15 +19,18 @@ import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice import type { IRoutingInfo, PageComponent } from '@umbraco-cms/backoffice/router'; export class UmbScriptWorkspaceContext - extends UmbEditableWorkspaceContextBase - implements UmbSaveableWorkspaceContextInterface, UmbRoutableWorkspaceContext + extends UmbSaveableWorkspaceContextBase + implements UmbSaveableWorkspaceContext, UmbRoutableWorkspaceContext { public readonly repository = new UmbScriptDetailRepository(this); - #parent?: { entityType: string; unique: string | null }; + #parent = new UmbObjectState<{ entityType: string; unique: string | null } | undefined>(undefined); + readonly parentUnique = this.#parent.asObservablePart((parent) => (parent ? parent.unique : undefined)); #data = new UmbObjectState(undefined); + readonly data = this.#data.asObservable(); + readonly unique = this.#data.asObservablePart((data) => data?.unique); readonly name = this.#data.asObservablePart((data) => data?.name); readonly content = this.#data.asObservablePart((data) => data?.content); readonly path = this.#data.asObservablePart((data) => data?.path); @@ -115,7 +118,7 @@ export class UmbScriptWorkspaceContext async create(parent: { entityType: string; unique: string | null }) { this.resetState(); - this.#parent = parent; + this.#parent.setValue(parent); const { data } = await this.repository.createScaffold(); if (data) { @@ -130,15 +133,16 @@ export class UmbScriptWorkspaceContext let newData = undefined; if (this.getIsNew()) { - if (!this.#parent) throw new Error('Parent is not set'); - const { data } = await this.repository.create(this.#data.value, this.#parent.unique); + const parent = this.#parent.getValue(); + if (!parent) throw new Error('Parent is not set'); + const { data } = await this.repository.create(this.#data.value, parent.unique); newData = data; // TODO: this might not be the right place to alert the tree, but it works for now const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); const event = new UmbReloadTreeItemChildrenRequestEntityActionEvent({ - entityType: this.#parent.entityType, - unique: this.#parent.unique, + entityType: parent.entityType, + unique: parent.unique, }); eventContext.dispatchEvent(event); } else { diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/manifests.ts index 1c9fbda363..28bf047ad6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/manifests.ts @@ -1,5 +1,5 @@ import { manifests as repositoryManifests } from './repository/manifests.js'; -import { manifests as menuItemManifests } from './menu-item/manifests.js'; +import { manifests as menuManifests } from './menu/manifests.js'; import { manifests as treeManifests } from './tree/manifests.js'; import { manifests as workspaceManifests } from './workspace/manifests.js'; import { manifests as entityActionManifests } from './entity-actions/manifests.js'; @@ -7,7 +7,7 @@ import { manifests as componentManifests } from './components/manifests.js'; export const manifests = [ ...repositoryManifests, - ...menuItemManifests, + ...menuManifests, ...treeManifests, ...workspaceManifests, ...entityActionManifests, diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/menu-item/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/menu-item/manifests.ts deleted file mode 100644 index 4c6e87b49b..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/menu-item/manifests.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { UMB_STYLESHEET_TREE_ALIAS } from '../tree/manifests.js'; -import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; - -const menuItem: ManifestTypes = { - type: 'menuItem', - kind: 'tree', - alias: 'Umb.MenuItem.Stylesheets', - name: 'Stylesheets Menu Item', - weight: 20, - meta: { - label: 'Stylesheets', - treeAlias: UMB_STYLESHEET_TREE_ALIAS, - menus: ['Umb.Menu.Templating'], - }, -}; - -export const manifests = [menuItem]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/menu/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/menu/manifests.ts new file mode 100644 index 0000000000..2602fcf894 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/menu/manifests.ts @@ -0,0 +1,40 @@ +import { UMB_STYLESHEET_TREE_ALIAS } from '../tree/index.js'; + +export const manifests = [ + { + type: 'menuItem', + kind: 'tree', + alias: 'Umb.MenuItem.Stylesheets', + name: 'Stylesheets Menu Item', + weight: 20, + meta: { + label: 'Stylesheets', + treeAlias: UMB_STYLESHEET_TREE_ALIAS, + menus: ['Umb.Menu.Templating'], + }, + }, + { + type: 'workspaceContext', + name: 'Stylesheet Menu Structure Workspace Context', + alias: 'Umb.Context.Stylesheet.Menu.Structure', + api: () => import('./stylesheet-menu-structure.context.js'), + conditions: [ + { + alias: 'Umb.Condition.WorkspaceAlias', + match: 'Umb.Workspace.Stylesheet', + }, + ], + }, + { + type: 'workspaceFooterApp', + kind: 'menuBreadcrumb', + alias: 'Umb.WorkspaceFooterApp.Stylesheet.Breadcrumb', + name: 'Stylesheet Breadcrumb Workspace Footer App', + conditions: [ + { + alias: 'Umb.Condition.WorkspaceAlias', + match: 'Umb.Workspace.Stylesheet', + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/menu/stylesheet-menu-structure.context.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/menu/stylesheet-menu-structure.context.ts new file mode 100644 index 0000000000..53dbaa7d79 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/menu/stylesheet-menu-structure.context.ts @@ -0,0 +1,11 @@ +import { UMB_STYLESHEET_TREE_REPOSITORY_ALIAS } from '../tree/index.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbMenuTreeStructureWorkspaceContextBase } from '@umbraco-cms/backoffice/menu'; + +export class UmbStylesheetMenuStructureWorkspaceContext extends UmbMenuTreeStructureWorkspaceContextBase { + constructor(host: UmbControllerHost) { + super(host, { treeRepositoryAlias: UMB_STYLESHEET_TREE_REPOSITORY_ALIAS }); + } +} + +export default UmbStylesheetMenuStructureWorkspaceContext; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/tree/stylesheet-tree.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/tree/stylesheet-tree.server.data-source.ts index 9ee7197a01..eb7ed09eef 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/tree/stylesheet-tree.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/tree/stylesheet-tree.server.data-source.ts @@ -4,7 +4,11 @@ import { UmbServerFilePathUniqueSerializer } from '@umbraco-cms/backoffice/serve import type { FileSystemTreeItemPresentationModel } from '@umbraco-cms/backoffice/external/backend-api'; import { StylesheetResource } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import type { UmbTreeChildrenOfRequestArgs, UmbTreeRootItemsRequestArgs } from '@umbraco-cms/backoffice/tree'; +import type { + UmbTreeAncestorsOfRequestArgs, + UmbTreeChildrenOfRequestArgs, + UmbTreeRootItemsRequestArgs, +} from '@umbraco-cms/backoffice/tree'; import { UmbTreeServerDataSourceBase } from '@umbraco-cms/backoffice/tree'; /** @@ -26,6 +30,7 @@ export class UmbStylesheetTreeServerDataSource extends UmbTreeServerDataSourceBa super(host, { getRootItems, getChildrenOf, + getAncestorsOf, mapper, }); } @@ -48,6 +53,16 @@ const getChildrenOf = (args: UmbTreeChildrenOfRequestArgs) => { } }; +const getAncestorsOf = (args: UmbTreeAncestorsOfRequestArgs) => { + const descendantPath = new UmbServerFilePathUniqueSerializer().toServerPath(args.descendantUnique); + if (!descendantPath) throw new Error('Descendant path is not available'); + + // eslint-disable-next-line local-rules/no-direct-api-import + return StylesheetResource.getTreeStylesheetAncestors({ + descendantPath, + }); +}; + const mapper = (item: FileSystemTreeItemPresentationModel): UmbStylesheetTreeItemModel => { const serializer = new UmbServerFilePathUniqueSerializer(); diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/manifests.ts index 5cb79e3ca8..ebada289ad 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/manifests.ts @@ -5,7 +5,7 @@ import type { } from '@umbraco-cms/backoffice/extension-registry'; import { UmbSaveWorkspaceAction } from '@umbraco-cms/backoffice/workspace'; -export const UMB_STYLESHEET_WORKSPACE_ALIAS = 'Umb.Workspace.StyleSheet'; +export const UMB_STYLESHEET_WORKSPACE_ALIAS = 'Umb.Workspace.Stylesheet'; const workspace: ManifestWorkspaces = { type: 'workspace', diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace-editor.element.ts index ad7e644241..3adf3431dd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace-editor.element.ts @@ -1,7 +1,6 @@ -import type { UmbStylesheetWorkspaceContext } from './stylesheet-workspace.context.js'; +import { UMB_STYLESHEET_WORKSPACE_CONTEXT } from './stylesheet-workspace.context-token.js'; import type { UUIInputElement, UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; -import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; @@ -10,21 +9,16 @@ export class UmbStylesheetWorkspaceEditorElement extends UmbLitElement { @state() private _isNew?: boolean = false; - @state() - private _path?: string; - @state() private _name?: string; - #workspaceContext?: UmbStylesheetWorkspaceContext; + #workspaceContext?: typeof UMB_STYLESHEET_WORKSPACE_CONTEXT.TYPE; constructor() { super(); - this.consumeContext(UMB_WORKSPACE_CONTEXT, (instance) => { - this.#workspaceContext = instance as UmbStylesheetWorkspaceContext; - - this.observe(this.#workspaceContext.path, (path) => (this._path = path), '_observeStylesheetPath'); + this.consumeContext(UMB_STYLESHEET_WORKSPACE_CONTEXT, (context) => { + this.#workspaceContext = context; this.observe(this.#workspaceContext.name, (name) => (this._name = name), '_observeStylesheetName'); @@ -52,7 +46,6 @@ export class UmbStylesheetWorkspaceEditorElement extends UmbLitElement { @input="${this.#onNameChange}" ?readonly=${this._isNew === false}> - /css${this._path} `; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace.context-token.ts index cb0c5e25af..71b9cda4f1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace.context-token.ts @@ -1,10 +1,10 @@ import { UMB_STYLESHEET_ENTITY_TYPE } from '../entity.js'; import type { UmbStylesheetWorkspaceContext } from './stylesheet-workspace.context.js'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import type { UmbSaveableWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; +import type { UmbSaveableWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; export const UMB_STYLESHEET_WORKSPACE_CONTEXT = new UmbContextToken< - UmbSaveableWorkspaceContextInterface, + UmbSaveableWorkspaceContext, UmbStylesheetWorkspaceContext >( 'UmbWorkspaceContext', diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace.context.ts index db162fac11..181d4eb60e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace.context.ts @@ -4,8 +4,8 @@ import { UMB_STYLESHEET_ENTITY_TYPE } from '../entity.js'; import { UMB_STYLESHEET_WORKSPACE_ALIAS } from './manifests.js'; import { UmbStylesheetWorkspaceEditorElement } from './stylesheet-workspace-editor.element.js'; import { - type UmbSaveableWorkspaceContextInterface, - UmbEditableWorkspaceContextBase, + type UmbSaveableWorkspaceContext, + UmbSaveableWorkspaceContextBase, UmbWorkspaceRouteManager, UmbWorkspaceIsNewRedirectController, type UmbRoutableWorkspaceContext, @@ -19,15 +19,17 @@ import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice import type { IRoutingInfo, PageComponent } from '@umbraco-cms/backoffice/router'; export class UmbStylesheetWorkspaceContext - extends UmbEditableWorkspaceContextBase - implements UmbSaveableWorkspaceContextInterface, UmbRoutableWorkspaceContext + extends UmbSaveableWorkspaceContextBase + implements UmbSaveableWorkspaceContext, UmbRoutableWorkspaceContext { public readonly repository = new UmbStylesheetDetailRepository(this); - #parent?: { entityType: string; unique: string | null }; + #parent = new UmbObjectState<{ entityType: string; unique: string | null } | undefined>(undefined); + readonly parentUnique = this.#parent.asObservablePart((parent) => (parent ? parent.unique : undefined)); #data = new UmbObjectState(undefined); readonly data = this.#data.asObservable(); + readonly unique = this.#data.asObservablePart((data) => data?.unique); readonly name = this.#data.asObservablePart((data) => data?.name); readonly content = this.#data.asObservablePart((data) => data?.content); readonly path = this.#data.asObservablePart((data) => data?.path); @@ -116,7 +118,7 @@ export class UmbStylesheetWorkspaceContext async create(parent: { entityType: string; unique: string | null }) { this.resetState(); - this.#parent = parent; + this.#parent.setValue(parent); const { data } = await this.repository.createScaffold(); if (data) { @@ -131,15 +133,16 @@ export class UmbStylesheetWorkspaceContext let newData = undefined; if (this.getIsNew()) { - if (!this.#parent) throw new Error('Parent is not set'); - const { data } = await this.repository.create(this.#data.value, this.#parent.unique); + const parent = this.#parent.getValue(); + if (!parent) throw new Error('Parent is not set'); + const { data } = await this.repository.create(this.#data.value, parent.unique); newData = data; // TODO: this might not be the right place to alert the tree, but it works for now const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); const event = new UmbReloadTreeItemChildrenRequestEntityActionEvent({ - entityType: this.#parent.entityType, - unique: this.#parent.unique, + entityType: parent.entityType, + unique: parent.unique, }); eventContext.dispatchEvent(event); } else { diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/views/rich-text-rule/stylesheet-rich-text-rule-workspace-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/views/rich-text-rule/stylesheet-rich-text-rule-workspace-view.element.ts index f55d91bf51..047069c5db 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/views/rich-text-rule/stylesheet-rich-text-rule-workspace-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/views/rich-text-rule/stylesheet-rich-text-rule-workspace-view.element.ts @@ -1,10 +1,9 @@ -import type { UmbStylesheetWorkspaceContext } from '../../stylesheet-workspace.context.js'; import type { UmbStylesheetRule } from '../../../types.js'; import type { UmbStylesheetRuleInputElement } from '../../../components/index.js'; import { UmbStylesheetRuleManager } from '../../../utils/index.js'; +import { UMB_STYLESHEET_WORKSPACE_CONTEXT } from '../../stylesheet-workspace.context-token.js'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; import type { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; @@ -13,15 +12,15 @@ export class UmbStylesheetRichTextRuleWorkspaceViewElement extends UmbLitElement @state() _rules: UmbStylesheetRule[] = []; - #context?: UmbStylesheetWorkspaceContext; + #context?: typeof UMB_STYLESHEET_WORKSPACE_CONTEXT.TYPE; #stylesheetRuleManager = new UmbStylesheetRuleManager(); #stylesheetContent = ''; constructor() { super(); - this.consumeContext(UMB_WORKSPACE_CONTEXT, (workspaceContext) => { - this.#context = workspaceContext as UmbStylesheetWorkspaceContext; + this.consumeContext(UMB_STYLESHEET_WORKSPACE_CONTEXT, (context) => { + this.#context = context; this.#observeContent(); }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/manifests.ts index d455cacfe6..8eaf1de363 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/manifests.ts @@ -1,5 +1,5 @@ import { manifests as repositoryManifests } from './repository/manifests.js'; -import { manifests as menuItemManifests } from './menu-item/manifests.js'; +import { manifests as menuManifests } from './menu/manifests.js'; import { manifests as treeManifests } from './tree/manifests.js'; import { manifests as entityActionsManifests } from './entity-actions/manifests.js'; import { manifests as workspaceManifests } from './workspace/manifests.js'; @@ -8,7 +8,7 @@ import { manifests as modalManifests } from './modals/manifests.js'; export const manifests = [ ...modalManifests, ...repositoryManifests, - ...menuItemManifests, + ...menuManifests, ...treeManifests, ...entityActionsManifests, ...workspaceManifests, diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/menu-item/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/menu-item/manifests.ts deleted file mode 100644 index 7cf603c07f..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/menu-item/manifests.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { UMB_TEMPLATE_TREE_ALIAS } from '../tree/index.js'; -import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; - -const menuItem: ManifestTypes = { - type: 'menuItem', - kind: 'tree', - alias: 'Umb.MenuItem.Templates', - name: 'Templates Menu Item', - weight: 40, - meta: { - label: 'Templates', - entityType: 'template', - treeAlias: UMB_TEMPLATE_TREE_ALIAS, - menus: ['Umb.Menu.Templating'], - }, -}; - -export const manifests = [menuItem]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/menu/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/menu/manifests.ts new file mode 100644 index 0000000000..99a781953f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/menu/manifests.ts @@ -0,0 +1,41 @@ +import { UMB_TEMPLATE_TREE_ALIAS } from '../tree/index.js'; + +export const manifests = [ + { + type: 'menuItem', + kind: 'tree', + alias: 'Umb.MenuItem.Templates', + name: 'Templates Menu Item', + weight: 40, + meta: { + label: 'Templates', + entityType: 'template', + treeAlias: UMB_TEMPLATE_TREE_ALIAS, + menus: ['Umb.Menu.Templating'], + }, + }, + { + type: 'workspaceContext', + name: 'Template Menu Structure Workspace Context', + alias: 'Umb.Context.Template.Menu.Structure', + api: () => import('./template-menu-structure.context.js'), + conditions: [ + { + alias: 'Umb.Condition.WorkspaceAlias', + match: 'Umb.Workspace.Template', + }, + ], + }, + { + type: 'workspaceFooterApp', + kind: 'menuBreadcrumb', + alias: 'Umb.WorkspaceFooterApp.Template.Breadcrumb', + name: 'Template Breadcrumb Workspace Footer App', + conditions: [ + { + alias: 'Umb.Condition.WorkspaceAlias', + match: 'Umb.Workspace.Template', + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/menu/template-menu-structure.context.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/menu/template-menu-structure.context.ts new file mode 100644 index 0000000000..a3e8c70aa2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/menu/template-menu-structure.context.ts @@ -0,0 +1,11 @@ +import { UMB_TEMPLATE_TREE_REPOSITORY_ALIAS } from '../tree/index.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbMenuTreeStructureWorkspaceContextBase } from '@umbraco-cms/backoffice/menu'; + +export class UmbTemplateMenuStructureWorkspaceContext extends UmbMenuTreeStructureWorkspaceContextBase { + constructor(host: UmbControllerHost) { + super(host, { treeRepositoryAlias: UMB_TEMPLATE_TREE_REPOSITORY_ALIAS }); + } +} + +export default UmbTemplateMenuStructureWorkspaceContext; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/tree/template-tree.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/tree/template-tree.server.data-source.ts index a8c15cc1b1..d4b21ae0d0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/tree/template-tree.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/tree/template-tree.server.data-source.ts @@ -1,6 +1,10 @@ import { UMB_TEMPLATE_ENTITY_TYPE } from '../entity.js'; import type { UmbTemplateTreeItemModel } from './types.js'; -import type { UmbTreeChildrenOfRequestArgs, UmbTreeRootItemsRequestArgs } from '@umbraco-cms/backoffice/tree'; +import type { + UmbTreeAncestorsOfRequestArgs, + UmbTreeChildrenOfRequestArgs, + UmbTreeRootItemsRequestArgs, +} from '@umbraco-cms/backoffice/tree'; import { UmbTreeServerDataSourceBase } from '@umbraco-cms/backoffice/tree'; import type { NamedEntityTreeItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; import { TemplateResource } from '@umbraco-cms/backoffice/external/backend-api'; @@ -25,6 +29,7 @@ export class UmbTemplateTreeServerDataSource extends UmbTreeServerDataSourceBase super(host, { getRootItems, getChildrenOf, + getAncestorsOf, mapper, }); } @@ -45,6 +50,12 @@ const getChildrenOf = (args: UmbTreeChildrenOfRequestArgs) => { } }; +const getAncestorsOf = (args: UmbTreeAncestorsOfRequestArgs) => + // eslint-disable-next-line local-rules/no-direct-api-import + TemplateResource.getTreeTemplateAncestors({ + descendantId: args.descendantUnique, + }); + const mapper = (item: NamedEntityTreeItemResponseModel): UmbTemplateTreeItemModel => { return { unique: item.id, diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace.context-token.ts index 808766310c..34029fdd7d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace.context-token.ts @@ -1,9 +1,9 @@ import type { UmbTemplateWorkspaceContext } from './template-workspace.context.js'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import type { UmbSaveableWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; +import type { UmbSaveableWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; export const UMB_TEMPLATE_WORKSPACE_CONTEXT = new UmbContextToken< - UmbSaveableWorkspaceContextInterface, + UmbSaveableWorkspaceContext, UmbTemplateWorkspaceContext >( 'UmbWorkspaceContext', diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace.context.ts index 2461875df8..7a1184520d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace.context.ts @@ -4,12 +4,9 @@ import { UmbTemplateDetailRepository, UmbTemplateItemRepository } from '../repos import { UMB_TEMPLATE_WORKSPACE_ALIAS } from './manifests.js'; import { UmbTemplateWorkspaceEditorElement } from './template-workspace-editor.element.js'; import { loadCodeEditor } from '@umbraco-cms/backoffice/code-editor'; -import type { - UmbRoutableWorkspaceContext, - UmbSaveableWorkspaceContextInterface, -} from '@umbraco-cms/backoffice/workspace'; +import type { UmbRoutableWorkspaceContext, UmbSaveableWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; import { - UmbEditableWorkspaceContextBase, + UmbSaveableWorkspaceContextBase, UmbWorkspaceIsNewRedirectController, UmbWorkspaceRouteManager, } from '@umbraco-cms/backoffice/workspace'; @@ -21,13 +18,14 @@ import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice import type { IRoutingInfo, PageComponent } from '@umbraco-cms/backoffice/router'; export class UmbTemplateWorkspaceContext - extends UmbEditableWorkspaceContextBase - implements UmbSaveableWorkspaceContextInterface, UmbRoutableWorkspaceContext + extends UmbSaveableWorkspaceContextBase + implements UmbSaveableWorkspaceContext, UmbRoutableWorkspaceContext { public readonly detailRepository = new UmbTemplateDetailRepository(this); public readonly itemRepository = new UmbTemplateItemRepository(this); - #parent?: { entityType: string; unique: string | null }; + #parent = new UmbObjectState<{ entityType: string; unique: string | null } | undefined>(undefined); + readonly parentUnique = this.#parent.asObservablePart((parent) => (parent ? parent.unique : undefined)); #data = new UmbObjectState(undefined); data = this.#data.asObservable(); @@ -36,7 +34,7 @@ export class UmbTemplateWorkspaceContext name = this.#data.asObservablePart((data) => data?.name); alias = this.#data.asObservablePart((data) => data?.alias); content = this.#data.asObservablePart((data) => data?.content); - unique = this.#data.asObservablePart((data) => data?.unique); + readonly unique = this.#data.asObservablePart((data) => data?.unique); masterTemplateUnique = this.#data.asObservablePart((data) => data?.masterTemplate?.unique); #isCodeEditorReady = new UmbBooleanState(false); @@ -178,14 +176,14 @@ ${currentContent}`; async create(parent: { entityType: string; unique: string | null }) { this.resetState(); - this.#parent = parent; + this.#parent.setValue(parent); const { data } = await this.detailRepository.createScaffold(); if (!data) return; this.setIsNew(true); this.#data.setValue(data); - if (!this.#parent) return; - await this.setMasterTemplate(this.#parent.unique); + if (!parent) return; + await this.setMasterTemplate(parent.unique); } async save() { @@ -194,15 +192,16 @@ ${currentContent}`; let newData = undefined; if (this.getIsNew()) { - if (!this.#parent) throw new Error('Parent is not set'); - const { data } = await this.detailRepository.create(this.#data.value, this.#parent.unique); + const parent = this.#parent.getValue(); + if (!parent) throw new Error('Parent is not set'); + const { data } = await this.detailRepository.create(this.#data.value, parent.unique); newData = data; // TODO: this might not be the right place to alert the tree, but it works for now const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); const event = new UmbReloadTreeItemChildrenRequestEntityActionEvent({ - entityType: this.#parent.entityType, - unique: this.#parent.unique, + entityType: parent.entityType, + unique: parent.unique, }); eventContext.dispatchEvent(event); } else { diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/input-tiny-mce.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/input-tiny-mce.element.ts index 94518a0814..7ccc036d14 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/input-tiny-mce.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/input-tiny-mce.element.ts @@ -138,7 +138,8 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) { const stylesheetResponses = await Promise.all(promises); stylesheetResponses.forEach(({ data }) => { - if (!data) return; + if (!data?.content) return; + const rulesFromContent = this.#umbStylesheetRuleManager.extractRules(data.content); rulesFromContent.forEach((rule) => { @@ -194,8 +195,8 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) { // create an object by merging the configuration onto the fallback config const configurationOptions: RawEditorOptions = { ...defaultFallbackConfig, - height: dimensions?.height, - width: dimensions?.width, + height: dimensions?.height || undefined, + width: dimensions?.width || undefined, content_css: stylesheets, style_formats: styleFormats, }; @@ -207,10 +208,12 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) { } } - // set the configured toolbar if any + // set the configured toolbar if any, otherwise false const toolbar = this.configuration?.getValueByAlias('toolbar'); - if (toolbar) { - configurationOptions.toolbar = toolbar.join(' '); + if (toolbar && toolbar.length) { + configurationOptions.toolbar = toolbar?.join(' '); + } else { + configurationOptions.toolbar = false; } // set the configured inline mode @@ -221,7 +224,7 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) { // set the maximum image size const maxImageSize = this.configuration?.getValueByAlias('maxImageSize'); - if (maxImageSize !== undefined) { + if (maxImageSize) { configurationOptions.maxImageSize = maxImageSize; } @@ -332,7 +335,6 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) { } }); }); - editor.on('init', () => editor.setContent(this.value?.toString() ?? '')); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/index.ts b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/index.ts index 8a8c2711ca..97c9d86639 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/index.ts @@ -1 +1,2 @@ export * from './components/index.js'; +export * from './modals/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/manifests.ts index 79e9894cd5..23c631bd79 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/manifests.ts @@ -1,4 +1,5 @@ import { manifests as propertyEditors } from './property-editors/manifests.js'; import { manifests as plugins } from './plugins/manifests.js'; +import { manifests as modalManifests } from './modals/manifests.js'; -export const manifests = [...propertyEditors, ...plugins]; +export const manifests = [...propertyEditors, ...plugins, ...modalManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/modals/index.ts b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/modals/index.ts new file mode 100644 index 0000000000..011799ec00 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/modals/index.ts @@ -0,0 +1 @@ +export * from './media-caption-alt-text/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/modals/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/modals/manifests.ts new file mode 100644 index 0000000000..20054110d1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/modals/manifests.ts @@ -0,0 +1,12 @@ +import type { ManifestModal } from '@umbraco-cms/backoffice/extension-registry'; + +const modals: Array = [ + { + type: 'modal', + alias: 'Umb.Modal.MediaCaptionAltText', + name: 'Media Caption Alt Text', + js: () => import('./media-caption-alt-text/media-caption-alt-text-modal.element.js'), + }, +]; + +export const manifests = [...modals]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/modals/media-caption-alt-text/index.ts b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/modals/media-caption-alt-text/index.ts new file mode 100644 index 0000000000..39ec155360 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/modals/media-caption-alt-text/index.ts @@ -0,0 +1,2 @@ +export * from './media-caption-alt-text-modal.element.js'; +export * from './media-caption-alt-text-modal.token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/modals/media-caption-alt-text/media-caption-alt-text-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/modals/media-caption-alt-text/media-caption-alt-text-modal.element.ts new file mode 100644 index 0000000000..b4168cb083 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/modals/media-caption-alt-text/media-caption-alt-text-modal.element.ts @@ -0,0 +1,87 @@ +import type { + UmbMediaCaptionAltTextModalData, + UmbMediaCaptionAltTextModalValue, +} from './media-caption-alt-text-modal.token.js'; +import { css, html, customElement, state, ifDefined } from '@umbraco-cms/backoffice/external/lit'; +import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; +import '@umbraco-cms/backoffice/block-type'; +import { UmbMediaDetailRepository } from '@umbraco-cms/backoffice/media'; +import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; + +@customElement('umb-media-caption-alt-text-modal') +export class UmbMediaCaptionAltTextModalElement extends UmbModalBaseElement< + UmbMediaCaptionAltTextModalData, + UmbMediaCaptionAltTextModalValue +> { + #mediaUnique?: string; + #mediaDetailRepository = new UmbMediaDetailRepository(this); + + connectedCallback() { + super.connectedCallback(); + this.#mediaUnique = this.data?.mediaUnique; + this.#getMediaDetail(); + } + + async #getMediaDetail() { + if (!this.#mediaUnique) return; + const { data } = await this.#mediaDetailRepository.requestByUnique(this.#mediaUnique); + if (!data) return; + + this.value = { altText: data.variants[0].name, caption: undefined, url: data.urls[0]?.url ?? '' }; + } + + render() { + return html` + +
    + ${this.localize.term('content_altTextOptional')} + + (this.value = { ...this.value, altText: e.target.value as string })}> + + ${this.localize.term('content_captionTextOptional')} + + (this.value = { ...this.value, caption: e.target.value as string })}> + + ${this.value?.altText + ${this.value?.caption ?? ''} +
    +
    + + +
    +
    + `; + } + + static styles = [ + css` + uui-input { + margin-bottom: var(--uui-size-layout-1); + } + + #wrapper { + display: flex; + flex-direction: column; + } + `, + ]; +} + +export default UmbMediaCaptionAltTextModalElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-media-caption-alt-text-modal': UmbMediaCaptionAltTextModalElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/modals/media-caption-alt-text/media-caption-alt-text-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/modals/media-caption-alt-text/media-caption-alt-text-modal.token.ts new file mode 100644 index 0000000000..dadf0318f3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/modals/media-caption-alt-text/media-caption-alt-text-modal.token.ts @@ -0,0 +1,21 @@ +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; + +export interface UmbMediaCaptionAltTextModalData { + mediaUnique: string; +} + +export type UmbMediaCaptionAltTextModalValue = { + altText?: string; + caption?: string; + url: string; +}; + +export const UMB_MEDIA_CAPTION_ALT_TEXT_MODAL = new UmbModalToken< + UmbMediaCaptionAltTextModalData, + UmbMediaCaptionAltTextModalValue +>('Umb.Modal.MediaCaptionAltText', { + modal: { + type: 'sidebar', + size: 'small', + }, +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-embeddedmedia.plugin.ts b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-embeddedmedia.plugin.ts index a1e1454bc7..7d389d2281 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-embeddedmedia.plugin.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-embeddedmedia.plugin.ts @@ -1,9 +1,5 @@ import { type TinyMcePluginArguments, UmbTinyMcePluginBase } from '../components/input-tiny-mce/tiny-mce-plugin.js'; -import type { - UmbEmbeddedMediaModalData, - UmbEmbeddedMediaModalValue, - UmbModalManagerContext, -} from '@umbraco-cms/backoffice/modal'; +import type { UmbEmbeddedMediaModalData, UmbEmbeddedMediaModalValue } from '@umbraco-cms/backoffice/modal'; import { UMB_EMBEDDED_MEDIA_MODAL, UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; export default class UmbTinyMceEmbeddedMediaPlugin extends UmbTinyMcePluginBase { diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts index ac52746f7e..8bae4b1abc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts @@ -1,5 +1,5 @@ +import { UMB_MEDIA_CAPTION_ALT_TEXT_MODAL } from '../modals/media-caption-alt-text/media-caption-alt-text-modal.token.js'; import { type TinyMcePluginArguments, UmbTinyMcePluginBase } from '../components/input-tiny-mce/tiny-mce-plugin.js'; -import type { UmbModalManagerContext } from '@umbraco-cms/backoffice/modal'; import { UMB_MEDIA_TREE_PICKER_MODAL, UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; import type { UMB_CURRENT_USER_CONTEXT, UmbCurrentUserModel } from '@umbraco-cms/backoffice/current-user'; import type { RawEditorOptions } from '@umbraco-cms/backoffice/external/tinymce'; @@ -27,6 +27,7 @@ interface MediaPickerResultData { export default class UmbTinyMceMediaPickerPlugin extends UmbTinyMcePluginBase { #currentUser?: UmbCurrentUserModel; #currentUserContext?: typeof UMB_CURRENT_USER_CONTEXT.TYPE; + #modalManager?: typeof UMB_MODAL_MANAGER_CONTEXT.TYPE; #temporaryFileRepository; constructor(args: TinyMcePluginArguments) { @@ -34,6 +35,10 @@ export default class UmbTinyMceMediaPickerPlugin extends UmbTinyMcePluginBase { this.#temporaryFileRepository = new UmbTemporaryFileRepository(args.host); + this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (instance) => { + this.#modalManager = instance; + }); + // TODO => this breaks tests. disabling for now // will ignore user media start nodes // this.host.consumeContext(UMB_CURRENT_USER_CONTEXT, (instance) => { @@ -131,10 +136,12 @@ export default class UmbTinyMceMediaPickerPlugin extends UmbTinyMcePluginBase { */ // TODO => startNodeId and startNodeIsVirtual do not exist on ContentTreeItemResponseModel - const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); - const modalHandler = modalManager.open(this, UMB_MEDIA_TREE_PICKER_MODAL, { + + const modalHandler = this.#modalManager?.open(this, UMB_MEDIA_TREE_PICKER_MODAL, { data: { multiple: false, + hideTreeRoot: true, + //startNodeId, //startNodeIsVirtual, }, @@ -145,31 +152,48 @@ export default class UmbTinyMceMediaPickerPlugin extends UmbTinyMcePluginBase { if (!modalHandler) return; - const { selection } = await modalHandler.onSubmit(); - if (!selection.length) return; + const { selection } = await modalHandler.onSubmit().catch(() => ({ selection: undefined })); + if (!selection || !selection.length) return; - this.#insertInEditor(selection[0]); + this.#showMediaCaptionAltText(selection[0]); this.editor.dispatch('Change'); } - // TODO => mediaPicker returns a UDI, so need to fetch it. Wait for backend CLI before implementing - async #insertInEditor(img: any) { - if (!img) return; + async #showMediaCaptionAltText(mediaUnique: string | null) { + if (!mediaUnique) return; + + const modalHandler = this.#modalManager?.open(this, UMB_MEDIA_CAPTION_ALT_TEXT_MODAL, { data: { mediaUnique } }); + + await modalHandler?.onSubmit().catch(() => undefined); + const mediaData = modalHandler?.getValue(); + + const media: MediaPickerTargetData = { + altText: mediaData?.altText, + caption: mediaData?.caption, + url: mediaData?.url, + udi: 'umb://media/' + mediaUnique?.replace(/-/g, ''), + }; + + this.#insertInEditor(media); + } + + async #insertInEditor(media: MediaPickerTargetData) { + if (!media) return; // We need to create a NEW DOM element to insert // setting an attribute of ID to __mcenew, so we can gather a reference to the node, to be able to update its size accordingly to the size of the image. - const data: MediaPickerResultData = { - alt: img.altText || '', - src: img.url ? img.url : 'nothing.jpg', + const img: MediaPickerResultData = { + alt: media.altText, + src: media.url ? media.url : 'nothing.jpg', id: '__mcenew', - 'data-udi': img.udi, - 'data-caption': img.caption, + 'data-udi': media.udi, + 'data-caption': media.caption, }; - const newImage = this.editor.dom.createHTML('img', data as Record); + const newImage = this.editor.dom.createHTML('img', img as Record); const parentElement = this.editor.selection.getNode().parentElement; - if (img.caption && parentElement) { - const figCaption = this.editor.dom.createHTML('figcaption', {}, img.caption); + if (img['data-caption'] && parentElement) { + const figCaption = this.editor.dom.createHTML('figcaption', {}, img['data-caption']); const combined = newImage + figCaption; if (parentElement.nodeName !== 'FIGURE') { @@ -196,7 +220,7 @@ export default class UmbTinyMceMediaPickerPlugin extends UmbTinyMcePluginBase { // When image is loaded we are ready to call sizeImageInEditor. const onImageLoaded = () => { - sizeImageInEditor(this.editor, imgElm, img.url); + sizeImageInEditor(this.editor, imgElm, img.src); this.editor.dispatch('Change'); }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/manifests.ts index 061aef02a3..4309ae4240 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/manifests.ts @@ -212,7 +212,7 @@ export const manifest: ManifestPropertyEditorUi = { alias: 'mode', label: 'Mode', description: 'Select the mode for the editor', - propertyEditorUiAlias: 'Umb.PropertyEditorUi.Dropdown', + propertyEditorUiAlias: 'Umb.PropertyEditorUi.RadioButtonList', config: [ { alias: 'items', @@ -232,6 +232,12 @@ export const manifest: ManifestPropertyEditorUi = { propertyEditorUiAlias: 'Umb.PropertyEditorUi.Toggle', }, ], + defaultData: [ + { + alias: 'mode', + value: 'Classic', + }, + ], }, }, }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.element.ts index 155b58e253..6d49f9ba0c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.element.ts @@ -1,6 +1,6 @@ import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; -import { customElement, css, html, property, map, state } from '@umbraco-cms/backoffice/external/lit'; +import { customElement, css, html, property, state, repeat } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; @@ -25,7 +25,7 @@ export class UmbPropertyEditorUITinyMceToolbarConfigurationElement extends UmbLitElement implements UmbPropertyEditorUiElement { - @property() + @property({ attribute: false }) set value(value: string | string[] | null) { if (!value) return; @@ -48,7 +48,6 @@ export class UmbPropertyEditorUITinyMceToolbarConfigurationElement v.selected = this.#selectedValues.includes(v.alias); }); } - get value(): string[] { return this.#selectedValues; } @@ -101,19 +100,20 @@ export class UmbPropertyEditorUITinyMceToolbarConfigurationElement const checkbox = event.target as HTMLInputElement; const alias = checkbox.value; - if (checkbox.checked) { - this.value = [...this.value, alias]; - } else { - this.value = this.value.filter((v) => v !== alias); - } + const value = this._toolbarConfig + .filter((t) => (t.alias !== alias && t.selected) || (t.alias === alias && checkbox.checked)) + .map((v) => v.alias); + + this.value = value; this.dispatchEvent(new CustomEvent('property-value-change')); } render() { return html`
      - ${map( + ${repeat( this._toolbarConfig, + (v) => v.alias, (v) => html`
    • diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group-workspace.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group-workspace.context-token.ts index 3303651a97..d9f70a906b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group-workspace.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group-workspace.context-token.ts @@ -1,10 +1,10 @@ import { UMB_USER_GROUP_ENTITY_TYPE } from '../index.js'; import type { UmbUserGroupWorkspaceContext } from './user-group-workspace.context.js'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import type { UmbSaveableWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; +import type { UmbSaveableWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; export const UMB_USER_GROUP_WORKSPACE_CONTEXT = new UmbContextToken< - UmbSaveableWorkspaceContextInterface, + UmbSaveableWorkspaceContext, UmbUserGroupWorkspaceContext >( 'UmbWorkspaceContext', diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group-workspace.context.ts index 19cfa3752e..54b7390b03 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group-workspace.context.ts @@ -2,12 +2,9 @@ import { UmbUserGroupDetailRepository } from '../repository/detail/index.js'; import type { UmbUserGroupDetailModel } from '../types.js'; import { UmbUserGroupWorkspaceEditorElement } from './user-group-workspace-editor.element.js'; import type { UmbUserPermissionModel } from '@umbraco-cms/backoffice/user-permission'; -import type { - UmbRoutableWorkspaceContext, - UmbSaveableWorkspaceContextInterface, -} from '@umbraco-cms/backoffice/workspace'; +import type { UmbRoutableWorkspaceContext, UmbSaveableWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; import { - UmbEditableWorkspaceContextBase, + UmbSaveableWorkspaceContextBase, UmbWorkspaceIsNewRedirectController, UmbWorkspaceRouteManager, } from '@umbraco-cms/backoffice/workspace'; @@ -15,8 +12,8 @@ import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; export class UmbUserGroupWorkspaceContext - extends UmbEditableWorkspaceContextBase - implements UmbSaveableWorkspaceContextInterface, UmbRoutableWorkspaceContext + extends UmbSaveableWorkspaceContextBase + implements UmbSaveableWorkspaceContext, UmbRoutableWorkspaceContext { // public readonly repository: UmbUserGroupDetailRepository = new UmbUserGroupDetailRepository(this); @@ -24,6 +21,7 @@ export class UmbUserGroupWorkspaceContext #data = new UmbObjectState(undefined); data = this.#data.asObservable(); + readonly unique = this.#data.asObservablePart((data) => data?.unique); readonly name = this.#data.asObservablePart((data) => data?.name || ''); readonly icon = this.#data.asObservablePart((data) => data?.icon || null); readonly sections = this.#data.asObservablePart((data) => data?.sections || []); diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user-workspace-editor.element.ts index 7445d002ae..d3a169032e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user-workspace-editor.element.ts @@ -1,11 +1,11 @@ import type { UmbUserDetailModel } from '../index.js'; import { UMB_USER_ENTITY_TYPE } from '../entity.js'; import type { UmbUserWorkspaceContext } from './user-workspace.context.js'; +import { UMB_USER_WORKSPACE_CONTEXT } from './user-workspace.context-token.js'; import type { UUIInputElement } from '@umbraco-cms/backoffice/external/uui'; import { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; import { css, html, nothing, customElement, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; // import local components. Theses are not meant to be used outside of this component. @@ -24,8 +24,8 @@ export class UmbUserWorkspaceEditorElement extends UmbLitElement { constructor() { super(); - this.consumeContext(UMB_WORKSPACE_CONTEXT, (workspaceContext) => { - this.#workspaceContext = workspaceContext as UmbUserWorkspaceContext; + this.consumeContext(UMB_USER_WORKSPACE_CONTEXT, (context) => { + this.#workspaceContext = context; this.#observeUser(); }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user-workspace.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user-workspace.context-token.ts index fdf725a359..bd3fc158b0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user-workspace.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user-workspace.context-token.ts @@ -1,12 +1,9 @@ import { UMB_USER_ENTITY_TYPE } from '../entity.js'; import type { UmbUserWorkspaceContext } from './user-workspace.context.js'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import type { UmbSaveableWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; +import type { UmbSaveableWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; -export const UMB_USER_WORKSPACE_CONTEXT = new UmbContextToken< - UmbSaveableWorkspaceContextInterface, - UmbUserWorkspaceContext ->( +export const UMB_USER_WORKSPACE_CONTEXT = new UmbContextToken( 'UmbWorkspaceContext', undefined, (context): context is UmbUserWorkspaceContext => context.getEntityType?.() === UMB_USER_ENTITY_TYPE, diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user-workspace.context.ts index 744a84767e..c83f21c2f2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user-workspace.context.ts @@ -4,16 +4,16 @@ import { UmbUserDetailRepository } from '../repository/index.js'; import { UmbUserAvatarRepository } from '../repository/avatar/index.js'; import { UMB_USER_WORKSPACE_ALIAS } from './manifests.js'; import { UmbUserWorkspaceEditorElement } from './user-workspace-editor.element.js'; -import type { UmbSaveableWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; -import { UmbEditableWorkspaceContextBase, UmbWorkspaceRouteManager } from '@umbraco-cms/backoffice/workspace'; +import type { UmbSaveableWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; +import { UmbSaveableWorkspaceContextBase, UmbWorkspaceRouteManager } from '@umbraco-cms/backoffice/workspace'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; type EntityType = UmbUserDetailModel; export class UmbUserWorkspaceContext - extends UmbEditableWorkspaceContextBase - implements UmbSaveableWorkspaceContextInterface + extends UmbSaveableWorkspaceContextBase + implements UmbSaveableWorkspaceContext { public readonly detailRepository: UmbUserDetailRepository = new UmbUserDetailRepository(this); public readonly avatarRepository: UmbUserAvatarRepository = new UmbUserAvatarRepository(this); diff --git a/src/Umbraco.Web.UI.Client/src/packages/webhook/menu-item/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/webhook/menu-item/manifests.ts index cba613ace3..0d56d596fd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/webhook/menu-item/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/webhook/menu-item/manifests.ts @@ -10,7 +10,7 @@ const menuItem: ManifestMenuItem = { label: 'Webhooks', icon: 'icon-webhook', entityType: UMB_WEBHOOK_ENTITY_TYPE, - menus: ['Umb.Menu.Settings'], + menus: ['Umb.Menu.AdvancedSettings'], }, }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/webhook/workspace/webhook/webhook-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/webhook/workspace/webhook/webhook-workspace.context.ts index a98faea18c..7b1a61f4c9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/webhook/workspace/webhook/webhook-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/webhook/workspace/webhook/webhook-workspace.context.ts @@ -2,20 +2,16 @@ import { UMB_WEBHOOK_ENTITY_TYPE, UMB_WEBHOOK_WORKSPACE } from '../../entity.js' import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import type { UmbWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; +import type { UmbWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; -export class UmbWebhookWorkspaceContext extends UmbControllerBase implements UmbWorkspaceContextInterface { +export class UmbWebhookWorkspaceContext extends UmbControllerBase implements UmbWorkspaceContext { public readonly workspaceAlias = UMB_WEBHOOK_WORKSPACE; getEntityType() { return UMB_WEBHOOK_ENTITY_TYPE; } - getUnique() { - return undefined; - } - constructor(host: UmbControllerHost) { super(host); this.provideContext(UMB_WORKSPACE_CONTEXT, this); diff --git a/src/Umbraco.Web.UI.Client/tsconfig.json b/src/Umbraco.Web.UI.Client/tsconfig.json index 364077e447..33bf53145e 100644 --- a/src/Umbraco.Web.UI.Client/tsconfig.json +++ b/src/Umbraco.Web.UI.Client/tsconfig.json @@ -101,6 +101,7 @@ "@umbraco-cms/backoffice/user-permission": ["./src/packages/user/user-permission/index.ts"], "@umbraco-cms/backoffice/user": ["./src/packages/user/user/index.ts"], "@umbraco-cms/backoffice/utils": ["./src/packages/core/utils/index.ts"], + "@umbraco-cms/backoffice/validation": ["./src/packages/core/validation/index.ts"], "@umbraco-cms/backoffice/variant": ["./src/packages/core/variant/index.ts"], "@umbraco-cms/backoffice/webhook": ["./src/packages/webhook/index.ts"], "@umbraco-cms/backoffice/workspace": ["./src/packages/core/workspace/index.ts"],