Merge branch 'main' into bugfix-duplicate-to-entity-action-(part-1)
This commit is contained in:
@@ -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/',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
94
src/Umbraco.Web.UI.Client/package-lock.json
generated
94
src/Umbraco.Web.UI.Client/package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(',') ?? ''),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './stack.element.js';
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
},
|
||||
};
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { manifests as workspaceManifests } from './workspace/manifests.js';
|
||||
|
||||
export const manifests = [...workspaceManifests];
|
||||
@@ -0,0 +1,3 @@
|
||||
import { contentEditorManifest } from './views/edit/manifest.js';
|
||||
|
||||
export const manifests = [contentEditorManifest];
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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!');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 } }],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>`
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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';
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
*/
|
||||
@@ -0,0 +1,2 @@
|
||||
export type * from './validation-message-translator.interface.js';
|
||||
export * from './variant-values-validation-message-translator.controller.js';
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface UmbValidationMessageTranslator {
|
||||
match(message: string): boolean;
|
||||
translate(message: string): string;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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(' && ')})`;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './data-path-value-filter.function.js';
|
||||
@@ -1 +0,0 @@
|
||||
export * from './form-control.validator.js';
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>>;
|
||||
}
|
||||
|
||||
@@ -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>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { UmbDocumentValidationRepository } from './document-validation.repository.js';
|
||||
export { UMB_DOCUMENT_VALIDATION_REPOSITORY_ALIAS } from './manifests.js';
|
||||
@@ -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];
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './property-editor-ui-media-entity-picker.element.js';
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user