Merge branch 'main' into chore/move-member-group-picker-property-editor-ui

This commit is contained in:
Lee Kelleher
2024-04-23 10:46:18 +01:00
committed by GitHub
73 changed files with 9192 additions and 506 deletions

View File

@@ -20,6 +20,7 @@ import { UmbIconRegistry } from '../src/packages/core/icon-registry/icon.registr
import { UmbLitElement } from '../src/packages/core/lit-element';
import { umbLocalizationRegistry } from '../src/packages/core/localization';
import customElementManifests from '../dist-cms/custom-elements.json';
import icons from '../src/packages/core/icon-registry/icons/icons';
import '../src/libs/context-api/provide/context-provider.element';
import '../src/packages/core/components';
@@ -36,6 +37,7 @@ class UmbStoryBookElement extends UmbLitElement {
constructor() {
super();
this._umbIconRegistry.setIcons(icons);
this._umbIconRegistry.attach(this);
this._registerExtensions(documentManifests);
new UmbModalManagerContext(this);

View File

@@ -18,11 +18,10 @@ const run = async () => {
var icons = await collectDictionaryIcons();
icons = await collectDiskIcons(icons);
writeIconsToDisk(icons);
generateJSON(icons);
generateJS(icons);
};
const collectDictionaryIcons = async () => {
const rawData = readFileSync(iconMapJson);
const fileRaw = rawData.toString();
const fileJSON = JSON.parse(fileRaw);
@@ -32,11 +31,11 @@ const collectDictionaryIcons = async () => {
// Lucide:
fileJSON.lucide.forEach((iconDef) => {
if (iconDef.file && iconDef.name) {
const path = lucideSvgDirectory + "/" + iconDef.file;
const path = lucideSvgDirectory + '/' + iconDef.file;
try {
const rawData = readFileSync(path);
// For Lucide icons specially we adjust the icons a bit for them to work in our case:
// For Lucide icons specially we adjust the icons a bit for them to work in our case: [NL]
let svg = rawData.toString().replace(' width="24"\n', '');
svg = svg.replace(' height="24"\n', '');
svg = svg.replace('stroke-width="2"', 'stroke-width="1.75"');
@@ -60,11 +59,11 @@ const collectDictionaryIcons = async () => {
// SimpleIcons:
fileJSON.simpleIcons.forEach((iconDef) => {
if (iconDef.file && iconDef.name) {
const path = simpleIconsSvgDirectory + "/" + iconDef.file;
const path = simpleIconsSvgDirectory + '/' + iconDef.file;
try {
const rawData = readFileSync(path);
let svg = rawData.toString()
let svg = rawData.toString();
const iconFileName = iconDef.name;
// SimpleIcons need to use fill="currentColor"
@@ -91,11 +90,11 @@ const collectDictionaryIcons = async () => {
// Umbraco:
fileJSON.umbraco.forEach((iconDef) => {
if (iconDef.file && iconDef.name) {
const path = umbracoSvgDirectory + "/" + iconDef.file;
const path = umbracoSvgDirectory + '/' + iconDef.file;
try {
const rawData = readFileSync(path);
const svg = rawData.toString()
const svg = rawData.toString();
const iconFileName = iconDef.name;
const icon = {
@@ -136,8 +135,7 @@ const collectDiskIcons = async (icons) => {
const iconName = iconFileName;
// Only append not already defined icons:
if (!icons.find(x => x.name === iconName)) {
if (!icons.find((x) => x.name === iconName)) {
const icon = {
name: iconName,
legacy: true,
@@ -169,20 +167,20 @@ const writeIconsToDisk = (icons) => {
});
};
const generateJSON = (icons) => {
const JSONPath = `${iconsOutputDirectory}/icons.json`;
const generateJS = (icons) => {
const JSPath = `${iconsOutputDirectory}/icons.ts`;
const iconDescriptors = icons.map((icon) => {
return {
name: icon.name,
legacy: icon.legacy,
path: `./icons/${icon.fileName}.js`,
};
return `{
name: "${icon.name}",
${icon.legacy ? 'legacy: true,' : ''}
path: "./icons/${icon.fileName}.js",
}`.replace(/\t/g, ''); // Regex removes white space [NL]
});
const content = `${JSON.stringify(iconDescriptors)}`;
const content = `export default [${iconDescriptors.join(',')}];`;
writeFileWithDir(JSONPath, content, (err) => {
writeFileWithDir(JSPath, content, (err) => {
if (err) {
// eslint-disable-next-line no-undef
console.log(err);

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,12 @@
{
"name": "@umbraco-cms/backoffice",
"version": "14.0.0-rc1",
"version": "14.0.0-rc2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@umbraco-cms/backoffice",
"version": "14.0.0-rc1",
"version": "14.0.0-rc2",
"license": "MIT",
"dependencies": {
"@types/diff": "^5.0.9",

View File

@@ -1,7 +1,7 @@
{
"name": "@umbraco-cms/backoffice",
"license": "MIT",
"version": "14.0.0-rc1",
"version": "14.0.0-rc2",
"type": "module",
"exports": {
".": null,

View File

@@ -1,19 +1,28 @@
import { css, html, nothing, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import type { ProblemDetails } from '@umbraco-cms/backoffice/external/backend-api';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
/**
* A full page error element that can be used either solo or for instance as the error 500 page and BootFailed
*/
@customElement('umb-app-error')
export class UmbAppErrorElement extends UmbLitElement {
/**
* The headline to display
*
* @attr
*/
@property()
errorHeadline?: string | null;
/**
* The error message to display
*
* @attr
*/
@property()
errorMessage?: string;
errorMessage?: string | null;
/**
* The error to display
@@ -23,31 +32,128 @@ export class UmbAppErrorElement extends UmbLitElement {
@property()
error?: unknown;
private renderProblemDetails = (problemDetails: ProblemDetails) => html`
<h2>${problemDetails.title}</h2>
constructor() {
super();
this.#generateErrorFromSearchParams();
}
/**
* Generates an error from the search params before the properties are set
*/
#generateErrorFromSearchParams() {
const searchParams = new URLSearchParams(window.location.search);
const flow = searchParams.get('flow');
if (flow === 'external-login-callback') {
this.errorHeadline = this.localize.term('errors_externalLoginError');
console.log('External login error', searchParams.get('error'));
const status = searchParams.get('status');
// "Status" is controlled by Umbraco and is a string
if (status) {
switch (status) {
case 'unauthorized':
this.errorMessage = this.localize.term('errors_unauthorized');
break;
case 'user-not-found':
this.errorMessage = this.localize.term('errors_userNotFound');
break;
case 'external-info-not-found':
this.errorMessage = this.localize.term('errors_externalInfoNotFound');
break;
case 'failed':
this.errorMessage = this.localize.term('errors_externalLoginFailed');
break;
default:
this.errorMessage = this.localize.term('errors_defaultError');
break;
}
}
return;
}
if (flow === 'external-login') {
/**
* "Error" is controlled by OpenID and is a string
* @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1
*/
const error = searchParams.get('error');
this.errorHeadline = this.localize.term('errors_externalLoginError');
switch (error) {
case 'access_denied':
this.errorMessage = this.localize.term('openidErrors_accessDenied');
break;
case 'invalid_request':
this.errorMessage = this.localize.term('openidErrors_invalidRequest');
break;
case 'invalid_client':
this.errorMessage = this.localize.term('openidErrors_invalidClient');
break;
case 'invalid_grant':
this.errorMessage = this.localize.term('openidErrors_invalidGrant');
break;
case 'unauthorized_client':
this.errorMessage = this.localize.term('openidErrors_unauthorizedClient');
break;
case 'unsupported_grant_type':
this.errorMessage = this.localize.term('openidErrors_unsupportedGrantType');
break;
case 'invalid_scope':
this.errorMessage = this.localize.term('openidErrors_invalidScope');
break;
case 'server_error':
this.errorMessage = this.localize.term('openidErrors_serverError');
break;
case 'temporarily_unavailable':
this.errorMessage = this.localize.term('openidErrors_temporarilyUnavailable');
break;
default:
this.errorMessage = this.localize.term('errors_defaultError');
break;
}
// Set the error object with the original error parameters from the search params
let detail = searchParams.get('error_description');
const errorUri = searchParams.get('error_uri');
if (errorUri) {
detail = `${detail} (${errorUri})`;
}
this.error = { title: `External error code: ${error}`, detail };
return;
}
}
#renderProblemDetails = (problemDetails: ProblemDetails) => html`
<p><strong>${problemDetails.title}</strong></p>
<p>${problemDetails.detail}</p>
<pre>${problemDetails.stack}</pre>
`;
private renderErrorObj = (error: Error) => html`
<h2>${error.name}</h2>
#renderErrorObj = (error: Error) => html`
<p><strong>${error.name}</strong></p>
<p>${error.message}</p>
<pre>${error.stack}</pre>
`;
private isProblemDetails(error: unknown): error is ProblemDetails {
#isProblemDetails(error: unknown): error is ProblemDetails {
return typeof error === 'object' && error !== null && 'detail' in error && 'title' in error;
}
private isError(error: unknown): error is Error {
#isError(error: unknown): error is Error {
return typeof error === 'object' && error !== null && error instanceof Error;
}
private renderError(error: unknown) {
if (this.isProblemDetails(error)) {
return this.renderProblemDetails(error);
} else if (this.isError(error)) {
return this.renderErrorObj(error);
#renderError(error: unknown) {
if (this.#isProblemDetails(error)) {
return this.#renderProblemDetails(error);
} else if (this.#isError(error)) {
return this.#renderErrorObj(error);
}
return nothing;
@@ -56,73 +162,93 @@ export class UmbAppErrorElement extends UmbLitElement {
render = () => html`
<div id="background"></div>
<div id="logo">
<img src="/umbraco/backoffice/assets/umbraco_logomark_white.svg'" alt="Umbraco" />
<div id="logo" aria-hidden="true">
<img src="/umbraco/backoffice/assets/umbraco_logomark_white.svg" alt="Umbraco" />
</div>
<div id="container">
<uui-box id="box">
<h1>Something went wrong</h1>
<p>${this.errorMessage}</p>
<div id="container" class="uui-text">
<uui-box id="box" headline-variant="h1">
<uui-button
slot="header-actions"
label=${this.localize.term('general_back')}
look="secondary"
@click=${() => (location.href = '')}></uui-button>
<div slot="headline">
${this.errorHeadline
? this.errorHeadline
: html` <umb-localize key="errors_defaultError">An unknown failure has occured</umb-localize> `}
</div>
<div id="message">${this.errorMessage}</div>
${this.error
? html`
<details>
<summary>Details</summary>
${this.renderError(this.error)}
<summary><umb-localize key="general_details">Details</umb-localize></summary>
${this.#renderError(this.error)}
</details>
`
`
: nothing}
</uui-box>
</div>
`;
static styles = css`
#background {
position: fixed;
overflow: hidden;
background-position: 50%;
background-repeat: no-repeat;
background-size: cover;
background-image: url('/umbraco/backoffice/assets/umbraco_background.jpg');
width: 100vw;
height: 100vh;
}
static styles = [
UmbTextStyles,
css`
#background {
position: fixed;
overflow: hidden;
background-position: 50%;
background-repeat: no-repeat;
background-size: cover;
background-image: url('/umbraco/backoffice/assets/installer-illustration.svg');
width: 100vw;
height: 100vh;
}
#logo {
position: fixed;
top: var(--uui-size-space-5);
left: var(--uui-size-space-5);
height: 30px;
}
#logo {
position: fixed;
top: var(--uui-size-space-5);
left: var(--uui-size-space-5);
height: 30px;
}
#logo img {
height: 100%;
}
#logo img {
height: 100%;
}
#container {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100vw;
height: 100vh;
}
#container {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100vw;
height: 100vh;
}
#box {
width: 50vw;
padding: var(--uui-size-space-6) var(--uui-size-space-5) var(--uui-size-space-5) var(--uui-size-space-5);
}
#box {
width: 400px;
max-width: 80vw;
}
details {
padding: var(--uui-size-space-2) var(--uui-size-space-3);
background: var(--uui-color-surface-alt);
}
#message {
margin-bottom: var(--uui-size-space-3);
}
pre {
width: 100%;
overflow: auto;
}
`;
details {
padding: var(--uui-size-space-2) var(--uui-size-space-3);
background: var(--uui-color-surface-alt);
}
details summary {
cursor: pointer;
}
pre {
width: 100%;
overflow: auto;
}
`,
];
}
export default UmbAppErrorElement;

View File

@@ -7,7 +7,6 @@ import type { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth';
import { UmbAuthContext } from '@umbraco-cms/backoffice/auth';
import { css, html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import { UUIIconRegistryEssential } from '@umbraco-cms/backoffice/external/uui';
import { UmbIconRegistry } from '@umbraco-cms/backoffice/icon';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { Guard, UmbRoute } from '@umbraco-cms/backoffice/router';
import { pathWithoutBasePath } from '@umbraco-cms/backoffice/router';
@@ -50,6 +49,10 @@ export class UmbAppElement extends UmbLitElement {
bypassAuth = false;
private _routes: UmbRoute[] = [
{
path: 'error',
component: () => import('./app-error.element.js'),
},
{
path: 'install',
component: () => import('../installer/installer.element.js'),
@@ -85,7 +88,6 @@ export class UmbAppElement extends UmbLitElement {
new UmbBundleExtensionInitializer(this, umbExtensionsRegistry);
new UmbAppEntryPointExtensionInitializer(this, umbExtensionsRegistry);
new UmbIconRegistry().attach(this);
new UUIIconRegistryEssential().attach(this);
new UmbContextDebugController(this);

View File

@@ -10,33 +10,31 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import './components/index.js';
// TODO: temp solution to load core packages
const CORE_PACKAGES = [
import('../../packages/audit-log/umbraco-package.js'),
import('../../packages/block/umbraco-package.js'),
import('../../packages/data-type/umbraco-package.js'),
import('../../packages/dictionary/umbraco-package.js'),
import('../../packages/umbraco-news/umbraco-package.js'),
import('../../packages/documents/umbraco-package.js'),
import('../../packages/dynamic-root/umbraco-package.js'),
import('../../packages/health-check/umbraco-package.js'),
import('../../packages/language/umbraco-package.js'),
import('../../packages/log-viewer/umbraco-package.js'),
import('../../packages/markdown-editor/umbraco-package.js'),
import('../../packages/data-type/umbraco-package.js'),
import('../../packages/media/umbraco-package.js'),
import('../../packages/members/umbraco-package.js'),
import('../../packages/models-builder/umbraco-package.js'),
//import('../../packages/object-type/umbraco-package.js'),// This had nothing to register.
import('../../packages/packages/umbraco-package.js'),
import('../../packages/relations/umbraco-package.js'),
import('../../packages/search/umbraco-package.js'),
import('../../packages/settings/umbraco-package.js'),
import('../../packages/language/umbraco-package.js'),
import('../../packages/static-file/umbraco-package.js'),
import('../../packages/dynamic-root/umbraco-package.js'),
import('../../packages/block/umbraco-package.js'),
import('../../packages/tags/umbraco-package.js'),
import('../../packages/templating/umbraco-package.js'),
import('../../packages/tiny-mce/umbraco-package.js'),
import('../../packages/umbraco-news/umbraco-package.js'),
import('../../packages/markdown-editor/umbraco-package.js'),
import('../../packages/templating/umbraco-package.js'),
import('../../packages/dictionary/umbraco-package.js'),
import('../../packages/user/umbraco-package.js'),
import('../../packages/health-check/umbraco-package.js'),
import('../../packages/audit-log/umbraco-package.js'),
import('../../packages/webhook/umbraco-package.js'),
import('../../packages/relations/umbraco-package.js'),
import('../../packages/models-builder/umbraco-package.js'),
import('../../packages/log-viewer/umbraco-package.js'),
import('../../packages/packages/umbraco-package.js'),
];
@customElement('umb-backoffice')
@@ -55,13 +53,13 @@ export class UmbBackofficeElement extends UmbLitElement {
new UmbBackofficeEntryPointExtensionInitializer(this, umbExtensionsRegistry);
new UmbEntryPointExtensionInitializer(this, umbExtensionsRegistry);
new UmbServerExtensionRegistrator(this, umbExtensionsRegistry).registerPrivateExtensions();
// So far local packages are this simple to registerer, so no need for a manager to do that:
CORE_PACKAGES.forEach(async (packageImport) => {
const packageModule = await packageImport;
umbExtensionsRegistry.registerMany(packageModule.extensions);
});
new UmbServerExtensionRegistrator(this, umbExtensionsRegistry).registerPrivateExtensions();
}
render() {

View File

@@ -81,7 +81,7 @@ export class UmbBackofficeHeaderSectionsElement extends UmbLitElement {
#tabs {
height: 60px;
flex-basis: 100%;
font-size: 16px;
font-size: 16px; /* specific for the header */
--uui-tab-text: var(--uui-color-header-contrast);
--uui-tab-text-hover: var(--uui-color-header-contrast-emphasis);
--uui-tab-text-active: var(--uui-color-header-contrast-emphasis);

View File

@@ -1,6 +1,6 @@
import type { UmbBackofficeContext } from '../backoffice.context.js';
import { UMB_BACKOFFICE_CONTEXT } from '../backoffice.context.js';
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { css, html, customElement, state, nothing } from '@umbraco-cms/backoffice/external/lit';
import { UmbSectionContext, UMB_SECTION_CONTEXT } from '@umbraco-cms/backoffice/section';
import type { UmbRoute, UmbRouterSlotChangeEvent } from '@umbraco-cms/backoffice/router';
import type { ManifestSection, UmbSectionElement } from '@umbraco-cms/backoffice/extension-registry';
@@ -99,17 +99,18 @@ export class UmbBackofficeMainElement extends UmbLitElement {
render() {
return this._routes.length > 0
? html`<umb-router-slot .routes=${this._routes} @change=${this._onRouteChange}></umb-router-slot>`
: '';
: nothing;
}
static styles = [
css`
:host {
background-color: var(--uui-color-background);
display: block;
background-color: var(--uui-color-background);
width: 100%;
height: calc(
100% - 60px
); // 60 => top header height, TODO: Make sure this comes from somewhere so it is maintainable and eventually responsive.
); /* 60 => top header height, TODO: Make sure this comes from somewhere so it is maintainable and eventually responsive. */
}
`,
];

View File

@@ -690,6 +690,8 @@ export default {
errorRegExpWithoutTab: '%0% er ikke i et korrekt format',
},
errors: {
defaultError: 'Der er sket en ukendt fejl',
concurrencyError: 'Optimistisk samtidighedsfejl, objektet er blevet ændret',
receivedErrorFromServer: 'Der skete en fejl på severen',
dissallowedMediaType: 'Denne filttype er blevet deaktiveret af administratoren',
codemirroriewarning:
@@ -707,8 +709,22 @@ export default {
tableColMergeLeft: 'Du skal stå til venstre for de 2 celler du ønsker at samle!',
tableSplitNotSplittable: 'Du kan ikke opdele en celle, som ikke allerede er delt.',
propertyHasErrors: 'Denne egenskab er ugyldig',
defaultError: 'An unknown failure has occurred',
concurrencyError: 'Optimistic concurrency failure, object has been modified',
externalLoginError: 'Der opstod en fejl under login med eksternt login',
unauthorized: 'Du har ikke tilladelse til at udføre denne handling',
userNotFound: 'Den angivne bruger blev ikke fundet i databasen',
externalInfoNotFound: 'Serveren kunne ikke kommunikere med den eksterne loginudbyder',
externalLoginFailed: 'Serveren mislykkedes i at logge ind med den eksterne loginudbyder',
},
openidErrors: {
accessDenied: 'Access denied',
invalidRequest: 'Ugyldig forespørgsel',
invalidClient: 'Ugyldig klient',
invalidGrant: 'Ugyldig tildeling',
unauthorizedClient: 'Uautoriseret klient',
unsupportedGrantType: 'Ikke understøttet tildelingstype',
invalidScope: 'Ugyldigt område',
serverError: 'Serverfejl',
temporarilyUnavailable: 'Servicen er midlertidigt utilgængelig',
},
general: {
options: 'Valgmuligheder',

View File

@@ -714,6 +714,22 @@ export default {
tableColMergeLeft: 'Please place cursor at the left of the two cells you wish to merge',
tableSplitNotSplittable: "You cannot split a cell that hasn't been merged.",
propertyHasErrors: 'This property is invalid',
externalLoginError: 'External login',
unauthorized: 'You were not authorized before performing this action',
userNotFound: 'The local user was not found in the database',
externalInfoNotFound: 'The server did not succeed in communicating with the external login provider',
externalLoginFailed: 'The server failed to authorize you against the external login provider',
},
openidErrors: {
accessDenied: 'Access denied',
invalidRequest: 'Invalid request',
invalidClient: 'Invalid client',
invalidGrant: 'Invalid grant',
unauthorizedClient: 'Unauthorized client',
unsupportedGrantType: 'Unsupported grant type',
invalidScope: 'Invalid scope',
serverError: 'Server error',
temporarilyUnavailable: 'The service is temporarily unavailable',
},
general: {
options: 'Options',

View File

@@ -3,151 +3,164 @@ const { rest } = window.MockServiceWorker;
import type { PackageManifestResponse } from '../../packages/packages/types.js';
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
const privateManifests: PackageManifestResponse = [
{
name: 'My Package Name',
version: '1.0.0',
extensions: [
{
type: 'bundle',
alias: 'My.Package.Bundle',
name: 'My Package Bundle',
js: '/App_Plugins/custom-bundle-package/index.js',
},
],
},
{
name: 'Named Package',
version: '1.0.0',
extensions: [
{
type: 'section',
alias: 'My.Section.Custom',
name: 'Custom Section',
js: '/App_Plugins/section.js',
elementName: 'my-section-custom',
weight: 1,
meta: {
label: 'Custom',
pathname: 'my-custom',
},
},
{
type: 'propertyEditorUi',
alias: 'My.PropertyEditorUI.Custom',
name: 'My Custom Property Editor UI',
js: '/App_Plugins/property-editor.js',
elementName: 'my-property-editor-ui-custom',
meta: {
label: 'My Custom Property',
icon: 'document',
group: 'Common',
propertyEditorSchema: 'Umbraco.TextBox',
},
},
],
},
{
name: 'Package with an entry point',
extensions: [
{
type: 'backofficeEntryPoint',
name: 'My Custom Entry Point',
alias: 'My.Entrypoint.Custom',
js: '/App_Plugins/custom-entrypoint.js',
},
],
},
{
name: 'My MFA Package',
extensions: [
{
type: 'mfaLoginProvider',
alias: 'My.MfaLoginProvider.Custom.Google',
name: 'My Custom Google MFA Provider',
forProviderName: 'Google Authenticator',
},
{
type: 'mfaLoginProvider',
alias: 'My.MfaLoginProvider.Custom.SMS',
name: 'My Custom SMS MFA Provider',
forProviderName: 'sms',
meta: {
label: 'Setup SMS Verification',
},
},
],
},
{
name: 'Package with a view',
extensions: [
{
type: 'packageView',
alias: 'My.PackageView.Custom',
name: 'My Custom Package View',
js: '/App_Plugins/package-view.js',
meta: {
packageName: 'Package with a view',
},
},
],
},
{
name: 'My MFA Package',
extensions: [
{
type: 'mfaLoginProvider',
alias: 'My.MfaLoginProvider.Custom',
name: 'My Custom MFA Provider',
forProviderName: 'sms',
meta: {
label: 'Setup SMS Verification',
},
},
],
},
];
const publicManifests: PackageManifestResponse = [
{
name: 'My Auth Package',
extensions: [
{
type: 'authProvider',
alias: 'My.AuthProvider.Google',
name: 'My Custom Auth Provider',
forProviderName: 'Umbraco.Google',
meta: {
label: 'Google',
defaultView: {
icon: 'icon-google',
},
linking: {
allowManualLinking: true,
},
},
},
{
type: 'authProvider',
alias: 'My.AuthProvider.Github',
name: 'My Github Auth Provider',
forProviderName: 'Umbraco.Github',
meta: {
label: 'GitHub',
defaultView: {
look: 'primary',
icon: 'icon-github',
color: 'success',
},
linking: {
allowManualLinking: true,
},
},
},
],
},
];
export const manifestDevelopmentHandlers = [
rest.get(umbracoPath('/manifest/manifest/private'), (_req, res, ctx) => {
return res(
// Respond with a 200 status code
ctx.status(200),
ctx.json<PackageManifestResponse>([
{
name: 'My Package Name',
version: '1.0.0',
extensions: [
{
type: 'bundle',
alias: 'My.Package.Bundle',
name: 'My Package Bundle',
js: '/App_Plugins/custom-bundle-package/index.js',
},
],
},
{
name: 'Named Package',
version: '1.0.0',
extensions: [
{
type: 'section',
alias: 'My.Section.Custom',
name: 'Custom Section',
js: '/App_Plugins/section.js',
elementName: 'my-section-custom',
weight: 1,
meta: {
label: 'Custom',
pathname: 'my-custom',
},
},
{
type: 'propertyEditorUi',
alias: 'My.PropertyEditorUI.Custom',
name: 'My Custom Property Editor UI',
js: '/App_Plugins/property-editor.js',
elementName: 'my-property-editor-ui-custom',
meta: {
label: 'My Custom Property',
icon: 'document',
group: 'Common',
propertyEditorSchema: 'Umbraco.TextBox',
},
},
],
},
{
name: 'Package with an entry point',
extensions: [
{
type: 'backofficeEntryPoint',
name: 'My Custom Entry Point',
alias: 'My.Entrypoint.Custom',
js: '/App_Plugins/custom-entrypoint.js',
},
],
},
{
name: 'My MFA Package',
extensions: [
{
type: 'mfaLoginProvider',
alias: 'My.MfaLoginProvider.Custom.Google',
name: 'My Custom Google MFA Provider',
forProviderName: 'Google Authenticator',
},
{
type: 'mfaLoginProvider',
alias: 'My.MfaLoginProvider.Custom.SMS',
name: 'My Custom SMS MFA Provider',
forProviderName: 'sms',
meta: {
label: 'Setup SMS Verification',
},
},
],
},
{
name: 'Package with a view',
extensions: [
{
type: 'packageView',
alias: 'My.PackageView.Custom',
name: 'My Custom Package View',
js: '/App_Plugins/package-view.js',
meta: {
packageName: 'Package with a view',
},
},
],
},
{
name: 'My MFA Package',
extensions: [
{
type: 'mfaLoginProvider',
alias: 'My.MfaLoginProvider.Custom',
name: 'My Custom MFA Provider',
forProviderName: 'sms',
meta: {
label: 'Setup SMS Verification',
},
},
],
},
]),
ctx.json<PackageManifestResponse>(privateManifests),
);
}),
rest.get(umbracoPath('/manifest/manifest/public'), (_req, res, ctx) => {
return res(
ctx.status(200),
ctx.json<PackageManifestResponse>([
{
name: 'My Auth Package',
extensions: [
{
type: 'authProvider',
alias: 'My.AuthProvider.Google',
name: 'My Custom Auth Provider',
forProviderName: 'Umbraco.Google',
meta: {
label: 'Sign in with Google',
},
},
{
type: 'authProvider',
alias: 'My.AuthProvider.Github',
name: 'My Github Auth Provider',
forProviderName: 'Umbraco.Github',
meta: {
label: 'GitHub',
defaultView: {
look: 'primary',
icon: 'icon-github',
color: 'success',
},
},
},
],
},
]),
);
return res(ctx.status(200), ctx.json<PackageManifestResponse>(publicManifests));
}),
rest.get(umbracoPath('/manifest/manifest'), (_req, res, ctx) => {
return res(ctx.status(200), ctx.json<PackageManifestResponse>([...privateManifests, ...publicManifests]));
}),
];
@@ -158,4 +171,7 @@ export const manifestEmptyHandlers = [
rest.get(umbracoPath('/manifest/manifest/public'), (_req, res, ctx) => {
return res(ctx.status(200), ctx.json<PackageManifestResponse>([]));
}),
rest.get(umbracoPath('/manifest/manifest'), (_req, res, ctx) => {
return res(ctx.status(200), ctx.json<PackageManifestResponse>([]));
}),
];

View File

@@ -1,6 +1,7 @@
const { rest } = window.MockServiceWorker;
import { umbUserMockDb } from '../../data/user/user.db.js';
import { UMB_SLUG } from './slug.js';
import type { LinkedLoginsRequestModel } from '@umbraco-cms/backoffice/external/backend-api';
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
export const handlers = [
@@ -8,6 +9,19 @@ export const handlers = [
const loggedInUser = umbUserMockDb.getCurrentUser();
return res(ctx.status(200), ctx.json(loggedInUser));
}),
rest.get<LinkedLoginsRequestModel>(umbracoPath(`${UMB_SLUG}/current/logins`), (_req, res, ctx) => {
return res(
ctx.status(200),
ctx.json<LinkedLoginsRequestModel>({
linkedLogins: [
{
providerKey: 'google',
providerName: 'Umbraco.Google',
},
],
}),
);
}),
rest.get(umbracoPath(`${UMB_SLUG}/current/2fa`), (_req, res, ctx) => {
const mfaLoginProviders = umbUserMockDb.getMfaLoginProviders();
return res(ctx.status(200), ctx.json(mfaLoginProviders));

View File

@@ -92,7 +92,10 @@ export class UmbExtensionSlotElement extends UmbLitElement {
public defaultElement?: string;
@property()
public renderMethod?: (extension: UmbExtensionElementInitializer) => TemplateResult | HTMLElement | null | undefined;
public renderMethod?: (
extension: UmbExtensionElementInitializer,
index: number,
) => TemplateResult | HTMLElement | null | undefined;
connectedCallback(): void {
super.connectedCallback();
@@ -130,7 +133,7 @@ export class UmbExtensionSlotElement extends UmbLitElement {
? repeat(
this._permitted,
(ext) => ext.alias,
(ext) => (this.renderMethod ? this.renderMethod(ext) : ext.component),
(ext, i) => (this.renderMethod ? this.renderMethod(ext, i) : ext.component),
)
: html`<slot></slot>`;
}

View File

@@ -139,6 +139,7 @@ export class UmbExtensionWithApiSlotElement extends UmbLitElement {
@property()
public renderMethod?: (
extension: UmbExtensionElementAndApiInitializer,
index: number,
) => TemplateResult | HTMLElement | null | undefined;
connectedCallback(): void {
@@ -181,7 +182,7 @@ export class UmbExtensionWithApiSlotElement extends UmbLitElement {
? repeat(
this._permitted,
(ext) => ext.alias,
(ext) => (this.renderMethod ? this.renderMethod(ext) : ext.component),
(ext, i) => (this.renderMethod ? this.renderMethod(ext, i) : ext.component),
)
: html`<slot></slot>`;
}

View File

@@ -246,7 +246,7 @@ export class UmbContentTypeContainerStructureHelper<T extends UmbContentTypeMode
/** Manipulate methods: */
async insertContainer(container: UmbPropertyTypeContainerModel, sortOrder = 0) {
/*async insertContainer(container: UmbPropertyTypeContainerModel, sortOrder = 0) {
await this.#init;
if (!this.#structure) return false;
@@ -254,7 +254,7 @@ export class UmbContentTypeContainerStructureHelper<T extends UmbContentTypeMode
await this.#structure.insertContainer(null, newContainer);
return true;
}
}*/
async addContainer(parentContainerId?: string | null, sortOrder?: number) {
if (!this.#structure) return;

View File

@@ -278,6 +278,22 @@ export class UmbContentTypeStructureManager<
return clonedContainer;
}
ensureContainerNames(
contentTypeUnique: string | null,
type: UmbPropertyContainerTypes,
parentId: string | null = null,
) {
contentTypeUnique = contentTypeUnique ?? this.#ownerContentTypeUnique!;
this.getOwnerContainers(type, parentId)?.forEach((container) => {
if (container.name === '') {
const newName = 'Unnamed';
this.updateContainer(null, container.id, {
name: this.makeContainerNameUniqueForOwnerContentType(container.id, newName, type, parentId) ?? newName,
});
}
});
}
async createContainer(
contentTypeUnique: string | null,
parentId: string | null = null,
@@ -295,9 +311,11 @@ export class UmbContentTypeStructureManager<
sortOrder: sortOrder ?? 0,
};
const containers = [
...(this.#contentTypes.getValue().find((x) => x.unique === contentTypeUnique)?.containers ?? []),
];
// Ensure
this.ensureContainerNames(contentTypeUnique, type, parentId);
const contentTypes = this.#contentTypes.getValue();
const containers = [...(contentTypes.find((x) => x.unique === contentTypeUnique)?.containers ?? [])];
containers.push(container);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -308,7 +326,7 @@ export class UmbContentTypeStructureManager<
return container;
}
async insertContainer(contentTypeUnique: string | null, container: UmbPropertyTypeContainerModel) {
/*async insertContainer(contentTypeUnique: string | null, container: UmbPropertyTypeContainerModel) {
await this.#init;
contentTypeUnique = contentTypeUnique ?? this.#ownerContentTypeUnique!;
@@ -330,24 +348,34 @@ export class UmbContentTypeStructureManager<
// @ts-ignore
// TODO: fix TS partial complaint
this.#contentTypes.updateOne(contentTypeUnique, { containers });
}
}*/
makeEmptyContainerName(
containerId: string,
containerType: UmbPropertyContainerTypes,
parentId: string | null = null,
) {
return (
this.makeContainerNameUniqueForOwnerContentType(containerId, 'Unnamed', containerType, parentId) ?? 'Unnamed'
);
}
makeContainerNameUniqueForOwnerContentType(
containerId: string,
newName: string,
containerType: UmbPropertyContainerTypes = 'Tab',
containerType: UmbPropertyContainerTypes,
parentId: string | null = null,
) {
const ownerRootContainers = this.getOwnerContainers(containerType, parentId); //getRootContainers() can't differentiates between compositions and locals
if (!ownerRootContainers) {
return null;
}
let changedName = newName;
if (ownerRootContainers) {
while (ownerRootContainers.find((tab) => tab.name === changedName && tab.id !== parentId)) {
changedName = incrementString(changedName);
}
return changedName === newName ? null : changedName;
while (ownerRootContainers.find((con) => con.name === changedName && con.id !== containerId)) {
changedName = incrementString(changedName);
}
return null;
return changedName === newName ? null : changedName;
}
async updateContainer(

View File

@@ -6,7 +6,7 @@ export type UmbPropertyContainerTypes = 'Group' | 'Tab';
export interface UmbPropertyTypeContainerModel {
id: string;
parent: { id: string } | null; // TODO: change to unique
name: string | null;
name: string;
type: UmbPropertyContainerTypes;
sortOrder: number;
}

View File

@@ -1,6 +1,6 @@
import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { css, html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement, umbFocus } from '@umbraco-cms/backoffice/lit-element';
import { css, html, customElement, property, state, nothing } from '@umbraco-cms/backoffice/external/lit';
import type {
UmbContentTypeContainerStructureHelper,
UmbContentTypeModel,
@@ -92,11 +92,24 @@ export class UmbContentTypeWorkspaceViewEditGroupElement extends UmbLitElement {
let newName = (e.target as HTMLInputElement).value;
const changedName = this.groupStructureHelper
.getStructureManager()!
.makeContainerNameUniqueForOwnerContentType(newName, 'Group', this._group.parent?.id ?? null);
.makeContainerNameUniqueForOwnerContentType(this._group.id, newName, 'Group', this._group.parent?.id ?? null);
if (changedName) {
newName = changedName;
}
this._singleValueUpdate('name', newName);
(e.target as HTMLInputElement).value = newName;
}
#blurGroup(e: InputEvent) {
if (!this.groupStructureHelper || !this._group) return;
const newName = (e.target as HTMLInputElement).value;
if (newName === '') {
const changedName = this.groupStructureHelper
.getStructureManager()!
.makeEmptyContainerName(this._group.id, 'Group', this._group.parent?.id ?? null);
this._singleValueUpdate('name', changedName);
(e.target as HTMLInputElement).value = changedName;
}
}
render() {
@@ -118,9 +131,11 @@ export class UmbContentTypeWorkspaceViewEditGroupElement extends UmbLitElement {
<uui-input
label=${this.localize.term('contentTypeEditor_group')}
placeholder=${this.localize.term('placeholders_entername')}
.value=${this.group!.name}
.value=${this._group!.name}
?disabled=${!this._hasOwnerContainer}
@change=${this.#renameGroup}></uui-input>
@change=${this.#renameGroup}
@blur=${this.#blurGroup}
${this._group!.name === '' ? umbFocus() : nothing}></uui-input>
</div>
${this.sortModeActive
? html` <uui-input

View File

@@ -116,14 +116,22 @@ export class UmbContentTypeDesignEditorTabElement extends UmbLitElement {
'_observeIsSorting',
);
});
this.observe(this.#groupStructureHelper.mergedContainers, (groups) => {
this._groups = groups;
this.#sorter.setModel(this._groups);
});
this.observe(this.#groupStructureHelper.hasProperties, (hasProperties) => {
this._hasProperties = hasProperties;
this.requestUpdate('_hasProperties');
});
this.observe(
this.#groupStructureHelper.mergedContainers,
(groups) => {
this._groups = groups;
this.#sorter.setModel(this._groups);
},
null,
);
this.observe(
this.#groupStructureHelper.hasProperties,
(hasProperties) => {
this._hasProperties = hasProperties;
this.requestUpdate('_hasProperties');
},
null,
);
}
#onAddGroup = () => {

View File

@@ -292,9 +292,9 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements
let newName = (event.target as HTMLInputElement).value;
const changedName = this.#workspaceContext?.structure.makeContainerNameUniqueForOwnerContentType(
tab.id,
newName,
'Tab',
tab.id,
);
// Check if it collides with another tab name of this same content-type, if so adjust name:
@@ -309,7 +309,19 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements
});
}
async #tabNameBlur() {
async #tabNameBlur(event: FocusEvent, tab: UmbPropertyTypeContainerModel) {
if (!this._activeTabId) return;
const newName = (event.target as HTMLInputElement | undefined)?.value;
if (newName === '') {
const changedName = this.#workspaceContext!.structure.makeEmptyContainerName(this._activeTabId, 'Tab');
(event.target as HTMLInputElement).value = changedName;
this.#tabsStructureHelper.partialUpdateContainer(tab.id!, {
name: changedName,
});
}
this._activeTabId = undefined;
}
@@ -476,7 +488,7 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements
auto-width
@change=${(e: InputEvent) => this.#tabNameChanged(e, tab)}
@input=${(e: InputEvent) => this.#tabNameChanged(e, tab)}
@blur=${() => this.#tabNameBlur()}>
@blur=${(e: FocusEvent) => this.#tabNameBlur(e, tab)}>
${this.renderDeleteFor(tab)}
</uui-input>
</div>`;

View File

@@ -1,4 +1,5 @@
import type { UmbEntityAction } from '../entity-action.interface.js';
import type { UmbEntityActionElement } from '../entity-action-element.interface.js';
import { UmbActionExecutedEvent } from '@umbraco-cms/backoffice/event';
import { html, nothing, ifDefined, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
import type { UUIMenuItemEvent } from '@umbraco-cms/backoffice/external/uui';
@@ -6,10 +7,13 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { ManifestEntityAction, MetaEntityActionDefaultKind } from '@umbraco-cms/backoffice/extension-registry';
@customElement('umb-entity-action')
export class UmbEntityActionElement<
MetaType extends MetaEntityActionDefaultKind = MetaEntityActionDefaultKind,
ApiType extends UmbEntityAction<MetaType> = UmbEntityAction<MetaType>,
> extends UmbLitElement {
export class UmbEntityActionDefaultElement<
MetaType extends MetaEntityActionDefaultKind = MetaEntityActionDefaultKind,
ApiType extends UmbEntityAction<MetaType> = UmbEntityAction<MetaType>,
>
extends UmbLitElement
implements UmbEntityActionElement
{
#api?: ApiType;
// TODO: Do these need to be properties? [NL]
@@ -36,6 +40,11 @@ export class UmbEntityActionElement<
@state()
_href?: string;
async focus() {
await this.updateComplete;
this.shadowRoot?.querySelector('uui-menu-item')?.focus();
}
async #onClickLabel(event: UUIMenuItemEvent) {
if (!this._href) {
event.stopPropagation();
@@ -66,10 +75,10 @@ export class UmbEntityActionElement<
`;
}
}
export default UmbEntityActionElement;
export default UmbEntityActionDefaultElement;
declare global {
interface HTMLElementTagNameMap {
'umb-entity-action': UmbEntityActionElement;
'umb-entity-action': UmbEntityActionDefaultElement;
}
}

View File

@@ -0,0 +1,3 @@
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
export interface UmbEntityActionElement extends UmbControllerHostElement {}

View File

@@ -30,6 +30,7 @@ export class UmbEntityActionListElement extends UmbLitElement {
return this._props.unique;
}
public set unique(value: string | null | undefined) {
if (value === this._props.unique) return;
this._props.unique = value;
this.#generateApiArgs();
this.requestUpdate('_props');
@@ -51,12 +52,14 @@ export class UmbEntityActionListElement extends UmbLitElement {
this.#entityContext.setEntityType(this._props.entityType);
this.#entityContext.setUnique(this._props.unique);
this.#hasRenderedOnce = false;
this._apiArgs = (manifest: ManifestEntityAction<MetaEntityAction>) => {
return [{ entityType: this._props.entityType!, unique: this._props.unique!, meta: manifest.meta }];
};
}
#hasRenderedOnce?: boolean;
render() {
return this._filter
? html`
@@ -64,7 +67,22 @@ export class UmbEntityActionListElement extends UmbLitElement {
type="entityAction"
.filter=${this._filter}
.elementProps=${this._props}
.apiArgs=${this._apiArgs}></umb-extension-with-api-slot>
.apiArgs=${this._apiArgs}
.renderMethod=${(ext: any, i: number) => {
if (!this.#hasRenderedOnce && i === 0) {
// TODO: Replace this block:
ext.component?.updateComplete.then(async () => {
const menuitem = ext.component?.shadowRoot?.querySelector('uui-menu-item');
menuitem?.updateComplete.then(async () => {
menuitem?.shadowRoot?.querySelector('#label-button')?.focus?.();
});
});
// end of block, with this, when this PR is part of UI Lib: https://github.com/umbraco/Umbraco.UI/pull/789
// ext.component?.focus();
this.#hasRenderedOnce = true;
}
return ext.component;
}}></umb-extension-with-api-slot>
`
: '';
}

View File

@@ -4,6 +4,7 @@ export * from './entity-action-list.element.js';
export * from './entity-action.event.js';
export * from './entity-action.interface.js';
export * from './types.js';
export type * from './entity-action-element.interface.js';
export { UmbRequestReloadStructureForEntityEvent } from './request-reload-structure-for-entity.event.js';
export { UMB_ENTITY_ACTION_DEFAULT_KIND_MANIFEST } from './default/default.action.kind.js';

View File

@@ -1,6 +1,5 @@
import type { ConditionTypes } from '../conditions/types.js';
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import type { UmbEntityAction } from '@umbraco-cms/backoffice/entity-action';
import type { UmbEntityAction, UmbEntityActionElement } from '@umbraco-cms/backoffice/entity-action';
import type { ManifestElementAndApi, ManifestWithDynamicConditions } from '@umbraco-cms/backoffice/extension-api';
import type { UmbModalToken, UmbPickerModalData, UmbPickerModalValue } from '@umbraco-cms/backoffice/modal';
@@ -9,7 +8,7 @@ import type { UmbModalToken, UmbPickerModalData, UmbPickerModalValue } from '@um
* For example for content you may wish to create a new document etc
*/
export interface ManifestEntityAction<MetaType extends MetaEntityAction = MetaEntityAction>
extends ManifestElementAndApi<UmbControllerHostElement, UmbEntityAction<MetaType>>,
extends ManifestElementAndApi<UmbEntityActionElement, UmbEntityAction<MetaType>>,
ManifestWithDynamicConditions<ConditionTypes> {
type: 'entityAction';
forEntityTypes: Array<string>;

View File

@@ -0,0 +1,6 @@
import type { UmbIconDictionary } from '@umbraco-cms/backoffice/icon';
import type { ManifestPlainJs } from '@umbraco-cms/backoffice/extension-api';
export interface ManifestIcons extends ManifestPlainJs<{ default: UmbIconDictionary }> {
type: 'icons';
}

View File

@@ -27,6 +27,8 @@ import type { ManifestExternalLoginProvider } from './external-login-provider.mo
import type { ManifestGlobalContext } from './global-context.model.js';
import type { ManifestHeaderApp, ManifestHeaderAppButtonKind } from './header-app.model.js';
import type { ManifestHealthCheck } from './health-check.model.js';
import type { ManifestIcons } from './icons.model.js';
import type { ManifestLocalization } from './localization.model.js';
import type { ManifestMenu } from './menu.model.js';
import type { ManifestMenuItem, ManifestMenuItemTreeKind } from './menu-item.model.js';
import type { ManifestModal } from './modal.model.js';
@@ -40,7 +42,6 @@ import type { ManifestSectionView } from './section-view.model.js';
import type { ManifestStore, ManifestTreeStore, ManifestItemStore } from './store.model.js';
import type { ManifestTheme } from './theme.model.js';
import type { ManifestTinyMcePlugin } from './tinymce-plugin.model.js';
import type { ManifestLocalization } from './localization.model.js';
import type { ManifestTree } from './tree.model.js';
import type { ManifestTreeItem } from './tree-item.model.js';
import type { ManifestUserProfileApp } from './user-profile-app.model.js';
@@ -66,6 +67,7 @@ import type { ManifestBackofficeEntryPoint } from './backoffice-entry-point.mode
import type { ManifestEntryPoint } from './entry-point.model.js';
import type { ManifestBase, ManifestBundle, ManifestCondition } from '@umbraco-cms/backoffice/extension-api';
export type * from './app-entry-point.model.js';
export type * from './auth-provider.model.js';
export type * from './backoffice-entry-point.model.js';
export type * from './block-editor-custom-view.model.js';
@@ -84,6 +86,7 @@ export type * from './external-login-provider.model.js';
export type * from './global-context.model.js';
export type * from './header-app.model.js';
export type * from './health-check.model.js';
export type * from './icons.model.js';
export type * from './localization.model.js';
export type * from './menu-item.model.js';
export type * from './menu.model.js';
@@ -109,7 +112,6 @@ export type * from './workspace-context.model.js';
export type * from './workspace-footer-app.model.js';
export type * from './workspace-view.model.js';
export type * from './workspace.model.js';
export type * from './app-entry-point.model.js';
export type ManifestEntityActions =
| ManifestEntityAction
@@ -163,6 +165,7 @@ export type ManifestTypes =
| ManifestHeaderApp
| ManifestHeaderAppButtonKind
| ManifestHealthCheck
| ManifestIcons
| ManifestItemStore
| ManifestMenu
| ManifestMenuItem

View File

@@ -0,0 +1,4 @@
import type { UmbIconRegistryContext } from './icon-registry.context.js';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
export const UMB_ICON_REGISTRY_CONTEXT = new UmbContextToken<UmbIconRegistryContext>('UmbIconRegistryContext');

View File

@@ -0,0 +1,49 @@
import { UmbIconRegistry } from './icon.registry.js';
import type { UmbIconDefinition } from './types.js';
import { UMB_ICON_REGISTRY_CONTEXT } from './icon-registry.context-token.js';
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { loadManifestPlainJs } from '@umbraco-cms/backoffice/extension-api';
import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
import { type ManifestIcons, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
export class UmbIconRegistryContext extends UmbContextBase<UmbIconRegistryContext> {
#registry: UmbIconRegistry;
#manifestMap = new Map();
#icons = new UmbArrayState<UmbIconDefinition>([], (x) => x.name);
readonly icons = this.#icons.asObservable();
readonly approvedIcons = this.#icons.asObservablePart((icons) => icons.filter((x) => x.legacy !== true));
constructor(host: UmbControllerHost) {
super(host, UMB_ICON_REGISTRY_CONTEXT);
this.#registry = new UmbIconRegistry();
this.#registry.attach(host.getHostElement());
this.observe(this.icons, (icons) => {
//if (icons.length > 0) {
this.#registry.setIcons(icons);
//}
});
this.observe(umbExtensionsRegistry.byType('icons'), (manifests) => {
manifests.forEach((manifest) => {
if (this.#manifestMap.has(manifest.alias)) return;
this.#manifestMap.set(manifest.alias, manifest);
// TODO: Should we unInit a entry point if is removed?
this.instantiateEntryPoint(manifest);
});
});
}
async instantiateEntryPoint(manifest: ManifestIcons) {
if (manifest.js) {
const js = await loadManifestPlainJs<{ default?: any }>(manifest.js);
if (!js || !js.default || !Array.isArray(js.default)) {
throw new Error('Icon manifest JS-file must export an array of icons as the default export.');
}
this.#icons.append(js.default);
}
}
}
export { UmbIconRegistryContext as api };

View File

@@ -1,5 +1,4 @@
import icons from './icons/icons.json' assert { type: 'json' };
import { UUIIconRegistry } from '@umbraco-cms/backoffice/external/uui';
import { type UUIIconHost, UUIIconRegistry } from '@umbraco-cms/backoffice/external/uui';
interface UmbIconDescriptor {
name: string;
@@ -13,26 +12,66 @@ interface UmbIconDescriptor {
* @description - Icon Registry. Provides icons from the icon manifest. Icons are loaded on demand. All icons are prefixed with 'icon-'
*/
export class UmbIconRegistry extends UUIIconRegistry {
#initResolve?: () => void;
#init: Promise<void> = new Promise((resolve) => {
this.#initResolve = resolve;
});
#icons: UmbIconDescriptor[] = [];
#unhandledProviders: Map<string, UUIIconHost> = new Map();
setIcons(icons: UmbIconDescriptor[]) {
const oldIcons = this.#icons;
this.#icons = icons;
if (this.#initResolve) {
this.#initResolve();
this.#initResolve = undefined;
}
// Go figure out which of the icons are new.
const newIcons = this.#icons.filter((i) => !oldIcons.find((o) => o.name === i.name));
newIcons.forEach((icon) => {
// Do we already have a request for this one, then lets initiate the load for those:
const unhandled = this.#unhandledProviders.get(icon.name);
if (unhandled) {
this.#loadIcon(icon.name, unhandled).then(() => {
this.#unhandledProviders.delete(icon.name);
});
}
});
}
appendIcons(icons: UmbIconDescriptor[]) {
this.#icons = [...this.#icons, ...icons];
}
/**
* @param {string} iconName
* @return {*} {boolean}
* @memberof UmbIconStore
*/
acceptIcon(iconName: string): boolean {
const iconManifest = icons.find((i: UmbIconDescriptor) => i.name === iconName);
if (!iconManifest) return false;
const iconProvider = this.provideIcon(iconName);
this.#loadIcon(iconName, iconProvider);
return true;
}
async #loadIcon(iconName: string, iconProvider: UUIIconHost): Promise<boolean> {
await this.#init;
const iconManifest = this.#icons.find((i: UmbIconDescriptor) => i.name === iconName);
// Icon not found, so lets add it to a list of unhandled requests.
if (!iconManifest) {
this.#unhandledProviders.set(iconName, iconProvider);
return false;
}
const icon = this.provideIcon(iconName);
const iconPath = iconManifest.path;
import(/* @vite-ignore */ iconPath)
.then((iconModule) => {
icon.svg = iconModule.default;
iconProvider.svg = iconModule.default;
})
.catch((err) => {
console.error(`Failed to load icon ${iconName} on path ${iconPath}`, err.message);
});
return true;
}
}

View File

@@ -1,5 +1,5 @@
import type { Meta, Story } from '@storybook/web-components';
import icons from './icons/icons.json';
import icons from './icons/icons.js';
import { html, repeat } from '@umbraco-cms/backoffice/external/lit';
export default {

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1,4 @@
export * from './icon-registry.context-token.js';
export * from './icon-registry.context.js';
export * from './icon.registry.js';
export * from './types.js';

View File

@@ -0,0 +1,14 @@
export const manifests = [
{
type: 'icons',
alias: 'Umb.Icons.Backoffice',
name: 'Backoffice Icons',
js: () => import('./icons/icons.js'),
},
{
type: 'globalContext',
alias: 'Umb.GlobalContext.Icons',
name: 'Icons Context',
api: () => import('./icon-registry.context.js'),
},
];

View File

@@ -0,0 +1,7 @@
export interface UmbIconDefinition {
name: string;
path: string;
legacy?: boolean;
}
export type UmbIconDictionary = Array<UmbIconDefinition>;

View File

@@ -6,6 +6,7 @@ import { manifests as cultureManifests } from './culture/manifests.js';
import { manifests as debugManifests } from './debug/manifests.js';
import { manifests as entityActionManifests } from './entity-action/manifests.js';
import { manifests as extensionManifests } from './extension-registry/manifests.js';
import { manifests as iconRegistryManifests } from './icon-registry/manifests.js';
import { manifests as localizationManifests } from './localization/manifests.js';
import { manifests as modalManifests } from './modal/common/manifests.js';
import { manifests as propertyActionManifests } from './property-action/manifests.js';
@@ -23,6 +24,7 @@ import type { ManifestTypes, UmbBackofficeManifestKind } from './extension-regis
export const manifests: Array<ManifestTypes | UmbBackofficeManifestKind> = [
...authManifests,
...extensionManifests,
...iconRegistryManifests,
...cultureManifests,
...localizationManifests,
...themeManifests,

View File

@@ -1,7 +1,7 @@
import { html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { UmbConfirmModalData, UmbConfirmModalValue, UmbModalContext } from '@umbraco-cms/backoffice/modal';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbLitElement, umbFocus } from '@umbraco-cms/backoffice/lit-element';
@customElement('umb-confirm-modal')
export class UmbConfirmModalElement extends UmbLitElement {
@@ -31,7 +31,8 @@ export class UmbConfirmModalElement extends UmbLitElement {
color="${this.data?.color || 'positive'}"
look="primary"
label="${this.data?.confirmLabel || 'Confirm'}"
@click=${this._handleConfirm}></uui-button>
@click=${this._handleConfirm}
${umbFocus()}></uui-button>
</uui-dialog-layout>
`;
}

View File

@@ -1,81 +1,97 @@
import icons from '../../../icon-registry/icons/icons.json' assert { type: 'json' };
import type { UUIColorSwatchesEvent } from '@umbraco-cms/backoffice/external/uui';
import type { UUIColorSwatchesEvent, UUIIconElement } from '@umbraco-cms/backoffice/external/uui';
import { css, html, customElement, state, repeat } from '@umbraco-cms/backoffice/external/lit';
import { css, html, customElement, state, repeat, query, nothing } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { UmbIconPickerModalData, UmbIconPickerModalValue } from '@umbraco-cms/backoffice/modal';
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
import { extractUmbColorVariable, umbracoColors } from '@umbraco-cms/backoffice/resources';
import { umbFocus } from '@umbraco-cms/backoffice/lit-element';
import { UMB_ICON_REGISTRY_CONTEXT, type UmbIconDefinition } from '@umbraco-cms/backoffice/icon';
// TODO: Make use of UmbPickerLayoutBase
// TODO: to prevent element extension we need to move the Picker logic into a separate class we can reuse across all pickers
@customElement('umb-icon-picker-modal')
export class UmbIconPickerModalElement extends UmbModalBaseElement<UmbIconPickerModalData, UmbIconPickerModalValue> {
private _iconList = icons.filter((icon) => !icon.legacy);
#icons?: Array<UmbIconDefinition>;
@query('#search')
private _searchInput?: HTMLInputElement;
@state()
private _iconListFiltered: Array<(typeof icons)[0]> = [];
private _iconsFiltered?: Array<UmbIconDefinition>;
@state()
private _colorList = umbracoColors;
private _colorList = umbracoColors.filter((color) => !color.legacy);
@state()
private _modalValue?: UmbIconPickerModalValue;
private _currentIcon?: string;
@state()
private _currentAlias = 'text';
private _currentColor = 'text';
#changeIcon(e: { target: HTMLInputElement; type: string; key: unknown }) {
if (e.type == 'click' || (e.type == 'keyup' && e.key == 'Enter')) {
this.modalContext?.updateValue({ icon: e.target.id });
}
constructor() {
super();
this.consumeContext(UMB_ICON_REGISTRY_CONTEXT, (context) => {
this.observe(context.approvedIcons, (icons) => {
this.#icons = icons;
this.#filterIcons();
});
});
}
#filterIcons(e: { target: HTMLInputElement }) {
if (e.target.value) {
this._iconListFiltered = this._iconList.filter((icon) =>
icon.name.toLowerCase().includes(e.target.value.toLowerCase()),
);
#filterIcons() {
if (!this.#icons) return;
const value = this._searchInput?.value;
if (value) {
this._iconsFiltered = this.#icons.filter((icon) => icon.name.toLowerCase().includes(value.toLowerCase()));
} else {
this._iconListFiltered = this._iconList;
this._iconsFiltered = this.#icons;
}
}
#onColorChange(e: UUIColorSwatchesEvent) {
this.modalContext?.updateValue({ color: e.target.value });
this._currentAlias = e.target.value;
}
connectedCallback() {
super.connectedCallback();
this._iconListFiltered = this._iconList;
this._iconsFiltered = this.#icons;
if (this.modalContext) {
this.observe(
this.modalContext?.value,
(newValue) => {
this._modalValue = newValue;
this._currentAlias = newValue?.color ?? 'text';
this._currentIcon = newValue?.icon;
this._currentColor = newValue?.color ?? 'text';
},
'_observeModalContextValue',
);
}
}
#changeIcon(e: InputEvent | KeyboardEvent) {
if (e.type == 'click' || (e.type == 'keyup' && (e as KeyboardEvent).key == 'Enter')) {
const iconName = (e.target as UUIIconElement).name;
if (iconName) {
this.modalContext?.updateValue({ icon: iconName });
}
}
}
#onColorChange(e: UUIColorSwatchesEvent) {
const colorAlias = e.target.value;
this.modalContext?.updateValue({ color: colorAlias });
this._currentColor = colorAlias;
}
render() {
// TODO: Missing localization in general. [NL]
return html`
<umb-body-layout headline="Select Icon">
<div id="container">
${this.renderSearchbar()}
${this.renderSearch()}
<hr />
<uui-color-swatches
.value=${this._currentAlias}
.value=${this._currentColor}
label="Color switcher for icons"
@change=${this.#onColorChange}>
${
// TODO: Missing translation for the color aliases.
// TODO: Missing localization for the color aliases. [NL]
this._colorList.map(
(color) => html`
<uui-color-swatch
@@ -88,7 +104,7 @@ export class UmbIconPickerModalElement extends UmbModalBaseElement<UmbIconPicker
}
</uui-color-swatches>
<hr />
<uui-scroll-container id="icon-selection">${this.renderIconSelection()}</uui-scroll-container>
<uui-scroll-container id="icons">${this.renderIcons()}</uui-scroll-container>
</div>
<uui-button
slot="actions"
@@ -104,36 +120,37 @@ export class UmbIconPickerModalElement extends UmbModalBaseElement<UmbIconPicker
`;
}
renderSearchbar() {
renderSearch() {
return html` <uui-input
type="search"
placeholder=${this.localize.term('placeholders_filter')}
label=${this.localize.term('placeholders_filter')}
id="searchbar"
id="search"
@keyup="${this.#filterIcons}"
${umbFocus()}>
<uui-icon name="search" slot="prepend" id="searchbar_icon"></uui-icon>
<uui-icon name="search" slot="prepend" id="search_icon"></uui-icon>
</uui-input>`;
}
renderIconSelection() {
return repeat(
this._iconListFiltered,
(icon) => icon.name,
(icon) => html`
<uui-icon
tabindex="0"
style="--uui-icon-color: var(${extractUmbColorVariable(this._currentAlias)})"
class="icon ${icon.name === this._modalValue?.icon ? 'selected' : ''}"
title="${icon.name}"
name="${icon.name}"
label="${icon.name}"
id="${icon.name}"
@click="${this.#changeIcon}"
@keyup="${this.#changeIcon}">
</uui-icon>
`,
);
renderIcons() {
return this._iconsFiltered
? repeat(
this._iconsFiltered,
(icon) => icon.name,
(icon) => html`
<uui-button
label="${icon.name}"
class="${icon.name === this._currentIcon ? 'selected' : ''}"
@click="${this.#changeIcon}"
@keyup="${this.#changeIcon}">
<uui-icon
style="--uui-icon-color: var(${extractUmbColorVariable(this._currentColor)})"
name="${icon.name}">
</uui-icon>
</uui-button>
`,
)
: nothing;
}
static styles = [
@@ -160,15 +177,15 @@ export class UmbIconPickerModalElement extends UmbModalBaseElement<UmbIconPicker
margin: 20px 0;
}
#searchbar {
#search {
width: 100%;
align-items: center;
}
#searchbar_icon {
#search_icon {
padding-left: var(--uui-size-space-2);
}
#icon-selection {
#icons {
line-height: 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(40px, calc((100% / 12) - 10px)));
@@ -179,27 +196,17 @@ export class UmbIconPickerModalElement extends UmbModalBaseElement<UmbIconPicker
padding: 2px;
}
#icon-selection .icon {
display: inline-block;
#icons uui-button {
border-radius: var(--uui-border-radius);
width: 100%;
height: 100%;
padding: var(--uui-size-space-3);
box-sizing: border-box;
cursor: pointer;
font-size: 16px; /* specific for icons */
}
#icon-selection .icon-container {
display: inline-block;
}
#icon-selection .icon:focus,
#icon-selection .icon:hover,
#icon-selection .icon.selected {
#icons uui-button:focus,
#icons uui-button:hover,
#icons uui-button.selected {
outline: 2px solid var(--uui-color-selected);
}
uui-button {
uui-button[slot='actions'] {
margin-left: var(--uui-size-space-4);
}

View File

@@ -122,7 +122,7 @@ export class UmbModalContext<ModalPreset extends object = object, ModalValue = a
* @memberof UmbModalContext
*/
public setValue(value: ModalValue) {
this.#value.update(value);
this.#value.setValue(value);
}
/**

View File

@@ -18,7 +18,7 @@ export const manifest: ManifestPropertyEditorSchema = {
alias: 'storageType',
label: 'Storage Type',
description: '',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.Dropdown',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.Select',
config: [
{
alias: 'items',

View File

@@ -6,18 +6,17 @@ import { manifest as dropdown } from './dropdown/manifests.js';
import { manifest as eyeDropper } from './eye-dropper/manifests.js';
import { manifest as iconPicker } from './icon-picker/manifests.js';
import { manifest as label } from './label/manifests.js';
import { manifest as memberPicker } from './member-picker/manifests.js';
import { manifest as multipleTextString } from './multiple-text-string/manifests.js';
import { manifest as multiUrlPicker } from './multi-url-picker/manifests.js';
import { manifest as numberRange } from './number-range/manifests.js';
import { manifest as orderDirection } from './order-direction/manifests.js';
import { manifest as overlaySize } from './overlay-size/manifests.js';
import { manifest as radioButtonList } from './radio-button-list/manifests.js';
import { manifest as select } from './select/manifests.js';
import { manifest as slider } from './slider/manifests.js';
import { manifest as textArea } from './textarea/manifests.js';
import { manifest as toggle } from './toggle/manifests.js';
import { manifest as uploadField } from './upload-field/manifests.js';
import { manifest as userPicker } from './user-picker/manifests.js';
import { manifest as valueType } from './value-type/manifests.js';
import { manifests as collectionView } from './collection-view/manifests.js';
import { manifests as numbers } from './number/manifests.js';
@@ -34,18 +33,17 @@ export const manifests: Array<ManifestPropertyEditorUi> = [
eyeDropper,
iconPicker,
label,
memberPicker,
multipleTextString,
multiUrlPicker,
numberRange,
orderDirection,
overlaySize,
radioButtonList,
select,
slider,
textArea,
toggle,
uploadField,
userPicker,
valueType,
...collectionView,
...numbers,

View File

@@ -1,14 +0,0 @@
import type { ManifestPropertyEditorUi } from '@umbraco-cms/backoffice/extension-registry';
export const manifest: ManifestPropertyEditorUi = {
type: 'propertyEditorUi',
alias: 'Umb.PropertyEditorUi.MemberPicker',
name: 'Member Picker Property Editor UI',
element: () => import('./property-editor-ui-member-picker.element.js'),
meta: {
label: 'Member Picker',
propertyEditorSchemaAlias: 'Umbraco.MemberPicker',
icon: 'icon-user',
group: 'people',
},
};

View File

@@ -0,0 +1,22 @@
import type { ManifestPropertyEditorUi } from '@umbraco-cms/backoffice/extension-registry';
export const manifest: ManifestPropertyEditorUi = {
type: 'propertyEditorUi',
alias: 'Umb.PropertyEditorUi.Select',
name: 'Select Property Editor UI',
element: () => import('./property-editor-ui-select.element.js'),
meta: {
label: 'Select',
icon: 'icon-list',
group: 'pickers',
settings: {
properties: [
{
alias: 'items',
label: 'Add options',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.MultipleTextString',
},
],
},
},
};

View File

@@ -0,0 +1,42 @@
import { html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor';
import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry';
import type { UUISelectEvent } from '@umbraco-cms/backoffice/external/uui';
/**
* @element umb-property-editor-ui-select
*/
@customElement('umb-property-editor-ui-select')
export class UmbPropertyEditorUISelectElement extends UmbLitElement implements UmbPropertyEditorUiElement {
@property()
value?: string = '';
@state()
private _list: Array<Option> = [];
public set config(config: UmbPropertyEditorConfigCollection | undefined) {
if (!config) return;
const listData = config.getValueByAlias<string[]>('items');
this._list = listData?.map((option) => ({ value: option, name: option, selected: option === this.value })) ?? [];
}
#onChange(event: UUISelectEvent) {
this.value = event.target.value as string;
this.dispatchEvent(new UmbPropertyValueChangeEvent());
}
render() {
return html`<uui-select .options=${this._list} @change=${this.#onChange}></uui-select>`;
}
}
export default UmbPropertyEditorUISelectElement;
declare global {
interface HTMLElementTagNameMap {
'umb-property-editor-ui-select': UmbPropertyEditorUISelectElement;
}
}

View File

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

View File

@@ -0,0 +1,21 @@
import { expect, fixture, html } from '@open-wc/testing';
import { UmbPropertyEditorUISelectElement } from './property-editor-ui-select.element.js';
import { type UmbTestRunnerWindow, defaultA11yConfig } from '@umbraco-cms/internal/test-utils';
describe('UmbPropertyEditorUISelectElement', () => {
let element: UmbPropertyEditorUISelectElement;
beforeEach(async () => {
element = await fixture(html` <umb-property-editor-ui-select></umb-property-editor-ui-select> `);
});
it('is defined with its own instance', () => {
expect(element).to.be.instanceOf(UmbPropertyEditorUISelectElement);
});
if ((window as UmbTestRunnerWindow).__UMBRACO_TEST_RUN_A11Y_TEST) {
it('passes the a11y audit', async () => {
await expect(element).shadowDom.to.be.accessible(defaultA11yConfig);
});
}
});

View File

@@ -1,14 +0,0 @@
import type { ManifestPropertyEditorUi } from '@umbraco-cms/backoffice/extension-registry';
export const manifest: ManifestPropertyEditorUi = {
type: 'propertyEditorUi',
alias: 'Umb.PropertyEditorUi.UserPicker',
name: 'User Picker Property Editor UI',
element: () => import('./property-editor-ui-user-picker.element.js'),
meta: {
label: 'User Picker',
propertyEditorSchemaAlias: 'Umbraco.UserPicker',
icon: 'icon-user',
group: 'people',
},
};

View File

@@ -1,13 +1,25 @@
export const umbracoColors = [
{ alias: 'text', varName: '--uui-color-text' },
{ alias: 'black', varName: '--uui-color-text' },
{ alias: 'yellow', varName: '--uui-palette-sunglow' },
{ alias: 'pink', varName: '--uui-palette-spanish-pink' },
{ alias: 'dark', varName: '--uui-palette-gunmetal' },
{ alias: 'darkblue', varName: '--uui-palette-space-cadet' },
{ alias: 'blue', varName: '--uui-palette-violet-blue' },
{ alias: 'light-blue', varName: '--uui-palette-malibu' },
{ alias: 'red', varName: '--uui-palette-maroon-flush' },
{ alias: 'green', varName: '--uui-palette-jungle-green' },
{ alias: 'brown', varName: '--uui-palette-chamoisee' },
{ alias: 'grey', varName: '--uui-palette-dusty-grey' },
{ alias: 'blue-grey', legacy: true, varName: '--uui-palette-dusty-grey' },
{ alias: 'indigo', legacy: true, varName: '--uui-palette-malibu' },
{ alias: 'purple', legacy: true, varName: '--uui-palette-space-cadet' },
{ alias: 'deep-purple', legacy: true, varName: '--uui-palette-space-cadet' },
{ alias: 'cyan', legacy: true, varName: '-uui-palette-jungle-green' },
{ alias: 'light-green', legacy: true, varName: '-uui-palette-jungle-green' },
{ alias: 'lime', legacy: true, varName: '-uui-palette-jungle-green' },
{ alias: 'amber', legacy: true, varName: '--uui-palette-chamoisee' },
{ alias: 'orange', legacy: true, varName: '--uui-palette-chamoisee' },
{ alias: 'deep-orange', legacy: true, varName: '--uui-palette-cocoa-brown' },
];
export function extractUmbColorVariable(colorAlias: string): string | undefined {

View File

@@ -86,7 +86,7 @@ export class UmbSectionDefaultElement extends UmbLitElement implements UmbSectio
(app) => app.component,
)}
</umb-section-sidebar>
`
`
: nothing}
<umb-section-main>
${this._routes && this._routes.length > 0
@@ -105,10 +105,6 @@ export class UmbSectionDefaultElement extends UmbLitElement implements UmbSectio
height: 100%;
display: flex;
}
h3 {
padding: var(--uui-size-4) var(--uui-size-8);
}
`,
];
}

View File

@@ -98,7 +98,7 @@ export class UmbSectionMainViewElement extends UmbLitElement {
</umb-router-slot>
</umb-body-layout>
`
: html`${nothing}`;
: nothing;
}
#renderDashboards() {
@@ -117,7 +117,7 @@ export class UmbSectionMainViewElement extends UmbLitElement {
})}
</uui-tab-group>
`
: '';
: nothing;
}
#renderViews() {
@@ -140,7 +140,7 @@ export class UmbSectionMainViewElement extends UmbLitElement {
})}
</uui-tab-group>
`
: '';
: nothing;
}
static styles = [

View File

@@ -23,6 +23,8 @@ export class UmbSubmitWorkspaceAction extends UmbWorkspaceActionBase<UmbSubmitta
// We can't save if we don't have a unique
if (unique === undefined) {
this.disable();
} else {
this.enable();
}
},
'saveWorkspaceActionUniqueObserver',

View File

@@ -1,8 +1,8 @@
import { UmbApi } from '@umbraco-cms/backoffice/extension-api';
import type { UmbDataTypeDetailModel } from '../../types.js';
import { UmbDataTypeServerDataSource } from './data-type-detail.server.data-source.js';
import type { UmbDataTypeDetailStore } from './data-type-detail.store.js';
import { UMB_DATA_TYPE_DETAIL_STORE_CONTEXT } from './data-type-detail.store.js';
import { UmbApi } from '@umbraco-cms/backoffice/extension-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbDetailRepositoryBase } from '@umbraco-cms/backoffice/repository';
export class UmbDataTypeDetailRepository extends UmbDetailRepositoryBase<UmbDataTypeDetailModel> {

View File

@@ -9,7 +9,7 @@ import type {
import { DocumentTypeService } from '@umbraco-cms/backoffice/external/backend-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
import type { UmbPropertyTypeContainerModel } from '@umbraco-cms/backoffice/content-type';
import type { UmbPropertyContainerTypes, UmbPropertyTypeContainerModel } from '@umbraco-cms/backoffice/content-type';
/**
* A data source for the Document Type that fetches data from the server
@@ -236,7 +236,15 @@ export class UmbDocumentTypeDetailServerDataSource implements UmbDetailDataSourc
appearance: property.appearance,
};
}),
containers: model.containers,
containers: model.containers.map((container) => {
return {
id: container.id,
parent: container.parent ? { id: container.parent.id } : null,
name: container.name ?? '',
type: container.type as UmbPropertyContainerTypes, // TODO: check if the value is valid
sortOrder: container.sortOrder,
};
}),
allowedDocumentTypes: model.allowedContentTypes.map((allowedContentType) => {
return {
documentType: { id: allowedContentType.contentType.unique },

View File

@@ -9,7 +9,7 @@ import type {
import { MediaTypeService } from '@umbraco-cms/backoffice/external/backend-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
import type { UmbPropertyTypeContainerModel } from '@umbraco-cms/backoffice/content-type';
import type { UmbPropertyContainerTypes, UmbPropertyTypeContainerModel } from '@umbraco-cms/backoffice/content-type';
/**
* A data source for the Media Type that fetches data from the server
@@ -100,7 +100,15 @@ export class UmbMediaTypeServerDataSource implements UmbDetailDataSource<UmbMedi
appearance: property.appearance,
};
}),
containers: data.containers as UmbPropertyTypeContainerModel[],
containers: data.containers.map((container) => {
return {
id: container.id,
parent: container.parent ? { id: container.parent.id } : null,
name: container.name ?? '',
type: container.type as UmbPropertyContainerTypes, // TODO: check if the value is valid
sortOrder: container.sortOrder,
};
}),
allowedContentTypes: data.allowedMediaTypes.map((allowedMediaType) => {
return {
contentType: { unique: allowedMediaType.mediaType.id },

View File

@@ -104,7 +104,7 @@ export class UmbMemberTypeServerDataSource implements UmbDetailDataSource<UmbMem
return {
id: container.id,
parent: container.parent ? { id: container.parent.id } : null,
name: container.name || null,
name: container.name ?? '',
type: container.type as UmbPropertyContainerTypes, // TODO: check if the value is valid
sortOrder: container.sortOrder,
};

View File

@@ -1,15 +1,17 @@
import { manifests as collectionManifests } from './collection/manifests.js';
import { manifests as entityActionManifests } from './entity-actions/manifests.js';
import { manifests as memberPickerModalManifests } from './components/member-picker-modal/manifests.js';
import { manifests as propertyEditorManifests } from './property-editor/manifests.js';
import { manifests as repositoryManifests } from './repository/manifests.js';
import { manifests as sectionViewManifests } from './section-view/manifests.js';
import { manifests as workspaceManifests } from './workspace/manifests.js';
import { manifests as collectionManifests } from './collection/manifests.js';
import { manifests as memberPickerModalManifests } from './components/member-picker-modal/manifests.js';
export const manifests = [
...collectionManifests,
...entityActionManifests,
...memberPickerModalManifests,
...propertyEditorManifests,
...repositoryManifests,
...sectionViewManifests,
...workspaceManifests,
...collectionManifests,
...memberPickerModalManifests,
];

View File

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

View File

@@ -0,0 +1,16 @@
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
export const manifests: Array<ManifestTypes> = [
{
type: 'propertyEditorUi',
alias: 'Umb.PropertyEditorUi.MemberPicker',
name: 'Member Picker Property Editor UI',
element: () => import('./property-editor-ui-member-picker.element.js'),
meta: {
label: 'Member Picker',
propertyEditorSchemaAlias: 'Umbraco.MemberPicker',
icon: 'icon-user',
group: 'people',
},
},
];

View File

@@ -1,54 +0,0 @@
import { UMB_CURRENT_USER_MFA_MODAL } from '../modals/current-user-mfa/current-user-mfa-modal.token.js';
import { html, customElement, state, nothing } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs';
@customElement('umb-mfa-providers-current-user-app')
export class UmbMfaProvidersCurrentUserAppElement extends UmbLitElement {
@state()
_hasProviders = false;
constructor() {
super();
this.#init();
}
async #init() {
this._hasProviders = (await firstValueFrom(umbExtensionsRegistry.byType('mfaLoginProvider'))).length > 0;
}
render() {
if (!this._hasProviders) {
return nothing;
}
return html`
<uui-button
type="button"
look="primary"
label="${this.localize.term('user_configureTwoFactor')}"
@click=${this.#onClick}>
<uui-icon name="icon-rectangle-ellipsis"></uui-icon>
<umb-localize key="user_configureTwoFactor">Configure Two Factor</umb-localize>
</uui-button>
`;
}
async #onClick() {
const modalManagerContext = await this.getContext(UMB_MODAL_MANAGER_CONTEXT);
await modalManagerContext.open(this, UMB_CURRENT_USER_MFA_MODAL).onSubmit();
}
static styles = [UmbTextStyles];
}
export default UmbMfaProvidersCurrentUserAppElement;
declare global {
interface HTMLElementTagNameMap {
'umb-mfa-providers-current-user-app': UmbMfaProvidersCurrentUserAppElement;
}
}

View File

@@ -79,9 +79,7 @@ export class UmbCurrentUserMfaModalElement extends UmbLitElement {
)}
</div>
<div slot="actions">
<uui-button @click=${this.#close} look="secondary" .label=${this.localize.term('general_close')}>
${this.localize.term('general_close')}
</uui-button>
<uui-button @click=${this.#close} look="secondary" .label=${this.localize.term('general_close')}></uui-button>
</div>
</umb-body-layout>
`;
@@ -98,7 +96,6 @@ export class UmbCurrentUserMfaModalElement extends UmbLitElement {
() => html`
<p style="margin-top:0">
<umb-localize key="user_2faProviderIsEnabled">This two-factor provider is enabled</umb-localize>
<uui-icon icon="check"></uui-icon>
</p>
<uui-button
type="button"

View File

@@ -1,21 +1,23 @@
import { manifests as collectionManifests } from './collection/manifests.js';
import { manifests as inviteManifests } from './invite/manifests.js';
import { manifests as repositoryManifests } from './repository/manifests.js';
import { manifests as workspaceManifests } from './workspace/manifests.js';
import { manifests as modalManifests } from './modals/manifests.js';
import { manifests as sectionViewManifests } from './section-view/manifests.js';
import { manifests as conditionsManifests } from './conditions/manifests.js';
import { manifests as entityActionsManifests } from './entity-actions/manifests.js';
import { manifests as entityBulkActionManifests } from './entity-bulk-actions/manifests.js';
import { manifests as conditionsManifests } from './conditions/manifests.js';
import { manifests as inviteManifests } from './invite/manifests.js';
import { manifests as modalManifests } from './modals/manifests.js';
import { manifests as repositoryManifests } from './repository/manifests.js';
import { manifests as sectionViewManifests } from './section-view/manifests.js';
import { manifests as propertyEditorManifests } from './property-editor/manifests.js';
import { manifests as workspaceManifests } from './workspace/manifests.js';
export const manifests = [
...collectionManifests,
...inviteManifests,
...repositoryManifests,
...workspaceManifests,
...modalManifests,
...sectionViewManifests,
...conditionsManifests,
...entityActionsManifests,
...entityBulkActionManifests,
...conditionsManifests,
...inviteManifests,
...modalManifests,
...repositoryManifests,
...sectionViewManifests,
...propertyEditorManifests,
...workspaceManifests,
];

View File

@@ -79,9 +79,7 @@ export class UmbUserMfaModalElement extends UmbLitElement {
)}
</div>
<div slot="actions">
<uui-button @click=${this.#close} look="secondary" .label=${this.localize.term('general_close')}>
${this.localize.term('general_close')}
</uui-button>
<uui-button @click=${this.#close} look="secondary" .label=${this.localize.term('general_close')}></uui-button>
</div>
</umb-body-layout>
`;
@@ -98,7 +96,6 @@ export class UmbUserMfaModalElement extends UmbLitElement {
() => html`
<p style="margin-top:0">
<umb-localize key="user_2faProviderIsEnabled">This two-factor provider is enabled</umb-localize>
<uui-icon icon="check"></uui-icon>
</p>
<uui-button
type="button"

View File

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

View File

@@ -0,0 +1,16 @@
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
export const manifests: Array<ManifestTypes> = [
{
type: 'propertyEditorUi',
alias: 'Umb.PropertyEditorUi.UserPicker',
name: 'User Picker Property Editor UI',
element: () => import('./property-editor-ui-user-picker.element.js'),
meta: {
label: 'User Picker',
propertyEditorSchemaAlias: 'Umbraco.UserPicker',
icon: 'icon-user',
group: 'people',
},
},
];