Merge branch 'main' into bugfix-duplicate-to-entity-action-(part-1)

This commit is contained in:
Mads Rasmussen
2024-04-15 19:16:32 +02:00
committed by GitHub
113 changed files with 1783 additions and 1644 deletions

View File

@@ -38,8 +38,8 @@ const config: StorybookConfig = {
},
refs: {
uui: {
title: 'Umbraco UI Library (1.6.0)',
url: 'https://04709c3--62189360eeb21b003ab2f4ad.chromatic.com/',
title: 'Umbraco UI Library',
url: 'https://62189360eeb21b003ab2f4ad-vfnpsanjps.chromatic.com/',
},
},
};

View File

@@ -12,7 +12,7 @@
"@types/diff": "^5.0.9",
"@types/dompurify": "^3.0.5",
"@types/uuid": "^9.0.8",
"@umbraco-ui/uui": "1.8.0-rc.1",
"@umbraco-ui/uui": "1.8.0-rc.2",
"@umbraco-ui/uui-css": "1.8.0-rc.0",
"base64-js": "^1.5.1",
"diff": "^5.2.0",
@@ -6879,9 +6879,9 @@
}
},
"node_modules/@umbraco-ui/uui": {
"version": "1.8.0-rc.1",
"resolved": "https://registry.npmjs.org/@umbraco-ui/uui/-/uui-1.8.0-rc.1.tgz",
"integrity": "sha512-YSM3HoUAAUiDNfbbI13X586BPucPvqLV8UGSj7hGQN+DgqiydQCxup8bYWIQwtoQzXtd7wgn8N8e7/5sv01PDw==",
"version": "1.8.0-rc.2",
"resolved": "https://registry.npmjs.org/@umbraco-ui/uui/-/uui-1.8.0-rc.2.tgz",
"integrity": "sha512-KYySEmXsl0Ga1lAqAiClsjCMquSAZpo/9HZUcnrkw1dgZN8XaHmt/0O+b1QOty7WHZihurcvPQh169+BfMwUFw==",
"dependencies": {
"@umbraco-ui/uui-action-bar": "1.8.0-rc.0",
"@umbraco-ui/uui-avatar": "1.8.0-rc.0",
@@ -6891,7 +6891,7 @@
"@umbraco-ui/uui-boolean-input": "1.8.0-rc.0",
"@umbraco-ui/uui-box": "1.8.0-rc.1",
"@umbraco-ui/uui-breadcrumbs": "1.8.0-rc.0",
"@umbraco-ui/uui-button": "1.8.0-rc.0",
"@umbraco-ui/uui-button": "1.8.0-rc.2",
"@umbraco-ui/uui-button-group": "1.8.0-rc.0",
"@umbraco-ui/uui-button-inline-create": "1.8.0-rc.0",
"@umbraco-ui/uui-card": "1.8.0-rc.0",
@@ -6906,7 +6906,7 @@
"@umbraco-ui/uui-color-slider": "1.8.0-rc.0",
"@umbraco-ui/uui-color-swatch": "1.8.0-rc.0",
"@umbraco-ui/uui-color-swatches": "1.8.0-rc.0",
"@umbraco-ui/uui-combobox": "1.8.0-rc.0",
"@umbraco-ui/uui-combobox": "1.8.0-rc.2",
"@umbraco-ui/uui-combobox-list": "1.8.0-rc.0",
"@umbraco-ui/uui-css": "1.8.0-rc.0",
"@umbraco-ui/uui-dialog": "1.8.0-rc.0",
@@ -6920,8 +6920,8 @@
"@umbraco-ui/uui-icon-registry": "1.8.0-rc.0",
"@umbraco-ui/uui-icon-registry-essential": "1.8.0-rc.0",
"@umbraco-ui/uui-input": "1.8.0-rc.0",
"@umbraco-ui/uui-input-file": "1.8.0-rc.0",
"@umbraco-ui/uui-input-lock": "1.8.0-rc.0",
"@umbraco-ui/uui-input-file": "1.8.0-rc.2",
"@umbraco-ui/uui-input-lock": "1.8.0-rc.2",
"@umbraco-ui/uui-input-password": "1.8.0-rc.0",
"@umbraco-ui/uui-keyboard-shortcut": "1.8.0-rc.0",
"@umbraco-ui/uui-label": "1.8.0-rc.0",
@@ -6929,8 +6929,8 @@
"@umbraco-ui/uui-loader-bar": "1.8.0-rc.0",
"@umbraco-ui/uui-loader-circle": "1.8.0-rc.0",
"@umbraco-ui/uui-menu-item": "1.8.0-rc.1",
"@umbraco-ui/uui-modal": "1.8.0-rc.0",
"@umbraco-ui/uui-pagination": "1.8.0-rc.0",
"@umbraco-ui/uui-modal": "1.8.0-rc.2",
"@umbraco-ui/uui-pagination": "1.8.0-rc.2",
"@umbraco-ui/uui-popover": "1.8.0-rc.0",
"@umbraco-ui/uui-popover-container": "1.8.0-rc.0",
"@umbraco-ui/uui-progress-bar": "1.8.0-rc.0",
@@ -6957,11 +6957,11 @@
"@umbraco-ui/uui-symbol-more": "1.8.0-rc.0",
"@umbraco-ui/uui-symbol-sort": "1.8.0-rc.0",
"@umbraco-ui/uui-table": "1.8.0-rc.0",
"@umbraco-ui/uui-tabs": "1.8.0-rc.0",
"@umbraco-ui/uui-tabs": "1.8.0-rc.2",
"@umbraco-ui/uui-tag": "1.8.0-rc.0",
"@umbraco-ui/uui-textarea": "1.8.0-rc.0",
"@umbraco-ui/uui-toast-notification": "1.8.0-rc.0",
"@umbraco-ui/uui-toast-notification-container": "1.8.0-rc.0",
"@umbraco-ui/uui-toast-notification": "1.8.0-rc.2",
"@umbraco-ui/uui-toast-notification-container": "1.8.0-rc.2",
"@umbraco-ui/uui-toast-notification-layout": "1.8.0-rc.0",
"@umbraco-ui/uui-toggle": "1.8.0-rc.0",
"@umbraco-ui/uui-visually-hidden": "1.8.0-rc.0"
@@ -7035,9 +7035,9 @@
}
},
"node_modules/@umbraco-ui/uui-button": {
"version": "1.8.0-rc.0",
"resolved": "https://registry.npmjs.org/@umbraco-ui/uui-button/-/uui-button-1.8.0-rc.0.tgz",
"integrity": "sha512-V6Tl+uqBvy4ciKeoohylK8t4rPBl4aJNqVwxG13YCDOu95k+q+0neglAznkK+s5thhKThBHuW5XesRIEOW2Q3g==",
"version": "1.8.0-rc.2",
"resolved": "https://registry.npmjs.org/@umbraco-ui/uui-button/-/uui-button-1.8.0-rc.2.tgz",
"integrity": "sha512-5JAS247c0NdjsOdzdXOqjOEsfb1HxvPWvBc2KUMOi2hjh/TQbp765BXB0lvc5RqePwuJbwogeAhbesLuRvCCwQ==",
"dependencies": {
"@umbraco-ui/uui-base": "1.8.0-rc.0",
"@umbraco-ui/uui-icon-registry-essential": "1.8.0-rc.0"
@@ -7172,12 +7172,12 @@
}
},
"node_modules/@umbraco-ui/uui-combobox": {
"version": "1.8.0-rc.0",
"resolved": "https://registry.npmjs.org/@umbraco-ui/uui-combobox/-/uui-combobox-1.8.0-rc.0.tgz",
"integrity": "sha512-+7N0+LSCZ4SO1Y3XvG7zJU68dxKiqhkouuAWMibzTo7S5WeDQx6R3zATgM5iM+lCIVcnWzt8b34sHOug1rpatg==",
"version": "1.8.0-rc.2",
"resolved": "https://registry.npmjs.org/@umbraco-ui/uui-combobox/-/uui-combobox-1.8.0-rc.2.tgz",
"integrity": "sha512-71AbVcHweB36g3jUCur/PKIKbpSHMvJq2iQou84NgVtO+hBM0PxH2JOsLRCMFG76D8fjIUd03tNp6szBHH1RMQ==",
"dependencies": {
"@umbraco-ui/uui-base": "1.8.0-rc.0",
"@umbraco-ui/uui-button": "1.8.0-rc.0",
"@umbraco-ui/uui-button": "1.8.0-rc.2",
"@umbraco-ui/uui-combobox-list": "1.8.0-rc.0",
"@umbraco-ui/uui-icon": "1.8.0-rc.0",
"@umbraco-ui/uui-popover-container": "1.8.0-rc.0",
@@ -7298,25 +7298,25 @@
}
},
"node_modules/@umbraco-ui/uui-input-file": {
"version": "1.8.0-rc.0",
"resolved": "https://registry.npmjs.org/@umbraco-ui/uui-input-file/-/uui-input-file-1.8.0-rc.0.tgz",
"integrity": "sha512-GHFlUIprhObBt6c+o5AWcScwVaFK2dN0GU4wfKXFcYvG9SZxuYJqC87z0ovX8qjXE8VgTTIlaF2T8GgNBmXjHg==",
"version": "1.8.0-rc.2",
"resolved": "https://registry.npmjs.org/@umbraco-ui/uui-input-file/-/uui-input-file-1.8.0-rc.2.tgz",
"integrity": "sha512-WO4boW7+K4cFF+wo+qunBtiWyfn2XOdd3tl+8M/+lBwmCDIxuLbhrDosZEiUKvhyG4BjZxK1+C5JFqROZSQrkg==",
"dependencies": {
"@umbraco-ui/uui-action-bar": "1.8.0-rc.0",
"@umbraco-ui/uui-base": "1.8.0-rc.0",
"@umbraco-ui/uui-button": "1.8.0-rc.0",
"@umbraco-ui/uui-button": "1.8.0-rc.2",
"@umbraco-ui/uui-file-dropzone": "1.8.0-rc.0",
"@umbraco-ui/uui-icon": "1.8.0-rc.0",
"@umbraco-ui/uui-icon-registry-essential": "1.8.0-rc.0"
}
},
"node_modules/@umbraco-ui/uui-input-lock": {
"version": "1.8.0-rc.0",
"resolved": "https://registry.npmjs.org/@umbraco-ui/uui-input-lock/-/uui-input-lock-1.8.0-rc.0.tgz",
"integrity": "sha512-GMKi/DFZPLIUG4IfAb9HFsuDwTmhnHgq/qHg+YbfZ2fPMjvuNQhFaWYbuYMIksLu13hQsiZdMjnHpBnM8leVpw==",
"version": "1.8.0-rc.2",
"resolved": "https://registry.npmjs.org/@umbraco-ui/uui-input-lock/-/uui-input-lock-1.8.0-rc.2.tgz",
"integrity": "sha512-k8Dv83zUuEQvQBOFE+oD6tBNXB+UBd6pnQmoqLvohDobWVmjWo8o0vL2AszroLR9XFPGbPE+UpCNODM7OAEw9A==",
"dependencies": {
"@umbraco-ui/uui-base": "1.8.0-rc.0",
"@umbraco-ui/uui-button": "1.8.0-rc.0",
"@umbraco-ui/uui-button": "1.8.0-rc.2",
"@umbraco-ui/uui-icon": "1.8.0-rc.0",
"@umbraco-ui/uui-input": "1.8.0-rc.0"
}
@@ -7382,20 +7382,20 @@
}
},
"node_modules/@umbraco-ui/uui-modal": {
"version": "1.8.0-rc.0",
"resolved": "https://registry.npmjs.org/@umbraco-ui/uui-modal/-/uui-modal-1.8.0-rc.0.tgz",
"integrity": "sha512-hJBFF0siAeXYh6tYkAvA8zQlDsOxKhdrPTEMwqU46zHgnDOSXQ6xvt6RqY9nLmXF2x4VT4L0HUrrWmK5YAN8Ug==",
"version": "1.8.0-rc.2",
"resolved": "https://registry.npmjs.org/@umbraco-ui/uui-modal/-/uui-modal-1.8.0-rc.2.tgz",
"integrity": "sha512-ISi2kRV729SHFXgpXnLlIjEJuOSBxo64gg8KMkXdFpUMXgfq6qlIWFrlY9D5L6m3c7mB3QfhDOejp0rwOeHO6A==",
"dependencies": {
"@umbraco-ui/uui-base": "1.8.0-rc.0"
}
},
"node_modules/@umbraco-ui/uui-pagination": {
"version": "1.8.0-rc.0",
"resolved": "https://registry.npmjs.org/@umbraco-ui/uui-pagination/-/uui-pagination-1.8.0-rc.0.tgz",
"integrity": "sha512-j28PjKCJ08b9jtPz91d5gZptDUZs6a4t3WLp8GcVV+obGB7v2+Cr9jBlpRu14iwXVFYRl/TN2MdGqVuFKBC55w==",
"version": "1.8.0-rc.2",
"resolved": "https://registry.npmjs.org/@umbraco-ui/uui-pagination/-/uui-pagination-1.8.0-rc.2.tgz",
"integrity": "sha512-T4vw2M5EJliqwy8YI03eA7pg+gcym9fyeO95eGQvriUV6/OB60CrzjEQ2tXREkVQq1oW3RIBEUEikUgRktMpwA==",
"dependencies": {
"@umbraco-ui/uui-base": "1.8.0-rc.0",
"@umbraco-ui/uui-button": "1.8.0-rc.0",
"@umbraco-ui/uui-button": "1.8.0-rc.2",
"@umbraco-ui/uui-button-group": "1.8.0-rc.0"
}
},
@@ -7616,12 +7616,12 @@
}
},
"node_modules/@umbraco-ui/uui-tabs": {
"version": "1.8.0-rc.0",
"resolved": "https://registry.npmjs.org/@umbraco-ui/uui-tabs/-/uui-tabs-1.8.0-rc.0.tgz",
"integrity": "sha512-81bvMrb2KqrMpruV83wuWGcI/5Zx+2FeD7ibpQ0HmdGg2EKiMX+XZCsEPyc+LD4/JImkVPs90PQmhBj8IzLBrQ==",
"version": "1.8.0-rc.2",
"resolved": "https://registry.npmjs.org/@umbraco-ui/uui-tabs/-/uui-tabs-1.8.0-rc.2.tgz",
"integrity": "sha512-MLtDabiXsOEqOxfgEuqU3ji1XTgY9ABbhqOHC23cFaaGBwlqAbUyi9hAMJhfso406vkQa/9t9A7yK8qpMqKdrA==",
"dependencies": {
"@umbraco-ui/uui-base": "1.8.0-rc.0",
"@umbraco-ui/uui-button": "1.8.0-rc.0",
"@umbraco-ui/uui-button": "1.8.0-rc.2",
"@umbraco-ui/uui-popover-container": "1.8.0-rc.0",
"@umbraco-ui/uui-symbol-more": "1.8.0-rc.0"
}
@@ -7643,24 +7643,24 @@
}
},
"node_modules/@umbraco-ui/uui-toast-notification": {
"version": "1.8.0-rc.0",
"resolved": "https://registry.npmjs.org/@umbraco-ui/uui-toast-notification/-/uui-toast-notification-1.8.0-rc.0.tgz",
"integrity": "sha512-inBfJnrPtWgvUd4KBF168xB1EOI5B2HEK1GEaYYuKuFYZdeiJAZPP0GCgkiMC9K0YShIxh9H3q31iwvtWA1KhA==",
"version": "1.8.0-rc.2",
"resolved": "https://registry.npmjs.org/@umbraco-ui/uui-toast-notification/-/uui-toast-notification-1.8.0-rc.2.tgz",
"integrity": "sha512-ICvxWZVuDO1X/f1udYgtY1prHYbj26g3ZecKq2V2FVs9Ej5kYNIWU1nVGj6tWkdyKGnVPjoLfYmq/W8i9BJb9g==",
"dependencies": {
"@umbraco-ui/uui-base": "1.8.0-rc.0",
"@umbraco-ui/uui-button": "1.8.0-rc.0",
"@umbraco-ui/uui-button": "1.8.0-rc.2",
"@umbraco-ui/uui-css": "1.8.0-rc.0",
"@umbraco-ui/uui-icon": "1.8.0-rc.0",
"@umbraco-ui/uui-icon-registry-essential": "1.8.0-rc.0"
}
},
"node_modules/@umbraco-ui/uui-toast-notification-container": {
"version": "1.8.0-rc.0",
"resolved": "https://registry.npmjs.org/@umbraco-ui/uui-toast-notification-container/-/uui-toast-notification-container-1.8.0-rc.0.tgz",
"integrity": "sha512-PA1IKPMJOSxYiOnSVyd2AhbovhSN/aU7d2Z/4/HFfvmnhedjsfRYzsgFDfoLUCenV7hN1dIxFNEEW6FxEpxEqQ==",
"version": "1.8.0-rc.2",
"resolved": "https://registry.npmjs.org/@umbraco-ui/uui-toast-notification-container/-/uui-toast-notification-container-1.8.0-rc.2.tgz",
"integrity": "sha512-iQ1xDQBgKrvTtCAUsT/3DJayCNVPWb+T9B5V+MyfuHnV9qOnmPtchs7l9r8cFwabOO5ZpxMke/tltsgMawwajQ==",
"dependencies": {
"@umbraco-ui/uui-base": "1.8.0-rc.0",
"@umbraco-ui/uui-toast-notification": "1.8.0-rc.0"
"@umbraco-ui/uui-toast-notification": "1.8.0-rc.2"
}
},
"node_modules/@umbraco-ui/uui-toast-notification-layout": {

View File

@@ -24,6 +24,7 @@
"./code-editor": "./dist-cms/packages/templating/code-editor/index.js",
"./collection": "./dist-cms/packages/core/collection/index.js",
"./components": "./dist-cms/packages/core/components/index.js",
"./content": "./dist-cms/packages/core/content/index.js",
"./content-type": "./dist-cms/packages/core/content-type/index.js",
"./culture": "./dist-cms/packages/core/culture/index.js",
"./current-user": "./dist-cms/packages/user/current-user/index.js",
@@ -169,7 +170,7 @@
"@types/diff": "^5.0.9",
"@types/dompurify": "^3.0.5",
"@types/uuid": "^9.0.8",
"@umbraco-ui/uui": "1.8.0-rc.1",
"@umbraco-ui/uui": "1.8.0-rc.2",
"@umbraco-ui/uui-css": "1.8.0-rc.0",
"base64-js": "^1.5.1",
"diff": "^5.2.0",

View File

@@ -17,9 +17,6 @@ export function dispatchRouteChangeEvent<D = any>($elem: HTMLElement, detail: IR
*/
export function dispatchGlobalRouterEvent<D = any>(name: GlobalRouterEvent, detail?: IRoutingInfo<D>) {
GLOBAL_ROUTER_EVENTS_TARGET.dispatchEvent(new CustomEvent(name, { detail }));
// if ("debugRouterSlot" in window) {
// console.log(`%c [router-slot]: ${name}`, `color: #286ee0`, detail);
// }
}
/**

View File

@@ -368,3 +368,88 @@ describe('UmbExtensionRegistry with kinds', () => {
.unsubscribe();
});
});
describe('UmbExtensionRegistry with exclusions', () => {
let extensionRegistry: UmbExtensionRegistry<any>;
let manifests: Array<
ManifestElementWithElementName | TestManifestWithMeta | ManifestKind<ManifestElementWithElementName>
>;
beforeEach(() => {
extensionRegistry = new UmbExtensionRegistry<any>();
manifests = [
{
type: 'kind',
alias: 'Umb.Test.Kind',
matchType: 'section',
matchKind: 'test-kind',
manifest: {
type: 'section',
elementName: 'my-kind-element',
meta: {
label: 'my-kind-meta-label',
},
},
},
{
type: 'section',
kind: 'test-kind' as unknown as undefined, // We do not know about this one, so it makes good sense that its not a valid option.
name: 'test-section-1',
alias: 'Umb.Test.Section.1',
weight: 1,
meta: {
pathname: 'test-section-1',
},
},
{
type: 'section',
name: 'test-section-2',
alias: 'Umb.Test.Section.2',
weight: 200,
meta: {
label: 'Test Section 2',
pathname: 'test-section-2',
},
},
];
manifests.forEach((manifest) => extensionRegistry.register(manifest));
});
it('should have the extensions registered', () => {
expect(extensionRegistry.isRegistered('Umb.Test.Kind')).to.be.true;
expect(extensionRegistry.isRegistered('Umb.Test.Section.1')).to.be.true;
expect(extensionRegistry.isRegistered('Umb.Test.Section.2')).to.be.true;
});
it('must not say that Umb.Test.Section.1d is registered, when its added to the exclusion list', () => {
extensionRegistry.exclude('Umb.Test.Section.1');
expect(extensionRegistry.isRegistered('Umb.Test.Section.1')).to.be.false;
// But check that the other ones are still registered:
expect(extensionRegistry.isRegistered('Umb.Test.Kind')).to.be.true;
expect(extensionRegistry.isRegistered('Umb.Test.Section.2')).to.be.true;
});
it('does not affect kinds when a kind-alias is put in the exclusion list', () => {
extensionRegistry.exclude('Umb.Test.Kind');
// This had no effect, so all of them are still available.
expect(extensionRegistry.isRegistered('Umb.Test.Kind')).to.be.true;
expect(extensionRegistry.isRegistered('Umb.Test.Section.1')).to.be.true;
expect(extensionRegistry.isRegistered('Umb.Test.Section.2')).to.be.true;
});
it('prevents late comers from begin registered', () => {
extensionRegistry.exclude('Umb.Test.Section.Late');
extensionRegistry.register({
type: 'section',
name: 'test-section-late',
alias: 'Umb.Test.Section.Late',
weight: 200,
meta: {
label: 'Test Section Late',
pathname: 'test-section-Late',
},
});
expect(extensionRegistry.isRegistered('Umb.Test.Section.Late')).to.be.false;
});
});

View File

@@ -80,6 +80,7 @@ export class UmbExtensionRegistry<
private _kinds = new UmbBasicState<Array<ManifestKind<ManifestTypes>>>([]);
public readonly kinds = this._kinds.asObservable();
#exclusions: Array<string> = [];
defineKind(kind: ManifestKind<ManifestTypes>): void {
const extensionsValues = this._extensions.getValue();
@@ -105,6 +106,15 @@ export class UmbExtensionRegistry<
this._kinds.setValue(nextData);
}
exclude(alias: string): void {
this.#exclusions.push(alias);
this._extensions.setValue(this._extensions.getValue().filter(this.#acceptExtension));
}
#acceptExtension = (ext: ManifestTypes): boolean => {
return !this.#exclusions.includes(ext.alias);
};
register(manifest: ManifestTypes | ManifestKind<ManifestTypes>): void {
const isValid = this.#checkExtension(manifest);
if (!isValid) {
@@ -163,6 +173,10 @@ export class UmbExtensionRegistry<
return false;
}
if (!this.#acceptExtension(manifest as ManifestTypes)) {
return false;
}
const extensionsValues = this._extensions.getValue();
const extension = extensionsValues.find((extension) => extension.alias === (manifest as ManifestTypes).alias);

View File

@@ -1,14 +1,7 @@
import type { Observable, Subscription } from '@umbraco-cms/backoffice/external/rxjs';
export type { Observable } from '@umbraco-cms/backoffice/external/rxjs';
export type ObserverCallbackStack<T> = {
next: (_value: T) => void;
error?: (_value: unknown) => void;
complete?: () => void;
};
export type ObserverCallback<T> = (_value: T) => void;
// We do not use the ObserverCallbackStack type, and it was making things more complicated than they need to be so I have taken it out..
//export type ObserverCallback<T> = ((_value: T) => void) | ObserverCallbackStack<T>;
export type ObserverCallback<T> = (value: T) => void;
export class UmbObserver<T> {
#source!: Observable<T>;

View File

@@ -70,8 +70,8 @@ export class UmbArrayState<T> extends UmbDeepState<T[]> {
* myState.remove([1, 2]);
*/
remove(uniques: unknown[]) {
let next = this.getValue();
if (this.getUniqueMethod) {
let next = this.getValue();
uniques.forEach((unique) => {
next = next.filter((x) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -99,8 +99,8 @@ export class UmbArrayState<T> extends UmbDeepState<T[]> {
* myState.removeOne(1);
*/
removeOne(unique: unknown) {
let next = this.getValue();
if (this.getUniqueMethod) {
let next = this.getValue();
next = next.filter((x) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore

View File

@@ -605,7 +605,7 @@ export const data: Array<UmbMockDataTypeModel> = [
name: 'Media Picker',
id: 'dt-mediaPicker',
parent: null,
editorAlias: 'Umbraco.MediaPicker',
editorAlias: 'Umbraco.MediaPicker3',
editorUiAlias: 'Umb.PropertyEditorUi.MediaPicker',
hasChildren: false,
isFolder: false,

View File

@@ -45,18 +45,17 @@ export class UmbBlockGridAreaTypeWorkspaceContext
async load(unique: string) {
this.resetState();
this.consumeContext(UMB_PROPERTY_CONTEXT, (context) => {
this.observe(context.value, (value) => {
if (value) {
const blockTypeData = value.find((x: UmbBlockGridTypeAreaType) => x.key === unique);
if (blockTypeData) {
this.#data.setValue(blockTypeData);
return;
}
const context = await this.getContext(UMB_PROPERTY_CONTEXT);
this.observe(context.value, (value) => {
if (value) {
const blockTypeData = value.find((x: UmbBlockGridTypeAreaType) => x.key === unique);
if (blockTypeData) {
this.#data.setValue(blockTypeData);
return;
}
// Fallback to undefined:
this.#data.setValue(undefined);
});
}
// Fallback to undefined:
this.#data.setValue(undefined);
});
}
@@ -109,15 +108,16 @@ export class UmbBlockGridAreaTypeWorkspaceContext
}
async submit() {
if (!this.#data.value) return;
if (!this.#data.value) {
throw new Error('No data to submit.');
}
this.consumeContext(UMB_PROPERTY_CONTEXT, (context) => {
// TODO: We should most likely consume already, in this way I avoid having the reset this consumption.
context.setValue(appendToFrozenArray(context.getValue() ?? [], this.#data.getValue(), (x) => x?.key));
});
const context = await this.getContext(UMB_PROPERTY_CONTEXT);
// TODO: We should most likely consume already, in this way I avoid having the reset this consumption.
context.setValue(appendToFrozenArray(context.getValue() ?? [], this.#data.getValue(), (x) => x?.key));
this.setIsNew(false);
return true;
}
public destroy(): void {

View File

@@ -141,7 +141,6 @@ export class UmbPropertyEditorUIBlockListElement extends UmbLitElement implement
this.observe(this.#managerContext.layouts, (layouts) => {
this._value = { ...this._value, layout: { [UMB_BLOCK_LIST_PROPERTY_EDITOR_ALIAS]: layouts } };
// Notify that the value has changed.
//console.log('layout changed', this._value);
// TODO: idea: consider inserting an await here, so other changes could appear first? Maybe some mechanism to only fire change event onces?
//this.#entriesContext.setLayoutEntries(layouts);
this.#fireChangeEvent();

View File

@@ -85,18 +85,17 @@ export class UmbBlockTypeWorkspaceContext<BlockTypeData extends UmbBlockTypeWith
async load(unique: string) {
this.resetState();
this.consumeContext(UMB_PROPERTY_CONTEXT, (context) => {
this.observe(context.value, (value) => {
if (value) {
const blockTypeData = value.find((x: UmbBlockTypeBaseModel) => x.contentElementTypeKey === unique);
if (blockTypeData) {
this.#data.setValue(blockTypeData);
return;
}
const context = await this.getContext(UMB_PROPERTY_CONTEXT);
this.observe(context.value, (value) => {
if (value) {
const blockTypeData = value.find((x: UmbBlockTypeBaseModel) => x.contentElementTypeKey === unique);
if (blockTypeData) {
this.#data.setValue(blockTypeData);
return;
}
// Fallback to undefined:
this.#data.setValue(undefined);
});
}
// Fallback to undefined:
this.#data.setValue(undefined);
});
}
@@ -148,17 +147,17 @@ export class UmbBlockTypeWorkspaceContext<BlockTypeData extends UmbBlockTypeWith
}
async submit() {
if (!this.#data.value) return;
if (!this.#data.value) {
throw new Error('No data to submit.');
}
this.consumeContext(UMB_PROPERTY_CONTEXT, (context) => {
// TODO: We should most likely consume already, in this way I avoid having the reset this consumption.
context.setValue(
appendToFrozenArray(context.getValue() ?? [], this.#data.getValue(), (x) => x?.contentElementTypeKey),
);
});
const context = await this.getContext(UMB_PROPERTY_CONTEXT);
context.setValue(
appendToFrozenArray(context.getValue() ?? [], this.#data.getValue(), (x) => x?.contentElementTypeKey),
);
this.setIsNew(false);
return true;
}
public destroy(): void {

View File

@@ -303,7 +303,9 @@ export class UmbBlockWorkspaceContext<LayoutDataType extends UmbBlockLayoutBaseM
async submit() {
const layoutData = this.#layout.value;
const contentData = this.content.getData();
if (!layoutData || !this.#blockManager || !this.#blockEntries || !contentData || !this.#modalContext) return;
if (!layoutData || !this.#blockManager || !this.#blockEntries || !contentData || !this.#modalContext) {
throw new Error('Missing data');
}
const settingsData = this.settings.getData();
@@ -333,7 +335,6 @@ export class UmbBlockWorkspaceContext<LayoutDataType extends UmbBlockLayoutBaseM
}
this.setIsNew(false);
return true;
}
#modalRejected = () => {

View File

@@ -33,4 +33,5 @@ export * from './multiple-color-picker-input/index.js';
export * from './multiple-text-string-input/index.js';
export * from './popover-layout/index.js';
export * from './ref-item/index.js';
export * from './stack/index.js';
export * from './table/index.js';

View File

@@ -1,5 +1,4 @@
import { html, customElement, property, css, state, nothing } from '@umbraco-cms/backoffice/external/lit';
import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbRepositoryItemsManager } from '@umbraco-cms/backoffice/repository';
@@ -11,9 +10,10 @@ import {
} from '@umbraco-cms/backoffice/data-type';
import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal';
import type { UmbDataTypeItemModel } from '@umbraco-cms/backoffice/data-type';
import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation';
@customElement('umb-input-collection-configuration')
export class UmbInputCollectionConfigurationElement extends UUIFormControlMixin(UmbLitElement, {}) {
export class UmbInputCollectionConfigurationElement extends UmbFormControlMixin(UmbLitElement) {
protected getFormElement() {
return undefined;
}

View File

@@ -1,8 +1,8 @@
import { UmbConfigRepository } from '../../repository/config/config.repository.js';
import { html, ifDefined, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui';
import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui';
import { html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui';
import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui';
@customElement('umb-input-date')
export class UmbInputDateElement extends UUIFormControlMixin(UmbLitElement, '') {
@@ -22,12 +22,6 @@ export class UmbInputDateElement extends UUIFormControlMixin(UmbLitElement, '')
@property({ type: String })
displayValue?: string;
@property({ type: Boolean })
offsetTime = false;
@state()
private _offsetValue = 0;
@property({ type: String })
min?: string;
@@ -37,42 +31,25 @@ export class UmbInputDateElement extends UUIFormControlMixin(UmbLitElement, '')
@property({ type: Number })
step?: number;
private _configRepository = new UmbConfigRepository(this);
constructor() {
super();
}
connectedCallback(): void {
super.connectedCallback();
this.offsetTime ? this.#getOffset() : (this.displayValue = this.#UTCToLocal(this.value as string));
}
async #getOffset() {
const data = await this._configRepository.getServertimeOffset();
if (!data) return;
this._offsetValue = data.offset;
if (!this.value) return;
this.displayValue = this.#valueToServerOffset(this.value as string, true);
this.displayValue = this.#UTCToLocal(this.value as string);
}
#localToUTC(d: string) {
#localToUTC(date: string) {
if (this.type === 'time') {
return new Date(`${new Date().toJSON().slice(0, 10)} ${d}`).toISOString().slice(11, 16);
return new Date(`${new Date().toJSON().slice(0, 10)} ${date}`).toISOString().slice(11, 16);
} else {
const date = new Date(d);
const isoDate = date.toISOString();
return `${isoDate.substring(0, 10)}T${isoDate.substring(11, 19)}Z`;
return new Date(date).toJSON();
}
}
#UTCToLocal(d: string) {
if (this.type === 'time') {
const local = new Date(`${new Date().toJSON().slice(0, 10)} ${d}Z`)
.toLocaleTimeString(undefined, {
hourCycle: 'h23',
})
.toLocaleTimeString(undefined, { hourCycle: 'h23' })
.slice(0, 5);
return local;
} else {
@@ -89,58 +66,25 @@ export class UmbInputDateElement extends UUIFormControlMixin(UmbLitElement, '')
}
}
#dateToString(date: Date) {
return `${date.getFullYear()}-${('0' + (date.getMonth() + 1)).slice(-2)}-${('0' + date.getDate()).slice(-2)}T${(
'0' + date.getHours()
).slice(-2)}:${('0' + date.getMinutes()).slice(-2)}:${('0' + date.getSeconds()).slice(-2)}`;
}
#onChange(event: UUIInputEvent) {
const newValue = event.target.value as string;
if (!newValue) return;
#valueToServerOffset(d: string, utc = false) {
if (this.type === 'time') {
const newDate = new Date(`${new Date().toJSON().slice(0, 10)} ${d}`);
const dateOffset = new Date(
newDate.setTime(newDate.getTime() + (utc ? this._offsetValue * -1 : this._offsetValue) * 60 * 1000),
);
const time = dateOffset
.toLocaleTimeString(undefined, {
hourCycle: 'h23',
})
.slice(0, 5);
return time;
} else {
const newDate = new Date(d.replace('Z', ''));
const dateOffset = new Date(
newDate.setTime(newDate.getTime() + (utc ? this._offsetValue * -1 : this._offsetValue) * 60 * 1000),
);
return this.type === 'datetime-local'
? this.#dateToString(dateOffset)
: this.#dateToString(dateOffset).slice(0, 10);
}
}
#onChange(e: UUIInputEvent) {
e.stopPropagation();
const picked = e.target.value as string;
if (!picked) {
this.value = '';
this.displayValue = '';
return;
}
this.value = this.offsetTime ? this.#valueToServerOffset(picked) : this.#localToUTC(picked);
this.displayValue = picked;
this.dispatchEvent(new CustomEvent('change'));
this.value = this.#localToUTC(newValue);
this.displayValue = newValue;
this.dispatchEvent(new UmbChangeEvent());
}
render() {
return html`<uui-input
id="datetime"
label="Pick a date or time"
.type="${this.type}"
@change="${this.#onChange}"
min="${ifDefined(this.min)}"
max="${ifDefined(this.max)}"
.step="${this.step}"
.value="${this.displayValue?.replace('Z', '')}">
.min=${this.min}
.max=${this.max}
.step=${this.step}
.type=${this.type}
.value="${this.displayValue?.replace('Z', '')}"
@change=${this.#onChange}>
</uui-input>`;
}
}

View File

@@ -23,26 +23,30 @@ export class UmbInputEntityElement extends UUIFormControlMixin(UmbLitElement, ''
}
@property({ type: Number })
public set min(value: number) {
this.#min = value;
if (this.#pickerContext) {
this.#pickerContext.min = value;
}
}
public get min(): number {
return this.#pickerContext?.min ?? 0;
return this.#min;
}
#min: number = 0;
@property({ type: String, attribute: 'min-message' })
minMessage = 'This field need more items';
@property({ type: Number })
public set max(value: number) {
this.#max = value;
if (this.#pickerContext) {
this.#pickerContext.max = value;
}
}
public get max(): number {
return this.#pickerContext?.max ?? Infinity;
return this.#max;
}
#max: number = Infinity;
@property({ attribute: false })
getIcon?: (item: any) => string;
@@ -102,6 +106,9 @@ export class UmbInputEntityElement extends UUIFormControlMixin(UmbLitElement, ''
async #observePickerContext() {
if (!this.#pickerContext) return;
this.#pickerContext.min = this.min;
this.#pickerContext.max = this.max;
this.observe(
this.#pickerContext.selection,
(selection) => (this.value = selection?.join(',') ?? ''),

View File

@@ -64,9 +64,9 @@ export class UmbInputNumberRangeElement extends UmbFormControlMixin(UmbLitElemen
super();
this.addValidator(
'customError',
'patternMismatch',
() => {
return 'The low value must be less than the high value';
return 'The low value must not be exceed the high value';
},
() => {
return this._minValue !== undefined && this._maxValue !== undefined ? this._minValue > this._maxValue : false;
@@ -111,10 +111,10 @@ export class UmbInputNumberRangeElement extends UmbFormControlMixin(UmbLitElemen
}
static styles = css`
:host(:invalid) {
:host(:invalid:not([pristine])) {
color: var(--uui-color-danger);
}
:host(:invalid) uui-input {
:host(:invalid:not([pristine])) uui-input {
border-color: var(--uui-color-danger);
}
`;

View File

@@ -82,12 +82,11 @@ export class UmbInputUploadFieldElement extends UUIFormControlMixin(UmbLitElemen
super();
this.#manager = new UmbTemporaryFileManager(this);
this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, async (context) => {
/*this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, async (context) => {
this.observe(await context.propertyValueByAlias('umbracoExtension'), (value) => {
//const test = value;
//console.log('test', test);
});
});
});*/
this.#serverUrlPromise = this.consumeContext(UMB_APP_CONTEXT, (instance) => {
this.#serverUrl = instance.getServerUrl();
@@ -257,7 +256,6 @@ export class UmbInputUploadFieldElement extends UUIFormControlMixin(UmbLitElemen
// Extract the file extension from the path
const extension = path.split('.').pop()?.toLowerCase();
console.log('extension', extension, path);
if (!extension) return 'file';
if (['svg'].includes(extension)) return 'svg';
if (['mp3', 'weba', 'oga', 'opus'].includes(extension)) return 'audio';

View File

@@ -1,16 +1,16 @@
import type { UmbInputMultipleTextStringItemElement } from './input-multiple-text-string-item.element.js';
import { css, html, nothing, repeat, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
import type { UmbInputEvent, UmbDeleteEvent } from '@umbraco-cms/backoffice/event';
import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation';
/**
* @element umb-input-multiple-text-string
*/
@customElement('umb-input-multiple-text-string')
export class UmbInputMultipleTextStringElement extends UUIFormControlMixin(UmbLitElement, '') {
export class UmbInputMultipleTextStringElement extends UmbFormControlMixin(UmbLitElement) {
#sorter = new UmbSorterController(this, {
getUniqueOfElement: (element) => {
return element.getAttribute('data-sort-entry-id');
@@ -165,7 +165,7 @@ export class UmbInputMultipleTextStringElement extends UUIFormControlMixin(UmbLi
this.dispatchEvent(new UmbChangeEvent());
}
protected getFormElement() {
getFormElement() {
return undefined;
}

View File

@@ -0,0 +1 @@
export * from './stack.element.js';

View File

@@ -0,0 +1,84 @@
import { UmbLitElement } from "@umbraco-cms/backoffice/lit-element";
import { customElement, html, css, property, classMap } from "@umbraco-cms/backoffice/external/lit";
/**
* @element umb-stack
* @description - Element for displaying items in a stack with even spacing between
* @extends LitElement
*/
@customElement('umb-stack')
export class UmbStackElement extends UmbLitElement
{
/**
* Look
* @type {String}
* @memberof UmbStackElement
*/
@property({ type:String })
look: 'compact' | 'default' = 'default';
/**
* Divide
* @type {Boolean}
* @memberof UmbStackElement
*/
@property({ type:Boolean })
divide: boolean = false;
render() {
return html`<div class=${classMap({ divide: this.divide, compact: this.look === 'compact' })}>
<slot></slot>
</div>`;
}
static styles = [
css`
div {
display: block;
position: relative;
}
::slotted(*) {
position: relative;
margin-top: var(--uui-size-space-6);
}
.divide ::slotted(*)::before {
content: '';
position: absolute;
top: calc((var(--uui-size-space-6) / 2) * -1);
height: 0;
width: 100%;
border-top: solid 1px var(--uui-color-divider-standalone);
}
::slotted(*:first-child) {
margin-top: 0;
}
.divide ::slotted(*:first-child)::before {
display: none;
}
.compact ::slotted(*) {
margin-top: var(--uui-size-space-3);
}
.compact ::slotted(*:first-child) {
margin-top: 0;
}
.compact.divide ::slotted(*)::before {
display: none;
}
`
];
}
export default UmbStackElement;
declare global {
interface HTMLElementTagNameMap {
'umb-stack': UmbStackElement;
}
}

View File

@@ -0,0 +1,28 @@
import type { Meta, StoryObj } from '@storybook/web-components';
import './stack.element.js';
import type { UmbStackElement} from './stack.element.js';
const meta: Meta<UmbStackElement> = {
title: 'Components/Stack',
component: 'umb-stack',
};
export default meta;
type Story = StoryObj<UmbStackElement>;
export const Default: Story = {
args: { },
};
export const Divide: Story = {
args: {
divide: true
},
};
export const Compact: Story = {
args: {
look: 'compact'
},
};

View File

@@ -22,6 +22,9 @@ export class UmbPropertyTypeBasedPropertyElement extends UmbLitElement {
}
private _property?: UmbPropertyTypeModel;
@property({ type: String, attribute: 'data-path' })
public dataPath?: string;
@state()
private _propertyEditorUiAlias?: string;
@@ -64,12 +67,15 @@ export class UmbPropertyTypeBasedPropertyElement extends UmbLitElement {
}
render() {
return html`<umb-property
alias=${ifDefined(this._property?.alias)}
label=${ifDefined(this._property?.name)}
description=${ifDefined(this._property?.description || undefined)}
property-editor-ui-alias=${ifDefined(this._propertyEditorUiAlias)}
.config=${this._dataTypeData}></umb-property>`;
return this._propertyEditorUiAlias && this._property?.alias
? html`<umb-property
.dataPath=${this.dataPath}
.alias=${this._property.alias}
.label=${this._property.name}
.description=${this._property.description ?? undefined}
property-editor-ui-alias=${ifDefined(this._propertyEditorUiAlias)}
.config=${this._dataTypeData}></umb-property>`
: '';
}
static styles = [

View File

@@ -133,7 +133,6 @@ export class UmbPropertyTypeSettingsModalElement extends UmbModalBaseElement<
#onMandatoryChange(event: UUIBooleanInputEvent) {
const mandatory = event.target.checked;
this.value.validation!.mandatory = mandatory;
this.updateValue({
validation: { ...this.value.validation, mandatory },
});

View File

@@ -26,7 +26,9 @@ import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
* - {@link UmbContentTypePropertyStructureHelper} for managing the structure of properties, optional of another container or root.
* - {@link UmbContentTypeContainerStructureHelper} for managing the structure of containers, optional of another container or root.
*/
export class UmbContentTypeStructureManager<T extends UmbContentTypeModel> extends UmbControllerBase {
export class UmbContentTypeStructureManager<
T extends UmbContentTypeModel = UmbContentTypeModel,
> extends UmbControllerBase {
#init!: Promise<unknown>;
#repository: UmbDetailRepository<T>;
@@ -99,15 +101,15 @@ export class UmbContentTypeStructureManager<T extends UmbContentTypeModel> exten
*/
public async save() {
const contentType = this.getOwnerContentType();
if (!contentType || !contentType.unique) return false;
if (!contentType || !contentType.unique) throw new Error('Could not find the Content Type to save');
const { data } = await this.#repository.save(contentType);
if (!data) return false;
const { error, data } = await this.#repository.save(contentType);
if (error || !data) return { error, data };
// Update state with latest version:
this.#contentTypes.updateOne(contentType.unique, data);
return true;
return { error, data };
}
/**

View File

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

View File

@@ -0,0 +1,3 @@
import { contentEditorManifest } from './views/edit/manifest.js';
export const manifests = [contentEditorManifest];

View File

@@ -1,13 +1,17 @@
import { UMB_DOCUMENT_BLUEPRINT_WORKSPACE_CONTEXT } from '../../document-blueprint-workspace.context-token.js';
import { css, html, customElement, property, state, repeat } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { UmbPropertyTypeModel } from '@umbraco-cms/backoffice/content-type';
import type {
UmbContentTypeModel,
UmbContentTypeStructureManager,
UmbPropertyTypeModel,
} from '@umbraco-cms/backoffice/content-type';
import { UmbContentTypePropertyStructureHelper } from '@umbraco-cms/backoffice/content-type';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { UmbDocumentTypeDetailModel } from '@umbraco-cms/backoffice/document-type';
import { UmbDataPathValueFilter } from '@umbraco-cms/backoffice/validation';
import { UMB_PROPERTY_STRUCTURE_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace';
@customElement('umb-document-blueprint-workspace-view-edit-properties')
export class UmbDocumentBlueprintWorkspaceViewEditPropertiesElement extends UmbLitElement {
@customElement('umb-content-workspace-view-edit-properties')
export class UmbContentWorkspaceViewEditPropertiesElement extends UmbLitElement {
@property({ type: String, attribute: 'container-id', reflect: false })
public get containerId(): string | null | undefined {
return this.#propertyStructureHelper.getContainerId();
@@ -16,7 +20,7 @@ export class UmbDocumentBlueprintWorkspaceViewEditPropertiesElement extends UmbL
this.#propertyStructureHelper.setContainerId(value);
}
#propertyStructureHelper = new UmbContentTypePropertyStructureHelper<UmbDocumentTypeDetailModel>(this);
#propertyStructureHelper = new UmbContentTypePropertyStructureHelper<UmbContentTypeModel>(this);
@state()
_propertyStructure?: Array<UmbPropertyTypeModel>;
@@ -24,8 +28,11 @@ export class UmbDocumentBlueprintWorkspaceViewEditPropertiesElement extends UmbL
constructor() {
super();
this.consumeContext(UMB_DOCUMENT_BLUEPRINT_WORKSPACE_CONTEXT, (workspaceContext) => {
this.#propertyStructureHelper.setStructureManager(workspaceContext.structure);
this.consumeContext(UMB_PROPERTY_STRUCTURE_WORKSPACE_CONTEXT, (workspaceContext) => {
this.#propertyStructureHelper.setStructureManager(
// Assuming its the same content model type that we are working with here... [NL]
workspaceContext.structure as unknown as UmbContentTypeStructureManager<UmbContentTypeModel>,
);
});
this.observe(
this.#propertyStructureHelper.propertyStructure,
@@ -44,6 +51,7 @@ export class UmbDocumentBlueprintWorkspaceViewEditPropertiesElement extends UmbL
(property) =>
html`<umb-property-type-based-property
class="property"
.dataPath="$.values[${UmbDataPathValueFilter(property)}].value"
.property=${property}></umb-property-type-based-property> `,
)
: '';
@@ -62,10 +70,10 @@ export class UmbDocumentBlueprintWorkspaceViewEditPropertiesElement extends UmbL
];
}
export default UmbDocumentBlueprintWorkspaceViewEditPropertiesElement;
export default UmbContentWorkspaceViewEditPropertiesElement;
declare global {
interface HTMLElementTagNameMap {
'umb-document-blueprint-workspace-view-edit-properties': UmbDocumentBlueprintWorkspaceViewEditPropertiesElement;
'umb-content-workspace-view-edit-properties': UmbContentWorkspaceViewEditPropertiesElement;
}
}

View File

@@ -1,14 +1,17 @@
import { UMB_DOCUMENT_BLUEPRINT_WORKSPACE_CONTEXT } from '../../document-blueprint-workspace.context-token.js';
import { css, html, customElement, property, state, repeat } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { UmbPropertyTypeContainerModel } from '@umbraco-cms/backoffice/content-type';
import type {
UmbContentTypeModel,
UmbContentTypeStructureManager,
UmbPropertyTypeContainerModel,
} from '@umbraco-cms/backoffice/content-type';
import { UmbContentTypeContainerStructureHelper } from '@umbraco-cms/backoffice/content-type';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UMB_PROPERTY_STRUCTURE_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace';
import './content-editor-properties.element.js';
import './document-blueprint-workspace-view-edit-properties.element.js';
@customElement('umb-document-blueprint-workspace-view-edit-tab')
export class UmbDocumentBlueprintWorkspaceViewEditTabElement extends UmbLitElement {
@customElement('umb-content-workspace-view-edit-tab')
export class UmbContentWorkspaceViewEditTabElement extends UmbLitElement {
@property({ type: String })
public get containerId(): string | null | undefined {
return this._containerId;
@@ -20,7 +23,7 @@ export class UmbDocumentBlueprintWorkspaceViewEditTabElement extends UmbLitEleme
@state()
private _containerId?: string | null;
#groupStructureHelper = new UmbContentTypeContainerStructureHelper<any>(this);
#groupStructureHelper = new UmbContentTypeContainerStructureHelper<UmbContentTypeModel>(this);
@state()
_groups: Array<UmbPropertyTypeContainerModel> = [];
@@ -31,8 +34,11 @@ export class UmbDocumentBlueprintWorkspaceViewEditTabElement extends UmbLitEleme
constructor() {
super();
this.consumeContext(UMB_DOCUMENT_BLUEPRINT_WORKSPACE_CONTEXT, (workspaceContext) => {
this.#groupStructureHelper.setStructureManager(workspaceContext.structure);
this.consumeContext(UMB_PROPERTY_STRUCTURE_WORKSPACE_CONTEXT, (workspaceContext) => {
this.#groupStructureHelper.setStructureManager(
// Assuming its the same content model type that we are working with here... [NL]
workspaceContext.structure as unknown as UmbContentTypeStructureManager<UmbContentTypeModel>,
);
});
this.observe(this.#groupStructureHelper.mergedContainers, (groups) => {
this._groups = groups;
@@ -47,9 +53,9 @@ export class UmbDocumentBlueprintWorkspaceViewEditTabElement extends UmbLitEleme
${this._hasProperties
? html`
<uui-box>
<umb-document-blueprint-workspace-view-edit-properties
<umb-content-workspace-view-edit-properties
class="properties"
.containerId=${this._containerId}></umb-document-blueprint-workspace-view-edit-properties>
.containerId=${this._containerId}></umb-content-workspace-view-edit-properties>
</uui-box>
`
: ''}
@@ -58,9 +64,9 @@ export class UmbDocumentBlueprintWorkspaceViewEditTabElement extends UmbLitEleme
(group) => group.id,
(group) =>
html`<uui-box .headline=${group.name ?? ''}>
<umb-document-blueprint-workspace-view-edit-properties
<umb-content-workspace-view-edit-properties
class="properties"
.containerId=${group.id}></umb-document-blueprint-workspace-view-edit-properties>
.containerId=${group.id}></umb-content-workspace-view-edit-properties>
</uui-box>`,
)}
`;
@@ -79,10 +85,10 @@ export class UmbDocumentBlueprintWorkspaceViewEditTabElement extends UmbLitEleme
];
}
export default UmbDocumentBlueprintWorkspaceViewEditTabElement;
export default UmbContentWorkspaceViewEditTabElement;
declare global {
interface HTMLElementTagNameMap {
'umb-document-blueprint-workspace-view-edit-tab': UmbDocumentBlueprintWorkspaceViewEditTabElement;
'umb-content-workspace-view-edit-tab': UmbContentWorkspaceViewEditTabElement;
}
}

View File

@@ -1,16 +1,21 @@
import { UMB_DOCUMENT_BLUEPRINT_WORKSPACE_CONTEXT } from '../../document-blueprint-workspace.context-token.js';
import type { UmbDocumentBlueprintWorkspaceViewEditTabElement } from './document-blueprint-workspace-view-edit-tab.element.js';
import type { UmbContentWorkspaceViewEditTabElement } from './content-editor-tab.element.js';
import { css, html, customElement, state, repeat } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { UmbPropertyTypeContainerModel } from '@umbraco-cms/backoffice/content-type';
import type {
UmbContentTypeModel,
UmbContentTypeStructureManager,
UmbPropertyTypeContainerModel,
} from '@umbraco-cms/backoffice/content-type';
import { UmbContentTypeContainerStructureHelper } from '@umbraco-cms/backoffice/content-type';
import type { UmbRoute, UmbRouterSlotChangeEvent, UmbRouterSlotInitEvent } from '@umbraco-cms/backoffice/router';
import { encodeFolderName } from '@umbraco-cms/backoffice/router';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/extension-registry';
import { UMB_PROPERTY_STRUCTURE_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace';
import './content-editor-tab.element.js';
@customElement('umb-document-blueprint-workspace-view-edit')
export class UmbDocumentBlueprintWorkspaceViewEditElement extends UmbLitElement implements UmbWorkspaceViewElement {
@customElement('umb-content-workspace-view-edit')
export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements UmbWorkspaceViewElement {
//@state()
//private _hasRootProperties = false;
@@ -29,9 +34,9 @@ export class UmbDocumentBlueprintWorkspaceViewEditElement extends UmbLitElement
@state()
private _activePath = '';
private _workspaceContext?: typeof UMB_DOCUMENT_BLUEPRINT_WORKSPACE_CONTEXT.TYPE;
#structureManager?: UmbContentTypeStructureManager<UmbContentTypeModel>;
private _tabsStructureHelper = new UmbContentTypeContainerStructureHelper<any>(this);
private _tabsStructureHelper = new UmbContentTypeContainerStructureHelper<UmbContentTypeModel>(this);
constructor() {
super();
@@ -49,18 +54,18 @@ export class UmbDocumentBlueprintWorkspaceViewEditElement extends UmbLitElement
// _hasRootProperties can be gotten via _tabsStructureHelper.hasProperties. But we do not support root properties currently.
this.consumeContext(UMB_DOCUMENT_BLUEPRINT_WORKSPACE_CONTEXT, (workspaceContext) => {
this._workspaceContext = workspaceContext;
this.consumeContext(UMB_PROPERTY_STRUCTURE_WORKSPACE_CONTEXT, (workspaceContext) => {
this.#structureManager = workspaceContext.structure;
this._tabsStructureHelper.setStructureManager(workspaceContext.structure);
this._observeRootGroups();
});
}
private _observeRootGroups() {
if (!this._workspaceContext) return;
if (!this.#structureManager) return;
this.observe(
this._workspaceContext.structure.hasRootContainers('Group'),
this.#structureManager.hasRootContainers('Group'),
(hasRootGroups) => {
this._hasRootGroups = hasRootGroups;
this._createRoutes();
@@ -70,7 +75,7 @@ export class UmbDocumentBlueprintWorkspaceViewEditElement extends UmbLitElement
}
private _createRoutes() {
if (!this._tabs || !this._workspaceContext) return;
if (!this._tabs || !this.#structureManager) return;
const routes: UmbRoute[] = [];
if (this._tabs.length > 0) {
@@ -78,9 +83,9 @@ export class UmbDocumentBlueprintWorkspaceViewEditElement extends UmbLitElement
const tabName = tab.name ?? '';
routes.push({
path: `tab/${encodeFolderName(tabName).toString()}`,
component: () => import('./document-blueprint-workspace-view-edit-tab.element.js'),
component: () => import('./content-editor-tab.element.js'),
setup: (component) => {
(component as UmbDocumentBlueprintWorkspaceViewEditTabElement).containerId = tab.id;
(component as UmbContentWorkspaceViewEditTabElement).containerId = tab.id;
},
});
});
@@ -89,9 +94,9 @@ export class UmbDocumentBlueprintWorkspaceViewEditElement extends UmbLitElement
if (this._hasRootGroups) {
routes.push({
path: '',
component: () => import('./document-blueprint-workspace-view-edit-tab.element.js'),
component: () => import('./content-editor-tab.element.js'),
setup: (component) => {
(component as UmbDocumentBlueprintWorkspaceViewEditTabElement).containerId = null;
(component as UmbContentWorkspaceViewEditTabElement).containerId = null;
},
});
}
@@ -103,6 +108,12 @@ export class UmbDocumentBlueprintWorkspaceViewEditElement extends UmbLitElement
});
}
// Find the routes who are removed:
//const removedRoutes = this._routes.filter((route) => !routes.find((r) => r.path === route.path));
// Find the routes who are new:
//const newRoutes = routes.filter((route) => !this._routes.find((r) => r.path === route.path));
this._routes = routes;
}
@@ -160,10 +171,10 @@ export class UmbDocumentBlueprintWorkspaceViewEditElement extends UmbLitElement
];
}
export default UmbDocumentBlueprintWorkspaceViewEditElement;
export default UmbContentWorkspaceViewEditElement;
declare global {
interface HTMLElementTagNameMap {
'umb-document-blueprint-workspace-view-edit': UmbDocumentBlueprintWorkspaceViewEditElement;
'umb-content-workspace-view-edit': UmbContentWorkspaceViewEditElement;
}
}

View File

@@ -0,0 +1,19 @@
import type { UmbBackofficeManifestKind } from '@umbraco-cms/backoffice/extension-registry';
export const contentEditorManifest: UmbBackofficeManifestKind = {
type: 'kind',
alias: 'Umb.Kind.WorkspaceView.ContentEditor',
matchKind: 'contentEditor',
matchType: 'workspaceView',
manifest: {
type: 'workspaceView',
kind: 'contentEditor',
element: () => import('./content-editor.element.js'),
weight: 1000,
meta: {
label: 'Content',
pathname: 'edit',
icon: 'icon-document-line',
},
},
};

View File

@@ -1,5 +1,6 @@
import { manifests as authManifests } from './auth/manifests.js';
import { manifests as collectionManifests } from './collection/manifests.js';
import { manifests as contentManifests } from './content/manifests.js';
import { manifests as contentTypeManifests } from './content-type/manifests.js';
import { manifests as cultureManifests } from './culture/manifests.js';
import { manifests as debugManifests } from './debug/manifests.js';
@@ -29,6 +30,7 @@ export const manifests: Array<ManifestTypes | UmbBackofficeManifestKind> = [
...treeManifests,
...collectionManifests,
...workspaceManifests,
...contentManifests,
...contentTypeManifests,
...propertyEditorManifests,
...settingsManifests,

View File

@@ -6,6 +6,20 @@ export type UmbEntityBase = {
name?: string;
};
export interface UmbVariantableValueModel<T = unknown> extends UmbInvariantValueModel<T> {
culture?: string | null;
segment?: string | null;
}
export interface UmbVariantValueModel<T = unknown> extends UmbInvariantValueModel<T> {
culture: string | null;
segment: string | null;
}
export interface UmbInvariantValueModel<T = unknown> {
alias: string;
value: T;
}
export interface UmbSwatchDetails {
label: string;
value: string;

View File

@@ -87,7 +87,7 @@ The default layout will cover most cases, but there might be situations where we
```ts
import { html, LitElement } from '@umbraco-cms/backoffice/external/lit';
import { property } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from "@umbraco-cms/backoffice/style";
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { UmbNotificationHandler } from '@umbraco-cms/notification';
export interface UmbNotificationCustomData {
@@ -158,7 +158,7 @@ class MyElement extends LitElement {
notificationHandler.onClose().then((result) => {
if (result) {
console.log('He agreed!');
console.log('She agreed!');
}
});
}

View File

@@ -1,6 +1,6 @@
export class UmbPropertyValueChangeEvent extends Event {
public constructor() {
// mimics the native change event
super('property-value-change', { bubbles: true, composed: false, cancelable: false });
super('change', { bubbles: true, composed: false, cancelable: false });
}
}

View File

@@ -1,36 +1,10 @@
import type { ManifestPropertyEditorSchema } from '@umbraco-cms/backoffice/extension-registry';
// TODO: We won't include momentjs anymore so we need to find a way to handle date formats
export const manifest: ManifestPropertyEditorSchema = {
type: 'propertyEditorSchema',
name: 'Date/Time',
alias: 'Umbraco.DateTime',
meta: {
defaultPropertyEditorUiAlias: 'Umb.PropertyEditorUi.DatePicker',
settings: {
properties: [
{
alias: 'offsetTime',
label: 'Offset time',
description:
'When enabled the time displayed will be offset with the servers timezone, this is useful for scenarios like scheduled publishing when an editor is in a different timezone than the hosted server',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.Toggle',
config: [
{
alias: 'labelOff',
value: 'Adjust to local time',
},
{
alias: 'labelOn',
value: 'Adjust to local time',
},
{
alias: 'showLabels',
value: true,
},
],
},
],
},
},
};

View File

@@ -29,7 +29,8 @@ export const manifest: ManifestPropertyEditorSchema = {
{
alias: 'startNodeId',
label: 'Start node',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.MediaPicker',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.MediaEntityPicker',
config: [{ alias: 'validationLimit', value: { min: 0, max: 1 } }],
},
{
alias: 'enableLocalFocalPoint',

View File

@@ -12,7 +12,7 @@ export const manifest: ManifestPropertyEditorSchema = {
alias: 'mediaParentId',
label: 'Image Upload Folder',
description: 'Choose the upload location of pasted images',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.MediaPicker',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.MediaEntityPicker',
config: [{ alias: 'validationLimit', value: { min: 0, max: 1 } }],
},
{

View File

@@ -1,43 +1,15 @@
import type { UmbPropertyEditorConfigCollection } from '../../index.js';
import { UmbPropertyValueChangeEvent } from '../../index.js';
import { html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry';
import type { UmbPropertyEditorConfigCollection } from '../../index.js';
import { html, customElement, property, state, ifDefined } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { UmbInputDateElement } from '@umbraco-cms/backoffice/components';
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry';
/**
* @element umb-property-editor-ui-date-picker
*/
@customElement('umb-property-editor-ui-date-picker')
export class UmbPropertyEditorUIDatePickerElement extends UmbLitElement implements UmbPropertyEditorUiElement {
private _value?: Date;
private _valueString?: string;
@property()
set value(value: string | undefined) {
if (value) {
const d = new Date(value);
this._value = d;
this._valueString = `${d.getFullYear()}-${
d.getMonth() + 1
}-${d.getDate()}T${d.getHours()}:${d.getMinutes()}:${d.getSeconds()}`;
} else {
this._value = undefined;
this._valueString = undefined;
}
}
get value() {
return this._valueString;
}
private _onInput(e: InputEvent) {
const dateField = e.target as HTMLInputElement;
this.value = dateField.value;
this.dispatchEvent(new UmbPropertyValueChangeEvent());
}
private _format?: string;
@state()
private _inputType: UmbInputDateElement['type'] = 'datetime-local';
@@ -50,29 +22,34 @@ export class UmbPropertyEditorUIDatePickerElement extends UmbLitElement implemen
@state()
private _step?: number;
private _offsetTime?: boolean;
@property()
set value(value: string | undefined) {
if (value) {
// NOTE: If the `value` contains a space, then it doesn't contain the timezone, so may not be parsed as UTC. [LK]
const datetime = !value.includes(' ') ? value : value + ' +00';
this.#value = new Date(datetime).toJSON();
}
}
get value() {
return this.#value;
}
#value?: string;
public set config(config: UmbPropertyEditorConfigCollection | undefined) {
if (!config) return;
const oldVal = this._inputType;
// Format string prevalue/config
this._format = config.getValueByAlias('format');
const pickTime = this._format?.includes('H') || this._format?.includes('m');
if (pickTime) {
this._inputType = 'datetime-local';
} else {
this._inputType = 'date';
}
const format = config.getValueByAlias<string>('format');
const hasTime = format?.includes('H') || format?.includes('m');
this._inputType = hasTime ? 'datetime-local' : 'date';
// Based on the type of format string change the UUI-input type
const timeFormatPattern = /^h{1,2}:m{1,2}(:s{1,2})?\s?a?$/gim;
if (this._format?.toLowerCase().match(timeFormatPattern)) {
if (format?.toLowerCase().match(timeFormatPattern)) {
this._inputType = 'time';
}
//TODO:
this._offsetTime = config.getValueByAlias('offsetTime');
this._min = config.getValueByAlias('min');
this._max = config.getValueByAlias('max');
this._step = config.getValueByAlias('step');
@@ -80,16 +57,22 @@ export class UmbPropertyEditorUIDatePickerElement extends UmbLitElement implemen
this.requestUpdate('_inputType', oldVal);
}
#onChange(event: CustomEvent & { target: HTMLInputElement }) {
this.value = event.target.value;
this.dispatchEvent(new UmbPropertyValueChangeEvent());
}
render() {
return html`<umb-input-date
.type=${this._inputType}
@input=${this._onInput}
.datetime=${this._valueString}
.min=${this._min}
.max=${this._max}
.step=${this._step}
.offsetTime=${this._offsetTime || false}
label="Pick a date or time"></umb-input-date>`;
return html`
<umb-input-date
value="${ifDefined(this.value)}"
.min=${this._min}
.max=${this._max}
.step=${this._step}
.type=${this._inputType}
@change=${this.#onChange}>
</umb-input-date>
`;
}
}

View File

@@ -36,7 +36,7 @@ export class UmbPropertyEditorUINumberRangeElement
min: (event.target as UmbInputNumberRangeElement).minValue,
max: (event.target as UmbInputNumberRangeElement).maxValue,
};
this.dispatchEvent(new CustomEvent('property-value-change'));
this.dispatchEvent(new CustomEvent('change'));
}
@state()
@@ -49,6 +49,10 @@ export class UmbPropertyEditorUINumberRangeElement
this.addFormControlElement(this.shadowRoot!.querySelector('umb-input-number-range')!);
}
focus(): void {
this.shadowRoot!.querySelector('umb-input-number-range')!.focus();
}
render() {
return html`<umb-input-number-range
.minValue=${this._minValue}

View File

@@ -47,11 +47,22 @@ export class UmbPropertyLayoutElement extends LitElement {
@property({ type: String })
public description = '';
/**
* @description Make the property appear invalid
* @type {boolean}
* @attr
* @default undefined
*/
@property({ type: Boolean, reflect: true })
public invalid?: boolean;
render() {
// TODO: Only show alias on label if user has access to DocumentType within settings:
return html`
<div id="headerColumn">
<uui-label title=${this.alias}>${this.label}</uui-label>
<uui-label title=${this.alias}>
${this.label} ${this.invalid ? html`<uui-badge color="danger" attention>!</uui-badge>` : ''}
</uui-label>
<slot name="action-menu"></slot>
<div id="description">${this.description}</div>
<slot name="description"></slot>
@@ -69,7 +80,7 @@ export class UmbPropertyLayoutElement extends LitElement {
css`
:host {
display: grid;
grid-template-columns: 200px minmax(0,1fr);
grid-template-columns: 200px minmax(0, 1fr);
column-gap: var(--uui-size-layout-2);
border-bottom: 1px solid var(--uui-color-divider);
padding: var(--uui-size-layout-1) 0;
@@ -99,6 +110,16 @@ export class UmbPropertyLayoutElement extends LitElement {
}
/*}*/
uui-label {
position: relative;
}
:host([invalid]) uui-label {
color: var(--uui-color-danger);
}
uui-badge {
right: -30px;
}
#description {
color: var(--uui-color-text-alt);
}

View File

@@ -9,7 +9,11 @@ import type {
UmbPropertyEditorConfigCollection,
UmbPropertyEditorConfig,
} from '@umbraco-cms/backoffice/property-editor';
import { UmbFormControlValidator } from '@umbraco-cms/backoffice/validation';
import {
UmbBindValidationMessageToFormControl,
UmbFormControlValidator,
UmbObserveValidationStateController,
} from '@umbraco-cms/backoffice/validation';
/**
* @element umb-property
@@ -95,15 +99,33 @@ export class UmbPropertyElement extends UmbLitElement {
return this.#propertyContext.getConfig();
}
/**
* DataPath, declare the path to the value of the data that this property represents.
* @public
* @type {string}
* @attr
* @default ''
*/
@property({ type: String, attribute: false })
public set dataPath(dataPath: string | undefined) {
this.#dataPath = dataPath;
new UmbObserveValidationStateController(this, dataPath, (invalid) => {
this._invalid = invalid;
});
}
public get dataPath(): string | undefined {
return this.#dataPath;
}
#dataPath?: string;
@state()
private _variantDifference?: string;
@state()
private _element?: ManifestPropertyEditorUi['ELEMENT_TYPE'];
// Not begin used currently [NL]
//@state()
//private _value?: unknown;
@state()
private _invalid?: boolean;
@state()
private _alias?: string;
@@ -116,29 +138,52 @@ export class UmbPropertyElement extends UmbLitElement {
#propertyContext = new UmbPropertyContext(this);
#validator?: UmbFormControlValidator;
#controlValidator?: UmbFormControlValidator;
#validationMessageBinder?: UmbBindValidationMessageToFormControl;
#valueObserver?: UmbObserverController<unknown>;
#configObserver?: UmbObserverController<UmbPropertyEditorConfigCollection | undefined>;
constructor() {
super();
this.observe(this.#propertyContext.alias, (alias) => {
this._alias = alias;
});
this.observe(this.#propertyContext.label, (label) => {
this._label = label;
});
this.observe(this.#propertyContext.description, (description) => {
this._description = description;
});
this.observe(this.#propertyContext.variantDifference, (variantDifference) => {
this._variantDifference = variantDifference;
});
this.observe(
this.#propertyContext.alias,
(alias) => {
this._alias = alias;
},
null,
);
this.observe(
this.#propertyContext.label,
(label) => {
this._label = label;
},
null,
);
this.observe(
this.#propertyContext.description,
(description) => {
this._description = description;
},
null,
);
this.observe(
this.#propertyContext.variantDifference,
(variantDifference) => {
this._variantDifference = variantDifference;
},
null,
);
}
private _onPropertyEditorChange = (e: CustomEvent): void => {
const target = e.composedPath()[0] as any;
if (this._element !== target) {
console.error(
"Property Editor received a Change Event who's target is not the Property Editor Element. Do not make bubble and composed change events.",
);
return;
}
//this.value = target.value; // Sets value in context.
this.#propertyContext.setValue(target.value);
@@ -173,6 +218,8 @@ export class UmbPropertyElement extends UmbLitElement {
// cleanup:
this.#valueObserver?.destroy();
this.#configObserver?.destroy();
this.#controlValidator?.destroy();
oldElement?.removeEventListener('change', this._onPropertyEditorChange as any as EventListener);
oldElement?.removeEventListener('property-value-change', this._onPropertyEditorChange as any as EventListener);
this._element = el as ManifestPropertyEditorUi['ELEMENT_TYPE'];
@@ -180,25 +227,41 @@ export class UmbPropertyElement extends UmbLitElement {
this.#propertyContext.setEditor(this._element);
if (this._element) {
// TODO: Could this be changed to change event? (or additionally support the change event? [NL])
this._element.addEventListener('change', this._onPropertyEditorChange as any as EventListener);
this._element.addEventListener('property-value-change', this._onPropertyEditorChange as any as EventListener);
// No need for a controller alias, as the clean is handled via the observer prop:
this.#valueObserver = this.observe(this.#propertyContext.value, (value) => {
//this._value = value;// This was not used currently [NL]
this._element!.value = value;
});
this.#configObserver = this.observe(this.#propertyContext.config, (config) => {
if (config) {
this._element!.config = config;
}
});
this.#valueObserver = this.observe(
this.#propertyContext.value,
(value) => {
this._element!.value = value;
if (this.#validationMessageBinder) {
this.#validationMessageBinder.value = value;
}
},
null,
);
this.#configObserver = this.observe(
this.#propertyContext.config,
(config) => {
if (config) {
this._element!.config = config;
}
},
null,
);
if (this.#validator) {
this.#validator.destroy();
}
if ('checkValidity' in this._element) {
this.#validator = new UmbFormControlValidator(this, this._element as any);
this.#controlValidator = new UmbFormControlValidator(this, this._element as any, this.#dataPath);
// We trust blindly that the dataPath is available at this stage. [NL]
if (this.#dataPath) {
this.#validationMessageBinder = new UmbBindValidationMessageToFormControl(
this,
this._element as any,
this.#dataPath,
);
this.#validationMessageBinder.value = this.#propertyContext.getValue();
}
}
}
@@ -212,7 +275,8 @@ export class UmbPropertyElement extends UmbLitElement {
id="layout"
alias="${ifDefined(this._alias)}"
label="${ifDefined(this._label)}"
description="${ifDefined(this._description)}">
description="${ifDefined(this._description)}"
?invalid=${this._invalid}>
${this._renderPropertyActionMenu()}
${this._variantDifference
? html`<uui-tag look="secondary" slot="description">${this._variantDifference}</uui-tag>`

View File

@@ -58,7 +58,6 @@ export class UmbThemeContext extends UmbContextBase<UmbThemeContext> {
document.head.appendChild(this.#styleElement);
}
} else {
console.log('remove style element', this.#styleElement);
// We could not load a theme for this alias, so we remove the theme.
localStorage.removeItem(LOCAL_STORAGE_KEY);
this.#styleElement?.childNodes.forEach((node) => node.remove());

View File

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

View File

@@ -0,0 +1,6 @@
import type { UmbServerModelValidationContext } from './index.js';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
export const UMB_SERVER_MODEL_VALIDATION_CONTEXT = new UmbContextToken<UmbServerModelValidationContext>(
'UmbServerModelValidationContext',
);

View File

@@ -0,0 +1,158 @@
import type { UmbValidationMessageTranslator } from '../translators/validation-message-translator.interface.js';
import type { UmbValidator } from '../interfaces/validator.interface.js';
import { UMB_VALIDATION_CONTEXT } from './validation.context-token.js';
import { UMB_SERVER_MODEL_VALIDATION_CONTEXT } from './server-model-validation.context-token.js';
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type { ApiError, CancelError } from '@umbraco-cms/backoffice/external/backend-api';
type ServerFeedbackEntry = { path: string; messages: Array<string> };
export class UmbServerModelValidationContext
extends UmbContextBase<UmbServerModelValidationContext>
implements UmbValidator
{
#validatePromise?: Promise<void>;
#validatePromiseResolve?: () => void;
#context?: typeof UMB_VALIDATION_CONTEXT.TYPE;
#isValid = true;
#data: any;
getData(): any {
return this.#data;
}
#translators: Array<UmbValidationMessageTranslator> = [];
// Hold server feedback...
#serverFeedback: Array<ServerFeedbackEntry> = [];
constructor(host: UmbControllerHost) {
super(host, UMB_SERVER_MODEL_VALIDATION_CONTEXT);
this.consumeContext(UMB_VALIDATION_CONTEXT, (context) => {
if (this.#context) {
this.#context.removeValidator(this);
}
this.#context = context;
context.addValidator(this);
// Run translators?
});
}
async askServerForValidation(
data: unknown,
requestPromise: Promise<{ data: unknown; error: ApiError | CancelError | undefined }>,
): Promise<void> {
this.#context?.messages.removeMessagesByType('server');
this.#serverFeedback = [];
this.#isValid = false;
//this.#validatePromiseReject?.();
this.#validatePromise = new Promise<void>((resolve) => {
this.#validatePromiseResolve = resolve;
});
// Ask the server for validation...
//const { data: feedback, error } = await requestPromise;
await requestPromise;
//console.log('VALIDATE — Got server response:');
//console.log(data, error);
// Store this state of the data for translator look ups:
this.#data = data;
/*
const fixedData = {
type: 'Error',
title: 'Validation failed',
status: 400,
detail: 'One or more properties did not pass validation',
operationStatus: 'PropertyValidationError',
errors: {
'$.values[0].value': ['#validation.invalidPattern'],
} as Record<string, Array<string>>,
missingProperties: [],
};
Object.keys(fixedData.errors).forEach((path) => {
this.#serverFeedback.push({ path, messages: fixedData.errors[path] });
});*/
//this.#isValid = data ? true : false;
//this.#isValid = false;
this.#isValid = true;
this.#validatePromiseResolve?.();
this.#validatePromiseResolve = undefined;
//this.#validatePromise = undefined;
this.#serverFeedback = this.#serverFeedback.flatMap(this.#executeTranslatorsOnFeedback);
}
#executeTranslatorsOnFeedback = (feedback: ServerFeedbackEntry) => {
return this.#translators.flatMap((translator) => {
if (translator.match(feedback.path)) {
const newPath = translator.translate(feedback.path);
// TODO: I might need to restructure this part for adjusting existing feedback with a part-translation.
// Detect if some part is unhandled?
// If so only make a partial translation on the feedback, add a message for the handled part.
// then return [ of the partial translated feedback, and the partial handled part. ];
// TODO:Check if there was any temporary messages base on this path, like if it was partial-translated at one point..
this.#context?.messages.addMessages('server', newPath, feedback.messages);
// by not returning anything this feedback gets removed from server feedback..
return [];
}
return feedback;
});
};
addTranslator(translator: UmbValidationMessageTranslator): void {
if (this.#translators.indexOf(translator) === -1) {
this.#translators.push(translator);
}
}
removeTranslator(translator: UmbValidationMessageTranslator): void {
const index = this.#translators.indexOf(translator);
if (index !== -1) {
this.#translators.splice(index, 1);
}
}
get isValid(): boolean {
return this.#isValid;
}
async validate(): Promise<void> {
if (this.#validatePromise) {
await this.#validatePromise;
}
return this.#isValid ? Promise.resolve() : Promise.reject();
}
reset(): void {}
focusFirstInvalidElement(): void {}
hostConnected(): void {
super.hostConnected();
if (this.#context) {
this.#context.addValidator(this);
}
}
hostDisconnected(): void {
super.hostDisconnected();
if (this.#context) {
this.#context.removeValidator(this);
this.#context = undefined;
}
}
destroy(): void {
// TODO: make sure we destroy things properly:
this.#translators = [];
super.destroy();
}
}

View File

@@ -0,0 +1,102 @@
import type { Observable } from '@umbraco-cms/backoffice/external/rxjs';
import { UmbId } from '@umbraco-cms/backoffice/id';
import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
export type UmbValidationMessageType = 'client' | 'server';
export interface UmbValidationMessage {
type: UmbValidationMessageType;
key: string;
path: string;
message: string;
}
export class UmbValidationMessagesManager {
#messages = new UmbArrayState<UmbValidationMessage>([], (x) => x.key);
/*constructor() {
this.#messages.asObservable().subscribe((x) => console.log('messages:', x));
}*/
/*
serializeMessages(fromPath: string, toPath: string): void {
this.#messages.setValue(
this.#messages.getValue().map((x) => {
if (x.path.indexOf(fromPath) === 0) {
x.path = toPath + x.path.substring(fromPath.length);
}
return x;
}),
);
}
*/
getHasAnyMessages(): boolean {
return this.#messages.getValue().length !== 0;
}
/*messagesOf(path: string): Observable<Array<UmbValidationMessage>> {
// Find messages that starts with the given path, if the path is longer then require a dot or [ as the next character. using a more performant way than Regex:
return this.#messages.asObservablePart((msgs) =>
msgs.filter(
(x) =>
x.path.indexOf(path) === 0 &&
(x.path.length === path.length || x.path[path.length] === '.' || x.path[path.length] === '['),
),
);
}*/
messagesOfTypeAndPath(type: UmbValidationMessageType, path: string): Observable<Array<UmbValidationMessage>> {
// Find messages that matches the given type and path.
return this.#messages.asObservablePart((msgs) => msgs.filter((x) => x.type === type && x.path === path));
}
hasMessagesOfPathAndDescendant(path: string): Observable<boolean> {
return this.#messages.asObservablePart((msgs) =>
// Find messages that starts with the given path, if the path is longer then require a dot or [ as the next character. Using a more performant way than Regex:
msgs.some(
(x) =>
x.path.indexOf(path) === 0 &&
(x.path.length === path.length || x.path[path.length] === '.' || x.path[path.length] === '['),
),
);
}
getHasMessagesOfPathAndDescendant(path: string): boolean {
return this.#messages
.getValue()
.some(
(x) =>
x.path.indexOf(path) === 0 &&
(x.path.length === path.length || x.path[path.length] === '.' || x.path[path.length] === '['),
);
}
addMessage(type: UmbValidationMessageType, path: string, message: string): void {
this.#messages.appendOne({ type, key: UmbId.new(), path, message });
}
addMessages(type: UmbValidationMessageType, path: string, messages: Array<string>): void {
this.#messages.append(messages.map((message) => ({ type, key: UmbId.new(), path, message })));
}
/*
removeMessage(message: UmbValidationDataPath): void {
this.#messages.removeOne(message.key);
}*/
removeMessageByKey(key: string): void {
this.#messages.removeOne(key);
}
removeMessagesByTypeAndPath(type: UmbValidationMessageType, path: string): void {
this.#messages.filter((x) => !(x.type === type && x.path === path));
}
removeMessagesByType(type: UmbValidationMessageType): void {
this.#messages.filter((x) => x.type !== type);
}
reset(): void {
this.#messages.setValue([]);
}
destroy(): void {
this.#messages.destroy();
}
}

View File

@@ -1,4 +1,5 @@
import type { UmbValidator } from '../interfaces/validator.interface.js';
import { UmbValidationMessagesManager } from './validation-messages.manager.js';
import { UMB_VALIDATION_CONTEXT } from './validation.context-token.js';
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
@@ -7,7 +8,8 @@ export class UmbValidationContext extends UmbContextBase<UmbValidationContext> i
#validators: Array<UmbValidator> = [];
#validationMode: boolean = false;
#isValid: boolean = false;
//#preventFail: boolean = false;
public readonly messages = new UmbValidationMessagesManager();
constructor(host: UmbControllerHost) {
super(host, UMB_VALIDATION_CONTEXT);
@@ -17,53 +19,70 @@ export class UmbValidationContext extends UmbContextBase<UmbValidationContext> i
return this.#isValid;
}
/*
preventFail(): void {
this.#preventFail = true;
}
allowFail(): void {
this.#preventFail = false;
}
*/
addValidator(validator: UmbValidator) {
addValidator(validator: UmbValidator): void {
if (this.#validators.includes(validator)) return;
this.#validators.push(validator);
//validator.addEventListener('change', this.#runValidate);
//validator.addEventListener('change', this.#onValidatorChange);
if (this.#validationMode) {
this.validate();
}
}
removeValidator(validator: UmbValidator) {
removeValidator(validator: UmbValidator): void {
const index = this.#validators.indexOf(validator);
if (index !== -1) {
// Remove the validator:
this.#validators.splice(index, 1);
//validator.removeEventListener('change', this.#runValidate);
// If we are in validation mode then we should re-validate to focus next invalid element:
if (this.#validationMode) {
this.validate();
}
}
}
#runValidate = this.validate.bind(this);
/*#onValidatorChange = (e: Event) => {
const target = e.target as unknown as UmbValidator | undefined;
if (!target) {
console.error('Validator did not exist.');
return;
}
const dataPath = target.dataPath;
if (!dataPath) {
console.error('Validator did not exist or did not provide a data-path.');
return;
}
if (target.isValid) {
this.messages.removeMessagesByTypeAndPath('client', dataPath);
} else {
this.messages.addMessages('client', dataPath, target.getMessages());
}
};*/
/**
*
* @returns succeed {Promise<boolean>} - Returns a promise that resolves to true if the validator succeeded, this depends on the validators and wether forceSucceed is set.
*/
async validate(): Promise<boolean> {
async validate(): Promise<void> {
// TODO: clear server messages here?, well maybe only if we know we will get new server messages? Do the server messages hook into the system like another validator?
this.#validationMode = true;
const results = await Promise.all(this.#validators.map((v) => v.validate()));
const isValid = results.every((r) => r);
const resultsStatus = await Promise.all(this.#validators.map((v) => v.validate())).then(
() => Promise.resolve(true),
() => Promise.reject(false),
);
// If we have any messages then we are not valid, otherwise lets check the validation results: [NL]
// This enables us to keep client validations though UI is not present anymore — because the client validations got defined as messages. [NL]
const isValid = this.messages.getHasAnyMessages() ? false : resultsStatus;
this.#isValid = isValid;
// Focus first invalid element:
if (!isValid) {
if (isValid === false) {
// Focus first invalid element:
this.focusFirstInvalidElement();
return Promise.reject();
}
//return this.#preventFail ? true : isValid;
return isValid;
return Promise.resolve();
}
focusFirstInvalidElement(): void {
@@ -73,16 +92,12 @@ export class UmbValidationContext extends UmbContextBase<UmbValidationContext> i
}
}
getMessages(): string[] {
return this.#validators.reduce((acc, v) => acc.concat(v.getMessages()), [] as string[]);
}
reset(): void {
this.#validationMode = false;
this.#validators.forEach((v) => v.reset());
}
#destroyValidators() {
#destroyValidators(): void {
if (this.#validators === undefined || this.#validators.length === 0) return;
this.#validators.forEach((validator) => {
validator.destroy();

View File

@@ -0,0 +1,116 @@
import type { UmbValidationMessage } from '../context/validation-messages.manager.js';
import { UMB_VALIDATION_CONTEXT } from '../context/validation.context-token.js';
import type { UmbFormControlMixinInterface } from '../mixins/form-control.mixin.js';
import { jsonStringComparison } from '@umbraco-cms/backoffice/observable-api';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbControllerAlias, UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
const ctrlSymbol = Symbol();
const observeSymbol = Symbol();
export class UmbBindValidationMessageToFormControl extends UmbControllerBase {
readonly controllerAlias: UmbControllerAlias;
#context?: typeof UMB_VALIDATION_CONTEXT.TYPE;
#control: UmbFormControlMixinInterface<unknown, unknown>;
#controlValidator?: ReturnType<UmbFormControlMixinInterface<unknown, unknown>['addValidator']>;
#messages: Array<UmbValidationMessage> = [];
#isValid = false;
#value?: unknown;
set value(value: unknown) {
if (this.#isValid) {
// If valid lets just parse it on [NL]
this.#value = value;
} else {
// If not valid lets see if we should remove server validation [NL]
if (!jsonStringComparison(this.#value, value)) {
this.#value = value;
// Only remove server validations from validation context [NL]
this.#messages.forEach((message) => {
if (message.type === 'server') {
this.#context?.messages.removeMessageByKey(message.key);
}
});
}
}
}
constructor(host: UmbControllerHost, formControl: UmbFormControlMixinInterface<unknown, unknown>, dataPath: string) {
super(host, ctrlSymbol);
this.#control = formControl;
this.consumeContext(UMB_VALIDATION_CONTEXT, (context) => {
this.#context = context;
this.observe(
context.messages.messagesOfTypeAndPath('server', dataPath),
(messages) => {
this.#messages = messages;
this.#isValid = messages.length === 0;
if (!this.#isValid) {
this.#setup();
} else {
this.#demolish();
}
},
observeSymbol,
);
});
}
#setup() {
if (!this.#controlValidator) {
this.#controlValidator = this.#control.addValidator(
'customError',
() => this.#messages.map((x) => x.message).join(', '),
() => !this.#isValid,
);
//this.#control.addEventListener('change', this.#onControlChange);
// Legacy event, used by some controls:
//this.#control.addEventListener('property-value-change', this.#onControlChange);
}
this.#control.checkValidity();
}
#demolish() {
if (!this.#control || !this.#controlValidator) return;
this.#control.removeValidator(this.#controlValidator);
//this.#control.removeEventListener('change', this.#onControlChange);
// Legacy event, used by some controls:
//this.#control.removeEventListener('property-value-change', this.#onControlChange);
this.#controlValidator = undefined;
this.#control.checkValidity();
}
validate(): Promise<void> {
//this.#isValid = this.#control.checkValidity();
return this.#isValid ? Promise.resolve() : Promise.reject();
}
/**
* Resets the validation state of this validator.
*/
reset(): void {
this.#isValid = false;
this.#control.pristine = true; // Make sure the control goes back into not-validation-mode/'untouched'/pristine state.
}
/*getMessages(): string[] {
return [this.#control.validationMessage];
}*/
focusFirstInvalidElement(): void {
this.#control.focusFirstInvalidElement();
}
destroy(): void {
this.#context = undefined;
// Reset control setup.
this.#demolish();
this.#control = undefined as any;
super.destroy();
}
}

View File

@@ -7,21 +7,29 @@ import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbControllerAlias, UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
export class UmbFormControlValidator extends UmbControllerBase implements UmbValidator {
// The path to the data that this validator is validating. Public so the ValidationContext can access it.
readonly #dataPath?: string;
#context?: typeof UMB_VALIDATION_CONTEXT.TYPE;
#control: UmbFormControlMixinInterface<unknown, unknown>;
readonly controllerAlias: UmbControllerAlias;
#isValid = false;
#isValid = true;
constructor(host: UmbControllerHost, formControl: UmbFormControlMixinInterface<unknown, unknown>) {
constructor(host: UmbControllerHost, formControl: UmbFormControlMixinInterface<unknown, unknown>, dataPath?: string) {
super(host);
this.#dataPath = dataPath;
this.consumeContext(UMB_VALIDATION_CONTEXT, (context) => {
if (this.#context) {
this.#context.removeValidator(this);
}
this.#context = context;
context.addValidator(this);
// If we have a message already, then un-pristine the control:
if (dataPath && context.messages.getHasMessagesOfPathAndDescendant(dataPath)) {
formControl.pristine = false;
}
});
this.#control = formControl;
this.#control.addEventListener(UmbValidationInvalidEvent.TYPE, this.#setInvalid);
@@ -34,15 +42,23 @@ export class UmbFormControlValidator extends UmbControllerBase implements UmbVal
#setIsValid(newVal: boolean) {
if (this.#isValid === newVal) return;
this.#isValid = newVal;
this.dispatchEvent(new CustomEvent('change'));
if (this.#dataPath) {
if (newVal) {
this.#context?.messages.removeMessagesByTypeAndPath('client', this.#dataPath);
} else {
this.#context?.messages.addMessages('client', this.#dataPath, [this.#control.validationMessage]);
}
}
//this.dispatchEvent(new CustomEvent('change')); // To let the ValidationContext know that the validation state has changed.
}
#setInvalid = this.#setIsValid.bind(this, false);
#setValid = this.#setIsValid.bind(this, true);
validate(): Promise<boolean> {
validate(): Promise<void> {
this.#isValid = this.#control.checkValidity();
return Promise.resolve(this.#isValid);
return this.#isValid ? Promise.resolve() : Promise.reject();
}
/**
@@ -53,19 +69,33 @@ export class UmbFormControlValidator extends UmbControllerBase implements UmbVal
this.#control.pristine = true; // Make sure the control goes back into not-validation-mode/'untouched'/pristine state.
}
getMessages(): string[] {
/*getMessages(): string[] {
return [this.#control.validationMessage];
}
}*/
focusFirstInvalidElement(): void {
this.#control.focusFirstInvalidElement();
}
destroy(): void {
hostConnected(): void {
super.hostConnected();
if (this.#context) {
this.#context.addValidator(this);
}
}
hostDisconnected(): void {
super.hostDisconnected();
if (this.#context) {
this.#context.removeValidator(this);
// Remove any messages that this validator has added:
if (this.#dataPath) {
//this.#context.messages.removeMessagesByTypeAndPath('client', this.#dataPath);
}
this.#context = undefined;
}
}
destroy(): void {
if (this.#control) {
this.#control.removeEventListener(UmbValidationInvalidEvent.TYPE, this.#setInvalid);
this.#control.removeEventListener(UmbValidationValidEvent.TYPE, this.#setValid);

View File

@@ -0,0 +1,3 @@
export * from './bind-validation-message-to-form-control.controller.js';
export * from './observe-validation-state.controller.js';
export * from './form-control-validator.controller.js';

View File

@@ -0,0 +1,17 @@
import { UMB_VALIDATION_CONTEXT } from '../context/validation.context-token.js';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
const CtrlSymbol = Symbol();
const ObserveSymbol = Symbol();
export class UmbObserveValidationStateController extends UmbControllerBase {
constructor(host: UmbControllerHost, dataPath: string | undefined, callback: (invalid: boolean) => void) {
super(host, CtrlSymbol);
if (dataPath) {
this.consumeContext(UMB_VALIDATION_CONTEXT, (context) => {
this.observe(context.messages.hasMessagesOfPathAndDescendant(dataPath), callback, ObserveSymbol);
});
}
}
}

View File

@@ -1,5 +1,7 @@
export * from './context/index.js';
export * from './controllers/index.js';
export * from './events/index.js';
export * from './interfaces/index.js';
export * from './mixins/index.js';
export * from './validators/index.js';
export * from './translators/index.js';
export * from './utils/index.js';

View File

@@ -1,8 +1,13 @@
export interface UmbValidator {
export interface UmbValidator extends EventTarget {
/**
* The path to the data that the validator is validating.
*/
//readonly dataPath?: string;
/**
* Validate the form, will return a promise that resolves to true if what the Validator represents is valid.
*/
validate(): Promise<boolean>;
validate(): Promise<void>;
/**
* Reset the validator to its initial state.
@@ -21,7 +26,7 @@ export interface UmbValidator {
focusFirstInvalidElement(): void;
//getMessage(): string;
getMessages(): string[]; // Should we enable bringing multiple messages?
//getMessages(): string[]; // Should we enable bringing multiple messages?
destroy(): void;
}

View File

@@ -3,7 +3,10 @@ import { UmbValidationValidEvent } from '../events/validation-valid.event.js';
import { property, type LitElement } from '@umbraco-cms/backoffice/external/lit';
import type { HTMLElementConstructor } from '@umbraco-cms/backoffice/extension-api';
type UmbNativeFormControlElement = Pick<HTMLInputElement, 'validity' | 'checkValidity' | 'validationMessage'> &
type UmbNativeFormControlElement = Pick<
HTMLObjectElement,
'validity' | 'checkValidity' | 'validationMessage' | 'setCustomValidity'
> &
HTMLElement; // Eventually use a specific interface or list multiple options like appending these types: ... | HTMLTextAreaElement | HTMLSelectElement
/* FlagTypes type options originate from:
@@ -23,16 +26,18 @@ type FlagTypes =
| 'badInput'
| 'valid';
// Acceptable as an internal interface/type, BUT if exposed externally this should be turned into a public class in a separate file.
interface Validator {
// Acceptable as an internal interface/type, BUT if exposed externally this should be turned into a public interface in a separate file.
interface UmbFormControlValidationConfig {
flagKey: FlagTypes;
getMessageMethod: () => string;
checkMethod: () => boolean;
}
export interface UmbFormControlMixinInterface<ValueType, DefaultValueType> extends HTMLElement {
addValidator: (flagKey: FlagTypes, getMessageMethod: () => string, checkMethod: () => boolean) => void;
removeValidator: (obj: UmbFormControlValidationConfig) => void;
//static formAssociated: boolean;
getFormElement(): HTMLElement | undefined | null; // allows for null as it makes it simpler to just implement a querySelector as that might return null. [NL]
//protected getFormElement(): HTMLElement | undefined | null; // allows for null as it makes it simpler to just implement a querySelector as that might return null. [NL]
focusFirstInvalidElement(): void;
get value(): ValueType | DefaultValueType;
set value(newValue: ValueType | DefaultValueType);
@@ -40,13 +45,9 @@ export interface UmbFormControlMixinInterface<ValueType, DefaultValueType> exten
checkValidity(): boolean;
get validationMessage(): string;
get validity(): ValidityState;
setCustomValidity(error: string): void;
setCustomValidity(error?: string): void;
submit(): void;
pristine: boolean;
required: boolean;
requiredMessage: string;
error: boolean;
errorMessage: string;
}
export declare abstract class UmbFormControlMixinElement<ValueType, DefaultValueType>
@@ -55,11 +56,12 @@ export declare abstract class UmbFormControlMixinElement<ValueType, DefaultValue
{
protected _internals: ElementInternals;
protected _runValidators(): void;
protected addValidator: (flagKey: FlagTypes, getMessageMethod: () => string, checkMethod: () => boolean) => void;
addValidator: (flagKey: FlagTypes, getMessageMethod: () => string, checkMethod: () => boolean) => void;
removeValidator: (obj: UmbFormControlValidationConfig) => void;
protected addFormControlElement(element: UmbNativeFormControlElement): void;
//static formAssociated: boolean;
getFormElement(): HTMLElement | undefined | null;
protected getFormElement(): HTMLElement | undefined | null;
focusFirstInvalidElement(): void;
get value(): ValueType | DefaultValueType;
set value(newValue: ValueType | DefaultValueType);
@@ -67,13 +69,9 @@ export declare abstract class UmbFormControlMixinElement<ValueType, DefaultValue
checkValidity(): boolean;
get validationMessage(): string;
get validity(): ValidityState;
setCustomValidity(error: string): void;
setCustomValidity(error?: string): void;
submit(): void;
pristine: boolean;
required: boolean;
requiredMessage: string;
error: boolean;
errorMessage: string;
}
/**
@@ -85,10 +83,10 @@ export declare abstract class UmbFormControlMixinElement<ValueType, DefaultValue
export const UmbFormControlMixin = <
ValueType = FormDataEntryValue | FormData,
T extends HTMLElementConstructor<LitElement> = HTMLElementConstructor<LitElement>,
DefaultValueType = unknown,
DefaultValueType = undefined,
>(
superClass: T,
defaultValue: DefaultValueType,
defaultValue: DefaultValueType = undefined as DefaultValueType,
) => {
abstract class UmbFormControlMixinClass extends superClass {
/**
@@ -106,7 +104,7 @@ export const UmbFormControlMixin = <
* @attr value
* @default ''
*/
@property({ reflect: false }) // Do not 'reflect' as the attribute is used as fallback.
@property({ reflect: false }) // Do not 'reflect' as the attribute value is used as fallback. [NL]
get value(): ValueType | DefaultValueType {
return this.#value;
}
@@ -123,15 +121,24 @@ export const UmbFormControlMixin = <
* Determines wether the form control has been touched or interacted with, this determines wether the validation-status of this form control should be made visible.
* @type {boolean}
* @attr
* @default false
* @default true
*/
@property({ type: Boolean, reflect: true })
pristine: boolean = true;
public set pristine(value: boolean) {
if (this._pristine !== value) {
this._pristine = value;
this.#dispatchValidationState();
}
}
public get pristine(): boolean {
return this._pristine;
}
private _pristine: boolean = true;
#value: ValueType | DefaultValueType = defaultValue;
protected _internals: ElementInternals;
#form: HTMLFormElement | null = null;
#validators: Validator[] = [];
#validators: UmbFormControlValidationConfig[] = [];
#formCtrlElements: UmbNativeFormControlElement[] = [];
constructor(...args: any[]) {
@@ -150,7 +157,7 @@ export const UmbFormControlMixin = <
* @method getFormElement
* @returns {HTMLElement | undefined | null}
*/
getFormElement(): HTMLElement | undefined | null {
protected getFormElement(): HTMLElement | undefined | null {
return this.#formCtrlElements.find((el) => el.validity.valid === false);
}
@@ -183,7 +190,7 @@ export const UmbFormControlMixin = <
}
/**
* Add validator, to validate this Form Control.
* Add validation, to validate this Form Control.
* See https://developer.mozilla.org/en-US/docs/Web/API/ValidityState for available Validator FlagTypes.
*
* @example
@@ -197,17 +204,26 @@ export const UmbFormControlMixin = <
* @param {method} getMessageMethod method to retrieve relevant message. Is executed every time the validator is re-executed.
* @param {method} checkMethod method to determine if this validator should invalidate this form control. Return true if this should prevent submission.
*/
protected addValidator(flagKey: FlagTypes, getMessageMethod: () => string, checkMethod: () => boolean): Validator {
const obj = {
addValidator(
flagKey: FlagTypes,
getMessageMethod: () => string,
checkMethod: () => boolean,
): UmbFormControlValidationConfig {
const validator = {
flagKey: flagKey,
getMessageMethod: getMessageMethod,
checkMethod: checkMethod,
};
this.#validators.push(obj);
return obj;
this.#validators.push(validator);
return validator;
}
protected removeValidator(validator: Validator) {
/**
* Remove validation from this form control.
* @method removeValidator
* @param {UmbFormControlValidationConfig} validator - The specific validation configuration to remove.
*/
removeValidator(validator: UmbFormControlValidationConfig) {
const index = this.#validators.indexOf(validator);
if (index !== -1) {
this.#validators.splice(index, 1);
@@ -228,12 +244,37 @@ export const UmbFormControlMixin = <
this._runValidators();
});
// If we are in validationMode/'touched'/not-pristine then we need to validate this newly added control. [NL]
if (this.pristine === false) {
// I thin we could just execute validators for the new control, but now lets just run al of it again. [NL]
if (this._pristine === false) {
element.checkValidity();
// I think we could just execute validators for the new control, but now lets just run al of it again. [NL]
this._runValidators();
}
}
private _customValidityObject?: UmbFormControlValidationConfig;
/**
* @method setCustomValidity
* @description Set custom validity state, set to empty string to remove the custom message.
* @param message {string} - The message to be shown
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLObjectElement/setCustomValidity|HTMLObjectElement:setCustomValidity}
*/
protected setCustomValidity(message: string | null) {
if (this._customValidityObject) {
this.removeValidator(this._customValidityObject);
}
if (message != null && message !== '') {
this._customValidityObject = this.addValidator(
'customError',
(): string => message,
() => true,
);
}
this._runValidators();
}
/**
* @method _runValidators
* @description Run all validators and set the validityState of this form control.
@@ -242,8 +283,9 @@ export const UmbFormControlMixin = <
* Such are mainly properties that are not declared as a Lit state and or Lit property.
*/
protected _runValidators() {
//this._validityState = new UmbValidityState();
this.#validity = {};
const messages: Set<string> = new Set();
let innerFormControlEl: UmbNativeFormControlElement | undefined = undefined;
// Loop through inner native form controls to adapt their validityState. [NL]
this.#formCtrlElements.forEach((formCtrlEl) => {
@@ -251,7 +293,8 @@ export const UmbFormControlMixin = <
for (key in formCtrlEl.validity) {
if (key !== 'valid' && formCtrlEl.validity[key]) {
this.#validity[key] = true;
this._internals.setValidity(this.#validity, formCtrlEl.validationMessage, formCtrlEl);
messages.add(formCtrlEl.validationMessage);
innerFormControlEl ??= formCtrlEl;
}
}
});
@@ -260,7 +303,7 @@ export const UmbFormControlMixin = <
this.#validators.forEach((validator) => {
if (validator.checkMethod()) {
this.#validity[validator.flagKey] = true;
this._internals.setValidity(this.#validity, validator.getMessageMethod(), this.getFormElement() ?? undefined);
messages.add(validator.getMessageMethod());
}
});
@@ -269,11 +312,24 @@ export const UmbFormControlMixin = <
// https://developer.mozilla.org/en-US/docs/Web/API/ValidityState#valid
this.#validity.valid = !hasError;
if (hasError) {
this.dispatchEvent(new UmbValidationInvalidEvent());
} else {
this._internals.setValidity({});
// Transfer the new validityState to the ElementInternals. [NL]
this._internals.setValidity(
this.#validity,
// Turn messages into an array and join them with a comma. [NL]:
[...messages].join(', '),
innerFormControlEl ?? this.getFormElement() ?? undefined,
);
this.#dispatchValidationState();
}
#dispatchValidationState() {
// Do not fire validation events unless we are not pristine/'untouched'/not-in-validation-mode. [NL]
if (this._pristine === true) return;
if (this.#validity.valid) {
this.dispatchEvent(new UmbValidationValidEvent());
} else {
this.dispatchEvent(new UmbValidationInvalidEvent());
}
}
@@ -311,6 +367,7 @@ export const UmbFormControlMixin = <
public checkValidity() {
this.pristine = false;
this._runValidators();
for (const key in this.#formCtrlElements) {
if (this.#formCtrlElements[key].checkValidity() === false) {

View File

@@ -1,18 +0,0 @@
/*
NOt used currently [NL]
type Writeable<T> = { -readonly [P in keyof T]: T[P] };
export class UmbValidityState implements Writeable<ValidityState> {
badInput: boolean = true;
customError: boolean = true;
patternMismatch: boolean = true;
rangeOverflow: boolean = true;
rangeUnderflow: boolean = true;
stepMismatch: boolean = true;
tooLong: boolean = true;
tooShort: boolean = true;
typeMismatch: boolean = true;
valid: boolean = true;
valueMissing: boolean = true;
}
*/

View File

@@ -0,0 +1,2 @@
export type * from './validation-message-translator.interface.js';
export * from './variant-values-validation-message-translator.controller.js';

View File

@@ -0,0 +1,4 @@
export interface UmbValidationMessageTranslator {
match(message: string): boolean;
translate(message: string): string;
}

View File

@@ -0,0 +1,40 @@
import type { UmbServerModelValidationContext } from '../context/server-model-validation.context.js';
import { UmbDataPathValueFilter } from '../utils/data-path-value-filter.function.js';
import type { UmbValidationMessageTranslator } from './validation-message-translator.interface.js';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
export class UmbVariantValuesValidationMessageTranslator
extends UmbControllerBase
implements UmbValidationMessageTranslator
{
//
#context: UmbServerModelValidationContext;
constructor(host: UmbControllerHost, context: UmbServerModelValidationContext) {
super(host);
context.addTranslator(this);
this.#context = context;
}
match(message: string): boolean {
//return message.startsWith('values[');
// regex match, which starts with "$.values[" and then a number and then continues:
return message.indexOf('$.values[') === 0;
}
translate(path: string): string {
// retrieve the number from the message values index:
const index = parseInt(path.substring(9, path.indexOf(']')));
//
const data = this.#context.getData();
const specificValue = data.values[index];
// replace the values[ number ] with JSON-Path filter values[@.(...)], continues by the rest of the path:
return '$.values[' + UmbDataPathValueFilter(specificValue) + path.substring(path.indexOf(']'));
}
destroy(): void {
super.destroy();
this.#context.removeTranslator(this);
}
}

View File

@@ -0,0 +1,19 @@
import type { UmbVariantableValueModel } from '@umbraco-cms/backoffice/models';
/**
* write a JSON-Path filter similar to `?(@.alias = 'myAlias' && @.culture == 'en-us' && @.segment == 'mySegment')`
* where culture and segment are optional
* @param value
* @returns
*/
export function UmbDataPathValueFilter(value: Omit<UmbVariantableValueModel, 'value'>): string {
// write a array of strings for each property, where alias must be present and culture and segment are optional
const filters: Array<string> = [`@.alias = '${value.alias}'`];
if (value.culture) {
filters.push(`@.culture == '${value.culture}'`);
}
if (value.segment) {
filters.push(`@.segment == '${value.segment}'`);
}
return `?(${filters.join(' && ')})`;
}

View File

@@ -0,0 +1 @@
export * from './data-path-value-filter.function.js';

View File

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

View File

@@ -31,7 +31,7 @@ export class UmbSubmitWorkspaceAction extends UmbWorkspaceActionBase<UmbSubmitta
async execute() {
const workspaceContext = await this.getContext(UMB_SUBMITTABLE_WORKSPACE_CONTEXT);
return workspaceContext.requestSubmit();
return await workspaceContext.requestSubmit();
}
}

View File

@@ -18,7 +18,7 @@ export abstract class UmbSubmittableWorkspaceContextBase<WorkspaceDataModelType>
// TODO: We could make a base type for workspace modal data, and use this here: As well as a base for the result, to make sure we always include the unique (instead of the object type)
public readonly modalContext?: UmbModalContext<{ preset: object }>;
readonly #validation = new UmbValidationContext(this);
public readonly validation = new UmbValidationContext(this);
#submitPromise: Promise<void> | undefined;
#submitResolve: (() => void) | undefined;
@@ -48,16 +48,8 @@ export abstract class UmbSubmittableWorkspaceContextBase<WorkspaceDataModelType>
});
}
/*
protected passValidation() {
this.#validation.preventFail();
}
protected failValidation() {
this.#validation.allowFail();
}
*/
protected resetState() {
this.validation.reset();
this.#isNew.setValue(undefined);
}
@@ -70,10 +62,13 @@ export abstract class UmbSubmittableWorkspaceContextBase<WorkspaceDataModelType>
}
async requestSubmit(): Promise<void> {
return this.validateAndSubmit((valid) => (valid ? this.submit() : this.invalidSubmit()));
return this.validateAndSubmit(
() => this.submit(),
() => this.invalidSubmit(),
);
}
protected async validateAndSubmit(callback: (valid: boolean) => Promise<boolean | undefined>): Promise<void> {
protected async validateAndSubmit(onValid: () => Promise<void>, onInvalid: () => Promise<void>): Promise<void> {
if (this.#submitPromise) {
return this.#submitPromise;
}
@@ -81,49 +76,55 @@ export abstract class UmbSubmittableWorkspaceContextBase<WorkspaceDataModelType>
this.#submitResolve = resolve;
this.#submitReject = reject;
});
this.#validation.validate().then(async (valid: boolean) => {
if ((await callback(valid)) === true) {
this.#submitComplete();
} else {
this.#submitFailed();
}
});
this.validation.validate().then(
async () => {
onValid().then(this.#completeSubmit, this.#rejectSubmit);
},
async () => {
onInvalid().then(this.#resolveSubmit, this.#rejectSubmit);
},
);
return this.#submitPromise;
}
#submitFailed() {
#rejectSubmit = () => {
if (this.#submitPromise) {
this.#submitReject?.();
this.#submitPromise = undefined;
this.#submitResolve = undefined;
this.#submitReject = undefined;
}
}
};
#submitComplete() {
#resolveSubmit = () => {
// Resolve the submit promise:
this.#submitResolve?.();
this.#submitPromise = undefined;
this.#submitResolve = undefined;
this.#submitReject = undefined;
// Calling reset on the validation context here. [NL]
this.#validation.reset();
// If we do not want to close a modal when saving something with errors, then move this part down to #completeSubmit method. [NL]
if (this.modalContext) {
this.modalContext?.setValue(this.getData());
this.modalContext?.submit();
}
}
};
#completeSubmit = () => {
this.#resolveSubmit();
// Calling reset on the validation context here. [NL]
this.validation.reset();
};
//abstract getIsDirty(): Promise<boolean>;
abstract getUnique(): string | undefined;
abstract getEntityType(): string;
abstract getData(): WorkspaceDataModelType | undefined;
protected abstract submit(): Promise<boolean | undefined>;
protected invalidSubmit(): Promise<boolean | undefined> {
return Promise.resolve(false);
protected abstract submit(): Promise<void>;
protected invalidSubmit(): Promise<void> {
return Promise.reject();
}
}

View File

@@ -1,5 +1,6 @@
export * from './collection-workspace.context-token.js';
export * from './entity-workspace.context-token.js';
export * from './property-structure-workspace.context-token.js';
export * from './publishable-workspace.context-token.js';
export * from './routable-workspace.context-token.js';
export * from './submittable-workspace.context-token.js';

View File

@@ -1,7 +1,12 @@
import type { UmbEntityWorkspaceContext } from './entity-workspace-context.interface.js';
import type { UmbContentTypeModel, UmbContentTypeStructureManager } from '@umbraco-cms/backoffice/content-type';
import type { Observable } from '@umbraco-cms/backoffice/external/rxjs';
import type { UmbVariantPropertyValueModel } from '@umbraco-cms/backoffice/variant';
export interface UmbPropertyStructureWorkspaceContext extends UmbEntityWorkspaceContext {
export interface UmbPropertyStructureWorkspaceContext<
ContentTypeModel extends UmbContentTypeModel = UmbContentTypeModel,
> extends UmbEntityWorkspaceContext {
structure: UmbContentTypeStructureManager<ContentTypeModel>;
// TODO: propertyStructureById is not used by anything in the codebase, should we remove it? [NL]
propertyStructureById(id: string): Promise<Observable<UmbVariantPropertyValueModel | undefined>>;
}

View File

@@ -3,6 +3,7 @@ import { html, customElement, state, ifDefined, repeat } from '@umbraco-cms/back
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { PropertyEditorSettingsProperty } from '@umbraco-cms/backoffice/extension-registry';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbDataPathValueFilter } from '@umbraco-cms/backoffice/validation';
/**
* @element umb-property-editor-config
@@ -44,13 +45,15 @@ export class UmbPropertyEditorConfigElement extends UmbLitElement {
this._properties,
(property) => property.alias,
(property) =>
// TODO: Make a helper method to generate data-path entry for a property.
html`<umb-property
label="${property.label}"
description="${ifDefined(property.description)}"
alias="${property.alias}"
property-editor-ui-alias="${property.propertyEditorUiAlias}"
.dataPath="$.values[${UmbDataPathValueFilter(property)}].value"
label=${property.label}
description=${ifDefined(property.description)}
alias=${property.alias}
property-editor-ui-alias=${property.propertyEditorUiAlias}
.config=${property.config}></umb-property>`,
)
)
: html`<div>No configuration</div>`;
}

View File

@@ -318,13 +318,20 @@ export class UmbDataTypeWorkspaceContext
}
async submit() {
if (!this.#currentData.value) return;
if (!this.#currentData.value.unique) return;
if (!this.#currentData.value) {
throw new Error('Data is not set');
}
if (!this.#currentData.value.unique) {
throw new Error('Unique is not set');
}
if (this.getIsNew()) {
const parent = this.#parent.getValue();
if (!parent) throw new Error('Parent is not set');
await this.repository.create(this.#currentData.value, parent.unique);
const { error, data } = await this.repository.create(this.#currentData.value, parent.unique);
if (error || !data) {
throw error?.message ?? 'Repository did not return data after create.';
}
// TODO: this might not be the right place to alert the tree, but it works for now
const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
@@ -333,8 +340,12 @@ export class UmbDataTypeWorkspaceContext
unique: parent.unique,
});
eventContext.dispatchEvent(event);
this.setIsNew(false);
} else {
await this.repository.save(this.#currentData.value);
const { error, data } = await this.repository.save(this.#currentData.value);
if (error || !data) {
throw error?.message ?? 'Repository did not return data after create.';
}
const actionEventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
const event = new UmbRequestReloadStructureForEntityEvent({
@@ -344,9 +355,6 @@ export class UmbDataTypeWorkspaceContext
actionEventContext.dispatchEvent(event);
}
this.setIsNew(false);
return true;
}
async delete(unique: string) {

View File

@@ -124,15 +124,21 @@ export class UmbDictionaryWorkspaceContext
}
async submit() {
if (!this.#data.value) return;
if (!this.#data.value.unique) return;
if (!this.#data.value) {
throw new Error('No data to submit.');
}
if (!this.#data.value.unique) {
throw new Error('No unique value to submit.');
}
if (this.getIsNew()) {
const parent = this.#parent.getValue();
if (!parent) throw new Error('Parent is not set');
if (!parent) {
throw new Error('Parent is not set');
}
const { error } = await this.detailRepository.create(this.#data.value, parent.unique);
if (error) {
return;
throw new Error(error.message);
}
// TODO: this might not be the right place to alert the tree, but it works for now
@@ -155,12 +161,6 @@ export class UmbDictionaryWorkspaceContext
actionEventContext.dispatchEvent(event);
}
const data = this.getData();
if (!data) return;
this.setIsNew(false);
return true;
}
public destroy(): void {

View File

@@ -414,8 +414,6 @@ export class UmbDocumentBlueprintWorkspaceContext
const data = this.getData();
if (!data) throw new Error('Data is missing');
await this.#createOrSave();
this.setIsNew(false);
return true;
}
async delete() {

View File

@@ -22,9 +22,9 @@ const workspace: ManifestWorkspace = {
const workspaceViews: Array<ManifestWorkspaceView> = [
{
type: 'workspaceView',
kind: 'contentEditor',
alias: 'Umb.WorkspaceView.DocumentBlueprint.Edit',
name: 'Document Blueprint Workspace Edit View',
element: () => import('./views/edit/document-blueprint-workspace-view-edit.element.js'),
weight: 200,
meta: {
label: '#general_content',
@@ -50,7 +50,7 @@ const workspaceActions: Array<ManifestWorkspaceActions> = [
api: UmbSaveWorkspaceAction,
meta: {
label: 'Save',
look: 'secondary',
look: 'primary',
color: 'positive',
},
conditions: [

View File

@@ -220,7 +220,9 @@ export class UmbDocumentTypeWorkspaceContext
*/
async submit() {
const data = this.getData();
if (data === undefined) throw new Error('Cannot save, no data');
if (data === undefined) {
throw new Error('Cannot save, no data');
}
if (this.getIsNew()) {
const parent = this.#parent.getValue();
@@ -248,9 +250,6 @@ export class UmbDocumentTypeWorkspaceContext
actionEventContext.dispatchEvent(event);
}
this.setIsNew(false);
return true;
}
public destroy(): void {

View File

@@ -3,7 +3,6 @@ import { UmbDocumentServerDataSource } from './document-detail.server.data-sourc
import { UMB_DOCUMENT_DETAIL_STORE_CONTEXT } from './document-detail.store.js';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbDetailRepositoryBase } from '@umbraco-cms/backoffice/repository';
export class UmbDocumentDetailRepository extends UmbDetailRepositoryBase<UmbDocumentDetailModel> {
constructor(host: UmbControllerHost) {
super(host, UmbDocumentServerDataSource, UMB_DOCUMENT_DETAIL_STORE_CONTEXT);

View File

@@ -128,7 +128,7 @@ export class UmbDocumentServerDataSource implements UmbDetailDataSource<UmbDocum
/**
* Inserts a new Document on the server
* @param {UmbDocumentDetailModel} model
* @param {UmbDocumentDetailModel} model - Document Model
* @return {*}
* @memberof UmbDocumentServerDataSource
*/
@@ -162,7 +162,7 @@ export class UmbDocumentServerDataSource implements UmbDetailDataSource<UmbDocum
/**
* Updates a Document on the server
* @param {UmbDocumentDetailModel} Document
* @param {UmbDocumentDetailModel} model - Document Model
* @return {*}
* @memberof UmbDocumentServerDataSource
*/

View File

@@ -0,0 +1,46 @@
import type { UmbDocumentDetailModel } from '../../types.js';
import { UmbDocumentValidationServerDataSource } from './document-validation.server.data-source.js';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository';
type DetailModelType = UmbDocumentDetailModel;
export class UmbDocumentValidationRepository extends UmbRepositoryBase {
#validationDataSource: UmbDocumentValidationServerDataSource;
constructor(host: UmbControllerHost) {
super(host);
this.#validationDataSource = new UmbDocumentValidationServerDataSource(this);
}
/**
* Returns a promise with an observable of the detail for the given unique
* @param {DetailModelType} model
* @param {string | null} [parentUnique=null]
* @return {*}
* @memberof UmbDetailRepositoryBase
*/
async validateCreate(model: DetailModelType, parentUnique: string | null) {
if (!model) throw new Error('Data is missing');
const { data, error } = await this.#validationDataSource.validateCreate(model, parentUnique);
return { data, error };
}
/**
* Saves the given data
* @param {DetailModelType} model
* @return {*}
* @memberof UmbDetailRepositoryBase
*/
async validateSave(model: DetailModelType) {
if (!model) throw new Error('Data is missing');
if (!model.unique) throw new Error('Unique is missing');
const { data, error } = await this.#validationDataSource.validateUpdate(model);
return { data, error };
}
}

View File

@@ -0,0 +1,92 @@
import type { UmbDocumentDetailModel } from '../../types.js';
import {
type CreateDocumentRequestModel,
DocumentService,
type UpdateDocumentRequestModel,
} from '@umbraco-cms/backoffice/external/backend-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { tryExecute } from '@umbraco-cms/backoffice/resources';
/**
* A server data source for Document Validation
* @export
* @class UmbDocumentPublishingServerDataSource
* @implements {DocumentTreeDataSource}
*/
export class UmbDocumentValidationServerDataSource {
//#host: UmbControllerHost;
/**
* Creates an instance of UmbDocumentPublishingServerDataSource.
* @param {UmbControllerHost} host
* @memberof UmbDocumentPublishingServerDataSource
*/
constructor(host: UmbControllerHost) {
//this.#host = host;
}
/**
* Validate a new Document on the server
* @param {UmbDocumentDetailModel} model - Document Model
* @return {*}
*/
async validateCreate(model: UmbDocumentDetailModel, parentUnique: string | null = null) {
if (!model) throw new Error('Document is missing');
if (!model.unique) throw new Error('Document unique is missing');
// TODO: make data mapper to prevent errors
const requestBody: CreateDocumentRequestModel = {
id: model.unique,
parent: parentUnique ? { id: parentUnique } : null,
documentType: { id: model.documentType.unique },
template: model.template ? { id: model.template.unique } : null,
values: model.values,
variants: model.variants,
};
// Maybe use: tryExecuteAndNotify
const { data, error } = await tryExecute(
//this.#host,
DocumentService.postDocumentValidate({
requestBody,
}),
);
if (data) {
return { data };
}
return { error };
}
/**
* Validate a existing Document
* @param {UmbDocumentDetailModel} model - Document Model
* @return {*}
*/
async validateUpdate(model: UmbDocumentDetailModel) {
if (!model.unique) throw new Error('Unique is missing');
// TODO: make data mapper to prevent errors
const requestBody: UpdateDocumentRequestModel = {
template: model.template ? { id: model.template.unique } : null,
values: model.values,
variants: model.variants,
};
// Maybe use: tryExecuteAndNotify
const { data, error } = await tryExecute(
//this.#host,
DocumentService.putDocumentByIdValidate({
id: model.unique,
requestBody,
}),
);
if (!error) {
return { data };
}
return { error };
}
}

View File

@@ -0,0 +1,2 @@
export { UmbDocumentValidationRepository } from './document-validation.repository.js';
export { UMB_DOCUMENT_VALIDATION_REPOSITORY_ALIAS } from './manifests.js';

View File

@@ -0,0 +1,13 @@
import { UmbDocumentValidationRepository } from './document-validation.repository.js';
import type { ManifestRepository } from '@umbraco-cms/backoffice/extension-registry';
export const UMB_DOCUMENT_VALIDATION_REPOSITORY_ALIAS = 'Umb.Repository.Document.Validation';
const validationRepository: ManifestRepository = {
type: 'repository',
alias: UMB_DOCUMENT_VALIDATION_REPOSITORY_ALIAS,
name: 'Document Validation Repository',
api: UmbDocumentValidationRepository,
};
export const manifests = [validationRepository];

View File

@@ -1,6 +1,6 @@
import { UMB_ENTITY_CONTEXT } from '@umbraco-cms/backoffice/entity';
import { UMB_CURRENT_USER_CONTEXT } from '../../../user/current-user/current-user.context.js';
import { isDocumentUserPermission } from './utils.js';
import { UMB_ENTITY_CONTEXT } from '@umbraco-cms/backoffice/entity';
import { observeMultiple } from '@umbraco-cms/backoffice/observable-api';
import { UmbConditionBase } from '@umbraco-cms/backoffice/extension-registry';
import type {

View File

@@ -1,4 +1,3 @@
import { UmbEntityContext } from '@umbraco-cms/backoffice/entity';
import { UmbDocumentTypeDetailRepository } from '../../document-types/repository/detail/document-type-detail.repository.js';
import { UmbDocumentPropertyDataContext } from '../property-dataset-context/document-property-dataset-context.js';
import { UMB_DOCUMENT_ENTITY_TYPE } from '../entity.js';
@@ -18,7 +17,9 @@ import {
} from '../modals/index.js';
import { UmbDocumentPublishingRepository } from '../repository/publishing/index.js';
import { UmbUnpublishDocumentEntityAction } from '../entity-actions/unpublish.action.js';
import { UmbDocumentValidationRepository } from '../repository/validation/document-validation.repository.js';
import { UMB_DOCUMENT_WORKSPACE_ALIAS } from './manifests.js';
import { UmbEntityContext } from '@umbraco-cms/backoffice/entity';
import { UMB_INVARIANT_CULTURE, UmbVariantId } from '@umbraco-cms/backoffice/variant';
import { UmbContentTypeStructureManager } from '@umbraco-cms/backoffice/content-type';
import {
@@ -48,6 +49,10 @@ import { UmbRequestReloadTreeItemChildrenEvent } from '@umbraco-cms/backoffice/t
import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/entity-action';
import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
import type { UmbDocumentTypeDetailModel } from '@umbraco-cms/backoffice/document-type';
import {
UmbServerModelValidationContext,
UmbVariantValuesValidationMessageTranslator,
} from '@umbraco-cms/backoffice/validation';
import { UmbDocumentBlueprintDetailRepository } from '@umbraco-cms/backoffice/document-blueprint';
type EntityType = UmbDocumentDetailModel;
@@ -76,6 +81,10 @@ export class UmbDocumentWorkspaceContext
#languages = new UmbArrayState<UmbLanguageDetailModel>([], (x) => x.unique);
public readonly languages = this.#languages.asObservable();
#serverValidation = new UmbServerModelValidationContext(this);
#serverValidationValuesTranslator = new UmbVariantValuesValidationMessageTranslator(this, this.#serverValidation);
#validationRepository?: UmbDocumentValidationRepository;
#blueprintRepository = new UmbDocumentBlueprintDetailRepository(this);
/*#blueprint = new UmbObjectState<UmbDocumentBlueprintDetailModel | undefined>(undefined);
public readonly blueprint = this.#blueprint.asObservable();*/
@@ -242,7 +251,7 @@ export class UmbDocumentWorkspaceContext
/**TODO Explore bug: A way to make blueprintUnique undefined/null when no unique is given, rather than setting it to invariant */
if (blueprintUnique && blueprintUnique.toLowerCase() !== 'invariant') {
const { data } = await this.#blueprintRepository.requestByUnique(blueprintUnique);
console.log(data);
this.#getDataPromise = this.repository.createScaffold({
documentType: data?.documentType,
values: data?.values,
@@ -354,6 +363,15 @@ export class UmbDocumentWorkspaceContext
?.value as PropertyValueType,
);
}
// TODO: Re-evaluate if this is begin used, i wrote this as part of a POC... [NL]
async propertyIndexByAlias(
propertyAlias: string,
variantId?: UmbVariantId,
): Promise<Observable<number | undefined> | undefined> {
return this.#currentData.asObservablePart((data) =>
data?.values?.findIndex((x) => x?.alias === propertyAlias && (variantId ? variantId.compare(x) : true)),
);
}
/**
* Get the current value of the property with the given alias and variantId.
@@ -512,7 +530,7 @@ export class UmbDocumentWorkspaceContext
if (variantIdsToParseForValues.some((x) => x.compare(value))) {
return value;
} else {
// If not we will find the value in the persisted data and use that instead.
// If not, then we will find the value in the persisted data and use that instead.
return persistedData?.values.find(
(x) => x.alias === value.alias && x.culture === value.culture && x.segment === value.segment,
);
@@ -533,22 +551,20 @@ export class UmbDocumentWorkspaceContext
};
}
async #performSaveOrCreate(selectedVariants: Array<UmbVariantId>): Promise<boolean> {
const saveData = this.#buildSaveData(selectedVariants);
async #performSaveOrCreate(saveData: UmbDocumentDetailModel): Promise<void> {
if (this.getIsNew()) {
const parent = this.#parent.getValue();
if (!parent) throw new Error('Parent is not set');
const { data: create, error } = await this.repository.create(saveData, parent.unique);
if (!create || error) {
const { data, error } = await this.repository.create(saveData, parent.unique);
if (!data || error) {
console.error('Error creating document', error);
throw new Error('Error creating document');
}
this.setIsNew(false);
this.#persistedData.setValue(create);
this.#currentData.setValue(create);
this.#persistedData.setValue(data);
this.#currentData.setValue(data);
// TODO: this might not be the right place to alert the tree, but it works for now
const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
@@ -558,25 +574,23 @@ export class UmbDocumentWorkspaceContext
});
eventContext.dispatchEvent(event);
} else {
const { data: save, error } = await this.repository.save(saveData);
if (!save || error) {
const { data, error } = await this.repository.save(saveData);
if (!data || error) {
console.error('Error saving document', error);
throw new Error('Error saving document');
}
this.#persistedData.setValue(save);
this.#currentData.setValue(save);
this.#persistedData.setValue(data);
this.#currentData.setValue(data);
const actionEventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
const event = new UmbRequestReloadStructureForEntityEvent({
unique: this.getUnique()!,
entityType: this.getEntityType(),
});
actionEventContext.dispatchEvent(event);
eventContext.dispatchEvent(event);
}
return true;
}
async #handleSaveAndPublish() {
@@ -611,29 +625,45 @@ export class UmbDocumentWorkspaceContext
variantIds = result?.selection.map((x) => UmbVariantId.FromString(x)) ?? [];
}
const saveData = this.#buildSaveData(variantIds);
// Create the validation repository if it does not exist. (we first create this here when we need it) [NL]
this.#validationRepository ??= new UmbDocumentValidationRepository(this);
if (this.getIsNew()) {
const parent = this.#parent.getValue();
if (!parent) throw new Error('Parent is not set');
this.#serverValidation.askServerForValidation(
saveData,
this.#validationRepository.validateCreate(saveData, parent.unique),
);
} else {
this.#serverValidation.askServerForValidation(saveData, this.#validationRepository.validateSave(saveData));
}
// TODO: Only validate the specified selection.. [NL]
return this.validateAndSubmit(async (valid) => {
if (valid) {
return this.#performSaveAndPublish(variantIds);
} else {
return this.validateAndSubmit(
async () => {
return this.#performSaveAndPublish(variantIds, saveData);
},
async () => {
// If data of the selection is not valid Then just save:
await this.#performSaveOrCreate(variantIds);
// Return false, even thought the save was successful, but we did not publish, which is what we want to symbolize here. [NL]
return false;
}
});
await this.#performSaveOrCreate(saveData);
// Reject even thought the save was successful, but we did not publish, which is what we want to symbolize here. [NL]
return await Promise.reject();
},
);
}
async #performSaveAndPublish(variantIds: Array<UmbVariantId>): Promise<boolean> {
async #performSaveAndPublish(variantIds: Array<UmbVariantId>, saveData: UmbDocumentDetailModel): Promise<void> {
const unique = this.getUnique();
if (!unique) throw new Error('Unique is missing');
await this.#performSaveOrCreate(variantIds);
await this.#performSaveOrCreate(saveData);
await this.publishingRepository.publish(
unique,
variantIds.map((variantId) => ({ variantId })),
);
return true;
}
async #handleSave() {
@@ -665,22 +695,19 @@ export class UmbDocumentWorkspaceContext
variantIds = result?.selection.map((x) => UmbVariantId.FromString(x)) ?? [];
}
await this.#performSaveOrCreate(variantIds);
return true;
const saveData = this.#buildSaveData(variantIds);
return await this.#performSaveOrCreate(saveData);
}
public async requestSubmit() {
const success = await this.#handleSave();
if (!success) {
await Promise.reject();
}
public requestSubmit() {
return this.#handleSave();
}
public submit() {
return this.#handleSave();
}
public async invalidSubmit() {
return false;
public invalidSubmit() {
return this.#handleSave();
}
public async publish() {

View File

@@ -47,9 +47,9 @@ const workspaceViews: Array<ManifestWorkspaceView> = [
},
{
type: 'workspaceView',
kind: 'contentEditor',
alias: 'Umb.WorkspaceView.Document.Edit',
name: 'Document Workspace Edit View',
element: () => import('./views/edit/document-workspace-view-edit.element.js'),
weight: 200,
meta: {
label: '#general_content',

View File

@@ -1,71 +0,0 @@
import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from '../../document-workspace.context-token.js';
import { css, html, customElement, property, state, repeat } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { UmbPropertyTypeModel } from '@umbraco-cms/backoffice/content-type';
import { UmbContentTypePropertyStructureHelper } from '@umbraco-cms/backoffice/content-type';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { UmbDocumentTypeDetailModel } from '@umbraco-cms/backoffice/document-type';
@customElement('umb-document-workspace-view-edit-properties')
export class UmbDocumentWorkspaceViewEditPropertiesElement extends UmbLitElement {
@property({ type: String, attribute: 'container-id', reflect: false })
public get containerId(): string | null | undefined {
return this.#propertyStructureHelper.getContainerId();
}
public set containerId(value: string | null | undefined) {
this.#propertyStructureHelper.setContainerId(value);
}
#propertyStructureHelper = new UmbContentTypePropertyStructureHelper<UmbDocumentTypeDetailModel>(this);
@state()
_propertyStructure?: Array<UmbPropertyTypeModel>;
constructor() {
super();
this.consumeContext(UMB_DOCUMENT_WORKSPACE_CONTEXT, (workspaceContext) => {
this.#propertyStructureHelper.setStructureManager(workspaceContext.structure);
});
this.observe(
this.#propertyStructureHelper.propertyStructure,
(propertyStructure) => {
this._propertyStructure = propertyStructure;
},
null,
);
}
render() {
return this._propertyStructure
? repeat(
this._propertyStructure,
(property) => property.alias,
(property) =>
html`<umb-property-type-based-property
class="property"
.property=${property}></umb-property-type-based-property> `,
)
: '';
}
static styles = [
UmbTextStyles,
css`
.property {
border-bottom: 1px solid var(--uui-color-divider);
}
.property:last-child {
border-bottom: 0;
}
`,
];
}
export default UmbDocumentWorkspaceViewEditPropertiesElement;
declare global {
interface HTMLElementTagNameMap {
'umb-document-workspace-view-edit-properties': UmbDocumentWorkspaceViewEditPropertiesElement;
}
}

View File

@@ -1,87 +0,0 @@
import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from '../../document-workspace.context-token.js';
import { css, html, customElement, property, state, repeat } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { UmbPropertyTypeContainerModel } from '@umbraco-cms/backoffice/content-type';
import { UmbContentTypeContainerStructureHelper } from '@umbraco-cms/backoffice/content-type';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import './document-workspace-view-edit-properties.element.js';
@customElement('umb-document-workspace-view-edit-tab')
export class UmbDocumentWorkspaceViewEditTabElement extends UmbLitElement {
@property({ type: String })
public get containerId(): string | null | undefined {
return this._containerId;
}
public set containerId(value: string | null | undefined) {
this._containerId = value;
this.#groupStructureHelper.setContainerId(value);
}
@state()
private _containerId?: string | null;
#groupStructureHelper = new UmbContentTypeContainerStructureHelper<any>(this);
@state()
_groups: Array<UmbPropertyTypeContainerModel> = [];
@state()
_hasProperties = false;
constructor() {
super();
this.consumeContext(UMB_DOCUMENT_WORKSPACE_CONTEXT, (workspaceContext) => {
this.#groupStructureHelper.setStructureManager(workspaceContext.structure);
});
this.observe(this.#groupStructureHelper.mergedContainers, (groups) => {
this._groups = groups;
});
this.observe(this.#groupStructureHelper.hasProperties, (hasProperties) => {
this._hasProperties = hasProperties;
});
}
render() {
return html`
${this._hasProperties
? html`
<uui-box>
<umb-document-workspace-view-edit-properties
class="properties"
.containerId=${this._containerId}></umb-document-workspace-view-edit-properties>
</uui-box>
`
: ''}
${repeat(
this._groups,
(group) => group.id,
(group) =>
html`<uui-box .headline=${group.name ?? ''}>
<umb-document-workspace-view-edit-properties
class="properties"
.containerId=${group.id}></umb-document-workspace-view-edit-properties>
</uui-box>`,
)}
`;
}
static styles = [
UmbTextStyles,
css`
uui-box {
--uui-box-default-padding: 0 var(--uui-size-space-5);
}
uui-box:not(:first-child) {
margin-top: var(--uui-size-layout-1);
}
`,
];
}
export default UmbDocumentWorkspaceViewEditTabElement;
declare global {
interface HTMLElementTagNameMap {
'umb-document-workspace-view-edit-tab': UmbDocumentWorkspaceViewEditTabElement;
}
}

View File

@@ -1,169 +0,0 @@
import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from '../../document-workspace.context-token.js';
import type { UmbDocumentWorkspaceViewEditTabElement } from './document-workspace-view-edit-tab.element.js';
import { css, html, customElement, state, repeat } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { UmbPropertyTypeContainerModel } from '@umbraco-cms/backoffice/content-type';
import { UmbContentTypeContainerStructureHelper } from '@umbraco-cms/backoffice/content-type';
import type { UmbRoute, UmbRouterSlotChangeEvent, UmbRouterSlotInitEvent } from '@umbraco-cms/backoffice/router';
import { encodeFolderName } from '@umbraco-cms/backoffice/router';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/extension-registry';
@customElement('umb-document-workspace-view-edit')
export class UmbDocumentWorkspaceViewEditElement extends UmbLitElement implements UmbWorkspaceViewElement {
//@state()
//private _hasRootProperties = false;
@state()
private _hasRootGroups = false;
@state()
private _routes: UmbRoute[] = [];
@state()
private _tabs?: Array<UmbPropertyTypeContainerModel>;
@state()
private _routerPath?: string;
@state()
private _activePath = '';
private _workspaceContext?: typeof UMB_DOCUMENT_WORKSPACE_CONTEXT.TYPE;
private _tabsStructureHelper = new UmbContentTypeContainerStructureHelper<any>(this);
constructor() {
super();
this._tabsStructureHelper.setIsRoot(true);
this._tabsStructureHelper.setContainerChildType('Tab');
this.observe(
this._tabsStructureHelper.mergedContainers,
(tabs) => {
this._tabs = tabs;
this._createRoutes();
},
null,
);
// _hasRootProperties can be gotten via _tabsStructureHelper.hasProperties. But we do not support root properties currently.
this.consumeContext(UMB_DOCUMENT_WORKSPACE_CONTEXT, (workspaceContext) => {
this._workspaceContext = workspaceContext;
this._tabsStructureHelper.setStructureManager(workspaceContext.structure);
this._observeRootGroups();
});
}
private _observeRootGroups() {
if (!this._workspaceContext) return;
this.observe(
this._workspaceContext.structure.hasRootContainers('Group'),
(hasRootGroups) => {
this._hasRootGroups = hasRootGroups;
this._createRoutes();
},
'_observeGroups',
);
}
private _createRoutes() {
if (!this._tabs || !this._workspaceContext) return;
const routes: UmbRoute[] = [];
if (this._tabs.length > 0) {
this._tabs?.forEach((tab) => {
const tabName = tab.name ?? '';
routes.push({
path: `tab/${encodeFolderName(tabName).toString()}`,
component: () => import('./document-workspace-view-edit-tab.element.js'),
setup: (component) => {
(component as UmbDocumentWorkspaceViewEditTabElement).containerId = tab.id;
},
});
});
}
if (this._hasRootGroups) {
routes.push({
path: '',
component: () => import('./document-workspace-view-edit-tab.element.js'),
setup: (component) => {
(component as UmbDocumentWorkspaceViewEditTabElement).containerId = null;
},
});
}
if (routes.length !== 0) {
routes.push({
path: '',
redirectTo: routes[0]?.path,
});
}
this._routes = routes;
}
render() {
if (!this._routes || !this._tabs) return;
return html`
<umb-body-layout header-fit-height>
${this._routerPath && (this._tabs.length > 1 || (this._tabs.length === 1 && this._hasRootGroups))
? html` <uui-tab-group slot="header">
${this._hasRootGroups && this._tabs.length > 0
? html`
<uui-tab
label="Content"
.active=${this._routerPath + '/' === this._activePath}
href=${this._routerPath + '/'}
>Content</uui-tab
>
`
: ''}
${repeat(
this._tabs,
(tab) => tab.name,
(tab) => {
const path = this._routerPath + '/tab/' + encodeFolderName(tab.name || '');
return html`<uui-tab label=${tab.name ?? 'Unnamed'} .active=${path === this._activePath} href=${path}
>${tab.name}</uui-tab
>`;
},
)}
</uui-tab-group>`
: ''}
<umb-router-slot
.routes=${this._routes}
@init=${(event: UmbRouterSlotInitEvent) => {
this._routerPath = event.target.absoluteRouterPath;
}}
@change=${(event: UmbRouterSlotChangeEvent) => {
this._activePath = event.target.absoluteActiveViewPath || '';
}}>
</umb-router-slot>
</umb-body-layout>
`;
}
static styles = [
UmbTextStyles,
css`
:host {
display: block;
height: 100%;
--uui-tab-background: var(--uui-color-surface);
}
`,
];
}
export default UmbDocumentWorkspaceViewEditElement;
declare global {
interface HTMLElementTagNameMap {
'umb-document-workspace-view-edit': UmbDocumentWorkspaceViewEditElement;
}
}

View File

@@ -115,22 +115,22 @@ export class UmbLanguageWorkspaceContext
async submit() {
const newData = this.getData();
if (!newData) return;
if (!newData) {
throw new Error('No data to submit');
}
if (this.getIsNew()) {
const { data } = await this.repository.create(newData);
if (data) {
this.setIsNew(false);
return true;
const { error } = await this.repository.create(newData);
if (error) {
throw new Error(error.message);
}
this.setIsNew(false);
} else {
const { data } = await this.repository.save(newData);
if (data) {
return true;
const { error } = await this.repository.save(newData);
if (error) {
throw new Error(error.message);
}
// TODO: Show validation errors as warnings?
}
return false;
}
destroy(): void {

View File

@@ -197,16 +197,17 @@ export class UmbMediaTypeWorkspaceContext
*/
async submit() {
const data = this.getData();
if (!data) {
return Promise.reject('Something went wrong, there is no data for media type you want to save...');
throw new Error('Something went wrong, there is no data for media type you want to save...');
}
if (this.getIsNew()) {
const parent = this.#parent.getValue();
if (!parent) throw new Error('Parent is not set');
if ((await this.structure.create(parent.unique)) === true) {
if (!parent) throw new Error('Parent is not set');
const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
const event = new UmbRequestReloadTreeItemChildrenEvent({
entityType: parent.entityType,
@@ -226,9 +227,6 @@ export class UmbMediaTypeWorkspaceContext
actionEventContext.dispatchEvent(event);
}
this.setIsNew(false);
return true;
}
public destroy(): void {

View File

@@ -1,3 +1,4 @@
export * from './image-cropper/index.js';
export * from './image-crops-configuration/index.js';
export * from './media-entity-picker/index.js';
export * from './media-picker/index.js';

View File

@@ -1,5 +1,6 @@
import { manifest as mediaPicker } from '../../../media/media/property-editors/media-picker/manifests.js';
import { manifest as imageCropsConfiguration } from '../../../media/media/property-editors/image-crops-configuration/manifests.js';
import { manifest as imageCropper } from '../../../media/media/property-editors/image-cropper/manifests.js';
import { manifest as imageCropper } from './image-cropper/manifests.js';
import { manifest as imageCropsConfiguration } from './image-crops-configuration/manifests.js';
import { manifest as mediaEntityPicker } from './media-entity-picker/manifests.js';
import { manifest as mediaPicker } from './media-picker/manifests.js';
export const manifests = [mediaPicker, imageCropsConfiguration, imageCropper];
export const manifests = [imageCropper, imageCropsConfiguration, mediaEntityPicker, mediaPicker];

View File

@@ -0,0 +1 @@
export * from './property-editor-ui-media-entity-picker.element.js';

View File

@@ -0,0 +1,13 @@
import type { ManifestPropertyEditorUi } from '@umbraco-cms/backoffice/extension-registry';
export const manifest: ManifestPropertyEditorUi = {
type: 'propertyEditorUi',
alias: 'Umb.PropertyEditorUi.MediaEntityPicker',
name: 'Media Entity Picker Property Editor UI',
element: () => import('./property-editor-ui-media-entity-picker.element.js'),
meta: {
label: 'Media Entity Picker',
icon: 'icon-picture',
group: 'pickers',
},
};

View File

@@ -0,0 +1,65 @@
import { UmbMediaPickerContext } from '../../components/input-media/input-media.context.js';
import { html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import { splitStringToArray } from '@umbraco-cms/backoffice/utils';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor';
import type { NumberRangeValueType } from '@umbraco-cms/backoffice/models';
import type { UmbInputEntityElement } from '@umbraco-cms/backoffice/components';
import type { UmbMediaItemModel } from '@umbraco-cms/backoffice/media';
import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry';
@customElement('umb-property-editor-ui-media-entity-picker')
export class UmbPropertyEditorUIMediaEntityPickerElement extends UmbLitElement implements UmbPropertyEditorUiElement {
#min: number = 0;
#max: number = Infinity;
@property({ attribute: false })
public set value(value: string | null | undefined) {
this.#selection = value ? (Array.isArray(value) ? value : splitStringToArray(value)) : [];
}
public get value() {
return this.#selection.length > 0 ? this.#selection.join(',') : null;
}
#selection: Array<string> = [];
public set config(config: UmbPropertyEditorConfigCollection | undefined) {
if (!config) return;
const minMax = config?.getValueByAlias<NumberRangeValueType>('validationLimit');
if (!minMax) return;
this.#min = minMax.min ?? 0;
this.#max = minMax.max ?? Infinity;
}
public get config() {
return undefined;
}
#onChange(event: { target: UmbInputEntityElement }) {
this.value = event.target.selection?.join(',') ?? null;
this.dispatchEvent(new UmbPropertyValueChangeEvent());
}
render() {
return html`
<umb-input-entity
.getIcon=${(item: UmbMediaItemModel) => item.mediaType.icon ?? 'icon-picture'}
.min=${this.#min}
.max=${this.#max}
.pickerContext=${UmbMediaPickerContext}
.selection=${this.#selection}
@change=${this.#onChange}>
</umb-input-entity>
`;
}
}
export default UmbPropertyEditorUIMediaEntityPickerElement;
declare global {
interface HTMLElementTagNameMap {
'umb-property-editor-ui-media-entity-picker': UmbPropertyEditorUIMediaEntityPickerElement;
}
}

View File

@@ -39,9 +39,9 @@ const workspaceViews: Array<ManifestWorkspaceView> = [
},
{
type: 'workspaceView',
kind: 'contentEditor',
alias: 'Umb.WorkspaceView.Media.Edit',
name: 'Media Workspace Edit View',
js: () => import('./views/edit/media-workspace-view-edit.element.js'),
weight: 200,
meta: {
label: '#general_details',

View File

@@ -406,10 +406,11 @@ export class UmbMediaWorkspaceContext
async submit() {
const data = this.getData();
if (!data) throw new Error('Data is missing');
if (!data) {
throw new Error('Data is missing');
}
await this.#createOrSave();
this.setIsNew(false);
return true;
}
async delete() {

View File

@@ -1,64 +0,0 @@
import { UMB_MEDIA_WORKSPACE_CONTEXT } from '../../media-workspace.context-token.js';
import { css, html, customElement, property, state, repeat } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { UmbPropertyTypeModel } from '@umbraco-cms/backoffice/content-type';
import { UmbContentTypePropertyStructureHelper } from '@umbraco-cms/backoffice/content-type';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@customElement('umb-media-workspace-view-edit-properties')
export class UmbMediaWorkspaceViewEditPropertiesElement extends UmbLitElement {
@property({ type: String, attribute: 'container-name', reflect: false })
public get containerId(): string | null | undefined {
return this._propertyStructureHelper.getContainerId();
}
public set containerId(value: string | null | undefined) {
this._propertyStructureHelper.setContainerId(value);
}
_propertyStructureHelper = new UmbContentTypePropertyStructureHelper<any>(this);
@state()
_propertyStructure: Array<UmbPropertyTypeModel> = [];
constructor() {
super();
this.consumeContext(UMB_MEDIA_WORKSPACE_CONTEXT, (workspaceContext) => {
this._propertyStructureHelper.setStructureManager(workspaceContext.structure);
});
this.observe(this._propertyStructureHelper.propertyStructure, (propertyStructure) => {
this._propertyStructure = propertyStructure;
});
}
render() {
return repeat(
this._propertyStructure,
(property) => property.alias,
(property) =>
html`<umb-property-type-based-property
class="property"
.property=${property}></umb-property-type-based-property> `,
);
}
static styles = [
UmbTextStyles,
css`
.property {
border-bottom: 1px solid var(--uui-color-divider);
}
.property:last-child {
border-bottom: 0;
}
`,
];
}
export default UmbMediaWorkspaceViewEditPropertiesElement;
declare global {
interface HTMLElementTagNameMap {
'umb-media-workspace-view-edit-properties': UmbMediaWorkspaceViewEditPropertiesElement;
}
}

View File

@@ -1,87 +0,0 @@
import { UMB_MEDIA_WORKSPACE_CONTEXT } from '../../media-workspace.context-token.js';
import { css, html, customElement, property, state, repeat } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { UmbPropertyTypeContainerModel } from '@umbraco-cms/backoffice/content-type';
import { UmbContentTypeContainerStructureHelper } from '@umbraco-cms/backoffice/content-type';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import './media-workspace-view-edit-properties.element.js';
@customElement('umb-media-workspace-view-edit-tab')
export class UmbMediaWorkspaceViewEditTabElement extends UmbLitElement {
@property({ type: String })
public get containerId(): string | null | undefined {
return this._containerId;
}
public set containerId(value: string | null | undefined) {
this._containerId = value;
this.#groupStructureHelper.setContainerId(value);
}
@state()
private _containerId?: string | null;
#groupStructureHelper = new UmbContentTypeContainerStructureHelper<any>(this);
@state()
_groups: Array<UmbPropertyTypeContainerModel> = [];
@state()
_hasProperties = false;
constructor() {
super();
this.consumeContext(UMB_MEDIA_WORKSPACE_CONTEXT, (workspaceContext) => {
this.#groupStructureHelper.setStructureManager(workspaceContext.structure);
});
this.observe(this.#groupStructureHelper.mergedContainers, (groups) => {
this._groups = groups;
});
this.observe(this.#groupStructureHelper.hasProperties, (hasProperties) => {
this._hasProperties = hasProperties;
});
}
render() {
return html`
${this._hasProperties
? html`
<uui-box>
<umb-media-workspace-view-edit-properties
class="properties"
.containerId=${this._containerId}></umb-media-workspace-view-edit-properties>
</uui-box>
`
: ''}
${repeat(
this._groups,
(group) => group.name,
(group) =>
html`<uui-box .headline=${group.name || ''}>
<umb-media-workspace-view-edit-properties
class="properties"
.containerId=${group.id}></umb-media-workspace-view-edit-properties>
</uui-box>`,
)}
`;
}
static styles = [
UmbTextStyles,
css`
uui-box {
--uui-box-default-padding: 0 var(--uui-size-space-5);
}
uui-box:not(:first-child) {
margin-top: var(--uui-size-layout-1);
}
`,
];
}
export default UmbMediaWorkspaceViewEditTabElement;
declare global {
interface HTMLElementTagNameMap {
'umb-media-workspace-view-edit-tab': UmbMediaWorkspaceViewEditTabElement;
}
}

View File

@@ -1,166 +0,0 @@
import { UMB_MEDIA_WORKSPACE_CONTEXT } from '../../media-workspace.context-token.js';
import type { UmbMediaWorkspaceViewEditTabElement } from './media-workspace-view-edit-tab.element.js';
import { css, html, customElement, state, repeat, nothing } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { UmbPropertyTypeContainerModel } from '@umbraco-cms/backoffice/content-type';
import { UmbContentTypeContainerStructureHelper } from '@umbraco-cms/backoffice/content-type';
import type { UmbRoute, UmbRouterSlotChangeEvent, UmbRouterSlotInitEvent } from '@umbraco-cms/backoffice/router';
import { encodeFolderName } from '@umbraco-cms/backoffice/router';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/extension-registry';
@customElement('umb-media-workspace-view-edit')
export class UmbMediaWorkspaceViewEditElement extends UmbLitElement implements UmbWorkspaceViewElement {
//@state()
//private _hasRootProperties = false;
@state()
private _hasRootGroups = false;
@state()
private _routes: UmbRoute[] = [];
@state()
private _tabs?: Array<UmbPropertyTypeContainerModel>;
@state()
private _routerPath?: string;
@state()
private _activePath = '';
private _workspaceContext?: typeof UMB_MEDIA_WORKSPACE_CONTEXT.TYPE;
private _tabsStructureHelper = new UmbContentTypeContainerStructureHelper<any>(this);
constructor() {
super();
this._tabsStructureHelper.setIsRoot(true);
this._tabsStructureHelper.setContainerChildType('Tab');
this.observe(this._tabsStructureHelper.mergedContainers, (tabs) => {
this._tabs = tabs;
this._createRoutes();
});
// _hasRootProperties can be gotten via _tabsStructureHelper.hasProperties. But we do not support root properties currently.
this.consumeContext(UMB_MEDIA_WORKSPACE_CONTEXT, (workspaceContext) => {
this._workspaceContext = workspaceContext;
this._tabsStructureHelper.setStructureManager(workspaceContext.structure);
this._observeRootGroups();
});
}
private _observeRootGroups() {
if (!this._workspaceContext) return;
this.observe(
this._workspaceContext.structure.hasRootContainers('Group'),
(hasRootGroups) => {
this._hasRootGroups = hasRootGroups;
this._createRoutes();
},
'_observeGroups',
);
}
private _createRoutes() {
if (!this._tabs || !this._workspaceContext) return;
const routes: UmbRoute[] = [];
if (this._tabs.length > 0) {
this._tabs?.forEach((tab) => {
const tabName = tab.name ?? '';
routes.push({
path: `tab/${encodeFolderName(tabName).toString()}`,
component: () => import('./media-workspace-view-edit-tab.element.js'),
setup: (component) => {
(component as UmbMediaWorkspaceViewEditTabElement).containerId = tab.id;
},
});
});
}
if (this._hasRootGroups) {
routes.push({
path: '',
component: () => import('./media-workspace-view-edit-tab.element.js'),
setup: (component) => {
(component as UmbMediaWorkspaceViewEditTabElement).containerId = null;
},
});
}
if (routes.length !== 0) {
routes.push({
path: '',
redirectTo: routes[0]?.path,
});
}
this._routes = routes;
}
render() {
if (!this._routes || !this._tabs) return nothing;
return html`
<umb-body-layout header-fit-height>
${this._routerPath && (this._tabs.length > 1 || (this._tabs.length === 1 && this._hasRootGroups))
? html` <uui-tab-group slot="header">
${this._hasRootGroups && this._tabs.length > 0
? html`
<uui-tab
label="Content"
.active=${this._routerPath + '/' === this._activePath}
href=${this._routerPath + '/'}
>Content</uui-tab
>
`
: ''}
${repeat(
this._tabs,
(tab) => tab.name,
(tab) => {
const path = this._routerPath + '/tab/' + encodeFolderName(tab.name || '');
return html`<uui-tab label=${tab.name ?? 'Unnamed'} .active=${path === this._activePath} href=${path}
>${tab.name}</uui-tab
>`;
},
)}
</uui-tab-group>`
: ''}
<umb-router-slot
.routes=${this._routes}
@init=${(event: UmbRouterSlotInitEvent) => {
this._routerPath = event.target.absoluteRouterPath;
}}
@change=${(event: UmbRouterSlotChangeEvent) => {
this._activePath = event.target.absoluteActiveViewPath || '';
}}>
</umb-router-slot>
</umb-body-layout>
`;
}
static styles = [
UmbTextStyles,
css`
:host {
display: block;
height: 100%;
--uui-tab-background: var(--uui-color-surface);
}
`,
];
}
export default UmbMediaWorkspaceViewEditElement;
declare global {
interface HTMLElementTagNameMap {
'umb-media-workspace-view-edit': UmbMediaWorkspaceViewEditElement;
}
}

View File

@@ -106,13 +106,17 @@ export class UmbMemberGroupWorkspaceContext
if (!data) throw new Error('No data to save');
if (this.getIsNew()) {
await this.repository.create(data);
const { error } = await this.repository.create(data);
if (error) {
throw new Error(error.message);
}
this.setIsNew(false);
} else {
await this.repository.save(data);
const { error } = await this.repository.save(data);
if (error) {
throw new Error(error.message);
}
}
this.setIsNew(false);
return true;
}
getData() {

View File

@@ -177,7 +177,11 @@ export class UmbMemberTypeWorkspaceContext
if (this.getIsNew()) {
const parent = this.#parent.getValue();
if (!parent) throw new Error('Parent is not set');
await this.repository.create(data, parent.unique);
const { error } = await this.repository.create(data, parent.unique);
if (error) {
throw new Error(error.message);
}
this.setIsNew(false);
// TODO: this might not be the right place to alert the tree, but it works for now
const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
@@ -187,7 +191,10 @@ export class UmbMemberTypeWorkspaceContext
});
eventContext.dispatchEvent(event);
} else {
await this.structure.save();
const { error } = await this.structure.save();
if (error) {
throw new Error(error.message);
}
const actionEventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
const event = new UmbRequestReloadStructureForEntityEvent({
@@ -197,9 +204,6 @@ export class UmbMemberTypeWorkspaceContext
actionEventContext.dispatchEvent(event);
}
this.setIsNew(false);
return true;
}
public destroy(): void {

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