Merge remote-tracking branch 'origin/main' into feature/block-editor-type-workspace

# Conflicts:
#	package.json
#	src/packages/documents/document-types/components/index.ts
#	src/packages/documents/document-types/index.ts
This commit is contained in:
Niels Lyngsø
2024-01-11 11:04:19 +01:00
150 changed files with 3802 additions and 1893 deletions

View File

@@ -135,6 +135,7 @@ Ensure all property editors are properly localized.
- [ ] Toggle - [ ] Toggle
- [ ] Tree Picker - [ ] Tree Picker
- [ ] StartNode - [ ] StartNode
- [x] DynamicRoot
- [ ] Upload Field - [ ] Upload Field
- [ ] User Picker - [ ] User Picker
- [ ] Value Type - [ ] Value Type
@@ -235,4 +236,4 @@ Then we need your help! With Bellissima we added new localization keys, and we s
- [ ] `tr-TR` - Turkish (Turkey) - [ ] `tr-TR` - Turkish (Turkey)
- [ ] `ua-UA` - Ukrainian (Ukraine) - [ ] `ua-UA` - Ukrainian (Ukraine)
- [ ] `zh-CN` - Chinese (China) - [ ] `zh-CN` - Chinese (China)
- [ ] `zh-TW` - Chinese (Taiwan) - [ ] `zh-TW` - Chinese (Taiwan)

File diff suppressed because it is too large Load Diff

View File

@@ -50,6 +50,7 @@
"./repository": "./dist-cms/packages/core/repository/index.js", "./repository": "./dist-cms/packages/core/repository/index.js",
"./temporary-file": "./dist-cms/packages/core/temporary-file/index.js", "./temporary-file": "./dist-cms/packages/core/temporary-file/index.js",
"./block": "./dist-cms/packages/block/index.js", "./block": "./dist-cms/packages/block/index.js",
"./audit-log": "./dist-cms/packages/audit-log/index.js",
"./dictionary": "./dist-cms/packages/dictionary/dictionary/index.js", "./dictionary": "./dist-cms/packages/dictionary/dictionary/index.js",
"./document": "./dist-cms/packages/documents/documents/index.js", "./document": "./dist-cms/packages/documents/documents/index.js",
"./document-blueprint": "./dist-cms/packages/documents/document-blueprints/index.js", "./document-blueprint": "./dist-cms/packages/documents/document-blueprints/index.js",
@@ -63,7 +64,8 @@
"./data-type": "./dist-cms/packages/core/data-type/index.js", "./data-type": "./dist-cms/packages/core/data-type/index.js",
"./language": "./dist-cms/packages/settings/languages/index.js", "./language": "./dist-cms/packages/settings/languages/index.js",
"./logviewer": "./dist-cms/packages/settings/logviewer/index.js", "./logviewer": "./dist-cms/packages/settings/logviewer/index.js",
"./relation-type": "./dist-cms/packages/settings/relation-types/index.js", "./relation-type": "./dist-cms/packages/relations/relation-types/index.js",
"./relation": "./dist-cms/packages/relations/relations/index.js",
"./tags": "./dist-cms/packages/tags/index.js", "./tags": "./dist-cms/packages/tags/index.js",
"./static-file": "./dist-cms/packages/static-file/index.js", "./static-file": "./dist-cms/packages/static-file/index.js",
"./partial-view": "./dist-cms/packages/templating/partial-views/index.js", "./partial-view": "./dist-cms/packages/templating/partial-views/index.js",
@@ -144,11 +146,11 @@
"element-internals-polyfill": "^1.3.9", "element-internals-polyfill": "^1.3.9",
"lit": "^2.8.0", "lit": "^2.8.0",
"lodash-es": "4.17.21", "lodash-es": "4.17.21",
"marked": "^11.1.0", "marked": "^11.1.1",
"monaco-editor": "^0.45.0", "monaco-editor": "^0.45.0",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"tinymce-i18n": "^23.12.4",
"tinymce": "^6.8.2", "tinymce": "^6.8.2",
"tinymce-i18n": "^23.12.4",
"uuid": "^9.0.1" "uuid": "^9.0.1"
}, },
"devDependencies": { "devDependencies": {
@@ -159,54 +161,55 @@
"@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-json": "^6.1.0", "@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-node-resolve": "^15.2.3",
"@storybook/addon-a11y": "7.6.5", "@rollup/plugin-replace": "^5.0.5",
"@storybook/addon-actions": "7.6.5", "@storybook/addon-a11y": "7.6.7",
"@storybook/addon-essentials": "7.6.5", "@storybook/addon-actions": "7.6.7",
"@storybook/addon-links": "7.6.5", "@storybook/addon-essentials": "7.6.7",
"@storybook/addon-links": "7.6.7",
"@storybook/mdx2-csf": "^1.1.0", "@storybook/mdx2-csf": "^1.1.0",
"@storybook/web-components-vite": "7.6.5", "@storybook/web-components-vite": "7.6.7",
"@storybook/web-components": "7.6.5", "@storybook/web-components": "7.6.7",
"@types/chai": "^4.3.5", "@types/chai": "^4.3.5",
"@types/lodash-es": "^4.17.8", "@types/lodash-es": "^4.17.8",
"@types/mocha": "^10.0.1", "@types/mocha": "^10.0.1",
"@typescript-eslint/eslint-plugin": "^6.14.0", "@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0", "@typescript-eslint/parser": "^6.14.0",
"@web/dev-server-esbuild": "^0.4.1", "@web/dev-server-esbuild": "^1.0.1",
"@web/dev-server-import-maps": "^0.1.1", "@web/dev-server-import-maps": "^0.2.0",
"@web/dev-server-rollup": "^0.6.0", "@web/dev-server-rollup": "^0.6.1",
"@web/test-runner-playwright": "^0.11.0",
"@web/test-runner": "^0.18.0", "@web/test-runner": "^0.18.0",
"@web/test-runner-playwright": "^0.11.0",
"babel-loader": "^9.1.3", "babel-loader": "^9.1.3",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.1", "eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1", "eslint-plugin-import": "^2.29.1",
"eslint-plugin-lit-a11y": "^4.1.1",
"eslint-plugin-lit": "^1.10.1", "eslint-plugin-lit": "^1.10.1",
"eslint-plugin-lit-a11y": "^4.1.1",
"eslint-plugin-local-rules": "^2.0.1", "eslint-plugin-local-rules": "^2.0.1",
"eslint-plugin-storybook": "^0.6.15", "eslint-plugin-storybook": "^0.6.15",
"eslint-plugin-wc": "^2.0.4", "eslint-plugin-wc": "^2.0.4",
"eslint": "^8.56.0",
"lucide-static": "^0.290.0", "lucide-static": "^0.290.0",
"msw": "^1.3.2", "msw": "^1.3.2",
"openapi-typescript-codegen": "^0.25.0", "openapi-typescript-codegen": "^0.25.0",
"playwright-msw": "^3.0.1", "playwright-msw": "^3.0.1",
"plop": "^4.0.0", "plop": "^4.0.0",
"prettier": "3.0.3", "prettier": "3.0.3",
"react-dom": "^18.2.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0",
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",
"rollup": "^4.9.4",
"rollup-plugin-esbuild": "^6.1.0", "rollup-plugin-esbuild": "^6.1.0",
"rollup-plugin-import-css": "^3.3.5", "rollup-plugin-import-css": "^3.4.0",
"rollup-plugin-web-worker-loader": "^1.6.1", "rollup-plugin-web-worker-loader": "^1.6.1",
"rollup": "^4.9.0", "storybook": "7.6.7",
"storybook": "7.6.5",
"tiny-glob": "^0.2.9", "tiny-glob": "^0.2.9",
"tsc-alias": "^1.8.8", "tsc-alias": "^1.8.8",
"typescript": "^5.3.3",
"typescript-json-schema": "^0.62.0", "typescript-json-schema": "^0.62.0",
"typescript": "^5.3.2", "vite": "^5.0.11",
"vite-plugin-static-copy": "^0.17.0", "vite-plugin-static-copy": "^1.0.0",
"vite-tsconfig-paths": "^4.2.0", "vite-tsconfig-paths": "^4.2.3",
"vite": "^4.4.12",
"web-component-analyzer": "^2.0.0" "web-component-analyzer": "^2.0.0"
}, },
"msw": { "msw": {

View File

@@ -1,4 +1,4 @@
import { UmbBackofficeContext, UMB_BACKOFFICE_CONTEXT_TOKEN } from './backoffice.context.js'; import { UmbBackofficeContext } from './backoffice.context.js';
import { UmbServerExtensionRegistrator } from './server-extension-registrator.controller.js'; import { UmbServerExtensionRegistrator } from './server-extension-registrator.controller.js';
import { css, html, customElement } from '@umbraco-cms/backoffice/external/lit'; import { css, html, customElement } from '@umbraco-cms/backoffice/external/lit';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
@@ -12,9 +12,11 @@ import './components/index.js';
// TODO: temp solution to load core packages // TODO: temp solution to load core packages
const CORE_PACKAGES = [ const CORE_PACKAGES = [
import('../../packages/audit-log/umbraco-package.js'),
import('../../packages/core/umbraco-package.js'), import('../../packages/core/umbraco-package.js'),
import('../../packages/settings/umbraco-package.js'), import('../../packages/settings/umbraco-package.js'),
import('../../packages/documents/umbraco-package.js'), import('../../packages/documents/umbraco-package.js'),
import('../../packages/relations/umbraco-package.js'),
import('../../packages/media/umbraco-package.js'), import('../../packages/media/umbraco-package.js'),
import('../../packages/members/umbraco-package.js'), import('../../packages/members/umbraco-package.js'),
import('../../packages/block/umbraco-package.js'), import('../../packages/block/umbraco-package.js'),

View File

@@ -76,7 +76,7 @@ export class UmbBackofficeMainElement extends UmbLitElement {
private _onRouteChange = async (event: UmbRouterSlotChangeEvent) => { private _onRouteChange = async (event: UmbRouterSlotChangeEvent) => {
const currentPath = event.target.localActiveViewPath || ''; const currentPath = event.target.localActiveViewPath || '';
const section = this._sections.find((s) => this._routePrefix + (s.manifest as any).meta.pathname === currentPath); const section = this._sections.find((s) => this._routePrefix + s.manifest?.meta.pathname === currentPath);
if (!section) return; if (!section) return;
await section.asPromise(); await section.asPromise();
if (section.manifest) { if (section.manifest) {

View File

@@ -1,13 +1,11 @@
import './installer-database.element.js'; import './installer-database.element.js';
import { Meta, Story } from '@storybook/web-components'; import { Meta, Story } from '@storybook/web-components';
import { html } from '@umbraco-cms/backoffice/external/lit';
const { rest } = window.MockServiceWorker;
import { installerContextProvider } from '../shared/utils.story-helpers.js'; import { installerContextProvider } from '../shared/utils.story-helpers.js';
import type { UmbInstallerDatabaseElement } from './installer-database.element.js'; import type { UmbInstallerDatabaseElement } from './installer-database.element.js';
import type { InstallSettingsResponseModel } from '@umbraco-cms/backoffice/backend-api'; import { html } from '@umbraco-cms/backoffice/external/lit';
export default { export default {
title: 'Apps/Installer/Steps', title: 'Apps/Installer/Steps',
component: 'umb-installer-database', component: 'umb-installer-database',

View File

@@ -1151,6 +1151,8 @@ export default {
}, },
contentPicker: { contentPicker: {
allowedItemTypes: 'Du kan kun vælge følgende type(r) dokumenter: %0%', allowedItemTypes: 'Du kan kun vælge følgende type(r) dokumenter: %0%',
defineDynamicRoot: 'Definer Dynamisk Udgangspunkt',
defineRootNode: 'Vælg udgangspunkt',
pickedTrashedItem: 'Du har valgt et dokument som er slettet eller lagt i papirkurven', pickedTrashedItem: 'Du har valgt et dokument som er slettet eller lagt i papirkurven',
pickedTrashedItems: 'Du har valgt dokumenter som er slettede eller lagt i papirkurven', pickedTrashedItems: 'Du har valgt dokumenter som er slettede eller lagt i papirkurven',
}, },

View File

@@ -1148,6 +1148,8 @@ export default {
}, },
contentPicker: { contentPicker: {
allowedItemTypes: 'You can only select items of type(s): %0%', allowedItemTypes: 'You can only select items of type(s): %0%',
defineDynamicRoot: 'Specify a Dynamic Root',
defineRootNode: 'Pick root node',
pickedTrashedItem: 'You have picked a content item currently deleted or in the recycle bin', pickedTrashedItem: 'You have picked a content item currently deleted or in the recycle bin',
pickedTrashedItems: 'You have picked content items currently deleted or in the recycle bin', pickedTrashedItems: 'You have picked content items currently deleted or in the recycle bin',
}, },

View File

@@ -1,6 +1,6 @@
/* eslint-disable */ /* eslint-disable */
// @ts-ignore // @ts-ignore
import styles from 'monaco-editor/min/vs/editor/editor.main.css'; import styles from 'monaco-editor/min/vs/editor/editor.main.css?inline';
// @ts-ignore // @ts-ignore
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker.js?worker'; import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker.js?worker';
// @ts-ignore // @ts-ignore

View File

@@ -119,7 +119,7 @@ export const UmbClassMixin = <T extends ClassConstructor>(superClass: T) => {
public destroy() { public destroy() {
if (this._host) { if (this._host) {
this._host.removeController(this); this._host.removeController(this);
this._host = undefined as any; this._host = undefined as never;
} }
super.destroy(); super.destroy();
} }

View File

@@ -8,6 +8,7 @@ import { UmbBaseController } from './controller-base.class.js';
*/ */
export abstract class UmbContextBase< export abstract class UmbContextBase<
ContextType, ContextType,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
GivenContextToken extends UmbContextToken<any, ContextType> = UmbContextToken<any, ContextType>, GivenContextToken extends UmbContextToken<any, ContextType> = UmbContextToken<any, ContextType>,
> extends UmbBaseController { > extends UmbBaseController {
constructor(host: UmbControllerHost, contextToken: GivenContextToken | string) { constructor(host: UmbControllerHost, contextToken: GivenContextToken | string) {

View File

@@ -157,10 +157,12 @@ describe('UmbContextConsumer', () => {
type A = { prop: string }; type A = { prop: string };
function discriminator(instance: unknown): instance is A { function discriminator(instance: unknown): instance is A {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return typeof (instance as any).prop === 'string'; return typeof (instance as any).prop === 'string';
} }
function badDiscriminator(instance: unknown): instance is A { function badDiscriminator(instance: unknown): instance is A {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return typeof (instance as any).notExistingProp === 'string'; return typeof (instance as any).notExistingProp === 'string';
} }

View File

@@ -1,5 +1,5 @@
import { handlers as auditLogHandlers } from './handlers/audit-log.handlers.js';
import { handlers as dataTypeHandlers } from './handlers/data-type/index.js'; import { handlers as dataTypeHandlers } from './handlers/data-type/index.js';
import { handlers as relationTypeHandlers } from './handlers/relation-type.handlers.js';
import { handlers as documentTypeHandlers } from './handlers/document-type/index.js'; import { handlers as documentTypeHandlers } from './handlers/document-type/index.js';
import { handlers as installHandlers } from './handlers/install.handlers.js'; import { handlers as installHandlers } from './handlers/install.handlers.js';
import * as manifestsHandlers from './handlers/manifests.handlers.js'; import * as manifestsHandlers from './handlers/manifests.handlers.js';
@@ -11,6 +11,8 @@ import { handlers as telemetryHandlers } from './handlers/telemetry.handlers.js'
import { handlers as userGroupsHandlers } from './handlers/user-group/index.js'; import { handlers as userGroupsHandlers } from './handlers/user-group/index.js';
import { handlers as examineManagementHandlers } from './handlers/examine-management.handlers.js'; import { handlers as examineManagementHandlers } from './handlers/examine-management.handlers.js';
import { handlers as modelsBuilderHandlers } from './handlers/modelsbuilder.handlers.js'; import { handlers as modelsBuilderHandlers } from './handlers/modelsbuilder.handlers.js';
import { relationHandlers, relationTypeHandlers } from './handlers/relations/index.js';
import { handlers as objectTypeHandlers } from './handlers/object-type/index.js';
import { handlers as healthCheckHandlers } from './handlers/health-check.handlers.js'; import { handlers as healthCheckHandlers } from './handlers/health-check.handlers.js';
import { handlers as profilingHandlers } from './handlers/performance-profiling.handlers.js'; import { handlers as profilingHandlers } from './handlers/performance-profiling.handlers.js';
import { handlers as documentHandlers } from './handlers/document/index.js'; import { handlers as documentHandlers } from './handlers/document/index.js';
@@ -21,6 +23,7 @@ import { handlers as memberGroupHandlers } from './handlers/member-group.handler
import { handlers as memberHandlers } from './handlers/member.handlers.js'; import { handlers as memberHandlers } from './handlers/member.handlers.js';
import { handlers as memberTypeHandlers } from './handlers/member-type.handlers.js'; import { handlers as memberTypeHandlers } from './handlers/member-type.handlers.js';
import { handlers as templateHandlers } from './handlers/template.handlers.js'; import { handlers as templateHandlers } from './handlers/template.handlers.js';
import { handlers as trackedReferenceHandlers } from './handlers/tracked-reference.handlers.js';
import { handlers as languageHandlers } from './handlers/language.handlers.js'; import { handlers as languageHandlers } from './handlers/language.handlers.js';
import { handlers as cultureHandlers } from './handlers/culture.handlers.js'; import { handlers as cultureHandlers } from './handlers/culture.handlers.js';
import { handlers as redirectManagementHandlers } from './handlers/redirect-management.handlers.js'; import { handlers as redirectManagementHandlers } from './handlers/redirect-management.handlers.js';
@@ -36,6 +39,7 @@ import { handlers as scriptHandlers } from './handlers/scripts.handlers.js';
const handlers = [ const handlers = [
serverHandlers.serverVersionHandler, serverHandlers.serverVersionHandler,
...auditLogHandlers,
...configHandlers, ...configHandlers,
...cultureHandlers, ...cultureHandlers,
...dataTypeHandlers, ...dataTypeHandlers,
@@ -58,6 +62,8 @@ const handlers = [
...profilingHandlers, ...profilingHandlers,
...publishedStatusHandlers, ...publishedStatusHandlers,
...redirectManagementHandlers, ...redirectManagementHandlers,
...relationHandlers,
...objectTypeHandlers,
...relationTypeHandlers, ...relationTypeHandlers,
...rteEmbedHandlers, ...rteEmbedHandlers,
...scriptHandlers, ...scriptHandlers,
@@ -66,6 +72,7 @@ const handlers = [
...tagHandlers, ...tagHandlers,
...telemetryHandlers, ...telemetryHandlers,
...templateHandlers, ...templateHandlers,
...trackedReferenceHandlers,
...upgradeHandlers, ...upgradeHandlers,
...userGroupsHandlers, ...userGroupsHandlers,
...userHandlers, ...userHandlers,

View File

@@ -0,0 +1,58 @@
import { data as userData } from './user/user.data.js';
import { data as documentData } from './document.data.js';
import {
AuditLogResponseModel,
AuditLogWithUsernameResponseModel,
AuditTypeModel,
} from '@umbraco-cms/backoffice/backend-api';
const userId = userData[0].id;
const userName = userData[0].name;
const userAvatars = userData[0].avatarUrls;
const documentId = documentData[0].id;
export const logs: Array<AuditLogResponseModel> = [
{
userId: userId,
entityId: documentId,
entityType: 'Document',
timestamp: '2021-09-14T09:32:49.0000000Z',
logType: AuditTypeModel.SAVE,
comment: undefined,
parameters: undefined,
},
{
userId: userId,
entityId: documentId,
entityType: 'Document',
timestamp: '2022-09-14T11:30:49.0000000Z',
logType: AuditTypeModel.SAVE,
comment: undefined,
parameters: undefined,
},
{
userId: userId,
entityId: documentId,
entityType: 'Document',
timestamp: '2022-09-15T09:35:49.0000000Z',
logType: AuditTypeModel.SAVE,
comment: undefined,
parameters: undefined,
},
{
userId: userId,
entityId: documentId,
entityType: 'Document',
timestamp: '2023-01-09T12:00:00.0000000Z',
logType: AuditTypeModel.PUBLISH,
comment: undefined,
parameters: undefined,
},
];
export const logsWithUser: Array<AuditLogWithUsernameResponseModel> = logs.map((log) => ({
...log,
userName: userName,
userAvatars: userAvatars,
}));

View File

@@ -0,0 +1,32 @@
import { UmbEntityData } from '../entity.data.js';
import type { ObjectTypeResponseModel } from '@umbraco-cms/backoffice/backend-api';
export const data: Array<ObjectTypeResponseModel> = [
{
id: '1',
name: 'Media',
},
{
id: '2',
name: 'Content',
},
{
id: '3',
name: 'User',
},
{
id: '4',
name: 'Document',
},
];
// Temp mocked database
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
class UmbObjectTypeData extends UmbEntityData<ObjectTypeResponseModel> {
constructor() {
super(data);
}
}
export const umbObjectTypeData = new UmbObjectTypeData();

View File

@@ -1,5 +1,5 @@
import { UmbEntityData } from './entity.data.js'; import { UmbEntityData } from '../entity.data.js';
import { createEntityTreeItem } from './utils.js'; import { createEntityTreeItem } from '../utils.js';
import type { EntityTreeItemResponseModel, RelationTypeResponseModel } from '@umbraco-cms/backoffice/backend-api'; import type { EntityTreeItemResponseModel, RelationTypeResponseModel } from '@umbraco-cms/backoffice/backend-api';
// TODO: investigate why we don't get an entity type as part of the RelationTypeResponseModel // TODO: investigate why we don't get an entity type as part of the RelationTypeResponseModel

View File

@@ -0,0 +1,109 @@
import { UmbEntityData } from '../entity.data.js';
import type { RelationResponseModel } from '@umbraco-cms/backoffice/backend-api';
export const data: Array<RelationResponseModel> = [
{
parentId: 'e0d39ff5-71d8-453f-b682-9d8d31ee5e06',
parentName: 'Relate Document On Copy',
childId: '1',
childName: 'Child 1',
createDate: '2021-01-01',
comment: 'Comment 1',
},
{
parentId: 'e0d39ff5-71d8-453f-b682-9d8d31ee5e06',
parentName: 'Relate Document On Copy',
childId: '2',
childName: 'Child 2',
createDate: '2021-01-01',
comment: 'Comment 2',
},
{
parentId: 'e0d39ff5-71d8-453f-b682-9d8d31ee5e06',
parentName: 'Relate Document On Copy',
childId: '3',
childName: 'Child 3',
createDate: '2021-01-01',
comment: 'Comment 3',
},
{
parentId: 'e0d39ff5-71d8-453f-b682-9d8d31ee5e06',
parentName: 'Relate Document On Copy',
childId: '4',
childName: 'Child 4',
createDate: '2021-01-01',
comment: 'Comment 4',
},
{
parentId: 'ac68cde6-763f-4231-a751-1101b57defd2',
parentName: 'Relate Document On Copy',
childId: '5',
childName: 'Child 5',
createDate: '2021-01-01',
comment: 'Comment 5',
},
{
parentId: 'ac68cde6-763f-4231-a751-1101b57defd2',
parentName: 'Relate Document On Copy',
childId: '6',
childName: 'Child 6',
createDate: '2021-01-01',
comment: 'Comment 6',
},
{
parentId: '6f9b800c-762c-42d4-85d9-bf40a77d689e',
parentName: 'Relate Document On Copy',
childId: '7',
childName: 'Child 7',
createDate: '2021-01-01',
comment: 'Comment 7',
},
{
parentId: 'd421727d-43de-4205-b4c6-037404f309ad',
parentName: 'Relate Document On Copy',
childId: '8',
childName: 'Child 8',
createDate: '2021-01-01',
comment: 'Comment 8',
},
{
parentId: 'd421727d-43de-4205-b4c6-037404f309ad',
parentName: 'Relate Document On Copy',
childId: '9',
childName: 'Child 9',
createDate: '2021-01-01',
comment: 'Comment 9',
},
{
parentId: 'e9a0a28e-2d5b-4229-ac00-66f2df230513',
parentName: 'Relate Document On Copy',
childId: '10',
childName: 'Child 10',
createDate: '2021-01-01',
comment: 'Comment 10',
},
{
parentId: 'e9a0a28e-2d5b-4229-ac00-66f2df230513',
parentName: 'Relate Document On Copy',
childId: '11',
childName: 'Child 11',
createDate: '2021-01-01',
comment: 'Comment 11',
},
];
// Temp mocked database
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
class UmbRelationData extends UmbEntityData<RelationResponseModel> {
constructor() {
super(data);
}
getRelationsByParentId(id: string) {
const test = this.data.filter((relation) => relation.parentId === id);
return { items: test, total: test.length };
}
}
export const umbRelationData = new UmbRelationData();

View File

@@ -0,0 +1,16 @@
import { RelationItemResponseModel } from '@umbraco-cms/backoffice/backend-api';
export const items: Array<RelationItemResponseModel> = [
{
nodeId: 'simple-document-id',
nodeName: 'Simple Document',
nodeType: 'document',
nodePublished: true,
contentTypeIcon: 'icon-document',
contentTypeName: 'Simple Document Type',
contentTypeAlias: 'blogPost',
relationTypeIsBidirectional: false,
relationTypeIsDependency: true,
relationTypeName: 'Related Document',
},
];

View File

@@ -1,3 +1,4 @@
import { handlers as auditLogHandlers } from './handlers/audit-log.handlers.js';
import { handlers as dataTypeHandlers } from './handlers/data-type/index.js'; import { handlers as dataTypeHandlers } from './handlers/data-type/index.js';
import { handlers as documentTypeHandlers } from './handlers/document-type/index.js'; import { handlers as documentTypeHandlers } from './handlers/document-type/index.js';
import { handlers as installHandlers } from './handlers/install.handlers.js'; import { handlers as installHandlers } from './handlers/install.handlers.js';
@@ -15,11 +16,13 @@ import { handlers as languageHandlers } from './handlers/language.handlers.js';
import { handlers as redirectManagementHandlers } from './handlers/redirect-management.handlers.js'; import { handlers as redirectManagementHandlers } from './handlers/redirect-management.handlers.js';
import { handlers as packageHandlers } from './handlers/package.handlers.js'; import { handlers as packageHandlers } from './handlers/package.handlers.js';
import { handlers as configHandlers } from './handlers/config.handlers.js'; import { handlers as configHandlers } from './handlers/config.handlers.js';
import { handlers as trackedReferenceHandlers } from './handlers/tracked-reference.handlers.js';
export const handlers = [ export const handlers = [
serverHandlers.serverRunningHandler, serverHandlers.serverRunningHandler,
serverHandlers.serverVersionHandler, serverHandlers.serverVersionHandler,
manifestsHandlers.manifestEmptyHandler, manifestsHandlers.manifestEmptyHandler,
...auditLogHandlers,
...installHandlers, ...installHandlers,
...upgradeHandlers, ...upgradeHandlers,
...userHandlers, ...userHandlers,
@@ -35,4 +38,5 @@ export const handlers = [
...redirectManagementHandlers, ...redirectManagementHandlers,
...packageHandlers, ...packageHandlers,
...configHandlers, ...configHandlers,
...trackedReferenceHandlers,
]; ];

View File

@@ -0,0 +1,41 @@
import { logs, logsWithUser } from '../data/audit-log.data.js';
const { rest } = window.MockServiceWorker;
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
import {
PagedAuditLogResponseModel,
PagedAuditLogWithUsernameResponseModel,
} from '@umbraco-cms/backoffice/backend-api';
export const handlers = [
rest.get(umbracoPath('/audit-log'), (_req, res, ctx) => {
const PagedAuditLog = {
total: logsWithUser.length,
items: logsWithUser,
};
return res(ctx.status(200), ctx.json<PagedAuditLogWithUsernameResponseModel>(PagedAuditLog));
}),
rest.get(umbracoPath('/audit-log/:id'), (_req, res, ctx) => {
const id = _req.params.id as string;
if (!id) return;
const foundLogs = logs.filter((log) => log.entityId === id);
const PagedAuditLog = {
total: foundLogs.length,
items: foundLogs,
};
return res(ctx.status(200), ctx.json<PagedAuditLogResponseModel>(PagedAuditLog));
}),
rest.get(umbracoPath('/audit-log/type/:logType'), (_req, res, ctx) => {
const logType = _req.params.logType as string;
if (!logType) return;
const foundLogs = logs.filter((log) => log.entityType === logType);
const PagedAuditLog = {
total: foundLogs.length,
items: foundLogs,
};
return res(ctx.status(200), ctx.json<PagedAuditLogResponseModel>(PagedAuditLog));
}),
];

View File

@@ -0,0 +1,3 @@
import { handlers as itemHandlers } from './item.handlers.js';
export const handlers = [...itemHandlers];

View File

@@ -0,0 +1,11 @@
const { rest } = window.MockServiceWorker;
import { umbObjectTypeData } from '../../data/object-type/object-type.data.js';
import { UMB_SLUG } from './slug.js';
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
export const handlers = [
rest.get(umbracoPath(`${UMB_SLUG}`), (req, res, ctx) => {
const response = umbObjectTypeData.getAll();
return res(ctx.status(200), ctx.json(response));
}),
];

View File

@@ -0,0 +1 @@
export const UMB_SLUG = '/object-types';

View File

@@ -1,76 +0,0 @@
const { rest } = window.MockServiceWorker;
import { umbRelationTypeData } from '../data/relation-type.data.js';
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
// TODO: add schema
export const handlers = [
rest.delete<string[]>('/umbraco/backoffice/relation-type/:id', async (req, res, ctx) => {
const id = req.params.id as string;
if (!id) return;
umbRelationTypeData.delete([id]);
return res(ctx.status(200));
}),
rest.get('/umbraco/management/api/v1/tree/relation-type/root', (req, res, ctx) => {
const rootItems = umbRelationTypeData.getTreeRoot();
const response = {
total: rootItems.length,
items: rootItems,
};
return res(ctx.status(200), ctx.json(response));
}),
rest.get('/umbraco/management/api/v1/tree/relation-type/children', (req, res, ctx) => {
const parentId = req.url.searchParams.get('parentId');
if (!parentId) return;
const children = umbRelationTypeData.getTreeItemChildren(parentId);
const response = {
total: children.length,
items: children,
};
return res(ctx.status(200), ctx.json(response));
}),
rest.get('/umbraco/management/api/v1/tree/relation-type/item', (req, res, ctx) => {
const ids = req.url.searchParams.getAll('id');
if (!ids) return;
const items = umbRelationTypeData.getTreeItem(ids);
return res(ctx.status(200), ctx.json(items));
}),
rest.get(umbracoPath('/relation-type/:id'), (req, res, ctx) => {
const id = req.params.id as string;
if (!id) return;
const RelationType = umbRelationTypeData.getById(id);
return res(ctx.status(200), ctx.json(RelationType));
}),
rest.post(umbracoPath('/relation-type/:id'), async (req, res, ctx) => {
const id = req.params.id as string;
if (!id) return;
const data = await req.json();
if (!data) return;
umbRelationTypeData.insert(data);
return res(ctx.status(200));
}),
rest.put(umbracoPath('/relation-type/:id'), async (req, res, ctx) => {
const id = req.params.id as string;
if (!id) return;
const data = await req.json();
if (!data) return;
umbRelationTypeData.save(id, data);
return res(ctx.status(200));
}),
];

View File

@@ -0,0 +1,2 @@
export * from './relation/index.js';
export * from './relation-type/index.js';

View File

@@ -0,0 +1,47 @@
const { rest } = window.MockServiceWorker;
import { umbRelationTypeData } from '../../../data/relations/relation-type.data.js';
import { UMB_SLUG } from './slug.js';
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
// TODO: add schema
export const handlers = [
rest.delete<string[]>(umbracoPath(`${UMB_SLUG}/:id`), async (req, res, ctx) => {
const id = req.params.id as string;
if (!id) return;
umbRelationTypeData.delete([id]);
return res(ctx.status(200));
}),
rest.get(umbracoPath(`${UMB_SLUG}/:id`), (req, res, ctx) => {
const id = req.params.id as string;
if (!id) return;
const RelationType = umbRelationTypeData.getById(id);
return res(ctx.status(200), ctx.json(RelationType));
}),
rest.post(umbracoPath(`${UMB_SLUG}/:id`), async (req, res, ctx) => {
const id = req.params.id as string;
if (!id) return;
const data = await req.json();
if (!data) return;
umbRelationTypeData.insert(data);
return res(ctx.status(200));
}),
rest.put(umbracoPath(`${UMB_SLUG}/:id`), async (req, res, ctx) => {
const id = req.params.id as string;
if (!id) return;
const data = await req.json();
if (!data) return;
umbRelationTypeData.save(id, data);
return res(ctx.status(200));
}),
];

View File

@@ -0,0 +1,4 @@
import { handlers as detailHandlers } from './detail.handlers.js';
import { handlers as treeHandlers } from './tree.handlers.js';
export const relationTypeHandlers = [...detailHandlers, ...treeHandlers];

View File

@@ -0,0 +1 @@
export const UMB_SLUG = '/relation-type';

View File

@@ -0,0 +1,36 @@
const { rest } = window.MockServiceWorker;
import { umbRelationTypeData } from '../../../data/relations/relation-type.data.js';
import { UMB_SLUG } from './slug.js';
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
export const handlers = [
rest.get(umbracoPath(`/tree${UMB_SLUG}/root`), (req, res, ctx) => {
const rootItems = umbRelationTypeData.getTreeRoot();
const response = {
total: rootItems.length,
items: rootItems,
};
return res(ctx.status(200), ctx.json(response));
}),
rest.get(umbracoPath(`/tree${UMB_SLUG}/children`), (req, res, ctx) => {
const parentId = req.url.searchParams.get('parentId');
if (!parentId) return;
const children = umbRelationTypeData.getTreeItemChildren(parentId);
const response = {
total: children.length,
items: children,
};
return res(ctx.status(200), ctx.json(response));
}),
rest.get(umbracoPath(`/tree${UMB_SLUG}/item`), (req, res, ctx) => {
const ids = req.url.searchParams.getAll('id');
if (!ids) return;
const items = umbRelationTypeData.getTreeItem(ids);
return res(ctx.status(200), ctx.json(items));
}),
];

View File

@@ -0,0 +1,3 @@
import { handlers as itemHandlers } from './item.handlers.js';
export const relationHandlers = [...itemHandlers];

View File

@@ -0,0 +1,15 @@
const { rest } = window.MockServiceWorker;
import { umbRelationData } from '../../../data/relations/relation.data.js';
import { UMB_SLUG } from './slug.js';
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
export const handlers = [
rest.get(umbracoPath(`${UMB_SLUG}/type/:id`), (req, res, ctx) => {
const id = req.params.id as string;
if (!id) return;
const response = umbRelationData.getRelationsByParentId(id);
return res(ctx.status(200), ctx.json(response));
}),
];

View File

@@ -0,0 +1 @@
export const UMB_SLUG = '/relation';

View File

@@ -0,0 +1,18 @@
import { items } from '../data/tracked-reference.data.js';
const { rest } = window.MockServiceWorker;
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
import { PagedRelationItemResponseModel } from '@umbraco-cms/backoffice/backend-api';
export const handlers = [
rest.get(umbracoPath('/tracked-reference/:id'), (_req, res, ctx) => {
const id = _req.params.id as string;
if (!id) return;
const PagedTrackedReference = {
total: items.length,
items: items,
};
return res(ctx.status(200), ctx.json<PagedRelationItemResponseModel>(PagedTrackedReference));
}),
];

View File

@@ -0,0 +1,3 @@
import { manifests as repositoryManifests } from './repository/manifests.js';
export const manifests = [...repositoryManifests];

View File

@@ -0,0 +1,67 @@
import { UmbAuditLogServerDataSource } from './audit-log.server.data.js';
import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/notification';
import { AuditTypeModel, DirectionModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbBaseController } from '@umbraco-cms/backoffice/class-api';
export class UmbAuditLogRepository extends UmbBaseController {
#dataSource: UmbAuditLogServerDataSource;
#notificationService?: UmbNotificationContext;
#init;
constructor(host: UmbControllerHostElement) {
super(host);
this.#dataSource = new UmbAuditLogServerDataSource(host);
this.#init = new UmbContextConsumerController(host, UMB_NOTIFICATION_CONTEXT_TOKEN, (instance) => {
this.#notificationService = instance;
}).asPromise();
}
async getLog({
orderDirection,
sinceDate,
skip = 0,
take = 100,
}: {
orderDirection?: DirectionModel;
sinceDate?: string;
skip?: number;
take?: number;
}) {
await this.#init;
return this.#dataSource.getAuditLog({ orderDirection, sinceDate, skip, take });
}
async getAuditLogByUnique({
id,
orderDirection,
skip = 0,
take = 100,
}: {
id: string;
orderDirection?: DirectionModel;
skip?: number;
take?: number;
}) {
await this.#init;
return this.#dataSource.getAuditLogById({ id, orderDirection, skip, take });
}
async getAuditLogTypeByLogType({
logType,
sinceDate,
skip,
take,
}: {
logType: AuditTypeModel;
sinceDate?: string;
skip?: number;
take?: number;
}) {
await this.#init;
return this.#dataSource.getAuditLogTypeByLogType({ logType, sinceDate, skip, take });
}
}

View File

@@ -0,0 +1,80 @@
import { AuditLogResource, DirectionModel, AuditTypeModel } from '@umbraco-cms/backoffice/backend-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
/**
* A data source for Data Type items that fetches data from the server
* @export
* @class UmbAuditLogServerDataSource
*/
export class UmbAuditLogServerDataSource {
#host: UmbControllerHost;
/**
* Creates an instance of UmbAuditLogServerDataSource.
* @param {UmbControllerHost} host
* @memberof UmbAuditLogServerDataSource
*/
constructor(host: UmbControllerHost) {
this.#host = host;
}
/**
* Fetches the items for the given ids from the server
* @param {Array<string>} ids
* @return {*}
* @memberof UmbAuditLogServerDataSource
*/
async getAuditLog({
orderDirection,
sinceDate,
skip,
take,
}: {
orderDirection?: DirectionModel;
sinceDate?: string;
skip?: number;
take?: number;
}) {
return await tryExecuteAndNotify(
this.#host,
AuditLogResource.getAuditLog({ orderDirection, sinceDate, skip, take }),
);
}
async getAuditLogById({
id,
orderDirection,
sinceDate,
skip,
take,
}: {
id: string;
orderDirection?: DirectionModel;
sinceDate?: string;
skip?: number;
take?: number;
}) {
return await tryExecuteAndNotify(
this.#host,
AuditLogResource.getAuditLogById({ id, orderDirection, sinceDate, skip, take }),
);
}
async getAuditLogTypeByLogType({
logType,
sinceDate,
skip,
take,
}: {
logType: AuditTypeModel;
sinceDate?: string;
skip?: number;
take?: number;
}) {
return await tryExecuteAndNotify(
this.#host,
AuditLogResource.getAuditLogTypeByLogType({ logType, sinceDate, skip, take }),
);
}
}

View File

@@ -0,0 +1,2 @@
export { UMB_AUDIT_LOG_REPOSITORY_ALIAS as AUDIT_LOG_REPOSITORY_ALIAS } from './manifests.js';
export { UmbAuditLogRepository } from './audit-log.repository.js';

View File

@@ -0,0 +1,13 @@
import { UmbAuditLogRepository } from './audit-log.repository.js';
import type { ManifestRepository } from '@umbraco-cms/backoffice/extension-registry';
export const UMB_AUDIT_LOG_REPOSITORY_ALIAS = 'Umb.Repository.AuditLog';
const repository: ManifestRepository = {
type: 'repository',
alias: UMB_AUDIT_LOG_REPOSITORY_ALIAS,
name: 'AuditLog Repository',
api: UmbAuditLogRepository,
};
export const manifests = [repository];

View File

@@ -0,0 +1,9 @@
export const name = 'Umbraco.Core.AuditLog';
export const extensions = [
{
name: 'Audit Log Bundle',
alias: 'Umb.Bundle.AuditLog',
type: 'bundle',
js: () => import('./manifests.js'),
},
];

View File

@@ -1,4 +1,5 @@
import { css, html, customElement, property } from '@umbraco-cms/backoffice/external/lit'; import { UMB_APP_CONTEXT } from '@umbraco-cms/backoffice/app';
import { css, html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
@@ -13,20 +14,30 @@ export class UmbHistoryItemElement extends UmbLitElement {
@property({ type: String }) @property({ type: String })
detail?: string; detail?: string;
@state()
private _serverUrl?: string;
constructor() {
super();
this.consumeContext(UMB_APP_CONTEXT, (instance) => {
this._serverUrl = instance.getServerUrl();
});
}
render() { render() {
return html`<div id="wrapper"> return html`
<div class="user-info"> <div class="user-info">
<uui-avatar .name="${this.name ?? 'Unknown'}" ?src="${this.src}"></uui-avatar> <uui-avatar
.name="${this.name ?? 'Unknown'}"
.imgSrc="${this.src ? this._serverUrl + this.src : ''}"></uui-avatar>
<div> <div>
<span class="name">${this.name}</span> <span class="name">${this.name}</span>
<span class="detail">${this.detail}</span> <span class="detail">${this.detail}</span>
</div> </div>
</div> </div>
<div class="slots-wrapper"> <slot id="description"></slot>
<slot id="description"></slot> <slot id="actions-container" name="actions"></slot>
<slot id="actions-container" name="actions"></slot> `;
</div>
</div>`;
} }
static styles = [ static styles = [
@@ -34,20 +45,7 @@ export class UmbHistoryItemElement extends UmbLitElement {
css` css`
:host { :host {
--avatar-size: calc(2em + 4px); --avatar-size: calc(2em + 4px);
display: block; display: contents;
}
#wrapper {
display: flex;
width: 100%;
gap: calc(2 * var(--uui-size-space-5));
align-items: center;
}
.slots-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
flex: 1;
} }
slot[name='actions'] { slot[name='actions'] {
@@ -70,14 +68,17 @@ export class UmbHistoryItemElement extends UmbLitElement {
} }
.user-info { .user-info {
position: relative;
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
gap: var(--uui-size-space-5); gap: var(--uui-size-space-5);
} }
.user-info div { .user-info div {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.detail { .detail {
font-size: var(--uui-size-4); font-size: var(--uui-size-4);
color: var(--uui-color-text-alt); color: var(--uui-color-text-alt);

View File

@@ -5,19 +5,24 @@ import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
@customElement('umb-history-list') @customElement('umb-history-list')
export class UmbHistoryListElement extends UmbLitElement { export class UmbHistoryListElement extends UmbLitElement {
render() { render() {
return html`<div> return html`<slot></slot> `;
<slot></slot>
</div>`;
} }
static styles = [ static styles = [
UmbTextStyles, UmbTextStyles,
css` css`
:host { :host {
display: block; display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
--avatar-size: calc(2em + 4px); --avatar-size: calc(2em + 4px);
gap: var(--uui-size-6);
position: relative;
} }
/** TODO: This doesn't work due to "display:contents" in umb-history-item, but is needed for the way I put the grid together.
* Find a different solution so we can have the grey line that links each history item together (this is purely a visual effect, no rush)
::slotted(*) { ::slotted(*) {
position: relative; position: relative;
} }
@@ -25,7 +30,6 @@ export class UmbHistoryListElement extends UmbLitElement {
::slotted(*:not(:last-child)) { ::slotted(*:not(:last-child)) {
margin-bottom: calc(2 * var(--uui-size-space-3)); margin-bottom: calc(2 * var(--uui-size-space-3));
} }
::slotted(*:not(:last-child))::before { ::slotted(*:not(:last-child))::before {
content: ''; content: '';
border: 1px solid var(--uui-color-border); border: 1px solid var(--uui-color-border);
@@ -35,6 +39,7 @@ export class UmbHistoryListElement extends UmbLitElement {
top: var(--avatar-size); top: var(--avatar-size);
left: calc(-1px + var(--avatar-size) / 2); left: calc(-1px + var(--avatar-size) / 2);
} }
*/
`, `,
]; ];
} }

View File

@@ -23,7 +23,7 @@ export * from './input-number-range/index.js';
export * from './input-radio-button-list/index.js'; export * from './input-radio-button-list/index.js';
export * from './input-section/index.js'; export * from './input-section/index.js';
export * from './input-slider/index.js'; export * from './input-slider/index.js';
export * from './input-start-node/index.js'; export * from './input-tree-picker-source/index.js';
export * from './input-tiny-mce/index.js'; export * from './input-tiny-mce/index.js';
export * from './input-toggle/index.js'; export * from './input-toggle/index.js';
export * from './input-upload-field/index.js'; export * from './input-upload-field/index.js';

View File

@@ -1 +0,0 @@
export * from './input-start-node.element.js';

View File

@@ -1,119 +0,0 @@
import { UmbInputDocumentElement } from '@umbraco-cms/backoffice/document';
import { html, customElement, property, css, state } from '@umbraco-cms/backoffice/external/lit';
import { FormControlMixin, UUISelectEvent } from '@umbraco-cms/backoffice/external/uui';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { UmbInputMediaElement } from '@umbraco-cms/backoffice/media';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
export type ContentType = 'content' | 'member' | 'media';
export type StartNode = {
type?: ContentType;
id?: string | null;
query?: string | null;
};
@customElement('umb-input-start-node')
export class UmbInputStartNodeElement extends FormControlMixin(UmbLitElement) {
protected getFormElement() {
return undefined;
}
private _type: StartNode['type'] = 'content';
@property()
public set type(value: StartNode['type']) {
const oldValue = this._type;
this._options = this._options.map((option) =>
option.value === value ? { ...option, selected: true } : { ...option, selected: false },
);
this._type = value;
this.requestUpdate('type', oldValue);
}
public get type(): StartNode['type'] {
return this._type;
}
@property({ attribute: 'node-id' })
nodeId = '';
@property({ attribute: 'dynamic-path' })
dynamicPath = '';
@state()
_options: Array<Option> = [
{ value: 'content', name: 'Content' },
{ value: 'member', name: 'Members' },
{ value: 'media', name: 'Media' },
];
#onTypeChange(event: UUISelectEvent) {
this.type = event.target.value as StartNode['type'];
// Clear others
this.nodeId = '';
this.dynamicPath = '';
this.dispatchEvent(new UmbChangeEvent());
}
#onIdChange(event: CustomEvent) {
this.nodeId = (event.target as UmbInputDocumentElement | UmbInputMediaElement).selectedIds.join('');
this.dispatchEvent(new CustomEvent('change'));
}
render() {
return html`<umb-input-dropdown-list
.options=${this._options}
@change="${this.#onTypeChange}"></umb-input-dropdown-list>
${this.#renderType()}`;
}
#renderType() {
switch (this.type) {
case 'content':
return this.#renderTypeContent();
case 'media':
return this.#renderTypeMedia();
case 'member':
return this.#renderTypeMember();
default:
return 'No type found';
}
}
#renderTypeContent() {
const nodeId = this.nodeId ? [this.nodeId] : [];
//TODO: Dynamic paths
return html` <umb-input-document @change=${this.#onIdChange} .selectedIds=${nodeId} max="1"></umb-input-document> `;
}
#renderTypeMedia() {
const nodeId = this.nodeId ? [this.nodeId] : [];
//TODO => MediaTypes
return html` <umb-input-media @change=${this.#onIdChange} .selectedIds=${nodeId} max="1"></umb-input-media> `;
}
#renderTypeMember() {
const nodeId = this.nodeId ? [this.nodeId] : [];
//TODO => Members
return html` <umb-input-member @change=${this.#onIdChange} .selectedIds=${nodeId} max="1"></umb-input-member> `;
}
static styles = [
css`
:host {
display: flex;
flex-direction: column;
gap: var(--uui-size-4);
}
`,
];
}
export default UmbInputStartNodeElement;
declare global {
interface HTMLElementTagNameMap {
'umb-input-start-node': UmbInputStartNodeElement;
}
}

View File

@@ -0,0 +1 @@
export * from './input-tree-picker-source.element.js';

View File

@@ -0,0 +1,149 @@
import { UmbInputDocumentPickerRootElement } from '@umbraco-cms/backoffice/document';
import { html, customElement, property, css, state } from '@umbraco-cms/backoffice/external/lit';
import { FormControlMixin, UUISelectEvent } from '@umbraco-cms/backoffice/external/uui';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { UmbInputMediaElement } from '@umbraco-cms/backoffice/media';
//import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
export type UmbTreePickerSource = {
type?: UmbTreePickerSourceType;
id?: string | null;
dynamicRoot?: UmbTreePickerDynamicRoot | null;
};
export type UmbTreePickerSourceType = 'content' | 'member' | 'media';
export type UmbTreePickerDynamicRoot = {
originAlias: string;
querySteps?: Array<UmbTreePickerDynamicRootQueryStep> | null;
};
export type UmbTreePickerDynamicRootQueryStep = {
alias: string;
anyOfDocTypeKeys: Array<string>;
};
@customElement('umb-input-tree-picker-source')
export class UmbInputTreePickerSourceElement extends FormControlMixin(UmbLitElement) {
protected getFormElement() {
return undefined;
}
private _type: UmbTreePickerSource['type'] = 'content';
@property()
public set type(value: UmbTreePickerSource['type']) {
if (value === undefined) {
value = this._type;
}
const oldValue = this._type;
this._options = this._options.map((option) =>
option.value === value ? { ...option, selected: true } : { ...option, selected: false },
);
this._type = value;
this.requestUpdate('type', oldValue);
}
public get type(): UmbTreePickerSource['type'] {
return this._type;
}
@property({ attribute: 'node-id' })
nodeId?: string | null;
@property({ attribute: false })
dynamicRoot?: UmbTreePickerDynamicRoot | null;
@state()
_options: Array<Option> = [
{ value: 'content', name: 'Content' },
{ value: 'media', name: 'Media' },
{ value: 'member', name: 'Members' },
];
#onTypeChange(event: UUISelectEvent) {
//console.log('onTypeChange');
this.type = event.target.value as UmbTreePickerSource['type'];
this.nodeId = '';
// TODO: Appears that the event gets bubbled up. Will need to review. [LK]
//this.dispatchEvent(new UmbChangeEvent());
}
#onIdChange(event: CustomEvent) {
//console.log('onIdChange', event.target);
switch (this.type) {
case 'content':
this.nodeId = (<UmbInputDocumentPickerRootElement>event.target).nodeId;
break;
case 'media':
this.nodeId = (<UmbInputMediaElement>event.target).selectedIds.join('');
break;
default:
break;
}
this.dispatchEvent(new CustomEvent(event.type));
}
render() {
return html`<umb-input-dropdown-list
.options=${this._options}
@change="${this.#onTypeChange}"></umb-input-dropdown-list>
${this.#renderType()}`;
}
#renderType() {
switch (this.type) {
case 'content':
return this.#renderTypeContent();
case 'media':
return this.#renderTypeMedia();
case 'member':
return this.#renderTypeMember();
default:
return 'No type found';
}
}
#renderTypeContent() {
return html`<umb-input-document-picker-root
@change=${this.#onIdChange}
.nodeId=${this.nodeId}></umb-input-document-picker-root>`;
}
#renderTypeMedia() {
const nodeId = this.nodeId ? [this.nodeId] : [];
//TODO => MediaTypes
return html`<umb-input-media @change=${this.#onIdChange} .selectedIds=${nodeId} max="1"></umb-input-media>`;
}
#renderTypeMember() {
const nodeId = this.nodeId ? [this.nodeId] : [];
//TODO => Members
return html`<umb-input-member @change=${this.#onIdChange} .selectedIds=${nodeId} max="1"></umb-input-member>`;
}
static styles = [
css`
:host {
display: flex;
flex-direction: column;
gap: var(--uui-size-4);
}
`,
];
}
export default UmbInputTreePickerSourceElement;
declare global {
interface HTMLElementTagNameMap {
'umb-input-tree-picker-source': UmbInputTreePickerSourceElement;
}
}

View File

@@ -155,7 +155,11 @@ export class UmbTableElement extends LitElement {
render() { render() {
return html`<uui-table class="uui-text"> return html`<uui-table class="uui-text">
<uui-table-column style="width: 60px;"></uui-table-column> <uui-table-column
.style=${when(
!(this.config.allowSelection === false && this.config.hideIcon === true),
() => 'width: 60px',
)}></uui-table-column>
<uui-table-head> <uui-table-head>
${this._renderHeaderCheckboxCell()} ${this.columns.map((column) => this._renderHeaderCell(column))} ${this._renderHeaderCheckboxCell()} ${this.columns.map((column) => this._renderHeaderCell(column))}
</uui-table-head> </uui-table-head>

View File

@@ -101,3 +101,14 @@ export const WithHiddenIcons: Story = {
}, },
}, },
}; };
export const WithHiddenIconsAndDisallowedSelections: Story = {
args: {
items: items,
columns: columns,
config: {
allowSelection: false,
hideIcon: true,
},
},
};

View File

@@ -42,6 +42,7 @@ export * from './variant/index.js';
export * from './workspace/index.js'; export * from './workspace/index.js';
export * from './culture/index.js'; export * from './culture/index.js';
export * from './temporary-file/index.js'; export * from './temporary-file/index.js';
export * from './object-type/index.js';
const manifests: Array<ManifestTypes | UmbBackofficeManifestKind> = [ const manifests: Array<ManifestTypes | UmbBackofficeManifestKind> = [
...conditionManifests, ...conditionManifests,

View File

@@ -1,4 +1,4 @@
import { UmbTreeElement } from '../../../tree/tree.element.js'; import { UmbTreeElement, type UmbTreeSelectionConfiguration } from '@umbraco-cms/backoffice/tree';
import { css, html, nothing, customElement, query, state, styleMap } from '@umbraco-cms/backoffice/external/lit'; import { css, html, nothing, customElement, query, state, styleMap } from '@umbraco-cms/backoffice/external/lit';
import { UUIBooleanInputEvent, UUIInputElement } from '@umbraco-cms/backoffice/external/uui'; import { UUIBooleanInputEvent, UUIInputElement } from '@umbraco-cms/backoffice/external/uui';
import { import {
@@ -13,6 +13,13 @@ import { UMB_DOCUMENT_TREE_ALIAS } from '@umbraco-cms/backoffice/document';
@customElement('umb-link-picker-modal') @customElement('umb-link-picker-modal')
export class UmbLinkPickerModalElement extends UmbModalBaseElement<UmbLinkPickerModalData, UmbLinkPickerModalValue> { export class UmbLinkPickerModalElement extends UmbModalBaseElement<UmbLinkPickerModalData, UmbLinkPickerModalValue> {
@state()
private _selectionConfiguration: UmbTreeSelectionConfiguration = {
multiple: false,
selectable: true,
selection: [],
};
@state() @state()
_selectedKey?: string; _selectedKey?: string;
@@ -51,6 +58,7 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement<UmbLinkPicker
this.observe(this.modalContext.value, (value) => { this.observe(this.modalContext.value, (value) => {
(this._link as any) = value.link; (this._link as any) = value.link;
this._selectedKey = this._link?.udi ? getKeyFromUdi(this._link.udi) : undefined; this._selectedKey = this._link?.udi ? getKeyFromUdi(this._link.udi) : undefined;
this._selectionConfiguration.selection = this._selectedKey ? [this._selectedKey] : [];
}); });
} }
this._layout = this.data?.config; this._layout = this.data?.config;
@@ -76,11 +84,13 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement<UmbLinkPicker
//TODO: Update icon, published, trashed //TODO: Update icon, published, trashed
e.stopPropagation(); e.stopPropagation();
const element = e.target as UmbTreeElement; const element = e.target as UmbTreeElement;
const selectedKey = element.selection[element.selection.length - 1]; const selection = element.getSelection();
const selectedKey = selection[selection.length - 1];
if (!selectedKey) { if (!selectedKey) {
this.#partialUpdateLink({ udi: '', url: undefined }); this.#partialUpdateLink({ udi: '', url: undefined });
this._selectedKey = undefined; this._selectedKey = undefined;
this._selectionConfiguration.selection = [];
this.requestUpdate(); this.requestUpdate();
return; return;
} }
@@ -88,6 +98,7 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement<UmbLinkPicker
const udi = buildUdi(entityType, selectedKey); const udi = buildUdi(entityType, selectedKey);
this._selectedKey = selectedKey; this._selectedKey = selectedKey;
this._selectionConfiguration.selection = [this._selectedKey];
this.#partialUpdateLink({ udi: udi, url: udi }); this.#partialUpdateLink({ udi: udi, url: udi });
this.requestUpdate(); this.requestUpdate();
} }
@@ -177,11 +188,9 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement<UmbLinkPicker
label=${this.localize.term('placeholders_search')}></uui-input> label=${this.localize.term('placeholders_search')}></uui-input>
<umb-tree <umb-tree
?hide-tree-root=${true} ?hide-tree-root=${true}
?multiple=${false}
alias=${UMB_DOCUMENT_TREE_ALIAS} alias=${UMB_DOCUMENT_TREE_ALIAS}
@selection-change=${(event: CustomEvent) => this.#handleSelectionChange(event, 'document')} @selection-change=${(event: CustomEvent) => this.#handleSelectionChange(event, 'document')}
.selection=${[this._selectedKey ?? '']} .selectionConfiguration=${this._selectionConfiguration}></umb-tree>
selectable></umb-tree>
</div> </div>
<hr /> <hr />
<uui-symbol-expand <uui-symbol-expand
@@ -192,11 +201,9 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement<UmbLinkPicker
<div style="${styleMap({ display: !this.mediaExpanded ? 'block' : 'none' })}"> <div style="${styleMap({ display: !this.mediaExpanded ? 'block' : 'none' })}">
<umb-tree <umb-tree
?hide-tree-root=${true} ?hide-tree-root=${true}
?multiple=${false}
alias="Umb.Tree.Media" alias="Umb.Tree.Media"
@selection-change=${(event: CustomEvent) => this.#handleSelectionChange(event, 'media')} @selection-change=${(event: CustomEvent) => this.#handleSelectionChange(event, 'media')}
.selection=${[this._selectedKey ?? '']} .selectionConfiguration=${this._selectionConfiguration}></umb-tree>
selectable></umb-tree>
</div> </div>
`; `;
} }

View File

@@ -1,9 +1,8 @@
import { type UmbTreeElement } from '../../../tree/tree.element.js';
import { html, customElement, state, ifDefined } from '@umbraco-cms/backoffice/external/lit'; import { html, customElement, state, ifDefined } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbTreePickerModalData, UmbPickerModalValue, UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; import { UmbTreePickerModalData, UmbPickerModalValue, UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
import { UmbSelectionChangeEvent } from '@umbraco-cms/backoffice/event'; import { UmbSelectionChangeEvent } from '@umbraco-cms/backoffice/event';
import { UmbTreeItemModelBase } from '@umbraco-cms/backoffice/tree'; import { UmbTreeElement, UmbTreeItemModelBase, type UmbTreeSelectionConfiguration } from '@umbraco-cms/backoffice/tree';
@customElement('umb-tree-picker-modal') @customElement('umb-tree-picker-modal')
export class UmbTreePickerModalElement<TreeItemType extends UmbTreeItemModelBase> extends UmbModalBaseElement< export class UmbTreePickerModalElement<TreeItemType extends UmbTreeItemModelBase> extends UmbModalBaseElement<
@@ -11,10 +10,11 @@ export class UmbTreePickerModalElement<TreeItemType extends UmbTreeItemModelBase
UmbPickerModalValue UmbPickerModalValue
> { > {
@state() @state()
_selection: Array<string | null> = []; _selectionConfiguration: UmbTreeSelectionConfiguration = {
multiple: false,
@state() selectable: true,
_multiple = false; selection: [],
};
connectedCallback() { connectedCallback() {
super.connectedCallback(); super.connectedCallback();
@@ -22,17 +22,17 @@ export class UmbTreePickerModalElement<TreeItemType extends UmbTreeItemModelBase
// TODO: We should make a nicer way to observe the value.. // TODO: We should make a nicer way to observe the value..
if (this.modalContext) { if (this.modalContext) {
this.observe(this.modalContext.value, (value) => { this.observe(this.modalContext.value, (value) => {
this._selection = value?.selection ?? []; this._selectionConfiguration.selection = value?.selection ?? [];
}); });
} }
this._multiple = this.data?.multiple ?? false; this._selectionConfiguration.multiple = this.data?.multiple ?? false;
} }
#onSelectionChange(e: CustomEvent) { #onSelectionChange(e: CustomEvent) {
e.stopPropagation(); e.stopPropagation();
const element = e.target as UmbTreeElement; const element = e.target as UmbTreeElement;
this.value = { selection: element.selection }; this.value = { selection: element.getSelection() };
this.dispatchEvent(new UmbSelectionChangeEvent()); this.dispatchEvent(new UmbSelectionChangeEvent());
} }
@@ -44,15 +44,13 @@ export class UmbTreePickerModalElement<TreeItemType extends UmbTreeItemModelBase
?hide-tree-root=${this.data?.hideTreeRoot} ?hide-tree-root=${this.data?.hideTreeRoot}
alias=${ifDefined(this.data?.treeAlias)} alias=${ifDefined(this.data?.treeAlias)}
@selection-change=${this.#onSelectionChange} @selection-change=${this.#onSelectionChange}
.selection=${this._selection} .selectionConfiguration=${this._selectionConfiguration}
selectable
.filter=${this.data?.filter} .filter=${this.data?.filter}
.selectableFilter=${this.data?.pickableFilter} .selectableFilter=${this.data?.pickableFilter}></umb-tree>
?multiple=${this._multiple}></umb-tree>
</uui-box> </uui-box>
<div slot="actions"> <div slot="actions">
<uui-button label="Close" @click=${this._rejectModal}></uui-button> <uui-button label=${this.localize.term('general_close')} @click=${this._rejectModal}></uui-button>
<uui-button label="Submit" look="primary" color="positive" @click=${this._submitModal}></uui-button> <uui-button label=${this.localize.term('general_choose')} look="primary" color="positive" @click=${this._submitModal}></uui-button>
</div> </div>
</umb-body-layout> </umb-body-layout>
`; `;

View File

@@ -0,0 +1,18 @@
import { UmbModalToken, UmbPickerModalValue, UmbTreePickerModalData } from '@umbraco-cms/backoffice/modal';
import { UmbEntityTreeItemModel } from '@umbraco-cms/backoffice/tree';
export type UmbMemberTypePickerModalData = UmbTreePickerModalData<UmbEntityTreeItemModel>;
export type UmbMemberTypePickerModalValue = UmbPickerModalValue;
export const UMB_MEMBER_TYPE_PICKER_MODAL = new UmbModalToken<UmbMemberTypePickerModalData, UmbMemberTypePickerModalValue>(
'Umb.Modal.TreePicker',
{
modal: {
type: 'sidebar',
size: 'small',
},
data: {
treeAlias: 'Umb.Tree.MemberType',
},
},
);

View File

@@ -0,0 +1,2 @@
export * from './object-type.repository.js';
export * from './input-object-type.element.js';

View File

@@ -0,0 +1,57 @@
import { UmbObjectTypeRepository } from './object-type.repository.js';
import { html, customElement, property, query, state } from '@umbraco-cms/backoffice/external/lit';
import { FormControlMixin, UUISelectElement } from '@umbraco-cms/backoffice/external/uui';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
@customElement('umb-input-object-type')
export class UmbInputObjectTypeElement extends FormControlMixin(UmbLitElement) {
@query('uui-select')
private select!: UUISelectElement;
@property()
public set value(value: UUISelectElement['value']) {
this.select.value = value;
}
public get value(): UUISelectElement['value'] {
return this.select.value;
}
@state()
private _options: UUISelectElement['options'] = [];
#repository: UmbObjectTypeRepository;
constructor() {
super();
this.#repository = new UmbObjectTypeRepository(this);
this.#repository.read().then(({ data, error }) => {
if (!data) return;
this._options = data.items.map((item) => ({ value: item.id, name: item.name ?? '' }));
});
}
protected getFormElement() {
return undefined;
}
#onChange() {
this.dispatchEvent(new CustomEvent('change'));
}
render() {
return html`<uui-select .options=${this._options} @change=${this.#onChange}></uui-select> `;
}
static styles = [];
}
export default UmbInputObjectTypeElement;
declare global {
interface HTMLElementTagNameMap {
'umb-input-object-type': UmbInputObjectTypeElement;
}
}

View File

@@ -0,0 +1,25 @@
import { ObjectTypesResource } from '@umbraco-cms/backoffice/backend-api';
import { UmbBaseController } from '@umbraco-cms/backoffice/class-api';
import { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbApi } from '@umbraco-cms/backoffice/extension-api';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
export class UmbObjectTypeRepository extends UmbBaseController implements UmbApi {
#host: UmbControllerHost;
constructor(host: UmbControllerHost) {
super(host);
this.#host = host;
}
async #read() {
return tryExecuteAndNotify(this.#host, ObjectTypesResource.getObjectTypes({}));
}
async read() {
const { data, error } = await this.#read();
return { data, error };
}
}

View File

@@ -2,7 +2,7 @@ import type { ManifestPropertyEditorSchema } from '@umbraco-cms/backoffice/exten
export const manifest: ManifestPropertyEditorSchema = { export const manifest: ManifestPropertyEditorSchema = {
type: 'propertyEditorSchema', type: 'propertyEditorSchema',
name: 'Decimal', name: 'Integer',
alias: 'Umbraco.Integer', alias: 'Umbraco.Integer',
meta: { meta: {
defaultPropertyEditorUiAlias: 'Umb.PropertyEditorUi.Integer', defaultPropertyEditorUiAlias: 'Umb.PropertyEditorUi.Integer',

View File

@@ -6,8 +6,8 @@ import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/
@customElement('umb-property-editor-ui-number') @customElement('umb-property-editor-ui-number')
export class UmbPropertyEditorUINumberElement extends UmbLitElement implements UmbPropertyEditorUiElement { export class UmbPropertyEditorUINumberElement extends UmbLitElement implements UmbPropertyEditorUiElement {
@property() @property({ type: Number })
value = ''; value: undefined | number = undefined;
@state() @state()
private _max?: number; private _max?: number;
@@ -26,7 +26,7 @@ export class UmbPropertyEditorUINumberElement extends UmbLitElement implements U
} }
private onInput(e: InputEvent) { private onInput(e: InputEvent) {
this.value = (e.target as HTMLInputElement).value; this.value = Number((e.target as HTMLInputElement).value);
this.dispatchEvent(new CustomEvent('property-value-change')); this.dispatchEvent(new CustomEvent('property-value-change'));
} }

View File

@@ -2,11 +2,11 @@ import type { ManifestPropertyEditorUi } from '@umbraco-cms/backoffice/extension
export const manifest: ManifestPropertyEditorUi = { export const manifest: ManifestPropertyEditorUi = {
type: 'propertyEditorUi', type: 'propertyEditorUi',
alias: 'Umb.PropertyEditorUi.TreePicker.StartNode', alias: 'Umb.PropertyEditorUi.TreePicker.SourcePicker',
name: 'Tree Picker Start Node Property Editor UI', name: 'Tree Picker Source Picker Property Editor UI',
js: () => import('./property-editor-ui-tree-picker-start-node.element.js'), js: () => import('./property-editor-ui-tree-picker-source-picker.element.js'),
meta: { meta: {
label: 'Tree Picker Start Node', label: 'Tree Picker Source Picker',
icon: 'icon-page-add', icon: 'icon-page-add',
group: 'pickers', group: 'pickers',
propertyEditorSchemaAlias: '', propertyEditorSchemaAlias: '',

View File

@@ -0,0 +1,47 @@
import { type UmbTreePickerSource, UmbInputTreePickerSourceElement } from '@umbraco-cms/backoffice/components';
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry';
import { html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
/**
* @element umb-property-editor-ui-tree-picker-source-picker
*/
@customElement('umb-property-editor-ui-tree-picker-source-picker')
export class UmbPropertyEditorUITreePickerSourcePickerElement extends UmbLitElement implements UmbPropertyEditorUiElement {
@property({ type: Object })
value?: UmbTreePickerSource;
@property({ type: Object, attribute: false })
public config?: UmbPropertyEditorConfigCollection;
#onChange(event: CustomEvent) {
const target = event.target as UmbInputTreePickerSourceElement;
this.value = {
type: target.type,
id: target.nodeId,
dynamicRoot: target.dynamicRoot,
};
this.dispatchEvent(new CustomEvent('property-value-change'));
}
render() {
return html`<umb-input-tree-picker-source
@change=${this.#onChange}
.type=${this.value?.type}
.nodeId=${this.value?.id}></umb-input-tree-picker-source>`;
}
static styles = [UmbTextStyles];
}
export default UmbPropertyEditorUITreePickerSourcePickerElement;
declare global {
interface HTMLElementTagNameMap {
'umb-property-editor-ui-tree-picker-source-picker': UmbPropertyEditorUITreePickerSourcePickerElement;
}
}

View File

@@ -0,0 +1,15 @@
import { Meta, Story } from '@storybook/web-components';
import type { UmbPropertyEditorUITreePickerSourcePickerElement } from './property-editor-ui-tree-picker-source-picker.element.js';
import { html } from '@umbraco-cms/backoffice/external/lit';
import './property-editor-ui-tree-picker-source-picker.element.js';
export default {
title: 'Property Editor UIs/Tree Picker Start Node',
component: 'umb-property-editor-ui-tree-picker-source-picker',
id: 'umb-property-editor-ui-tree-picker-source-pickere',
} as Meta;
export const AAAOverview: Story<UmbPropertyEditorUITreePickerSourcePickerElement> = () =>
html`<umb-property-editor-ui-tree-picker-source-picker></umb-property-editor-ui-tree-picker-source-picker>`;
AAAOverview.storyName = 'Overview';

View File

@@ -1,18 +1,18 @@
import { expect, fixture, html } from '@open-wc/testing'; import { expect, fixture, html } from '@open-wc/testing';
import { UmbPropertyEditorUITreePickerStartNodeElement } from './property-editor-ui-tree-picker-start-node.element.js'; import { UmbPropertyEditorUITreePickerSourcePickerElement } from './property-editor-ui-tree-picker-source-picker.element.js';
import { defaultA11yConfig } from '@umbraco-cms/internal/test-utils'; import { defaultA11yConfig } from '@umbraco-cms/internal/test-utils';
describe('UmbPropertyEditorUITreePickerStartNodeElement', () => { describe('UmbPropertyEditorUITreePickerSourcePickerElement', () => {
let element: UmbPropertyEditorUITreePickerStartNodeElement; let element: UmbPropertyEditorUITreePickerSourcePickerElement;
beforeEach(async () => { beforeEach(async () => {
element = await fixture(html` element = await fixture(html`
<umb-property-editor-ui-tree-picker-start-node></umb-property-editor-ui-tree-picker-start-node> <umb-property-editor-ui-tree-picker-source-picker></umb-property-editor-ui-tree-picker-source-picker>
`); `);
}); });
it('is defined with its own instance', () => { it('is defined with its own instance', () => {
expect(element).to.be.instanceOf(UmbPropertyEditorUITreePickerStartNodeElement); expect(element).to.be.instanceOf(UmbPropertyEditorUITreePickerSourcePickerElement);
}); });
if ((window as any).__UMBRACO_TEST_RUN_A11Y_TEST) { if ((window as any).__UMBRACO_TEST_RUN_A11Y_TEST) {

View File

@@ -0,0 +1,14 @@
import type { ManifestPropertyEditorUi } from '@umbraco-cms/backoffice/extension-registry';
export const manifest: ManifestPropertyEditorUi = {
type: 'propertyEditorUi',
alias: 'Umb.PropertyEditorUi.TreePicker.SourceTypePicker',
name: 'Tree Picker Source Type Picker Property Editor UI',
js: () => import('./property-editor-ui-tree-picker-source-type-picker.element.js'),
meta: {
label: 'Tree Picker Source Type Picker',
icon: 'icon-page-add',
group: 'pickers',
propertyEditorSchemaAlias: '',
},
};

View File

@@ -0,0 +1,121 @@
import { UmbDocumentTypeInputElement } from '@umbraco-cms/backoffice/document-type';
import { UmbMediaTypeInputElement } from '@umbraco-cms/backoffice/media-type';
import { UmbMemberTypeInputElement } from '@umbraco-cms/backoffice/member-type';
import type { UmbTreePickerSource } from '@umbraco-cms/backoffice/components';
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry';
import { customElement, html, property, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property';
/**
* @element umb-property-editor-ui-tree-picker-source-type-picker
*/
@customElement('umb-property-editor-ui-tree-picker-source-type-picker')
export class UmbPropertyEditorUITreePickerSourceTypePickerElement extends UmbLitElement implements UmbPropertyEditorUiElement {
#datasetContext?: typeof UMB_PROPERTY_DATASET_CONTEXT.TYPE;
@property({ type: Array })
value?: string[];
@state()
private sourceType: string = 'content';
#initialized: boolean = false;
constructor() {
super();
this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, (datasetContext) => {
this.#datasetContext = datasetContext;
this._observeProperty();
});
}
private async _observeProperty() {
if (!this.#datasetContext) return;
this.observe(
await this.#datasetContext.propertyValueByAlias('startNode'),
(value) => {
const startNode = value as UmbTreePickerSource;
if (startNode.type) {
// If we had a sourceType before, we can see this as a change and not the initial value,
// so let's reset the value, so we don't carry over content-types to the new source type.
if (this.#initialized && this.sourceType !== startNode.type) {
this.value = [];
}
this.sourceType = startNode.type;
if (!this.#initialized) {
this.#initialized = true;
}
}
},
'observeValue',
);
}
#onChange(event: CustomEvent) {
switch (this.sourceType) {
case 'content':
this.value = (<UmbDocumentTypeInputElement>event.target).selectedIds;
break;
case 'media':
this.value = (<UmbMediaTypeInputElement>event.target).selectedIds;
break;
case 'member':
this.value = (<UmbMemberTypeInputElement>event.target).selectedIds;
break;
default:
break;
}
this.dispatchEvent(new CustomEvent('property-value-change'));
}
render() {
return this.#renderType();
}
#renderType() {
switch (this.sourceType) {
case 'content':
return this.#renderTypeContent();
case 'media':
return this.#renderTypeMedia();
case 'member':
return this.#renderTypeMember();
default:
return 'No source type found';
}
}
#renderTypeContent() {
return html`<umb-document-type-input
@change=${this.#onChange}
.selectedIds=${this.value || []}></umb-document-type-input>`;
}
#renderTypeMedia() {
return html`<umb-media-type-input
@change=${this.#onChange}
.selectedIds=${this.value || []}></umb-media-type-input>`;
}
#renderTypeMember() {
return html`<umb-input-member-type
@change=${this.#onChange}
.selectedIds=${this.value || []}></umb-input-member-type>`;
}
static styles = [UmbTextStyles];
}
export default UmbPropertyEditorUITreePickerSourceTypePickerElement;
declare global {
interface HTMLElementTagNameMap {
'umb-property-editor-ui-tree-picker-source-type-picker': UmbPropertyEditorUITreePickerSourceTypePickerElement;
}
}

View File

@@ -1,45 +0,0 @@
import { StartNode, UmbInputStartNodeElement } from '@umbraco-cms/backoffice/components';
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry';
import { html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
/**
* @element umb-property-editor-ui-tree-picker-start-node
*/
@customElement('umb-property-editor-ui-tree-picker-start-node')
export class UmbPropertyEditorUITreePickerStartNodeElement extends UmbLitElement implements UmbPropertyEditorUiElement {
@property({ type: Object })
value?: StartNode;
@property({ type: Object, attribute: false })
public config?: UmbPropertyEditorConfigCollection;
#onChange(event: CustomEvent) {
const target = event.target as UmbInputStartNodeElement;
this.value = {
type: target.type,
id: target.nodeId,
// TODO: Please check this makes sense, Check if we want to support XPath in this version, if not then make sure we handle DynamicRoot correct.
query: target.dynamicPath,
};
this.dispatchEvent(new CustomEvent('property-value-change'));
}
render() {
return html`<umb-input-start-node @change="${this.#onChange}" .type=${this.value?.type}></umb-input-start-node>`;
}
static styles = [UmbTextStyles];
}
export default UmbPropertyEditorUITreePickerStartNodeElement;
declare global {
interface HTMLElementTagNameMap {
'umb-property-editor-ui-tree-picker-start-node': UmbPropertyEditorUITreePickerStartNodeElement;
}
}

View File

@@ -1,15 +0,0 @@
import { Meta, Story } from '@storybook/web-components';
import type { UmbPropertyEditorUITreePickerStartNodeElement } from './property-editor-ui-tree-picker-start-node.element.js';
import { html } from '@umbraco-cms/backoffice/external/lit';
import './property-editor-ui-tree-picker-start-node.element.js';
export default {
title: 'Property Editor UIs/Tree Picker Start Node',
component: 'umb-property-editor-ui-tree-picker-start-node',
id: 'umb-property-editor-ui-tree-picker-start-node',
} as Meta;
export const AAAOverview: Story<UmbPropertyEditorUITreePickerStartNodeElement> = () =>
html`<umb-property-editor-ui-tree-picker-start-node></umb-property-editor-ui-tree-picker-start-node>`;
AAAOverview.storyName = 'Overview';

View File

@@ -1,4 +1,5 @@
import { manifest as startNode } from './config/start-node/manifests.js'; import { manifest as sourcePicker } from './config/source-picker/manifests.js';
import { manifest as sourceTypePicker } from './config/source-type-picker/manifests.js';
import type { ManifestPropertyEditorUi } from '@umbraco-cms/backoffice/extension-registry'; import type { ManifestPropertyEditorUi } from '@umbraco-cms/backoffice/extension-registry';
const manifest: ManifestPropertyEditorUi = { const manifest: ManifestPropertyEditorUi = {
@@ -17,13 +18,13 @@ const manifest: ManifestPropertyEditorUi = {
alias: 'startNode', alias: 'startNode',
label: 'Node type', label: 'Node type',
description: '', description: '',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.TreePicker.StartNode', propertyEditorUiAlias: sourcePicker.alias,
}, },
{ {
alias: 'filter', alias: 'filter',
label: 'Allow items of type', label: 'Allow items of type',
description: '', description: 'Select the applicable types',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.TreePicker', propertyEditorUiAlias: sourceTypePicker.alias,
}, },
{ {
alias: 'showOpenButton', alias: 'showOpenButton',
@@ -36,6 +37,6 @@ const manifest: ManifestPropertyEditorUi = {
}, },
}; };
const config: Array<ManifestPropertyEditorUi> = [startNode]; const config: Array<ManifestPropertyEditorUi> = [sourcePicker, sourceTypePicker];
export const manifests = [manifest, ...config]; export const manifests = [manifest, ...config];

View File

@@ -4,7 +4,7 @@ import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extensi
import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
import type { UmbInputTreeElement } from '@umbraco-cms/backoffice/tree'; import type { UmbInputTreeElement } from '@umbraco-cms/backoffice/tree';
import type { StartNode } from '@umbraco-cms/backoffice/components'; import type { UmbTreePickerSource } from '@umbraco-cms/backoffice/components';
/** /**
* @element umb-property-editor-ui-tree-picker * @element umb-property-editor-ui-tree-picker
@@ -16,7 +16,7 @@ export class UmbPropertyEditorUITreePickerElement extends UmbLitElement implemen
value = ''; value = '';
@state() @state()
type?: StartNode['type']; type?: UmbTreePickerSource['type'];
@state() @state()
startNodeId?: string | null; startNodeId?: string | null;
@@ -38,7 +38,7 @@ export class UmbPropertyEditorUITreePickerElement extends UmbLitElement implemen
@property({ attribute: false }) @property({ attribute: false })
public set config(config: UmbPropertyEditorConfigCollection | undefined) { public set config(config: UmbPropertyEditorConfigCollection | undefined) {
const startNode: StartNode | undefined = config?.getValueByAlias('startNode'); const startNode: UmbTreePickerSource | undefined = config?.getValueByAlias('startNode');
if (startNode) { if (startNode) {
this.type = startNode.type; this.type = startNode.type;
this.startNodeId = startNode.id; this.startNodeId = startNode.id;

View File

@@ -3,7 +3,7 @@ import { FormControlMixin } from '@umbraco-cms/backoffice/external/uui';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { UmbInputDocumentElement } from '@umbraco-cms/backoffice/document'; import { UmbInputDocumentElement } from '@umbraco-cms/backoffice/document';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import type { StartNode } from '@umbraco-cms/backoffice/components'; import type { UmbTreePickerSource } from '@umbraco-cms/backoffice/components';
@customElement('umb-input-tree') @customElement('umb-input-tree')
export class UmbInputTreeElement extends FormControlMixin(UmbLitElement) { export class UmbInputTreeElement extends FormControlMixin(UmbLitElement) {
@@ -11,16 +11,16 @@ export class UmbInputTreeElement extends FormControlMixin(UmbLitElement) {
return undefined; return undefined;
} }
private _type: StartNode['type'] = undefined; private _type: UmbTreePickerSource['type'] = undefined;
@property() @property()
public set type(newType: StartNode['type']) { public set type(newType: UmbTreePickerSource['type']) {
const oldType = this._type; const oldType = this._type;
if (newType?.toLowerCase() !== this._type) { if (newType?.toLowerCase() !== this._type) {
this._type = newType?.toLowerCase() as StartNode['type']; this._type = newType?.toLowerCase() as UmbTreePickerSource['type'];
this.requestUpdate('type', oldType); this.requestUpdate('type', oldType);
} }
} }
public get type(): StartNode['type'] { public get type(): UmbTreePickerSource['type'] {
return this._type; return this._type;
} }

View File

@@ -7,47 +7,42 @@ import { UmbObserverController } from '@umbraco-cms/backoffice/observable-api';
import './tree-item-default/tree-item.element.js'; import './tree-item-default/tree-item.element.js';
import './tree-item-base/tree-item-base.element.js'; import './tree-item-base/tree-item-base.element.js';
export type UmbTreeSelectionConfiguration = {
multiple?: boolean;
selectable?: boolean;
selection?: Array<string | null>;
};
@customElement('umb-tree') @customElement('umb-tree')
export class UmbTreeElement extends UmbLitElement { export class UmbTreeElement extends UmbLitElement {
@property({ type: String, reflect: true }) @property({ type: String, reflect: true })
get alias() {
return this.#treeContext.getTreeAlias();
}
set alias(newVal) { set alias(newVal) {
this.#treeContext.setTreeAlias(newVal); this.#treeContext.setTreeAlias(newVal);
} }
get alias() {
@property({ type: Boolean, reflect: true }) return this.#treeContext.getTreeAlias();
get selectable() {
return this.#treeContext.selection.getSelectable();
}
set selectable(newVal) {
this.#treeContext.selection.setSelectable(newVal);
} }
@property({ type: Array }) private _selectionConfiguration: UmbTreeSelectionConfiguration = {
get selection() { multiple: false,
return this.#treeContext.selection.getSelection(); selectable: true,
} selection: [],
set selection(newVal) { };
if (!Array.isArray(newVal)) return;
this.#treeContext?.selection.setSelection(newVal);
}
@property({ type: Boolean, reflect: true }) @property({ type: Object })
get multiple() { set selectionConfiguration(config: UmbTreeSelectionConfiguration) {
return this.#treeContext.selection.getMultiple(); this._selectionConfiguration = config;
this.#treeContext.selection.setMultiple(config.multiple ?? false);
this.#treeContext.selection.setSelectable(config.selectable ?? true);
this.#treeContext.selection.setSelection(config.selection ?? []);
} }
set multiple(newVal) { get selectionConfiguration(): UmbTreeSelectionConfiguration {
this.#treeContext.selection.setMultiple(newVal); return this._selectionConfiguration;
} }
// TODO: what is the best name for this functionality? // TODO: what is the best name for this functionality?
private _hideTreeRoot = false; private _hideTreeRoot = false;
@property({ type: Boolean, attribute: 'hide-tree-root' }) @property({ type: Boolean, attribute: 'hide-tree-root' })
get hideTreeRoot() {
return this._hideTreeRoot;
}
set hideTreeRoot(newVal: boolean) { set hideTreeRoot(newVal: boolean) {
const oldVal = this._hideTreeRoot; const oldVal = this._hideTreeRoot;
this._hideTreeRoot = newVal; this._hideTreeRoot = newVal;
@@ -57,22 +52,25 @@ export class UmbTreeElement extends UmbLitElement {
this.requestUpdate('hideTreeRoot', oldVal); this.requestUpdate('hideTreeRoot', oldVal);
} }
get hideTreeRoot() {
return this._hideTreeRoot;
}
@property() @property()
get selectableFilter() {
return this.#treeContext.selectableFilter;
}
set selectableFilter(newVal) { set selectableFilter(newVal) {
this.#treeContext.selectableFilter = newVal; this.#treeContext.selectableFilter = newVal;
} }
get selectableFilter() {
return this.#treeContext.selectableFilter;
}
@property() @property()
get filter() {
return this.#treeContext.filter;
}
set filter(newVal) { set filter(newVal) {
this.#treeContext.filter = newVal; this.#treeContext.filter = newVal;
} }
get filter() {
return this.#treeContext.filter;
}
@state() @state()
private _items: UmbTreeItemModelBase[] = []; private _items: UmbTreeItemModelBase[] = [];
@@ -111,6 +109,10 @@ export class UmbTreeElement extends UmbLitElement {
} }
} }
getSelection() {
return this.#treeContext.selection.getSelection();
}
render() { render() {
return html` ${this.#renderTreeRoot()} ${this.#renderRootItems()}`; return html` ${this.#renderTreeRoot()} ${this.#renderRootItems()}`;
} }

View File

@@ -8,7 +8,7 @@ import {
UmbModalBaseElement, UmbModalBaseElement,
} from '@umbraco-cms/backoffice/modal'; } from '@umbraco-cms/backoffice/modal';
import { UmbId } from '@umbraco-cms/backoffice/id'; import { UmbId } from '@umbraco-cms/backoffice/id';
import { UmbEntityTreeItemModel, UmbTreeElement } from '@umbraco-cms/backoffice/tree'; import { UmbEntityTreeItemModel, UmbTreeElement, type UmbTreeSelectionConfiguration } from '@umbraco-cms/backoffice/tree';
interface DictionaryItemPreview { interface DictionaryItemPreview {
name: string; name: string;
@@ -21,6 +21,13 @@ export class UmbImportDictionaryModalLayout extends UmbModalBaseElement<
UmbImportDictionaryModalData, UmbImportDictionaryModalData,
UmbImportDictionaryModalValue UmbImportDictionaryModalValue
> { > {
@state()
private _selectionConfiguration: UmbTreeSelectionConfiguration = {
multiple: false,
selectable: true,
selection: [],
};
@state() @state()
private _parentId?: string; private _parentId?: string;
@@ -93,6 +100,7 @@ export class UmbImportDictionaryModalLayout extends UmbModalBaseElement<
connectedCallback(): void { connectedCallback(): void {
super.connectedCallback(); super.connectedCallback();
this._parentId = this.data?.unique ?? undefined; this._parentId = this.data?.unique ?? undefined;
this._selectionConfiguration.selection = this._parentId ? [this._parentId] : [];
} }
#dictionaryPreviewBuilder(htmlString: string) { #dictionaryPreviewBuilder(htmlString: string) {
@@ -139,7 +147,7 @@ export class UmbImportDictionaryModalLayout extends UmbModalBaseElement<
} }
#onParentChange() { #onParentChange() {
this._parentId = this._treeElement?.selection[0] ?? undefined; this._parentId = this._treeElement?.getSelection()[0] ?? undefined;
} }
async #onFileInput() { async #onFileInput() {
@@ -189,11 +197,9 @@ export class UmbImportDictionaryModalLayout extends UmbModalBaseElement<
<umb-tree <umb-tree
?hide-tree-root=${true} ?hide-tree-root=${true}
?multiple=${false}
alias=${UMB_DICTIONARY_TREE_ALIAS} alias=${UMB_DICTIONARY_TREE_ALIAS}
@selection-change=${this.#onParentChange} @selection-change=${this.#onParentChange}
.selection=${[this._parentId ?? '']} .selectionConfiguration=${this._selectionConfiguration}></umb-tree>
selectable></umb-tree>
</div> </div>
${this.#renderNavigate()} ${this.#renderNavigate()}

View File

@@ -1 +1,2 @@
import './input-document-type/input-document-type.element.js'; import './input-document-type/input-document-type.element.js';
export * from './input-document-type/input-document-type.element.js';

View File

@@ -1,2 +1,3 @@
export * from './input-document/input-document.element.js'; export * from './input-document/input-document.element.js';
export * from './input-document-granular-permission/input-document-granular-permission.element.js'; export * from './input-document-granular-permission/input-document-granular-permission.element.js';
export * from './input-document-picker-root/input-document-picker-root.element.js';

View File

@@ -0,0 +1,104 @@
import { UmbDocumentPickerContext } from '../input-document/input-document.context.js';
import { html, customElement, property, state, ifDefined, repeat } from '@umbraco-cms/backoffice/external/lit';
import { FormControlMixin } from '@umbraco-cms/backoffice/external/uui';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import type { DocumentItemResponseModel } from '@umbraco-cms/backoffice/backend-api';
@customElement('umb-input-document-picker-root')
export class UmbInputDocumentPickerRootElement extends FormControlMixin(UmbLitElement) {
public get nodeId(): string | null | undefined {
return this.#documentPickerContext.getSelection()[0];
}
public set nodeId(id: string | null | undefined) {
const selection = id ? [id] : [];
this.#documentPickerContext.setSelection(selection);
}
@property()
public set value(id: string) {
this.nodeId = id;
}
@state()
private _items?: Array<DocumentItemResponseModel>;
#documentPickerContext = new UmbDocumentPickerContext(this);
// TODO: DynamicRoot - once feature implemented, wire up context and picker UI. [LK]
#dynamicRootPickerContext = {
openPicker: () => {
throw new Error('DynamicRoot picker has not been implemented yet.');
},
};
constructor() {
super();
this.#documentPickerContext.max = 1;
this.observe(this.#documentPickerContext.selection, (selection) => (super.value = selection.join(',')));
this.observe(this.#documentPickerContext.selectedItems, (selectedItems) => (this._items = selectedItems));
}
protected getFormElement() {
return undefined;
}
render() {
return html`
${this._items
? html` <uui-ref-list
>${repeat(
this._items,
(item) => item.id,
(item) => this._renderItem(item),
)}
</uui-ref-list>`
: ''}
${this.#renderButtons()}
`;
}
#renderButtons() {
if (this.nodeId) return;
//TODO: Dynamic paths
return html` <uui-button-group>
<uui-button
look="placeholder"
@click=${() => this.#documentPickerContext.openPicker()}
label=${this.localize.term('contentPicker_defineRootNode')}></uui-button>
<uui-button
look="placeholder"
@click=${() => this.#dynamicRootPickerContext.openPicker()}
label=${this.localize.term('contentPicker_defineDynamicRoot')}></uui-button>
</uui-button-group>`;
}
private _renderItem(item: DocumentItemResponseModel) {
if (!item.id) return;
return html`
<uui-ref-node name=${ifDefined(item.name)} detail=${ifDefined(item.id)}>
<!-- TODO: implement is trashed <uui-tag size="s" slot="tag" color="danger">Trashed</uui-tag> -->
<uui-action-bar slot="actions">
<uui-button @click=${() => this.#documentPickerContext.openPicker()} label="Edit document ${item.name}"
>Edit</uui-button
>
<uui-button
@click=${() => this.#documentPickerContext.requestRemoveItem(item.id!)}
label="Remove document ${item.name}"
>Remove</uui-button
>
</uui-action-bar>
</uui-ref-node>
`;
}
}
export default UmbInputDocumentPickerRootElement;
declare global {
interface HTMLElementTagNameMap {
'umb-input-document-picker-root': UmbInputDocumentPickerRootElement;
}
}

View File

@@ -115,7 +115,7 @@ export class UmbInputDocumentElement extends FormControlMixin(UmbLitElement) {
id="add-button" id="add-button"
look="placeholder" look="placeholder"
@click=${() => this.#pickerContext.openPicker()} @click=${() => this.#pickerContext.openPicker()}
label=${this.localize.term('general_add')}></uui-button>`; label=${this.localize.term('general_choose')}></uui-button>`;
} }
private _renderItem(item: DocumentItemResponseModel) { private _renderItem(item: DocumentItemResponseModel) {
@@ -127,7 +127,7 @@ export class UmbInputDocumentElement extends FormControlMixin(UmbLitElement) {
<uui-button <uui-button
@click=${() => this.#pickerContext.requestRemoveItem(item.id!)} @click=${() => this.#pickerContext.requestRemoveItem(item.id!)}
label="Remove document ${item.name}" label="Remove document ${item.name}"
>Remove</uui-button >${this.localize.term('general_remove')}</uui-button
> >
</uui-action-bar> </uui-action-bar>
</uui-ref-node> </uui-ref-node>

View File

@@ -1,6 +1,7 @@
import './components/index.js'; import './components/index.js';
export * from './repository/index.js'; export * from './repository/index.js';
export * from './tracked-reference/index.js';
export * from './workspace/index.js'; export * from './workspace/index.js';
export * from './recycle-bin/index.js'; export * from './recycle-bin/index.js';
export * from './user-permissions/index.js'; export * from './user-permissions/index.js';

View File

@@ -8,6 +8,7 @@ import { manifests as entityBulkActionManifests } from './entity-bulk-actions/ma
import { manifests as propertyEditorManifests } from './property-editors/manifests.js'; import { manifests as propertyEditorManifests } from './property-editors/manifests.js';
import { manifests as userPermissionManifests } from './user-permissions/manifests.js'; import { manifests as userPermissionManifests } from './user-permissions/manifests.js';
import { manifests as recycleBinManifests } from './recycle-bin/manifests.js'; import { manifests as recycleBinManifests } from './recycle-bin/manifests.js';
import { manifests as trackedReferenceManifests } from './tracked-reference/manifests.js';
export const manifests = [ export const manifests = [
...collectionManifests, ...collectionManifests,
@@ -20,4 +21,5 @@ export const manifests = [
...propertyEditorManifests, ...propertyEditorManifests,
...userPermissionManifests, ...userPermissionManifests,
...recycleBinManifests, ...recycleBinManifests,
...trackedReferenceManifests,
]; ];

View File

@@ -0,0 +1,2 @@
//export * from './manifests.js';
export * from './repository/index.js';

View File

@@ -0,0 +1,5 @@
import { manifests as repositoryManifests } from './repository/manifests.js';
export const UMB_TRACKED_REFERENCE_DOCUMENT = 'Umb.TrackedReference.Document';
export const manifests = [...repositoryManifests];

View File

@@ -0,0 +1,37 @@
import { UmbDocumentTrackedReferenceServerDataSource } from './document-tracked-reference.server.data.js';
import { type UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import { UmbBaseController } from '@umbraco-cms/backoffice/class-api';
export class UmbDocumentTrackedReferenceRepository extends UmbBaseController {
#trackedReferenceSource: UmbDocumentTrackedReferenceServerDataSource;
constructor(host: UmbControllerHostElement) {
super(host);
this.#trackedReferenceSource = new UmbDocumentTrackedReferenceServerDataSource(this);
}
async requestTrackedReference(unique: string, skip = 0, take = 20, filterMustBeIsDependency = false) {
if (!unique) throw new Error(`unique is required`);
return this.#trackedReferenceSource.getTrackedReferenceById(unique, skip, take, filterMustBeIsDependency);
}
async requestTrackedReferenceDescendantsFromParentUnique(
parentUnique: string,
skip = 0,
take = 20,
filterMustBeIsDependency = false,
) {
if (!parentUnique) throw new Error(`unique is required`);
return this.#trackedReferenceSource.getTrackedReferenceDescendantsByParentId(
parentUnique,
skip,
take,
filterMustBeIsDependency,
);
}
async requestTrackedReferenceItems(uniques: string[], skip = 0, take = 20, filterMustBeIsDependency = true) {
if (!uniques) throw new Error(`unique is required`);
return this.#trackedReferenceSource.getTrackedReferenceItem(uniques, skip, take, filterMustBeIsDependency);
}
}

View File

@@ -0,0 +1,75 @@
import { TrackedReferenceResource } from '@umbraco-cms/backoffice/backend-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
/**
* @export
* @class UmbUserGroupCollectionServerDataSource
* @implements {RepositoryDetailDataSource}
*/
export class UmbDocumentTrackedReferenceServerDataSource {
#host: UmbControllerHost;
/**
* Creates an instance of UmbDocumentTrackedReferenceServerDataSource.
* @param {UmbControllerHost} host
* @memberof UmbDocumentTrackedReferenceServerDataSource
*/
constructor(host: UmbControllerHost) {
this.#host = host;
}
/**
* Fetches the item for the given id from the server
* @param {Array<string>} ids
* @return {*}
* @memberof UmbDataTypeItemServerDataSource
*/
async getTrackedReferenceById(id: string, skip = 0, take = 20, filterMustBeIsDependency = false) {
return await tryExecuteAndNotify(
this.#host,
TrackedReferenceResource.getTrackedReferenceById({ id, skip, take, filterMustBeIsDependency }),
);
}
/**
* Fetches the item descendant for the given id from the server
* @param {Array<string>} ids
* @return {*}
* @memberof UmbDocumentTrackedReferenceServerDataSource
*/
async getTrackedReferenceDescendantsByParentId(
parentId: string,
skip = 0,
take = 20,
filterMustBeIsDependency = false,
) {
return await tryExecuteAndNotify(
this.#host,
TrackedReferenceResource.getTrackedReferenceDescendantsByParentId({
parentId,
skip,
take,
filterMustBeIsDependency,
}),
);
}
/**
* Fetches the items for the given ids from the server
* @param {Array<string>} ids
* @return {*}
* @memberof UmbDocumentTrackedReferenceServerDataSource
*/
async getTrackedReferenceItem(id: string[], skip = 0, take = 20, filterMustBeIsDependency = true) {
return await tryExecuteAndNotify(
this.#host,
TrackedReferenceResource.getTrackedReferenceItem({
id,
skip,
take,
filterMustBeIsDependency,
}),
);
}
}

View File

@@ -0,0 +1 @@
export * from './document-tracked-reference.repository.js';

View File

@@ -0,0 +1,13 @@
import { UmbDocumentTrackedReferenceRepository } from './document-tracked-reference.repository.js';
import { ManifestRepository } from '@umbraco-cms/backoffice/extension-registry';
export const UMB_DOCUMENT_TRACKED_REFERENCE_REPOSITORY_ALIAS = 'Umb.Repository.Document.TrackedReference';
const repository: ManifestRepository = {
type: 'repository',
alias: UMB_DOCUMENT_TRACKED_REFERENCE_REPOSITORY_ALIAS,
name: 'Document Tracked Reference Repository',
api: UmbDocumentTrackedReferenceRepository,
};
export const manifests = [repository];

View File

@@ -44,7 +44,7 @@ const workspaceViews: Array<ManifestWorkspaceView> = [
type: 'workspaceView', type: 'workspaceView',
alias: 'Umb.WorkspaceView.Document.Info', alias: 'Umb.WorkspaceView.Document.Info',
name: 'Document Workspace Info View', name: 'Document Workspace Info View',
js: () => import('./views/info/document-info-workspace-view.element.js'), js: () => import('./views/info/document-workspace-view-info.element.js'),
weight: 100, weight: 100,
meta: { meta: {
label: 'Info', label: 'Info',

View File

@@ -1,374 +0,0 @@
import { css, html, nothing, repeat, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { UUIPaginationEvent } from '@umbraco-cms/backoffice/external/uui';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace';
import { UMB_WORKSPACE_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
interface HistoryNode {
userId?: number;
userAvatars?: [];
userName?: string;
timestamp?: string;
comment?: string;
entityType?: string;
logType?: HistoryLogType;
nodeId?: string;
parameters?: string;
}
type HistoryLogType = 'Publish' | 'Save' | 'Unpublish' | 'ContentVersionEnableCleanup' | 'ContentVersionPreventCleanup';
@customElement('umb-document-info-workspace-view')
export class UmbDocumentInfoWorkspaceViewElement extends UmbLitElement {
@state()
private _historyList: HistoryNode[] = [
{
userId: -1,
userAvatars: [],
userName: 'Lone Iversen',
timestamp: 'December 5, 2022 2:59 PM',
comment: undefined,
entityType: 'Document',
logType: 'Save',
nodeId: '1058',
parameters: undefined,
},
{
userId: -1,
userAvatars: [],
userName: 'Lone Iversen',
timestamp: 'December 5, 2022 2:59 PM',
comment: undefined,
entityType: 'Document',
logType: 'Unpublish',
nodeId: '1058',
parameters: undefined,
},
{
userId: -1,
userAvatars: [],
userName: 'Lone Iversen',
timestamp: 'December 5, 2022 2:59 PM',
comment: undefined,
entityType: 'Document',
logType: 'Publish',
nodeId: '1058',
parameters: undefined,
},
{
userId: -1,
userAvatars: [],
userName: 'Lone Iversen',
timestamp: 'December 5, 2022 2:59 PM',
comment: undefined,
entityType: 'Document',
logType: 'Save',
nodeId: '1058',
parameters: undefined,
},
{
userId: -1,
userAvatars: [],
userName: 'Lone Iversen',
timestamp: 'December 5, 2022 2:59 PM',
comment: undefined,
entityType: 'Document',
logType: 'Save',
nodeId: '1058',
parameters: undefined,
},
];
@state()
private _total?: number;
@state()
private _currentPage = 1;
@state()
private _nodeName = '';
@state()
private _documentTypeId = '';
private _workspaceContext?: typeof UMB_WORKSPACE_CONTEXT.TYPE;
private itemsPerPage = 10;
@state()
private _editDocumentTypePath = '';
constructor() {
super();
new UmbModalRouteRegistrationController(this, UMB_WORKSPACE_MODAL)
.addAdditionalPath('document-type')
.onSetup(() => {
return { data: { entityType: 'document-type', preset: {} } };
})
.observeRouteBuilder((routeBuilder) => {
this._editDocumentTypePath = routeBuilder({});
});
this.consumeContext(UMB_WORKSPACE_CONTEXT, (nodeContext) => {
this._workspaceContext = nodeContext;
this._observeContent();
});
}
private _observeContent() {
if (!this._workspaceContext) return;
this._nodeName = 'TBD, with variants this is not as simple.';
this._documentTypeId = (this._workspaceContext as any).getContentTypeId();
/*
this.observe(this._workspaceContext.name, (name) => {
this._nodeName = name || '';
});
*/
}
#onPageChange(event: UUIPaginationEvent) {
if (this._currentPage === event.target.current) return;
this._currentPage = event.target.current;
//TODO: Run endpoint to get next history parts
}
render() {
return html`<div class="container">
<uui-box headline=${this.localize.term('general_links')} style="--uui-box-default-padding: 0;">
${this.#renderLinksSection()}
</uui-box>
<uui-box headline=${this.localize.term('general_history')}>
<umb-history-list>
${repeat(
this._historyList,
(item) => item.timestamp,
(item) => this.#renderHistory(item),
)}
</umb-history-list>
${this.#renderHistoryPagination()}
</uui-box>
</div>
<div class="container">
<uui-box headline="General" id="general-section">${this.#renderGeneralSection()}</uui-box>
</div>`;
}
#renderLinksSection() {
//repeat
return html`<div id="link-section">
<a href="http://google.com" target="_blank" class="link-item with-href">
<span class="link-language">da-DK</span>
<span class="link-content"> <uui-icon name="icon-out"></uui-icon>google.com </span>
</a>
<div class="link-item">
<span class="link-language">en-EN</span>
<span class="link-content italic"><umb-localize key="content_parentNotPublishedAnomaly"></umb-localize></span>
</div>
</div>`;
}
#renderGeneralSection() {
return html`
<div class="general-item">
<strong>${this.localize.term('content_publishStatus')}</strong>
<span
><uui-tag color="positive" look="primary" label=${this.localize.term('content_published')}
><umb-localize key="content_published"></umb-localize></uui-tag
></span>
</div>
<div class="general-item">
<strong><umb-localize key="content_createDate"></umb-localize></strong>
<span><umb-localize-date date="${new Date()}"></umb-localize-date></span>
</div>
<div class="general-item">
<strong><umb-localize key="content_documentType"></umb-localize></strong>
<uui-button
href=${this._editDocumentTypePath + 'edit/' + this._documentTypeId}
label=${this.localize.term('general_edit')}></uui-button>
</div>
<div class="general-item">
<strong><umb-localize key="template_template"></umb-localize></strong>
<span>IMPLEMENT template picker?</span>
</div>
<div class="general-item">
<strong><umb-localize key="template_id"></umb-localize></strong>
<span>...</span>
</div>
`;
}
#renderHistory(history: HistoryNode) {
return html` <umb-history-item .name="${history.userName}" .detail="${this.localize.date(history.timestamp!)}">
<span class="log-type"
>${this.#renderTag(history.logType)} ${this.#renderTagDescription(history.logType, history)}</span
>
<uui-button label=${this.localize.term('actions_rollback')} look="secondary" slot="actions">
<uui-icon name="icon-undo"></uui-icon>
<umb-localize key="actions_rollback"></umb-localize>
</uui-button>
</umb-history-item>`;
}
#renderHistoryPagination() {
if (!this._total) return nothing;
const totalPages = Math.ceil(this._total / this.itemsPerPage);
if (totalPages <= 1) return nothing;
return html`<div class="pagination">
<uui-pagination .total=${totalPages} @change="${this.#onPageChange}"></uui-pagination>
</div>`;
}
#renderTag(type?: HistoryLogType) {
switch (type) {
case 'Publish':
return html`<uui-tag look="primary" color="positive" label=${this.localize.term('content_publish')}
><umb-localize key="content_publish"></umb-localize
></uui-tag>`;
case 'Unpublish':
return html`<uui-tag look="primary" color="warning" label=${this.localize.term('content_unpublish')}
><umb-localize key="content_unpublish"></umb-localize
></uui-tag>`;
case 'Save':
return html`<uui-tag look="primary" label=${this.localize.term('auditTrails_smallSave')}
><umb-localize key="auditTrails_smallSave"></umb-localize
></uui-tag>`;
case 'ContentVersionEnableCleanup':
return html`<uui-tag
look="secondary"
label=${this.localize.term('contentTypeEditor_historyCleanupEnableCleanup')}
><umb-localize key="contentTypeEditor_historyCleanupEnableCleanup"></umb-localize
></uui-tag>`;
case 'ContentVersionPreventCleanup':
return html`<uui-tag
look="secondary"
label=${this.localize.term('contentTypeEditor_historyCleanupPreventCleanup')}
><umb-localize key="contentTypeEditor_historyCleanupPreventCleanup"></umb-localize
></uui-tag>`;
default:
return 'Could not detect log type';
}
}
#renderTagDescription(type?: HistoryLogType, params?: HistoryNode) {
switch (type) {
case 'Publish':
return this.localize.term('auditTrails_publish');
case 'Unpublish':
return this.localize.term('auditTrails_unpublish');
case 'Save':
return this.localize.term('auditTrails_save');
case 'ContentVersionEnableCleanup':
return this.localize.term('auditTrails_contentversionenablecleanup', [params?.nodeId]);
case 'ContentVersionPreventCleanup':
return this.localize.term('auditTrails_contentversionpreventcleanup', [params?.nodeId]);
default:
return 'Could not detect log type';
}
}
static styles = [
UmbTextStyles,
css`
:host {
display: grid;
gap: var(--uui-size-layout-1);
padding: var(--uui-size-layout-1);
grid-template-columns: 1fr 350px;
}
div.container {
display: flex;
flex-direction: column;
gap: var(--uui-size-layout-1);
}
//General section
#general-section {
display: flex;
flex-direction: column;
}
.general-item {
display: flex;
flex-direction: column;
gap: var(--uui-size-space-1);
}
.general-item:not(:last-child) {
margin-bottom: var(--uui-size-space-6);
}
// Link section
#link-section {
display: flex;
flex-direction: column;
text-align: left;
}
.link-item {
padding: var(--uui-size-space-4) var(--uui-size-space-6);
display: grid;
grid-template-columns: 75px 1fr;
color: inherit;
text-decoration: none;
}
.link-language {
color: var(--uui-color-divider-emphasis);
}
.link-content.italic {
font-style: italic;
}
.link-item uui-icon {
margin-right: var(--uui-size-space-2);
vertical-align: middle;
}
.link-item.with-href {
cursor: pointer;
}
.link-item.with-href:hover {
background: var(--uui-color-divider);
}
//History section
uui-tag uui-icon {
margin-right: var(--uui-size-space-1);
}
.log-type {
display: flex;
gap: var(--uui-size-space-2);
}
uui-pagination {
display: inline-block;
}
.pagination {
display: flex;
justify-content: center;
margin-top: var(--uui-size-space-4);
}
`,
];
}
export default UmbDocumentInfoWorkspaceViewElement;
declare global {
interface HTMLElementTagNameMap {
'umb-document-info-workspace-view': UmbDocumentInfoWorkspaceViewElement;
}
}

View File

@@ -0,0 +1,182 @@
import { HistoryTagStyleAndText, TimeOptions } from './utils.js';
import { UmbAuditLogRepository } from '@umbraco-cms/backoffice/audit-log';
import {
css,
html,
customElement,
state,
property,
nothing,
repeat,
ifDefined,
} from '@umbraco-cms/backoffice/external/lit';
import { UUIPaginationEvent } from '@umbraco-cms/backoffice/external/uui';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import {
AuditLogBaseModel,
AuditLogWithUsernameResponseModel,
DirectionModel,
} from '@umbraco-cms/backoffice/backend-api';
import { UmbCurrentUserContext } from '@umbraco-cms/backoffice/current-user';
@customElement('umb-document-workspace-view-info-history')
export class UmbDocumentWorkspaceViewInfoHistoryElement extends UmbLitElement {
#logRepository: UmbAuditLogRepository;
#itemsPerPage = 10;
@property()
documentUnique = '';
@state()
private _total?: number;
@state()
private _items?: Array<AuditLogWithUsernameResponseModel>;
@state()
private _currentPage = 1;
constructor() {
super();
this.#logRepository = new UmbAuditLogRepository(this);
}
protected firstUpdated(): void {
this.#getLogs();
}
async #getLogs() {
this._items = undefined; // Reset items to show loader
if (!this.documentUnique) return;
/*const { data } = await this.#logRepository.getAuditLogByUnique({
id: this.documentUnique,
orderDirection: DirectionModel.DESCENDING,
skip: (this._currentPage - 1) * this.#itemsPerPage,
take: this.#itemsPerPage,
});
if (!data) return;
this._total = data.total;
this._items = data.items;
*/
//TODO: I think there is an issue with the API (backend) - error with ID. Hacking for now.
// Uncomment previous code and delete the following when issue fixed.
// This should also make it load significantly faster
const { data } = await this.#logRepository.getLog({
orderDirection: DirectionModel.DESCENDING,
skip: 0,
take: 99999,
});
if (!data) return;
// Hack list to only get the items for the current document
const list = data.items.filter((item) => item.entityId === this.documentUnique);
this._total = list.length;
// Hack list to only get the items for the current page
this._items = list.slice(
(this._currentPage - 1) * this.#itemsPerPage,
(this._currentPage - 1) * this.#itemsPerPage + this.#itemsPerPage,
);
}
#onPageChange(event: UUIPaginationEvent) {
if (this._currentPage === event.target.current) return;
this._currentPage = event.target.current;
this.#getLogs();
}
render() {
return html`<uui-box headline=${this.localize.term('general_history')}>
${this._items ? this.#renderHistory() : html`<uui-loader-circle></uui-loader-circle> `}
</uui-box>
${this.#renderHistoryPagination()}`;
}
#renderHistory() {
if (this._items && this._items.length) {
return html`
<umb-history-list>
${repeat(
this._items,
(item) => item.timestamp,
(item) => {
const { text, style } = HistoryTagStyleAndText(item.logType);
return html`<umb-history-item
.name=${item.userName ?? 'Unknown'}
src=${ifDefined(
Array.isArray(item.userAvatars) ? item.userAvatars[item.userAvatars.length - 1] : undefined,
)}
detail=${this.localize.date(item.timestamp, TimeOptions)}>
<span class="log-type">
<uui-tag look=${style.look} color=${style.color}> ${this.localize.term(text.label)} </uui-tag>
${this.localize.term(text.desc, item.parameters)}
</span>
<uui-button label=${this.localize.term('actions_rollback')} look="secondary" slot="actions">
<uui-icon name="icon-undo"></uui-icon>
<umb-localize key="actions_rollback"></umb-localize>
</uui-button>
</umb-history-item>`;
},
)}
</umb-history-list>
`;
} else {
return html`${this.localize.term('content_noItemsToShow')}`;
}
}
#renderHistoryPagination() {
if (!this._total) return nothing;
const totalPages = Math.ceil(this._total / this.#itemsPerPage);
if (totalPages <= 1) return nothing;
return html`<div class="pagination">
<uui-pagination .total=${totalPages} @change="${this.#onPageChange}"></uui-pagination>
</div>`;
}
static styles = [
UmbTextStyles,
css`
uui-loader-circle {
font-size: 2rem;
}
uui-tag uui-icon {
margin-right: var(--uui-size-space-1);
}
.log-type {
flex-grow: 1;
gap: var(--uui-size-space-2);
}
uui-pagination {
flex: 1;
display: inline-block;
}
.pagination {
display: flex;
justify-content: center;
margin-top: var(--uui-size-space-4);
}
`,
];
}
export default UmbDocumentWorkspaceViewInfoHistoryElement;
declare global {
interface HTMLElementTagNameMap {
'umb-document-workspace-view-info-history': UmbDocumentWorkspaceViewInfoHistoryElement;
}
}

View File

@@ -0,0 +1,158 @@
import { css, html, customElement, state, nothing, repeat, property } from '@umbraco-cms/backoffice/external/lit';
import { UUIPaginationEvent } from '@umbraco-cms/backoffice/external/uui';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { RelationItemResponseModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbDocumentTrackedReferenceRepository } from '@umbraco-cms/backoffice/document';
import { UMB_WORKSPACE_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal';
@customElement('umb-document-workspace-view-info-reference')
export class UmbDocumentWorkspaceViewInfoReferenceElement extends UmbLitElement {
#itemsPerPage = 10;
#trackedReferenceRepository;
@property()
documentUnique = '';
@state()
private _editDocumentPath = '';
@state()
private _currentPage = 1;
@state()
private _total = 0;
@state()
private _items?: Array<RelationItemResponseModel> = [];
constructor() {
super();
this.#trackedReferenceRepository = new UmbDocumentTrackedReferenceRepository(this);
new UmbModalRouteRegistrationController(this, UMB_WORKSPACE_MODAL)
.addAdditionalPath('document')
.onSetup(() => {
return { data: { entityType: 'document', preset: {} } };
})
.observeRouteBuilder((routeBuilder) => {
this._editDocumentPath = routeBuilder({});
});
}
protected firstUpdated(): void {
this.#getReferences();
}
async #getReferences() {
const { data } = await this.#trackedReferenceRepository.requestTrackedReference(
this.documentUnique,
this._currentPage - 1 * this.#itemsPerPage,
this.#itemsPerPage,
);
if (!data) return;
this._total = data.total;
this._items = data.items;
}
#onPageChange(event: UUIPaginationEvent) {
if (this._currentPage === event.target.current) return;
this._currentPage = event.target.current;
this.#getReferences();
}
render() {
if (this._items && this._items.length > 0) {
return html`<strong>
<umb-localize key="references_labelUsedByItems">Referenced by the following items</umb-localize>
</strong>
<uui-box style="--uui-box-default-padding:0">
<uui-table>
<uui-table-head>
<uui-table-head-cell></uui-table-head-cell>
<uui-table-head-cell><umb-localize key="general_name">Name</umb-localize></uui-table-head-cell>
<uui-table-head-cell><umb-localize key="general_status">Status</umb-localize></uui-table-head-cell>
<uui-table-head-cell><umb-localize key="general_typeName">Type Name</umb-localize></uui-table-head-cell>
<uui-table-head-cell><umb-localize key="general_type">Type</umb-localize></uui-table-head-cell>
<uui-table-head-cell>
<umb-localize key="relationType_relation">Relation</umb-localize>
</uui-table-head-cell>
</uui-table-head>
${repeat(
this._items,
(item) => item.nodeId,
(item) =>
html`<uui-table-row>
<uui-table-cell>
<uui-icon style=" vertical-align: middle;" name="icon-document"></uui-icon>
</uui-table-cell>
<uui-table-cell class="link-cell">
<uui-button label="Edit" href=${`${this._editDocumentPath}edit/${item.nodeId}`}>
${item.nodeName}
</uui-button>
</uui-table-cell>
<uui-table-cell>
${item.nodePublished
? this.localize.term('content_published')
: this.localize.term('content_unpublished')}
</uui-table-cell>
<uui-table-cell>${item.contentTypeName}</uui-table-cell>
<uui-table-cell>${item.nodeType}</uui-table-cell>
<uui-table-cell>${item.relationTypeName}</uui-table-cell>
</uui-table-row>`,
)}
</uui-table>
</uui-box>
${this.#renderReferencePagination()}`;
} else {
return nothing;
}
}
#renderReferencePagination() {
if (!this._total) return nothing;
const totalPages = Math.ceil(this._total / this.#itemsPerPage);
if (totalPages <= 1) return nothing;
return html`<div class="pagination">
<uui-pagination .total=${totalPages} @change="${this.#onPageChange}"></uui-pagination>
</div>`;
}
static styles = [
UmbTextStyles,
css`
.link-cell {
font-weight: bold;
}
uui-table-cell:not(.link-cell) {
color: var(--uui-color-text-alt);
}
uui-pagination {
flex: 1;
display: inline-block;
}
.pagination {
display: flex;
justify-content: center;
margin-top: var(--uui-size-space-4);
}
`,
];
}
export default UmbDocumentWorkspaceViewInfoReferenceElement;
declare global {
interface HTMLElementTagNameMap {
'umb-document-workspace-view-info-reference': UmbDocumentWorkspaceViewInfoReferenceElement;
}
}

View File

@@ -0,0 +1,229 @@
import { TimeOptions } from './utils.js';
import { css, html, customElement, state, repeat } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace';
import { UMB_WORKSPACE_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import './document-workspace-view-info-history.element.js';
import './document-workspace-view-info-reference.element.js';
import { UmbDocumentWorkspaceContext } from '@umbraco-cms/backoffice/document';
import { ContentUrlInfoModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbCurrentUserContext } from '@umbraco-cms/backoffice/current-user';
@customElement('umb-document-workspace-view-info')
export class UmbDocumentWorkspaceViewInfoElement extends UmbLitElement {
@state()
private _nodeName = '';
@state()
private _documentTypeId = '';
@state()
private _documentUnique = '';
private _workspaceContext?: typeof UMB_WORKSPACE_CONTEXT.TYPE;
@state()
private _editDocumentTypePath = '';
@state()
private _urls?: Array<ContentUrlInfoModel>;
@state()
private _createDate = 'Unknown';
constructor() {
super();
new UmbModalRouteRegistrationController(this, UMB_WORKSPACE_MODAL)
.addAdditionalPath('document-type')
.onSetup(() => {
return { data: { entityType: 'document-type', preset: {} } };
})
.observeRouteBuilder((routeBuilder) => {
this._editDocumentTypePath = routeBuilder({});
});
this.consumeContext(UMB_WORKSPACE_CONTEXT, (nodeContext) => {
this._workspaceContext = nodeContext;
this._observeContent();
});
}
private _observeContent() {
if (!this._workspaceContext) return;
this._nodeName = 'TBD, with variants this is not as simple.';
this._documentTypeId = (this._workspaceContext as UmbDocumentWorkspaceContext).getContentTypeId()!;
this.observe((this._workspaceContext as UmbDocumentWorkspaceContext).urls, (urls) => {
this._urls = urls;
});
this.observe((this._workspaceContext as UmbDocumentWorkspaceContext).unique, (unique) => {
this._documentUnique = unique!;
});
/** TODO: Doubt this is the right way to get the create date... */
this.observe((this._workspaceContext as UmbDocumentWorkspaceContext).variants, (variants) => {
this._createDate = Array.isArray(variants) ? variants[0].createDate : 'Unknown';
});
}
render() {
return html`<div class="container">
<uui-box headline=${this.localize.term('general_links')} style="--uui-box-default-padding: 0;">
<div id="link-section">${this.#renderLinksSection()}</div>
</uui-box>
<umb-document-workspace-view-info-reference
.documentUnique=${this._documentUnique}></umb-document-workspace-view-info-reference>
<umb-document-workspace-view-info-history
.documentUnique=${this._documentUnique}></umb-document-workspace-view-info-history>
</div>
<div class="container">
<uui-box headline="General" id="general-section">${this.#renderGeneralSection()}</uui-box>
</div>`;
}
#renderLinksSection() {
/** TODO Make sure link section is completed */
if (this._urls && this._urls.length) {
return html`
${repeat(
this._urls,
(url) => url.culture,
(url) => html`
<a href=${url.url} target="_blank" class="link-item with-href">
<span class="link-language">${url.culture}</span>
<span class="link-content"> ${url.url}</span>
<uui-icon name="icon-out"></uui-icon>
</a>
`,
)}
`;
} else {
return html`<div class="link-item">
<span class="link-language">en-EN</span>
<span class="link-content italic"><umb-localize key="content_parentNotPublishedAnomaly"></umb-localize></span>
</div>`;
}
}
#renderGeneralSection() {
return html`
<div class="general-item">
<strong>${this.localize.term('content_publishStatus')}</strong>
<span>
<uui-tag color="positive" look="primary" label=${this.localize.term('content_published')}>
<umb-localize key="content_published"></umb-localize>
</uui-tag>
</span>
</div>
<div class="general-item">
<strong><umb-localize key="content_createDate"></umb-localize></strong>
<span>
<umb-localize-date .date=${this._createDate} .options=${TimeOptions}></umb-localize-date>
</span>
</div>
<div class="general-item">
<strong><umb-localize key="content_documentType"></umb-localize></strong>
<uui-button
look="secondary"
href=${this._editDocumentTypePath + 'edit/' + this._documentTypeId}
label=${this.localize.term('general_edit')}></uui-button>
</div>
<div class="general-item">
<strong><umb-localize key="template_template"></umb-localize></strong>
<uui-button look="secondary" label="Template picker TODO"></uui-button>
</div>
<div class="general-item">
<strong><umb-localize key="template_id"></umb-localize></strong>
<span>${this._documentTypeId}</span>
</div>
`;
}
static styles = [
UmbTextStyles,
css`
:host {
display: grid;
gap: var(--uui-size-layout-1);
padding: var(--uui-size-layout-1);
grid-template-columns: 1fr 350px;
}
div.container {
display: flex;
flex-direction: column;
gap: var(--uui-size-layout-1);
}
//General section
#general-section {
display: flex;
flex-direction: column;
}
.general-item {
display: flex;
flex-direction: column;
gap: var(--uui-size-space-1);
}
.general-item:not(:last-child) {
margin-bottom: var(--uui-size-space-6);
}
// Link section
#link-section {
display: flex;
flex-direction: column;
text-align: left;
}
.link-item {
padding: var(--uui-size-space-4) var(--uui-size-space-6);
display: grid;
grid-template-columns: auto 1fr auto;
gap: var(--uui-size-6);
color: inherit;
text-decoration: none;
}
.link-language {
color: var(--uui-color-divider-emphasis);
}
.link-content.italic {
font-style: italic;
}
.link-item uui-icon {
margin-right: var(--uui-size-space-2);
vertical-align: middle;
}
.link-item.with-href {
cursor: pointer;
}
.link-item.with-href:hover {
background: var(--uui-color-divider);
}
`,
];
}
export default UmbDocumentWorkspaceViewInfoElement;
declare global {
interface HTMLElementTagNameMap {
'umb-document-workspace-view-info': UmbDocumentWorkspaceViewInfoElement;
}
}

View File

@@ -1,7 +1,7 @@
import './document-info-workspace-view.element.js'; import './document-workspace-view-info.element.js';
import { Meta, Story } from '@storybook/web-components'; import { Meta, Story } from '@storybook/web-components';
import type { UmbDocumentInfoWorkspaceViewElement } from './document-info-workspace-view.element.js'; import type { UmbDocumentWorkspaceViewInfoElement } from './document-workspace-view-info.element.js';
import { html } from '@umbraco-cms/backoffice/external/lit'; import { html } from '@umbraco-cms/backoffice/external/lit';
// import { data } from '../../../../../../core/mocks/data/document.data.js'; // import { data } from '../../../../../../core/mocks/data/document.data.js';
@@ -9,8 +9,8 @@ import { html } from '@umbraco-cms/backoffice/external/lit';
export default { export default {
title: 'Workspaces/Documents/Views/Info', title: 'Workspaces/Documents/Views/Info',
component: 'umb-document-info-workspace-view', component: 'umb-document-workspace-view-info',
id: 'umb-document-info-workspace-view', id: 'umb-document-workspace-view-info',
decorators: [ decorators: [
(story) => { (story) => {
return html`TODO: make use of mocked workspace context??`; return html`TODO: make use of mocked workspace context??`;
@@ -21,6 +21,6 @@ export default {
], ],
} as Meta; } as Meta;
export const AAAOverview: Story<UmbDocumentInfoWorkspaceViewElement> = () => export const AAAOverview: Story<UmbDocumentWorkspaceViewInfoElement> = () =>
html` <umb-document-info-workspace-view></umb-document-info-workspace-view>`; html` <umb-document-workspace-view-info></umb-document-workspace-view-info>`;
AAAOverview.storyName = 'Overview'; AAAOverview.storyName = 'Overview';

View File

@@ -0,0 +1,78 @@
import { AuditTypeModel } from '@umbraco-cms/backoffice/backend-api';
interface HistoryStyleMap {
look: 'default' | 'primary' | 'secondary' | 'outline' | 'placeholder';
color: 'default' | 'danger' | 'warning' | 'positive';
}
interface HistoryLocalizeKeys {
label: string;
desc: string;
}
interface HistoryData {
style: HistoryStyleMap;
text: HistoryLocalizeKeys;
}
// Return label, color, look, desc
/**
* @description Helper function to get look and color for uui-tag and localization keys for the label and description.
* @param type AuditTypeModel
* @returns {HistoricData}
*/
export function HistoryTagStyleAndText(type: AuditTypeModel): HistoryData {
switch (type) {
case AuditTypeModel.SAVE:
return {
style: { look: 'primary', color: 'default' },
text: { label: 'auditTrails_smallSave', desc: 'auditTrails_save' },
};
case AuditTypeModel.PUBLISH:
return {
style: { look: 'primary', color: 'positive' },
text: { label: 'content_publish', desc: 'auditTrails_publish' },
};
case AuditTypeModel.UNPUBLISH:
return {
style: { look: 'primary', color: 'warning' },
text: { label: 'content_unpublish', desc: 'auditTrails_unpublish' },
};
case AuditTypeModel.CONTENT_VERSION_ENABLE_CLEANUP:
return {
style: { look: 'secondary', color: 'default' },
text: {
label: 'contentTypeEditor_historyCleanupEnableCleanup',
desc: 'auditTrails_contentversionenablecleanup',
},
};
case AuditTypeModel.CONTENT_VERSION_PREVENT_CLEANUP:
return {
style: { look: 'secondary', color: 'default' },
text: {
label: 'contentTypeEditor_historyCleanupPreventCleanup',
desc: 'auditTrails_contentversionpreventcleanup',
},
};
default:
return {
style: { look: 'placeholder', color: 'danger' },
text: { label: type, desc: 'TODO' },
};
}
}
export const TimeOptions: Intl.DateTimeFormatOptions = {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
};

View File

@@ -1 +1,3 @@
import './media-type-input/media-type-input.element.js'; import './media-type-input/media-type-input.element.js';
export * from './media-type-input/media-type-input.element.js';

View File

@@ -12,3 +12,5 @@ export {
UMB_MEDIA_TYPE_ENTITY_TYPE, UMB_MEDIA_TYPE_ENTITY_TYPE,
UMB_MEDIA_TYPE_FOLDER_ENTITY_TYPE, UMB_MEDIA_TYPE_FOLDER_ENTITY_TYPE,
} from './entity.js'; } from './entity.js';
export * from './components/index.js';

View File

@@ -101,9 +101,9 @@ export class UmbInputMediaElement extends FormControlMixin(UmbLitElement) {
#renderButton() { #renderButton() {
if (this._items && this.max && this._items.length >= this.max) return; if (this._items && this.max && this._items.length >= this.max) return;
return html` return html`
<uui-button id="add-button" look="placeholder" @click=${() => this.#pickerContext.openPicker()} label="open"> <uui-button id="add-button" look="placeholder" @click=${() => this.#pickerContext.openPicker()} label=${this.localize.term('general_choose')}>
<uui-icon name="icon-add"></uui-icon> <uui-icon name="icon-add"></uui-icon>
Add ${this.localize.term('general_choose')}
</uui-button> </uui-button>
`; `;
} }

View File

@@ -4,6 +4,8 @@ import { manifests as memberGroupManifests } from './member-groups/manifests.js'
import { manifests as memberTypeManifests } from './member-types/manifests.js'; import { manifests as memberTypeManifests } from './member-types/manifests.js';
import { manifests as memberManifests } from './members/manifests.js'; import { manifests as memberManifests } from './members/manifests.js';
import './members/components/index.js';
export const manifests = [ export const manifests = [
...memberSectionManifests, ...memberSectionManifests,
...menuSectionManifests, ...menuSectionManifests,

View File

@@ -0,0 +1,3 @@
import './input-member-type/input-member-type.element.js';
export * from './input-member-type/input-member-type.element.js';

View File

@@ -0,0 +1,13 @@
import { UMB_MEMBER_TYPE_PICKER_MODAL } from '../../../../core/modal/token/member-type-picker-modal.token.js';
import { UMB_MEMBER_TYPE_REPOSITORY_ALIAS } from '../../repository/index.js';
import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import { MemberTypeItemResponseModel } from '@umbraco-cms/backoffice/backend-api';
export class UmbMemberTypePickerContext extends UmbPickerInputContext<MemberTypeItemResponseModel> {
constructor(host: UmbControllerHostElement) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
super(host, UMB_MEMBER_TYPE_REPOSITORY_ALIAS, UMB_MEMBER_TYPE_PICKER_MODAL);
}
}

View File

@@ -0,0 +1,179 @@
import { UmbMemberTypePickerContext } from './input-member-type.context.js';
import { css, html, customElement, property, state, ifDefined, repeat } from '@umbraco-cms/backoffice/external/lit';
import { FormControlMixin } from '@umbraco-cms/backoffice/external/uui';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import type { MemberTypeItemResponseModel } from '@umbraco-cms/backoffice/backend-api';
import { splitStringToArray } from '@umbraco-cms/backoffice/utils';
@customElement('umb-input-member-type')
export class UmbMemberTypeInputElement extends FormControlMixin(UmbLitElement) {
/**
* This is a minimum amount of selected items in this input.
* @type {number}
* @attr
* @default 0
*/
@property({ type: Number })
public get min(): number {
return this.#pickerContext.min;
}
public set min(value: number) {
this.#pickerContext.min = value;
}
/**
* Min validation message.
* @type {boolean}
* @attr
* @default
*/
@property({ type: String, attribute: 'min-message' })
minMessage = 'This field need more items';
/**
* This is a maximum amount of selected items in this input.
* @type {number}
* @attr
* @default Infinity
*/
@property({ type: Number })
public get max(): number {
return this.#pickerContext.max;
}
public set max(value: number) {
this.#pickerContext.max = value;
}
/**
* Max validation message.
* @type {boolean}
* @attr
* @default
*/
@property({ type: String, attribute: 'min-message' })
maxMessage = 'This field exceeds the allowed amount of items';
public get selectedIds(): Array<string> {
return this.#pickerContext.getSelection();
}
public set selectedIds(ids: Array<string>) {
this.#pickerContext.setSelection(ids);
}
@property()
public set value(idsString: string) {
// Its with full purpose we don't call super.value, as thats being handled by the observation of the context selection.
this.selectedIds = splitStringToArray(idsString);
}
@property()
get pickableFilter() {
return this.#pickerContext.pickableFilter;
}
set pickableFilter(newVal) {
this.#pickerContext.pickableFilter = newVal;
}
@state()
private _items?: Array<MemberTypeItemResponseModel>;
#pickerContext = new UmbMemberTypePickerContext(this);
constructor() {
super();
}
connectedCallback() {
super.connectedCallback();
this.addValidator(
'rangeUnderflow',
() => this.minMessage,
() => !!this.min && this.#pickerContext.getSelection().length < this.min,
);
this.addValidator(
'rangeOverflow',
() => this.maxMessage,
() => !!this.max && this.#pickerContext.getSelection().length > this.max,
);
this.observe(this.#pickerContext.selection, (selection) => (super.value = selection.join(',')));
this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems));
}
protected _openPicker() {
this.#pickerContext.openPicker({
hideTreeRoot: true,
});
}
protected getFormElement() {
return undefined;
}
render() {
return html`
${this.#renderItems()}
${this.#renderAddButton()}
`;
}
#renderItems() {
if (!this._items) return;
// TODO: Add sorting. [LK]
return html`
<uui-ref-list
>${repeat(
this._items,
(item) => item.id,
(item) => this._renderItem(item),
)}</uui-ref-list
>
`;
}
#renderAddButton() {
if (this.max > 0 && this.selectedIds.length >= this.max) return;
return html`
<uui-button
id="add-button"
look="placeholder"
@click=${this._openPicker}
label="${this.localize.term('general_choose')}"
>${this.localize.term('general_choose')}</uui-button
>
`;
}
private _renderItem(item: MemberTypeItemResponseModel) {
if (!item.id) return;
return html`
<uui-ref-node-document-type name=${ifDefined(item.name)}>
<uui-action-bar slot="actions">
<uui-button
@click=${() => this.#pickerContext.requestRemoveItem(item.id!)}
label="Remove Member Type ${item.name}"
>${this.localize.term('general_remove')}</uui-button
>
</uui-action-bar>
</uui-ref-node-document-type>
`;
}
static styles = [
css`
#add-button {
width: 100%;
}
`,
];
}
export default UmbMemberTypeInputElement;
declare global {
interface HTMLElementTagNameMap {
'umb-input-member-type': UmbMemberTypeInputElement;
}
}

View File

@@ -0,0 +1,5 @@
import './components/index.js';
export * from './components/index.js';
export * from './repository/index.js';
export * from './entity.js';

View File

@@ -1,13 +1,15 @@
import { manifests as entityActionsManifests } from './entity-actions/manifests.js';
import { manifests as menuItemManifests } from './menu-item/manifests.js'; import { manifests as menuItemManifests } from './menu-item/manifests.js';
import { manifests as treeManifests } from './tree/manifests.js';
import { manifests as repositoryManifests } from './repository/manifests.js'; import { manifests as repositoryManifests } from './repository/manifests.js';
import { manifests as treeManifests } from './tree/manifests.js';
import { manifests as workspaceManifests } from './workspace/manifests.js'; import { manifests as workspaceManifests } from './workspace/manifests.js';
import { manifests as entityActionManifests } from './entity-actions/manifests.js';
import './components/index.js';
export const manifests = [ export const manifests = [
...entityActionsManifests,
...menuItemManifests, ...menuItemManifests,
...treeManifests,
...repositoryManifests, ...repositoryManifests,
...treeManifests,
...workspaceManifests, ...workspaceManifests,
...entityActionManifests,
]; ];

View File

@@ -1 +1,2 @@
export { UmbMemberTypeRepository } from './member-type.repository.js'; export * from './member-type.repository.js';
export * from './manifests.js';

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