Merge branch 'main' into bugfix/rename-file-system-file

This commit is contained in:
Mads Rasmussen
2024-03-26 19:41:47 +01:00
committed by GitHub
284 changed files with 3347 additions and 1196 deletions

View File

@@ -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);

View File

@@ -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"></umb-property>

View File

@@ -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",

View File

@@ -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) => {

View File

@@ -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);
}
}
}

View File

@@ -7,7 +7,6 @@ import type { ItemReferenceByIdResponseModel } from './ItemReferenceByIdResponse
export type RecycleBinItemResponseModelBaseModel = {
id: string;
type: string;
hasChildren: boolean;
parent?: ItemReferenceByIdResponseModel | null;
};

View File

@@ -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<Array<DataTypeTreeItemResponseModel>> {
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

View File

@@ -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<Array<(NamedEntityTreeItemResponseModel | DataTypeTreeItemResponseModel | DocumentBlueprintTreeItemResponseModel | DocumentTypeTreeItemResponseModel | FolderTreeItemResponseModel | MediaTypeTreeItemResponseModel | RelationTypeTreeItemResponseModel)>> {
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

View File

@@ -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<Array<DocumentTreeItemResponseModel>> {
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

View File

@@ -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<Array<DocumentTypeTreeItemResponseModel>> {
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

View File

@@ -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<Array<MediaTreeItemResponseModel>> {
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

View File

@@ -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<Array<MediaTypeTreeItemResponseModel>> {
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

View File

@@ -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<Array<FileSystemTreeItemPresentationModel>> {
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

View File

@@ -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<Array<FileSystemTreeItemPresentationModel>> {
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

View File

@@ -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<Array<FileSystemTreeItemPresentationModel>> {
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

View File

@@ -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<Array<FileSystemTreeItemPresentationModel>> {
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

View File

@@ -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<Array<(NamedEntityTreeItemResponseModel | DataTypeTreeItemResponseModel | DocumentBlueprintTreeItemResponseModel | DocumentTypeTreeItemResponseModel | FolderTreeItemResponseModel | MediaTypeTreeItemResponseModel | RelationTypeTreeItemResponseModel)>> {
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

View File

@@ -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';

View File

@@ -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);
}
}

View File

@@ -399,11 +399,7 @@ export const data: Array<UmbMockDataTypeModel> = [
},
{
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<UmbMockDataTypeModel> = [
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<UmbMockDataTypeModel> = [
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'],
},
],
},

View File

@@ -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<UmbBlockGridTypeAreaType>
implements UmbInvariantableWorkspaceContextInterface
extends UmbSaveableWorkspaceContextBase<UmbBlockGridTypeAreaType>
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',

View File

@@ -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, UmbBlockTypeWorkspaceContext>(
'UmbWorkspaceContext',
undefined,
(context): context is UmbBlockTypeWorkspaceContext => (context as any).IS_BLOCK_TYPE_WORKSPACE_CONTEXT,

View File

@@ -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<BlockTypeData extends UmbBlockTypeWithGroupKey = UmbBlockTypeWithGroupKey>
extends UmbEditableWorkspaceContextBase<BlockTypeData>
implements UmbInvariantableWorkspaceContextInterface, UmbRoutableWorkspaceContext
extends UmbSaveableWorkspaceContextBase<BlockTypeData>
implements UmbInvariantDatasetWorkspaceContext, UmbRoutableWorkspaceContext
{
// Just for context token safety:
public readonly IS_BLOCK_TYPE_WORKSPACE_CONTEXT = true;

View File

@@ -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<UmbWorkspaceContextInterface, UmbBlockWorkspaceContext>(
export const UMB_BLOCK_WORKSPACE_CONTEXT = new UmbContextToken<UmbWorkspaceContext, UmbBlockWorkspaceContext>(
'UmbWorkspaceContext',
undefined,
(context): context is UmbBlockWorkspaceContext => (context as any).IS_BLOCK_WORKSPACE_CONTEXT,

View File

@@ -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<LayoutDataType extends UmbBlockLayoutBaseModel = UmbBlockLayoutBaseModel>
extends UmbEditableWorkspaceContextBase<LayoutDataType>
extends UmbSaveableWorkspaceContextBase<LayoutDataType>
implements UmbRoutableWorkspaceContext
{
// Just for context token safety:
@@ -48,7 +48,7 @@ export class UmbBlockWorkspaceContext<LayoutDataType extends UmbBlockLayoutBaseM
#layout = new UmbObjectState<LayoutDataType | undefined>(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);

View File

@@ -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<unknown>;
#sectionSidebarContext?: UmbSectionSidebarContext;
@@ -40,36 +38,58 @@ export class UmbEntityActionsBundleElement extends UmbLitElement {
});
}
protected updated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): 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`
<uui-action-bar slot="actions">
<uui-button @click=${this._openActions} label="Open actions menu">
<uui-symbol-more></uui-symbol-more>
</uui-button>
</uui-action-bar>
`
: nothing}
`;
if (this._numberOfActions === 0) return nothing;
return html`<uui-action-bar slot="actions"> ${this.#renderFirstAction()} ${this.#renderMore()} </uui-action-bar>`;
}
#renderMore() {
if (this._numberOfActions === 1) return nothing;
return html`<uui-button @click=${this.#openContextMenu} label="Open actions menu">
<uui-symbol-more></uui-symbol-more>
</uui-button>`;
}
#renderFirstAction() {
if (!this._firstActionApi) return nothing;
return html`<uui-button
label=${ifDefined(this._firstActionManifest?.meta.label)}
@click=${this.#onFirstActionClick}>
<uui-icon name=${ifDefined(this._firstActionManifest?.meta.icon)}></uui-icon>
</uui-button>`;
}
}

View File

@@ -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<any> | Map<PropertyKey, unknown>): 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`<uui-input
type="number"
required
.value=${this._minValue}
@input=${this._onMinInput}
label=${this.minLabel}></uui-input>

View File

@@ -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<UmbPropertyTypeWorkspaceContext>
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',

View File

@@ -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<ContentTypeType extends UmbContentTypeModel = UmbContentTypeModel>
extends UmbSaveableWorkspaceContextInterface {
extends UmbSaveableWorkspaceContext {
readonly IS_CONTENT_TYPE_WORKSPACE_CONTEXT: true;
readonly name: Observable<string | undefined>;

View File

@@ -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'],
},
};

View File

@@ -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

View File

@@ -7,3 +7,13 @@ export interface ManifestWorkspaceFooterApp
ManifestWithDynamicConditions<ConditionTypes> {
type: 'workspaceFooterApp';
}
export interface ManifestWorkspaceFooterAppMenuBreadcrumbKind extends ManifestWorkspaceFooterApp {
type: 'workspaceFooterApp';
kind: 'menuBreadcrumb';
}
export interface ManifestWorkspaceFooterAppVariantMenuBreadcrumbKind extends ManifestWorkspaceFooterApp {
type: 'workspaceFooterApp';
kind: 'variantMenuBreadcrumb';
}

View File

@@ -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<ElementType, ApiType> {
type: 'workspace';
meta: MetaType;

View File

@@ -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

View File

@@ -7,7 +7,10 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
export class UmbExtensionRootWorkspaceElement extends UmbLitElement {
render() {
return html`
<umb-workspace-editor headline="Extensions" alias=${UMB_EXTENSION_ROOT_WORKSPACE_ALIAS} .enforceNoFooter=${true}>
<umb-workspace-editor
headline="Extension Insights"
alias=${UMB_EXTENSION_ROOT_WORKSPACE_ALIAS}
.enforceNoFooter=${true}>
<umb-collection alias=${UMB_EXTENSION_COLLECTION_ALIAS}></umb-collection>
</umb-workspace-editor>
`;

View File

@@ -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() {

View File

@@ -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';

View File

@@ -0,0 +1,6 @@
import type { UmbStructureItemModel } from './types.js';
import type { Observable } from '@umbraco-cms/backoffice/external/rxjs';
export interface UmbMenuStructureWorkspaceContext {
structure: Observable<UmbStructureItemModel[]>;
}

View File

@@ -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<unknown> {
#workspaceContext?: any;
#args: UmbMenuTreeStructureWorkspaceContextBaseArgs;
#structure = new UmbArrayState<UmbStructureItemModel>([], (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<UmbStructureItemModel> = [];
const treeRepository = await createExtensionApiByAlias<
UmbTreeRepository<UmbUniqueTreeItemModel, UmbUniqueTreeRootModel>
>(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);
}
}

View File

@@ -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<unknown> {
// TODO: add correct interface
#workspaceContext?: any;
#args: UmbMenuVariantTreeStructureWorkspaceContextBaseArgs;
#structure = new UmbArrayState<UmbVariantStructureItemModel>([], (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<UmbTreeRepository<any>>(
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);
}
}
}

View File

@@ -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 }>;
}

View File

@@ -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;

View File

@@ -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');
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -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>('UmbSectionContext');

View File

@@ -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: [
{

View File

@@ -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';

View File

@@ -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<TreeItemType extends UmbTreeItemModelBase> {
* @memberof UmbTreeDataSource
*/
getChildrenOf(args: UmbTreeChildrenOfRequestArgs): Promise<UmbDataSourceResponse<UmbPagedModel<TreeItemType>>>;
/**
* Gets the ancestors of the given item.
* @return {*} {Promise<UmbDataSourceResponse<Array<TreeItemType>>}
* @memberof UmbTreeDataSource
*/
getAncestorsOf(args: UmbTreeAncestorsOfRequestArgs): Promise<UmbDataSourceResponse<Array<TreeItemType>>>;
}

View File

@@ -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 {*}

View File

@@ -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<TreeItemType[]>;
}>;
/**
* Requests the ancestors of the given item.
* @param {UmbTreeAncestorsOfRequestArgs} args
* @memberof UmbTreeRepository
*/
requestTreeItemAncestors: (
args: UmbTreeAncestorsOfRequestArgs,
) => Promise<{ data?: TreeItemType[]; error?: ProblemDetails; asObservable?: () => Observable<TreeItemType[]> }>;
/**
* Returns an observable of the root items of the tree.
* @memberof UmbTreeRepository

View File

@@ -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<UmbPagedModel<ServerTreeItemType>>;
getChildrenOf: (args: UmbTreeChildrenOfRequestArgs) => Promise<UmbPagedModel<ServerTreeItemType>>;
getAncestorsOf: (args: UmbTreeAncestorsOfRequestArgs) => Promise<Array<ServerTreeItemType>>;
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 };
}
}

View File

@@ -8,3 +8,7 @@ export interface UmbTreeChildrenOfRequestArgs {
skip: number;
take: number;
}
export interface UmbTreeAncestorsOfRequestArgs {
descendantUnique: string;
}

View File

@@ -0,0 +1,2 @@
export * from './validation.context.js';
export * from './validation.context-token.js';

View File

@@ -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>('UmbValidationContext');

View File

@@ -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<UmbValidationContext> {
constructor(host: UmbControllerHost) {
super(host, UMB_VALIDATION_CONTEXT);
}
}

View File

@@ -0,0 +1,2 @@
export * from './validation-valid.event.js';
export * from './validation-invalid.event.js';

View File

@@ -0,0 +1,9 @@
import { UmbValidationEvent } from './validation.event.js';
export class UmbValidationInvalidEvent extends UmbValidationEvent {
static readonly TYPE = 'invalid';
public constructor() {
super(UmbValidationInvalidEvent.TYPE);
}
}

View File

@@ -0,0 +1,9 @@
import { UmbValidationEvent } from './validation.event.js';
export class UmbValidationValidEvent extends UmbValidationEvent {
static readonly TYPE = 'valid';
constructor() {
super(UmbValidationValidEvent.TYPE);
}
}

View File

@@ -0,0 +1,5 @@
export class UmbValidationEvent extends Event {
public constructor(type: string) {
super(type, { bubbles: true, composed: true, cancelable: false });
}
}

View File

@@ -0,0 +1,3 @@
export * from './events/index.js';
export * from './mixins/index.js';
export * from './context/index.js';

View File

@@ -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<ValueType, DefaultValueType> 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<ValueType, DefaultValueType>
extends LitElement
implements UmbFormControlMixinInterface<ValueType, DefaultValueType>
{
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<LitElement> = HTMLElementConstructor<LitElement>,
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<string | number | symbol, unknown>) {
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<ValueType, DefaultValueType>
> &
T;
};

View File

@@ -0,0 +1 @@
export * from './form-control.mixin.js';

View File

@@ -0,0 +1,15 @@
type Writeable<T> = { -readonly [P in keyof T]: T[P] };
export class UmbValidityState implements Writeable<ValidityState> {
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;
}

View File

@@ -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,
];

View File

@@ -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`
<uui-button
id="variant-selector-toggle"
compact
slot="append"
popovertarget="variant-selector-popover"
title=${this._variantTitleName}>
@@ -223,9 +224,9 @@ export class UmbVariantSelectorElement extends UmbLitElement {
<uui-button slot="append" compact id="variant-close" @click=${this.#closeSplitView}>
<uui-icon name="remove"></uui-icon>
</uui-button>
`
`
: ''}
`
`
: nothing
}
</uui-input>
@@ -264,7 +265,7 @@ export class UmbVariantSelectorElement extends UmbLitElement {
@click=${() => this.#openSplitView(variant)}>
Split view
</uui-button>
`}
`}
</li>
`,
)}
@@ -272,7 +273,7 @@ export class UmbVariantSelectorElement extends UmbLitElement {
</uui-scroll-container>
</div>
</uui-popover-container>
`
`
: nothing
}
</div>
@@ -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 {

View File

@@ -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<UmbSaveableWorkspaceContextInterface> {
constructor(host: UmbControllerHost, args: UmbWorkspaceActionArgs<UmbSaveableWorkspaceContextInterface>) {
export class UmbSaveWorkspaceAction extends UmbWorkspaceActionBase<UmbSaveableWorkspaceContext> {
constructor(host: UmbControllerHost, args: UmbWorkspaceActionArgs<UmbSaveableWorkspaceContext>) {
super(host, args);
// TODO: Could we make change label depending on the state?

View File

@@ -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];

View File

@@ -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<UmbStructureItemModel>;
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`
<uui-breadcrumbs>
${this._structure?.map(
(structureItem) =>
html`<uui-breadcrumb-item href="${ifDefined(this.#getHref(structureItem))}"
>${structureItem.name}</uui-breadcrumb-item
>`,
)}
<uui-breadcrumb-item>${this._name}</uui-breadcrumb-item>
</uui-breadcrumbs>
`;
}
static styles = [UmbTextStyles];
}
export default UmbWorkspaceBreadcrumbElement;
declare global {
interface HTMLElementTagNameMap {
'umb-workspace-breadcrumb': UmbWorkspaceBreadcrumbElement;
}
}

View File

@@ -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,
},
};

View File

@@ -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<UmbVariantStructureItemModel> = [];
@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<UmbVariantStructureItemModel>;
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`
<uui-breadcrumbs>
${this._structure.map(
(structureItem) =>
html`<uui-breadcrumb-item href="${ifDefined(this.#getHref(structureItem))}"
>${this.#getItemVariantName(structureItem)}</uui-breadcrumb-item
>`,
)}
<uui-breadcrumb-item>${this._name}</uui-breadcrumb-item>
</uui-breadcrumbs>
`;
}
static styles = [UmbTextStyles];
}
export default UmbWorkspaceVariantMenuBreadcrumbElement;
declare global {
interface HTMLElementTagNameMap {
'umb-workspace-variant-menu-breadcrumb': UmbWorkspaceVariantMenuBreadcrumbElement;
}
}

View File

@@ -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,
},
};

View File

@@ -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();
});

View File

@@ -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';

View File

@@ -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<WorkspaceAliasConditionConfig>) {
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) {

View File

@@ -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<WorkspaceHasCollectionConditionConfig>) {
super(host, args);
this.consumeContext(UMB_WORKSPACE_COLLECTION_CONTEXT, (context) => {
this.consumeContext(UMB_COLLECTION_WORKSPACE_CONTEXT, (context) => {
this.observe(
context.contentTypeHasCollection,
(hasCollection) => {

View File

@@ -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<UmbDefaultWorkspaceContext>
implements UmbWorkspaceContextInterface
implements UmbWorkspaceContext
{
public workspaceAlias!: string;
#entityType!: string;

View File

@@ -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';

View File

@@ -1,7 +0,0 @@
import type { UmbSaveableWorkspaceContextInterface } from './saveable-workspace-context.interface.js';
export interface UmbPublishableWorkspaceContextInterface extends UmbSaveableWorkspaceContextInterface {
saveAndPublish(): Promise<void>;
publish(): Promise<void>;
unpublish(): Promise<void>;
}

View File

@@ -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,
);

View File

@@ -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;
}

View File

@@ -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<WorkspaceDataModelType>
extends UmbContextBase<UmbEditableWorkspaceContextBase<WorkspaceDataModelType>>
implements UmbSaveableWorkspaceContextInterface
export abstract class UmbSaveableWorkspaceContextBase<WorkspaceDataModelType>
extends UmbContextBase<UmbSaveableWorkspaceContextBase<WorkspaceDataModelType>>
implements UmbSaveableWorkspaceContext
{
public readonly workspaceAlias: string;
@@ -18,6 +20,8 @@ export abstract class UmbEditableWorkspaceContextBase<WorkspaceDataModelType>
#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<WorkspaceDataModelType>
}
//abstract getIsDirty(): Promise<boolean>;
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<void>;
abstract readonly unique: Observable<string | null | undefined>;
}
/*
* @deprecated Use UmbSaveableWorkspaceContextBase instead Will be removed before RC.
* TODO: Delete before RC.
*/
export abstract class UmbEditableWorkspaceContextBase<
WorkspaceDataModelType,
> extends UmbSaveableWorkspaceContextBase<WorkspaceDataModelType> {}

View File

@@ -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<boolean | undefined>;
getIsNew(): boolean | undefined;
save(): Promise<void>;
setValidationErrors?(errorMap: any): void; // TODO: temp solution to bubble validation errors to the UI
destroy(): void;
}

View File

@@ -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,
);

View File

@@ -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<T extends UmbContentTypeModel>
extends UmbWorkspaceContextInterface {
export interface UmbCollectionWorkspaceContext<T extends UmbContentTypeModel> extends UmbEntityWorkspaceContext {
contentTypeHasCollection: Observable<boolean>;
getCollectionAlias(): string;
structure: UmbContentTypeStructureManager<T>;
}
/**
* @deprecated Use UmbCollectionWorkspaceContextInterface instead Will be removed before RC.
* TODO: Delete before RC.
*/
export interface UmbWorkspaceCollectionContextInterface<T extends UmbContentTypeModel>
extends UmbCollectionWorkspaceContext<T> {}

View File

@@ -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<UmbContentTypeModel>
>(
'UmbWorkspaceContext',
undefined,
(context): context is UmbCollectionWorkspaceContext<UmbContentTypeModel> =>
(context as UmbCollectionWorkspaceContext<UmbContentTypeModel>).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;

View File

@@ -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<UmbWorkspaceUniqueType | undefined>;
getUnique(): UmbWorkspaceUniqueType | undefined;
}

View File

@@ -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, UmbEntityWorkspaceContext>(
'UmbWorkspaceContext',
undefined,
(context): context is UmbEntityWorkspaceContext => (context as any).getUnique !== undefined,
);

View File

@@ -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';

View File

@@ -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<string | undefined>;
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 {}

View File

@@ -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<Observable<ValueModelBaseModel | undefined>>;
}

View File

@@ -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,
);

View File

@@ -0,0 +1,7 @@
import type { UmbSaveableWorkspaceContext } from './saveable-workspace-context.interface.js';
export interface UmbPublishableWorkspaceContext extends UmbSaveableWorkspaceContext {
saveAndPublish(): Promise<void>;
publish(): Promise<void>;
unpublish(): Promise<void>;
}

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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, UmbRoutableWorkspaceContext>(
'UmbWorkspaceContext',
undefined,
(context): context is UmbRoutableWorkspaceContext => 'routes' in context,
);

View File

@@ -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<boolean | undefined>;
getIsNew(): boolean | undefined;
save(): Promise<void>;
destroy(): void;
}
/**
* @deprecated Use `UmbSaveableWorkspaceContext` instead. This token will be removed in the RC version.
* TODO: Remove in RC
*/
export interface UmbSaveableWorkspaceContextInterface extends UmbSaveableWorkspaceContext {}

View File

@@ -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, UmbSaveableWorkspaceContext>(
'UmbWorkspaceContext',
undefined,
(context): context is UmbSaveableWorkspaceContext => 'getIsNew' in context,
);

View File

@@ -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<VariantType extends UmbVariantModel = UmbVariantModel>
extends UmbSaveableWorkspaceContextInterface {
export interface UmbVariantDatasetWorkspaceContext<VariantType extends UmbVariantModel = UmbVariantModel>
extends UmbSaveableWorkspaceContext {
// Name:
getName(variantId?: UmbVariantId): string | undefined;
setName(name: string, variantId?: UmbVariantId): void;
name(variantId?: UmbVariantId): Observable<string>;
// Variant:
variants: Observable<Array<UmbVariantModel>>;
@@ -18,14 +19,19 @@ export interface UmbVariantableWorkspaceContextInterface<VariantType extends Umb
getVariant(variantId: UmbVariantId): UmbVariantModel | undefined;
// Property:
// This one is async cause it needs to structure to provide this data:
// This one is async cause it needs to structure to provide this data: [NL]
propertyValueByAlias<ReturnValue = unknown>(
alias: string,
variantId?: UmbVariantId,
): Promise<Observable<ReturnValue | undefined> | undefined>;
getPropertyValue<ReturnValue = unknown>(alias: string, variantId?: UmbVariantId): ReturnValue | undefined;
setPropertyValue(alias: string, value: unknown, variantId?: UmbVariantId): Promise<void>;
//propertyDataByAlias(alias: string, variantId?: UmbVariantId): Observable<ValueModelBaseModel | undefined>;
createPropertyDatasetContext(host: UmbControllerHost, variantId?: UmbVariantId): UmbPropertyDatasetContext;
}
/**
* @deprecated Use UmbVariantWorkspaceContextInterface instead Will be removed before RC.
* TODO: Delete before RC.
*/
export interface UmbVariantableWorkspaceContextInterface extends UmbVariantDatasetWorkspaceContext {}

View File

@@ -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);

Some files were not shown because too many files have changed in this diff Show More