Merge remote-tracking branch 'origin/main' into v14/bugfix/correct-settings-data-depending-on-config

This commit is contained in:
Niels Lyngsø
2024-05-30 21:05:44 +02:00
54 changed files with 641 additions and 274 deletions

View File

@@ -48,6 +48,7 @@
"local-rules/prefer-import-aliases": "error",
"local-rules/prefer-static-styles-last": "warn",
"local-rules/umb-class-prefix": "error",
"local-rules/no-relative-import-to-import-map-module": "error",
"local-rules/enforce-umbraco-external-imports": [
"error",
{

View File

@@ -0,0 +1,56 @@
const fs = require('fs');
const path = require('path');
const getDirectories = (source) =>
fs
.readdirSync(source, { withFileTypes: true })
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name);
// TODO: get the correct list of modules. This is a temporary solution where we assume that a directory is equivalent to a module
// TODO: include package modules in this list
const coreRoot = path.join(__dirname, '../../../', 'src/packages/core');
const externalRoot = path.join(__dirname, '../../../', 'src/external');
const libsRoot = path.join(__dirname, '../../../', 'src/libs');
const coreModules = getDirectories(coreRoot).map((dir) => `/core/${dir}/`);
const externalModules = getDirectories(externalRoot).map((dir) => `/${dir}/`);
const libsModules = getDirectories(libsRoot).map((dir) => `/${dir}/`);
const modulePathIdentifiers = [...coreModules, ...externalModules, ...libsModules];
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Prevent relative import to a module that is in the import map.',
category: 'Best Practices',
recommended: true,
},
schema: [],
messages: {
unexpectedValue: 'Relative import paths should include "{{value}}".',
},
},
create: function (context) {
return {
ImportDeclaration(node) {
// exclude test and story files
if (context.filename.endsWith('.test.ts') || context.filename.endsWith('.stories.ts')) {
return {};
}
const importPath = node.source.value;
if (importPath.startsWith('./') || importPath.startsWith('../')) {
if (modulePathIdentifiers.some((moduleName) => importPath.includes(moduleName))) {
context.report({
node,
message: 'Use the correct import map alias instead of a relative import path: ' + importPath,
});
}
}
},
};
},
};

View File

@@ -26,7 +26,7 @@ const mainMap = buildMap(mainKeys);
const keys = Array.from(mainMap.keys());
const usedKeys = new Set();
const elementAndControllerFiles = await glob(`${__dirname}/../../src/**/*.ts`);
const elementAndControllerFiles = await glob(`${__dirname}/../../src/**/*.ts`, { filesOnly: true });
console.log(`Checking ${elementAndControllerFiles.length} files for unused keys`);

View File

@@ -10,6 +10,7 @@ const noDirectApiImportRule = require('./devops/eslint/rules/no-direct-api-impor
const preferImportAliasesRule = require('./devops/eslint/rules/prefer-import-aliases.cjs');
const preferStaticStylesLastRule = require('./devops/eslint/rules/prefer-static-styles-last.cjs');
const umbClassPrefixRule = require('./devops/eslint/rules/umb-class-prefix.cjs');
const noRelativeImportToImportMapModule = require('./devops/eslint/rules/no-relative-import-to-import-map-module.cjs');
module.exports = {
'bad-type-import': badTypeImportRule,
@@ -22,4 +23,5 @@ module.exports = {
'prefer-import-aliases': preferImportAliasesRule,
'prefer-static-styles-last': preferStaticStylesLastRule,
'umb-class-prefix': umbClassPrefixRule,
'no-relative-import-to-import-map-module': noRelativeImportToImportMapModule,
};

View File

@@ -3,7 +3,7 @@ import type {
UmbContextConsumerController,
UmbContextProviderController,
UmbContextToken,
} from '../context-api/index.js';
} from '@umbraco-cms/backoffice/context-api';
import type { UmbControllerAlias, UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type { ObserverCallback, UmbObserverController } from '@umbraco-cms/backoffice/observable-api';
import type { Observable } from '@umbraco-cms/backoffice/external/rxjs';

View File

@@ -1,7 +1,7 @@
import type { UmbContextToken } from '../context-api/index.js';
import type { UmbControllerHost } from '../controller-api/index.js';
import type { UmbContext } from './context.interface.js';
import { UmbControllerBase } from './controller-base.class.js';
import type { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
/**
* This base provides the necessary for a class to become a context-api controller.

View File

@@ -1,3 +1,3 @@
import type { UmbController } from '../controller-api/controller.interface.js';
import type { UmbController } from '@umbraco-cms/backoffice/controller-api';
export interface UmbContext extends UmbController {}

View File

@@ -1,5 +1,5 @@
import type { UmbController } from '../controller-api/controller.interface.js';
import { UmbClassMixin } from './class.mixin.js';
import type { UmbController } from '@umbraco-cms/backoffice/controller-api';
import type { ClassConstructor } from '@umbraco-cms/backoffice/extension-api';
/**

View File

@@ -1,6 +1,6 @@
import type { ClassConstructor } from '../extension-api/types/utils.js';
import type { UmbControllerHost } from './controller-host.interface.js';
import type { UmbController } from './controller.interface.js';
import type { ClassConstructor } from '@umbraco-cms/backoffice/extension-api';
interface UmbControllerHostBaseDeclaration extends Omit<UmbControllerHost, 'getHostElement'> {
hostConnected(): void;

View File

@@ -1,4 +1,4 @@
import type { UmbControllerHostElement } from '../controller-api/controller-host-element.interface.js';
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import type { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api';
import type { UmbClassInterface } from '@umbraco-cms/backoffice/class-api';

View File

@@ -153,15 +153,15 @@ export abstract class UmbBlockEntryContext<
// Consume block manager:
this.consumeContext(blockManagerContextToken, (manager) => {
this._manager = manager;
this.#gotManager();
this._gotManager();
this.#gotManager();
});
// Consume block entries:
this.consumeContext(blockEntriesContextToken, (entries) => {
this._entries = entries;
this.#gotEntries();
this._gotEntries();
this.#gotEntries();
});
// Observe UDI:

View File

@@ -6,7 +6,7 @@ import { type UUIInputElement, UUIInputEvent } from '@umbraco-cms/backoffice/ext
import { generateAlias } from '@umbraco-cms/backoffice/utils';
@customElement('umb-input-with-alias')
export class UmbInputWithAliasElement extends UmbFormControlMixin<string>(UmbLitElement) {
export class UmbInputWithAliasElement extends UmbFormControlMixin<string, typeof UmbLitElement>(UmbLitElement) {
@property({ type: String })
label: string = '';
@@ -31,36 +31,37 @@ export class UmbInputWithAliasElement extends UmbFormControlMixin<string>(UmbLit
}
#onNameChange(e: UUIInputEvent) {
if (e instanceof UUIInputEvent) {
const target = e.composedPath()[0] as UUIInputElement;
if (!(e instanceof UUIInputEvent)) return;
if (typeof target?.value === 'string') {
const oldName = this.value;
const oldAlias = this.alias ?? '';
this.value = e.target.value.toString();
if (this.autoGenerateAlias && this._aliasLocked) {
// If locked we will update the alias, but only if it matches the generated alias of the old name [NL]
const expectedOldAlias = generateAlias(oldName ?? '');
// Only update the alias if the alias matches a generated alias of the old name (otherwise the alias is considered one written by the user.) [NL]
if (expectedOldAlias === oldAlias) {
this.alias = generateAlias(this.value);
}
const target = e.composedPath()[0] as UUIInputElement;
if (typeof target?.value === 'string') {
const oldName = this.value;
const oldAlias = this.alias ?? '';
this.value = e.target.value.toString();
if (this.autoGenerateAlias && this._aliasLocked) {
// If locked we will update the alias, but only if it matches the generated alias of the old name [NL]
const expectedOldAlias = generateAlias(oldName ?? '');
// Only update the alias if the alias matches a generated alias of the old name (otherwise the alias is considered one written by the user.) [NL]
if (expectedOldAlias === oldAlias) {
this.alias = generateAlias(this.value);
}
this.dispatchEvent(new UmbChangeEvent());
}
this.dispatchEvent(new UmbChangeEvent());
}
}
#onAliasChange(e: UUIInputEvent) {
if (e instanceof UUIInputEvent) {
const target = e.composedPath()[0] as UUIInputElement;
if (typeof target?.value === 'string') {
this.alias = target.value;
console.log(this.alias);
this.dispatchEvent(new UmbChangeEvent());
}
}
e.stopPropagation();
if (!(e instanceof UUIInputEvent)) return;
const target = e.composedPath()[0] as UUIInputElement;
if (typeof target?.value === 'string') {
this.alias = target.value;
this.dispatchEvent(new UmbChangeEvent());
}
}
#onToggleAliasLock() {
@@ -68,25 +69,35 @@ export class UmbInputWithAliasElement extends UmbFormControlMixin<string>(UmbLit
}
render() {
// Localizations: [NL]
const nameLabel = this.label ?? this.localize.term('placeholders_entername');
const aliasLabel = this.localize.term('placeholders_enterAlias');
return html`
<uui-input id="name" placeholder="Enter a name..." label=${this.label} .value=${this.value} @input="${this.#onNameChange}">
<uui-input
id="name"
placeholder=${nameLabel}
label=${nameLabel}
.value=${this.value}
@input=${this.#onNameChange}>
<!-- TODO: should use UUI-LOCK-INPUT, but that does not fire an event when its locked/unlocked -->
<uui-input
auto-width
name="alias"
slot="append"
label="alias"
@input=${this.#onAliasChange}
label=${aliasLabel}
.value=${this.alias}
placeholder="Enter alias..."
placeholder=${aliasLabel}
?disabled=${this._aliasLocked && !this.aliasReadonly}
?readonly=${this.aliasReadonly}>
?readonly=${this.aliasReadonly}
@input=${this.#onAliasChange}>
<!-- TODO: validation for bad characters -->
${this.aliasReadonly
? nothing
: html`<div @click=${this.#onToggleAliasLock} @keydown=${() => ''} id="alias-lock" slot="prepend">
<uui-icon name=${this._aliasLocked ? 'icon-lock' : 'icon-unlocked'}></uui-icon>
</div>`}
: html`
<div @click=${this.#onToggleAliasLock} @keydown=${() => ''} id="alias-lock" slot="prepend">
<uui-icon name=${this._aliasLocked ? 'icon-lock' : 'icon-unlocked'}></uui-icon>
</div>
`}
</uui-input>
</uui-input>
`;
@@ -99,6 +110,10 @@ export class UmbInputWithAliasElement extends UmbFormControlMixin<string>(UmbLit
align-items: center;
}
#name > uui-input {
max-width: 50%;
}
:host(:invalid:not([pristine])) {
color: var(--uui-color-danger);
}

View File

@@ -46,6 +46,8 @@ export class UmbContentTypeStructureManager<
private readonly _contentTypeContainers = this.#contentTypes.asObservablePart((x) =>
x.flatMap((x) => x.containers ?? []),
);
readonly contentTypeUniques = this.#contentTypes.asObservablePart((x) => x.map((y) => y.unique));
readonly contentTypeAliases = this.#contentTypes.asObservablePart((x) => x.map((y) => y.alias));
#containers: UmbArrayState<UmbPropertyTypeContainerModel> = new UmbArrayState<UmbPropertyTypeContainerModel>(
[],
@@ -205,6 +207,12 @@ export class UmbContentTypeStructureManager<
getContentTypes() {
return this.#contentTypes.getValue();
}
getContentTypeUniques() {
return this.#contentTypes.getValue().map((x) => x.unique);
}
getContentTypeAliases() {
return this.#contentTypes.getValue().map((x) => x.alias);
}
// TODO: We could move the actions to another class?

View File

@@ -1,15 +1,15 @@
import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui';
import { css, customElement, html, nothing, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit';
import { umbConfirmModal } from '@umbraco-cms/backoffice/modal';
import { UmbLitElement, umbFocus } from '@umbraco-cms/backoffice/lit-element';
import { css, html, customElement, property, state, nothing, repeat } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type {
UmbContentTypeContainerStructureHelper,
UmbContentTypeModel,
UmbPropertyTypeContainerModel,
} from '@umbraco-cms/backoffice/content-type';
import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui';
import './content-type-design-editor-properties.element.js';
import { umbConfirmModal } from '@umbraco-cms/backoffice/modal';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
@customElement('umb-content-type-design-editor-group')
export class UmbContentTypeWorkspaceViewEditGroupElement extends UmbLitElement {
@@ -147,64 +147,78 @@ export class UmbContentTypeWorkspaceViewEditGroupElement extends UmbLitElement {
}
render() {
return this._inherited !== undefined && this._groupId
? html`
<uui-box>
${this.#renderContainerHeader()}
<umb-content-type-design-editor-properties
.editContentTypePath=${this.editContentTypePath}
container-id=${this._groupId}></umb-content-type-design-editor-properties>
</uui-box>
`
: '';
if (this._inherited === undefined || !this._groupId) return nothing;
return html`
<uui-box>
${this.#renderContainerHeader()}
<umb-content-type-design-editor-properties
.editContentTypePath=${this.editContentTypePath}
container-id=${this._groupId}></umb-content-type-design-editor-properties>
</uui-box>
`;
}
// TODO: impl UMB_EDIT_DOCUMENT_TYPE_PATH_PATTERN, but we need either a generic type or a way to get the path pattern.... [NL]
#renderContainerHeader() {
return html`<div slot="header">
return html`
<div slot="header">
<div>
${this.sortModeActive && this._hasOwnerContainer ? html`<uui-icon name="icon-navigation"></uui-icon>` : null}
${when(
this.sortModeActive && this._hasOwnerContainer,
() => html`<uui-icon name="icon-navigation"></uui-icon>`,
)}
<uui-input
id="group-name"
label=${this.localize.term('contentTypeEditor_group')}
placeholder=${this.localize.term('placeholders_entername')}
.value=${this._group!.name}
?disabled=${!this._hasOwnerContainer}
@change=${this.#renameGroup}
@blur=${this.#blurGroup}
@change=${this.#renameGroup}
${this._group!.name === '' ? umbFocus() : nothing}></uui-input>
</div>
</div>
<div slot="header-actions">
${this._hasOwnerContainer === false && this._inheritedFrom
? html`<uui-tag look="default" class="inherited">
${when(
this._hasOwnerContainer === false && this._inheritedFrom,
() => html`
<uui-tag look="default" class="inherited">
<uui-icon name="icon-merge"></uui-icon>
<span
>${this.localize.term('contentTypeEditor_inheritedFrom')}
${repeat(
this._inheritedFrom,
this._inheritedFrom!,
(inherited) => inherited.unique,
(inherited) => html`
<a href=${this.editContentTypePath + 'edit/' + inherited.unique}>${inherited.name}</a>
`,
)}
</span>
</uui-tag>`
: null}
${!this._inherited && !this.sortModeActive
? html`<uui-button compact label="${this.localize.term('actions_delete')}" @click="${this.#requestRemove}">
</uui-tag>
`,
)}
${when(
!this._inherited && !this.sortModeActive,
() => html`
<uui-button compact label=${this.localize.term('actions_delete')} @click=${this.#requestRemove}>
<uui-icon name="delete"></uui-icon>
</uui-button>`
: nothing}
${this.sortModeActive
? html` <uui-input
</uui-button>
`,
)}
${when(
this.sortModeActive,
() => html`
<uui-input
type="number"
label=${this.localize.term('sort_sortOrder')}
@change=${(e: UUIInputEvent) =>
this._singleValueUpdate('sortOrder', parseInt(e.target.value as string) || 0)}
.value=${this.group!.sortOrder ?? 0}
?disabled=${!this._hasOwnerContainer}></uui-input>`
: nothing}
</div> `;
?disabled=${!this._hasOwnerContainer}
@change=${(e: UUIInputEvent) =>
this._singleValueUpdate('sortOrder', parseInt(e.target.value as string) || 0)}></uui-input>
`,
)}
</div>
`;
}
static styles = [
@@ -229,6 +243,12 @@ export class UmbContentTypeWorkspaceViewEditGroupElement extends UmbLitElement {
display: flex;
align-items: center;
gap: var(--uui-size-3);
width: 100%;
}
#group-name {
--uui-input-border-color: transparent;
width: 100%;
}
uui-input[type='number'] {

View File

@@ -1,18 +1,28 @@
import './content-type-design-editor-property.element.js';
import { UMB_CONTENT_TYPE_WORKSPACE_CONTEXT } from '../../content-type-workspace.context-token.js';
import type { UmbContentTypeDesignEditorPropertyElement } from './content-type-design-editor-property.element.js';
import { UMB_CONTENT_TYPE_DESIGN_EDITOR_CONTEXT } from './content-type-design-editor.context.js';
import type { UmbContentTypeDesignEditorPropertyElement } from './content-type-design-editor-property.element.js';
import {
css,
customElement,
html,
ifDefined,
property,
repeat,
state,
when,
} from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { css, html, customElement, property, state, repeat, ifDefined } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { UmbContentTypeModel, UmbPropertyTypeModel } from '@umbraco-cms/backoffice/content-type';
import {
UmbContentTypePropertyStructureHelper,
UMB_PROPERTY_TYPE_SETTINGS_MODAL,
} from '@umbraco-cms/backoffice/content-type';
import type { UmbContentTypeModel, UmbPropertyTypeModel } from '@umbraco-cms/backoffice/content-type';
import { type UmbSorterConfig, UmbSorterController } from '@umbraco-cms/backoffice/sorter';
import { type UmbModalRouteBuilder, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router';
import './content-type-design-editor-property.element.js';
const SORTER_CONFIG: UmbSorterConfig<UmbPropertyTypeModel, UmbContentTypeDesignEditorPropertyElement> = {
getUniqueOfElement: (element) => {
return element.getAttribute('data-umb-property-id');
@@ -201,15 +211,16 @@ export class UmbContentTypeDesignEditorPropertiesElement extends UmbLitElement {
)}
</div>
${!this._sortModeActive
? html`<uui-button
${when(
!this._sortModeActive,
() => html`
<uui-button
id="btn-add"
href=${ifDefined(this._modalRouteBuilderNewProperty?.({ sortOrder: -1 }))}
label=${this.localize.term('contentTypeEditor_addProperty')}
id="add"
look="placeholder"
href=${ifDefined(this._modalRouteBuilderNewProperty?.({ sortOrder: -1 }))}>
<umb-localize key="contentTypeEditor_addProperty">Add property</umb-localize>
</uui-button> `
: ''}
look="placeholder"></uui-button>
`,
)}
`
: '';
}
@@ -217,8 +228,9 @@ export class UmbContentTypeDesignEditorPropertiesElement extends UmbLitElement {
static styles = [
UmbTextStyles,
css`
#add {
#btn-add {
width: 100%;
--uui-button-height: var(--uui-size-14);
}
#property-list[sort-mode-active]:not(:has(umb-content-type-design-editor-property)) {

View File

@@ -1,19 +1,17 @@
import { UMB_CONTENT_TYPE_WORKSPACE_CONTEXT } from '../../content-type-workspace.context-token.js';
import type { UmbContentTypeWorkspaceViewEditGroupElement } from './content-type-design-editor-group.element.js';
import { UMB_CONTENT_TYPE_DESIGN_EDITOR_CONTEXT } from './content-type-design-editor.context.js';
import type { UmbContentTypeWorkspaceViewEditGroupElement } from './content-type-design-editor-group.element.js';
import { css, customElement, html, nothing, property, repeat, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbContentTypeContainerStructureHelper } from '@umbraco-cms/backoffice/content-type';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { css, html, customElement, property, state, repeat, nothing } from '@umbraco-cms/backoffice/external/lit';
import {
UmbContentTypeContainerStructureHelper,
type UmbContentTypeModel,
type UmbPropertyTypeContainerModel,
} from '@umbraco-cms/backoffice/content-type';
import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router';
import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/modal';
import type { UmbContentTypeModel, UmbPropertyTypeContainerModel } from '@umbraco-cms/backoffice/content-type';
import type { UmbSorterConfig } from '@umbraco-cms/backoffice/sorter';
import './content-type-design-editor-properties.element.js';
import './content-type-design-editor-group.element.js';
import { type UmbSorterConfig, UmbSorterController } from '@umbraco-cms/backoffice/sorter';
import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/modal';
import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router';
const SORTER_CONFIG: UmbSorterConfig<UmbPropertyTypeContainerModel, UmbContentTypeWorkspaceViewEditGroupElement> = {
getUniqueOfElement: (element) => element.group?.id,
@@ -120,6 +118,7 @@ export class UmbContentTypeDesignEditorTabElement extends UmbLitElement {
this._editContentTypePath = routeBuilder({});
});
});
this.consumeContext(UMB_CONTENT_TYPE_DESIGN_EDITOR_CONTEXT, (context) => {
this.observe(
context.isSorting,
@@ -134,6 +133,7 @@ export class UmbContentTypeDesignEditorTabElement extends UmbLitElement {
'_observeIsSorting',
);
});
this.observe(
this.#groupStructureHelper.mergedContainers,
(groups) => {
@@ -142,6 +142,7 @@ export class UmbContentTypeDesignEditorTabElement extends UmbLitElement {
},
null,
);
this.observe(
this.#groupStructureHelper.hasProperties,
(hasProperties) => {
@@ -194,13 +195,13 @@ export class UmbContentTypeDesignEditorTabElement extends UmbLitElement {
#renderAddGroupButton() {
if (this._sortModeActive) return;
return html`<uui-button
label=${this.localize.term('contentTypeEditor_addGroup')}
id="add"
look="placeholder"
@click=${this.#onAddGroup}>
${this.localize.term('contentTypeEditor_addGroup')}
</uui-button>`;
return html`
<uui-button
id="btn-add"
label=${this.localize.term('contentTypeEditor_addGroup')}
look="placeholder"
@click=${this.#onAddGroup}></uui-button>
`;
}
static styles = [
@@ -213,12 +214,9 @@ export class UmbContentTypeDesignEditorTabElement extends UmbLitElement {
visibility: hidden;
}
#add {
#btn-add {
width: 100%;
}
#add:first-child {
margin-top: var(--uui-size-layout-1);
--uui-button-height: var(--uui-size-24);
}
uui-box,

View File

@@ -369,7 +369,7 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements
return html`
<umb-body-layout header-fit-height>
<div id="header" slot="header">
<div id="container-list" class="flex">${this.renderTabsNavigation()} ${this.renderAddButton()}</div>
<div id="container-list">${this.renderTabsNavigation()} ${this.renderAddButton()}</div>
${this.renderActions()}
</div>
<umb-router-slot
@@ -388,10 +388,12 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements
renderAddButton() {
// TODO: Localize this:
if (this._sortModeActive) return;
return html`<uui-button id="add-tab" @click="${this.#addTab}" label="Add tab">
<uui-icon name="icon-add"></uui-icon>
Add tab
</uui-button>`;
return html`
<uui-button id="add-tab" @click="${this.#addTab}" label="Add tab">
<uui-icon name="icon-add"></uui-icon>
Add tab
</uui-button>
`;
}
renderActions() {
@@ -399,37 +401,41 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements
? this.localize.term('general_reorderDone')
: this.localize.term('general_reorder');
return html`<div>
${this._compositionRepositoryAlias
? html`<uui-button
look="outline"
label=${this.localize.term('contentTypeEditor_compositions')}
compact
@click=${this.#openCompositionModal}>
<uui-icon name="icon-merge"></uui-icon>
${this.localize.term('contentTypeEditor_compositions')}
</uui-button>`
: ''}
<uui-button look="outline" label=${sortButtonText} compact @click=${this.#toggleSortMode}>
<uui-icon name="icon-navigation"></uui-icon>
${sortButtonText}
</uui-button>
</div>`;
return html`
<div id="actions">
${this._compositionRepositoryAlias
? html` <uui-button
look="outline"
label=${this.localize.term('contentTypeEditor_compositions')}
compact
@click=${this.#openCompositionModal}>
<uui-icon name="icon-merge"></uui-icon>
${this.localize.term('contentTypeEditor_compositions')}
</uui-button>`
: ''}
<uui-button look="outline" label=${sortButtonText} compact @click=${this.#toggleSortMode}>
<uui-icon name="icon-navigation"></uui-icon>
${sortButtonText}
</uui-button>
</div>
`;
}
renderTabsNavigation() {
if (!this._tabs || this._tabs.length === 0) return;
return html`<div id="tabs-group" class="flex">
<uui-tab-group>
${this.renderRootTab()}
${repeat(
this._tabs,
(tab) => tab.id,
(tab) => this.renderTab(tab),
)}
</uui-tab-group>
</div>`;
return html`
<div id="tabs-group">
<uui-tab-group>
${this.renderRootTab()}
${repeat(
this._tabs,
(tab) => tab.id,
(tab) => this.renderTab(tab),
)}
</uui-tab-group>
</div>
`;
}
renderRootTab() {
@@ -439,14 +445,16 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements
// If we don't have any root groups and we are not in sort mode, then we don't want to render the root tab.
return nothing;
}
return html`<uui-tab
id="root-tab"
class=${this._hasRootGroups || rootTabActive ? '' : 'content-tab-is-empty'}
label=${this.localize.term('general_generic')}
.active=${rootTabActive}
href=${rootTabPath}>
${this.localize.term('general_generic')}
</uui-tab>`;
return html`
<uui-tab
id="root-tab"
class=${this._hasRootGroups || rootTabActive ? '' : 'content-tab-is-empty'}
label=${this.localize.term('general_generic')}
.active=${rootTabActive}
href=${rootTabPath}>
${this.localize.term('general_generic')}
</uui-tab>
`;
}
renderTab(tab: UmbPropertyTypeContainerModel) {
@@ -556,17 +564,26 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements
#header {
width: 100%;
min-height: var(--uui-size-15);
min-height: var(--uui-size-16);
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: nowrap;
}
.flex {
#container-list {
display: flex;
}
#tabs-group {
display: flex;
}
#actions {
display: flex;
gap: var(--uui-size-space-2);
}
uui-tab-group {
flex-wrap: nowrap;
}

View File

@@ -134,6 +134,7 @@ export interface MetaEntityActionMoveToKind extends MetaEntityActionDefaultKind
moveRepositoryAlias: string;
treeRepositoryAlias: string;
treeAlias: string;
foldersOnly?: boolean;
}
// FOLDER

View File

@@ -33,7 +33,7 @@ export class UmbPropertyLayoutElement extends UmbLitElement {
/**
* Orientation: Horizontal is the default where label goes left and editor right.
* Vertical is where label goes above the editor.
* @type {string}
* @enum ['horizontal', 'vertical']
* @attr
* @default ''
*/
@@ -130,6 +130,14 @@ export class UmbPropertyLayoutElement extends UmbLitElement {
color: var(--uui-color-text-alt);
}
#description * {
max-width: 100%;
}
#description pre {
overflow: auto;
}
#editorColumn {
margin-top: var(--uui-size-space-3);
}

View File

@@ -1,19 +1,118 @@
import type { Meta, Story } from '@storybook/web-components';
import type { Meta, StoryObj } from '@storybook/web-components';
import type { UmbPropertyLayoutElement } from './property-layout.element.js';
import { html } from '@umbraco-cms/backoffice/external/lit';
import './property-layout.element.js';
export default {
const meta: Meta<UmbPropertyLayoutElement> = {
title: 'Workspaces/Property Layout',
component: 'umb-property-layout',
id: 'umb-property-layout',
} as Meta;
component: 'umb-property-layout',
args: {
alias: 'Umb.PropertyLayout.Text',
label: 'Text',
description: 'Description',
orientation: 'horizontal',
},
argTypes: {
orientation: {
options: ['horizontal', 'vertical'],
},
},
render: (storyObj) => {
return html`
<umb-property-layout
.invalid=${storyObj.invalid}
.alias=${storyObj.alias}
.label=${storyObj.label}
.description=${storyObj.description}
.orientation=${storyObj.orientation}>
<div slot="action-menu"><uui-button color="" look="placeholder">Action Menu</uui-button></div>
<div slot="editor"><uui-button color="" look="placeholder">Editor</uui-button></div>
</umb-property-layout>
`;
},
parameters: {
docs: {
source: {
code: `
<umb-property-layout alias="My.Alias" text="My label" description="My description">
<div slot="action-menu"><uui-button color="" look="placeholder">Action Menu</uui-button></div>
<div slot="editor"><uui-button color="" look="placeholder">Editor</uui-button></div>
</umb-property-layout>
`,
},
},
},
};
export const AAAOverview: Story<UmbPropertyLayoutElement> = () =>
html` <umb-property-layout label="Label" description="Description">
<div slot="action-menu"><uui-button color="" look="placeholder">Action Menu</uui-button></div>
export default meta;
type Story = StoryObj<UmbPropertyLayoutElement>;
<div slot="editor"><uui-button color="" look="placeholder">Editor</uui-button></div>
</umb-property-layout>`;
AAAOverview.storyName = 'Overview';
export const Overview: Story = {};
export const Vertical: Story = {
args: {
orientation: 'vertical',
},
render: (storyObj) => {
return html`
<umb-property-layout
style="width: 500px;"
.alias=${storyObj.alias}
.label=${storyObj.label}
.description=${storyObj.description}
.orientation=${storyObj.orientation}>
<uui-textarea slot="editor"></uui-textarea>
</umb-property-layout>
`;
},
};
export const WithInvalid: Story = {
args: {
invalid: true,
},
};
export const WithMarkdown: Story = {
decorators: [(story) => html` <div style="max-width: 500px; margin:1rem;">${story()}</div> `],
args: {
alias: 'Umb.PropertyLayout.Markdown',
label: 'Markdown',
description: `
# Markdown
This is a markdown description
## Subtitle
- List item 1
- List item 2
### Sub subtitle
1. Numbered list item 1
2. Numbered list item 2
\`\`\`html
<umb-property-layout>
<div slot="editor"></div>
</umb-property-layout>
\`\`\`
> Blockquote
**Bold text**
*Italic text*
[Link](https://umbraco.com)
![Image](https://umbraco.com/media/sybjwfmh/umbraco-support-working.webp?anchor=center&mode=crop&width=870&height=628&rnd=133425316954430000&quality=80&format=webp&format=webp)
<details>
<summary>Read more</summary>
Details content
</details>
`,
},
};

View File

@@ -72,16 +72,21 @@ export type resolvePlacementArgs<T, ElementType extends HTMLElement> = {
pointerY: number;
};
type UniqueType = string | symbol | number;
/**
* Internal type, which is adjusted to become the public one.
* @internal */
type INTERNAL_UmbSorterConfig<T, ElementType extends HTMLElement> = {
/**
* Define how to retrive the unique identifier of an element. If this method returns undefined, the move will be cancelled.
*/
getUniqueOfElement: (element: ElementType) => string | null | symbol | number | undefined;
getUniqueOfModel: (modeEntry: T) => string | null | symbol | number | undefined;
getUniqueOfElement: (element: ElementType) => UniqueType | null | undefined;
getUniqueOfModel: (modeEntry: T) => UniqueType | null | undefined;
/**
* Optionally define a unique identifier for each sorter experience, all Sorters that uses the same identifier to connect with other sorters.
*/
identifier: string | symbol;
identifier: UniqueType;
/**
* A query selector for the item element.
*/
@@ -107,6 +112,7 @@ type INTERNAL_UmbSorterConfig<T, ElementType extends HTMLElement> = {
* The selector to find the draggable element within the item.
*/
draggableSelector?: string;
//boundarySelector?: string;
dataTransferResolver?: (dataTransfer: DataTransfer | null, currentItem: T) => void;
onStart?: (argument: { item: T; element: ElementType }) => void;
@@ -166,7 +172,7 @@ type INTERNAL_UmbSorterConfig<T, ElementType extends HTMLElement> = {
performItemRemove?: (argument: { item: T }) => Promise<boolean> | boolean;
};
// External type with some properties optional, as they have defaults:
// External type with some properties optional, as they have fallback values:
export type UmbSorterConfig<T, ElementType extends HTMLElement = HTMLElement> = Omit<
INTERNAL_UmbSorterConfig<T, ElementType>,
'ignorerSelector' | 'containerSelector' | 'identifier'
@@ -178,6 +184,19 @@ export type UmbSorterConfig<T, ElementType extends HTMLElement = HTMLElement> =
* @class UmbSorterController
* @implements {UmbControllerInterface}
* @description This controller can make user able to sort items.
* @example
*
* This example shows how to setup a sorter controller with no special needs.
* Assuming your declaring this on a Umbraco Element(UmbControllerHostElement):
*
* ```ts
* const sorter = new UmbSorterController(this, {
* itemSelector: '.item',
* containerSelector: '.container',
* getUniqueOfElement: (element) => element.dataset.id,
* getUniqueOfModel: (model) => model.id
* });
* ```
*/
export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElement> extends UmbControllerBase {
//
@@ -274,16 +293,13 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
}
}
hasItem(unique: string) {
hasItem(unique: UniqueType) {
return this.#model.find((x) => this.#config.getUniqueOfModel(x) === unique) !== undefined;
}
/*
getItem(unique: string) {
if (!unique) return undefined;
getItem(unique: UniqueType) {
return this.#model.find((x) => this.#config.getUniqueOfModel(x) === unique);
}
*/
hostConnected() {
this.#isConnected = true;
@@ -453,7 +469,7 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
if (UmbSorterController.activeElement && UmbSorterController.activeElement !== element) {
// TODO: Remove this console log at one point.
console.log("drag start realized that something was already active, so we'll end it. -------!!!!#€#%#€");
console.error('drag start ws cancelled due to another drag was still active');
this.#handleDragEnd();
}
@@ -470,6 +486,7 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
UmbSorterController.activeItem = this.getItemOfElement(UmbSorterController.activeElement! as ElementType);
UmbSorterController.originalSorter = this as unknown as UmbSorterController<unknown>;
// Notice, it is acceptable here to get index via object reference, but only cause there has been no change at this stage, otherwise we cannot trust the object instance is represented in the model — it could have mutated or been cloned [NL]
UmbSorterController.originalIndex = this.#model.indexOf(UmbSorterController.activeItem);
if (!UmbSorterController.activeItem) {
@@ -478,7 +495,7 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
}
// Get the current index of the item:
UmbSorterController.activeIndex = this.#model.indexOf(UmbSorterController.activeItem as T);
UmbSorterController.activeIndex = UmbSorterController.originalIndex;
UmbSorterController.activeElement!.style.transform = 'translateZ(0)'; // Solves problem with FireFox and ShadowDom in the drag-image.
@@ -804,10 +821,21 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
}
// TODO: Could get item via attr.
public async moveItemInModel(newIndex: number, fromCtrl: UmbSorterController<unknown>) {
const item = UmbSorterController.activeItem;
public async moveItemInModel(newIndex: number, fromCtrl: UmbSorterController<T, ElementType>) {
if (!UmbSorterController.activeItem) {
console.error('There is no active item to move');
return false;
}
const itemUnique = this.#config.getUniqueOfModel(UmbSorterController.activeItem);
if (!itemUnique) {
console.error('Failed to retrieve active item unique');
return false;
}
// We use the getItem method to find the current item/object of this entry, as we cannot trust the object instance(activeItem) to be the same as in the model. [NL]
// So notice, item in this method is the real modal entry reference, where in many other cases we use the activeItem which might not be up to date with the real entry of the model. [NL]
const item = fromCtrl.getItem(itemUnique);
if (!item) {
console.error('Could not find item of sync item');
console.error('Could not find item of model to move', itemUnique, this.#model);
return false;
}
if (this.notifyRequestDrop({ item }) === false) {
@@ -819,10 +847,9 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
if (localMove) {
// Local move:
// TODO: Maybe this should be replaceable/configurable:
const oldIndex = this.#model.indexOf(item);
if (oldIndex === -1) {
console.error('Could not find item in model');
console.error('Could not find item in model when performing internal move', this.getHostElement(), this.#model);
return false;
}
@@ -847,30 +874,33 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
// Not a local move:
if ((await fromCtrl.removeItem(item)) !== true) {
console.error('Sync could not remove item');
console.error('Sync could not remove item when moving to a new container');
return false;
}
if (this.#config.performItemInsert) {
const result = await this.#config.performItemInsert({ item, newIndex });
if (result === false) {
console.error('Sync could not insert after a move a new container');
return false;
}
} else {
const newModel = [...this.#model];
newModel.splice(newIndex, 0, item);
this.#model = newModel;
this.#config.onContainerChange?.({
model: newModel,
item,
from: fromCtrl as unknown as UmbSorterController<T, ElementType>,
});
this.#config.onChange?.({ model: newModel, item });
}
// If everything went well, we can set new activeSorter to this:
UmbSorterController.activeSorter = this as unknown as UmbSorterController<unknown>;
UmbSorterController.activeIndex = newIndex;
// If everything went well, we can set the new activeSorter (and dropSorter) to this, as we are switching container. [NL]
UmbSorterController.activeSorter = this as unknown as UmbSorterController<unknown>;
UmbSorterController.dropSorter = this as unknown as UmbSorterController<unknown>;
UmbSorterController.activeIndex = newIndex;
}
}
return true;

View File

@@ -1,12 +1,14 @@
import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity';
export interface UmbTreeRootItemsRequestArgs {
foldersOnly?: boolean;
skip?: number;
take?: number;
}
export interface UmbTreeChildrenOfRequestArgs {
parent: UmbEntityModel;
foldersOnly?: boolean;
skip?: number;
take?: number;
}

View File

@@ -49,6 +49,9 @@ export class UmbDefaultTreeContext<
#startNode = new UmbObjectState<UmbTreeStartNode | undefined>(undefined);
startNode = this.#startNode.asObservable();
#foldersOnly = new UmbBooleanState(false);
foldersOnly = this.#foldersOnly.asObservable();
#manifest?: ManifestTree;
#repository?: UmbTreeRepository<TreeItemType, TreeRootType>;
#actionEventContext?: UmbActionEventContext;
@@ -178,6 +181,7 @@ export class UmbDefaultTreeContext<
// If we have a start node get children of that instead of the root
const startNode = this.getStartNode();
const foldersOnly = this.#foldersOnly.getValue();
const additionalArgs = this.#additionalRequestArgs.getValue();
const { data } = startNode?.unique
@@ -187,11 +191,13 @@ export class UmbDefaultTreeContext<
unique: startNode.unique,
entityType: startNode.entityType,
},
foldersOnly,
skip,
take,
})
: await this.#repository!.requestTreeRootItems({
...additionalArgs,
foldersOnly,
skip,
take,
});
@@ -241,6 +247,36 @@ export class UmbDefaultTreeContext<
this.loadTree();
}
/**
* Gets the startNode config
* @return {UmbTreeStartNode}
* @memberof UmbDefaultTreeContext
*/
getStartNode() {
return this.#startNode.getValue();
}
/**
* Sets the foldersOnly config
* @param {boolean} foldersOnly
* @memberof UmbDefaultTreeContext
*/
setFoldersOnly(foldersOnly: boolean) {
this.#foldersOnly.setValue(foldersOnly);
// we need to reset the tree if this config changes
this.#resetTree();
this.loadTree();
}
/**
* Gets the foldersOnly config
* @return {boolean}
* @memberof UmbDefaultTreeContext
*/
getFoldersOnly() {
return this.#foldersOnly.getValue();
}
/**
* Updates the requestArgs config and reloads the tree.
*/
@@ -254,15 +290,6 @@ export class UmbDefaultTreeContext<
return this.#additionalRequestArgs.getValue();
}
/**
* Gets the startNode config
* @return {UmbTreeStartNode}
* @memberof UmbDefaultTreeContext
*/
getStartNode() {
return this.#startNode.getValue();
}
#resetTree() {
this.#treeRoot.setValue(undefined);
this.#rootItems.setValue([]);

View File

@@ -31,6 +31,9 @@ export class UmbDefaultTreeElement extends UmbLitElement {
@property({ type: Object, attribute: false })
startNode?: UmbTreeStartNode;
@property({ type: Boolean, attribute: false })
foldersOnly?: boolean = false;
@property({ attribute: false })
selectableFilter: (item: UmbTreeItemModelBase) => boolean = () => true;
@@ -87,6 +90,10 @@ export class UmbDefaultTreeElement extends UmbLitElement {
this.#treeContext!.setHideTreeRoot(this.hideTreeRoot);
}
if (_changedProperties.has('foldersOnly')) {
this.#treeContext!.setFoldersOnly(this.foldersOnly ?? false);
}
if (_changedProperties.has('selectableFilter')) {
this.#treeContext!.selectableFilter = this.selectableFilter;
}

View File

@@ -13,8 +13,12 @@ export class UmbMoveToEntityAction extends UmbEntityActionBase<MetaEntityActionM
const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT);
const modalContext = modalManager.open(this, UMB_TREE_PICKER_MODAL, {
data: { treeAlias: this.args.meta.treeAlias },
}) as any; // TODO: make generic picker interface with selection
data: {
treeAlias: this.args.meta.treeAlias,
foldersOnly: this.args.meta.foldersOnly,
},
});
const value = await modalContext.onSubmit();
const destinationUnique = value.selection[0];
if (destinationUnique === undefined) throw new Error('Destination Unique is not available');

View File

@@ -65,6 +65,9 @@ export abstract class UmbTreeItemContextBase<
#path = new UmbStringState('');
readonly path = this.#path.asObservable();
#foldersOnly = new UmbBooleanState(false);
readonly foldersOnly = this.#foldersOnly.asObservable();
treeContext?: UmbDefaultTreeContext<TreeItemType, TreeRootType>;
#sectionContext?: UmbSectionContext;
#sectionSidebarContext?: UmbSectionSidebarContext;
@@ -175,6 +178,7 @@ export abstract class UmbTreeItemContextBase<
const skip = loadMore ? this.#paging.skip : 0;
const take = loadMore ? this.#paging.take : this.pagination.getCurrentPageNumber() * this.#paging.take;
const foldersOnly = this.#foldersOnly.getValue();
const additionalArgs = this.treeContext?.getAdditionalRequestArgs();
const { data } = await repository.requestTreeItemsOf({
@@ -182,6 +186,7 @@ export abstract class UmbTreeItemContextBase<
unique: this.unique,
entityType: this.entityType,
},
foldersOnly,
skip,
take,
...additionalArgs,
@@ -240,6 +245,7 @@ export abstract class UmbTreeItemContextBase<
this.treeContext = treeContext;
this.#observeIsSelectable();
this.#observeIsSelected();
this.#observeFoldersOnly();
});
this.consumeContext(UMB_ACTION_EVENT_CONTEXT, (instance) => {
@@ -311,6 +317,18 @@ export abstract class UmbTreeItemContextBase<
);
}
#observeFoldersOnly() {
if (!this.treeContext || this.unique === undefined) return;
this.observe(
this.treeContext.foldersOnly,
(foldersOnly) => {
this.#foldersOnly.setValue(foldersOnly);
},
'observeFoldersOnly',
);
}
#observeSectionPath() {
if (!this.#sectionContext) return;

View File

@@ -101,6 +101,7 @@ export class UmbTreePickerModalElement<TreeItemType extends UmbTreeItemModelBase
filter: this.data?.filter,
selectableFilter: this.data?.pickableFilter,
startNode: this.data?.startNode,
foldersOnly: this.data?.foldersOnly,
}}
@selection-change=${this.#onSelectionChange}
@selected=${this.#onSelected}

View File

@@ -21,6 +21,7 @@ export interface UmbTreePickerModalData<
// Consider if it makes sense to move this into the UmbPickerModalData interface, but for now this is a TreePicker feature. [NL]
createAction?: UmbTreePickerModalCreateActionData<PathPatternParamsType>;
startNode?: UmbTreeStartNode;
foldersOnly?: boolean;
}
export interface UmbTreePickerModalValue extends UmbPickerModalValue {}

View File

@@ -15,6 +15,7 @@ const entityActions: Array<ManifestTypes> = [
treeRepositoryAlias: UMB_DATA_TYPE_TREE_REPOSITORY_ALIAS,
moveRepositoryAlias: UMB_MOVE_DATA_TYPE_REPOSITORY_ALIAS,
treeAlias: UMB_DATA_TYPE_TREE_ALIAS,
foldersOnly: true,
},
},
];

View File

@@ -50,7 +50,11 @@ export class UmbDataTypeTreeServerDataSource extends UmbTreeServerDataSourceBase
const getRootItems = async (args: UmbTreeRootItemsRequestArgs) => {
// eslint-disable-next-line local-rules/no-direct-api-import
return DataTypeService.getTreeDataTypeRoot({ skip: args.skip, take: args.take });
return DataTypeService.getTreeDataTypeRoot({
foldersOnly: args.foldersOnly,
skip: args.skip,
take: args.take,
});
};
const getChildrenOf = (args: UmbTreeChildrenOfRequestArgs) => {
@@ -60,6 +64,7 @@ const getChildrenOf = (args: UmbTreeChildrenOfRequestArgs) => {
// eslint-disable-next-line local-rules/no-direct-api-import
return DataTypeService.getTreeDataTypeChildren({
parentId: args.parent.unique,
foldersOnly: args.foldersOnly,
skip: args.skip,
take: args.take,
});

View File

@@ -15,6 +15,7 @@ const entityActions: Array<ManifestTypes> = [
treeRepositoryAlias: UMB_DOCUMENT_BLUEPRINT_TREE_REPOSITORY_ALIAS,
moveRepositoryAlias: UMB_MOVE_DOCUMENT_BLUEPRINT_REPOSITORY_ALIAS,
treeAlias: UMB_DOCUMENT_BLUEPRINT_TREE_ALIAS,
foldersOnly: true,
},
},
];

View File

@@ -41,7 +41,11 @@ export class UmbDocumentBlueprintTreeServerDataSource extends UmbTreeServerDataS
const getRootItems = (args: UmbTreeRootItemsRequestArgs) =>
// eslint-disable-next-line local-rules/no-direct-api-import
DocumentBlueprintService.getTreeDocumentBlueprintRoot({ skip: args.skip, take: args.take });
DocumentBlueprintService.getTreeDocumentBlueprintRoot({
foldersOnly: args.foldersOnly,
skip: args.skip,
take: args.take,
});
const getChildrenOf = (args: UmbTreeChildrenOfRequestArgs) => {
if (args.parent.unique === null) {
@@ -50,6 +54,7 @@ const getChildrenOf = (args: UmbTreeChildrenOfRequestArgs) => {
// eslint-disable-next-line local-rules/no-direct-api-import
return DocumentBlueprintService.getTreeDocumentBlueprintChildren({
parentId: args.parent.unique,
foldersOnly: args.foldersOnly,
});
}
};

View File

@@ -78,14 +78,14 @@ export class UmbDocumentTypeWorkspaceEditorElement extends UmbLitElement {
return html`
<umb-workspace-editor alias="Umb.Workspace.DocumentType">
<div id="header" slot="header">
<uui-button id="icon" @click=${this._handleIconClick} label="icon" compact>
<uui-button id="icon" compact label="icon" look="outline" @click=${this._handleIconClick}>
<umb-icon name=${ifDefined(this._icon)}></umb-icon>
</uui-button>
<div id="editors">
<umb-input-with-alias
id="name"
label="name"
label=${this.localize.term('placeholders_entername')}
value=${this._name}
alias=${this._alias}
?auto-generate-alias=${this._isNew}
@@ -116,6 +116,7 @@ export class UmbDocumentTypeWorkspaceEditorElement extends UmbLitElement {
#header {
display: flex;
flex: 1 1 auto;
gap: var(--uui-size-space-2);
}
#editors {
@@ -140,9 +141,9 @@ export class UmbDocumentTypeWorkspaceEditorElement extends UmbLitElement {
}
#icon {
font-size: calc(var(--uui-size-layout-3) / 2);
margin-right: var(--uui-size-space-2);
margin-left: calc(var(--uui-size-space-4) * -1);
font-size: var(--uui-size-8);
height: 60px;
width: 60px;
}
`,
];

View File

@@ -127,7 +127,7 @@ export function getDocumentHistoryTagStyleAndText(type: UmbDocumentAuditLogType)
case UmbDocumentAuditLog.CUSTOM:
return {
style: { look: 'placeholder', color: 'default' },
text: { label: 'auditTrails_custom', desc: '' },
text: { label: 'auditTrails_smallCustom', desc: 'auditTrails_custom' },
};
default:

View File

@@ -15,6 +15,7 @@ const entityActions: Array<ManifestTypes> = [
treeRepositoryAlias: UMB_MEDIA_TYPE_TREE_REPOSITORY_ALIAS,
moveRepositoryAlias: UMB_MOVE_MEDIA_TYPE_REPOSITORY_ALIAS,
treeAlias: UMB_MEDIA_TYPE_TREE_ALIAS,
foldersOnly: true,
},
},
];

View File

@@ -41,7 +41,11 @@ export class UmbMediaTypeTreeServerDataSource extends UmbTreeServerDataSourceBas
const getRootItems = (args: UmbTreeRootItemsRequestArgs) =>
// eslint-disable-next-line local-rules/no-direct-api-import
MediaTypeService.getTreeMediaTypeRoot({ skip: args.skip, take: args.take });
MediaTypeService.getTreeMediaTypeRoot({
foldersOnly: args.foldersOnly,
skip: args.skip,
take: args.take,
});
const getChildrenOf = (args: UmbTreeChildrenOfRequestArgs) => {
if (args.parent.unique === null) {
@@ -50,6 +54,7 @@ const getChildrenOf = (args: UmbTreeChildrenOfRequestArgs) => {
// eslint-disable-next-line local-rules/no-direct-api-import
return MediaTypeService.getTreeMediaTypeChildren({
parentId: args.parent.unique,
foldersOnly: args.foldersOnly,
skip: args.skip,
take: args.take,
});

View File

@@ -80,32 +80,34 @@ export class UmbMediaTypeWorkspaceEditorElement extends UmbLitElement {
}
render() {
return html`<umb-workspace-editor alias="Umb.Workspace.MediaType">
<div id="header" slot="header">
<uui-button id="icon" @click=${this._handleIconClick} label="icon" compact>
<umb-icon name=${ifDefined(this._icon)}></umb-icon>
</uui-button>
return html`
<umb-workspace-editor alias="Umb.Workspace.MediaType">
<div id="header" slot="header">
<uui-button id="icon" compact label="icon" look="outline" @click=${this._handleIconClick}>
<umb-icon name=${ifDefined(this._icon)}></umb-icon>
</uui-button>
<div id="editors">
<umb-input-with-alias
id="name"
label="name"
value=${this._name}
alias=${this._alias}
?auto-generate-alias=${this._isNew}
@change="${this.#onNameAndAliasChange}"
${umbFocus()}>
</umb-input-with-alias>
<div id="editors">
<umb-input-with-alias
id="name"
label=${this.localize.term('placeholders_entername')}
value=${this._name}
alias=${this._alias}
?auto-generate-alias=${this._isNew}
@change="${this.#onNameAndAliasChange}"
${umbFocus()}>
</umb-input-with-alias>
<uui-input
id="description"
.label=${this.localize.term('placeholders_enterDescription')}
.value=${this._description}
.placeholder=${this.localize.term('placeholders_enterDescription')}
@input=${this.#onDescriptionChange}></uui-input>
<uui-input
id="description"
.label=${this.localize.term('placeholders_enterDescription')}
.value=${this._description}
.placeholder=${this.localize.term('placeholders_enterDescription')}
@input=${this.#onDescriptionChange}></uui-input>
</div>
</div>
</div>
</umb-workspace-editor>`;
</umb-workspace-editor>
`;
}
static styles = [
@@ -119,6 +121,7 @@ export class UmbMediaTypeWorkspaceEditorElement extends UmbLitElement {
#header {
display: flex;
flex: 1 1 auto;
gap: var(--uui-size-space-2);
}
#editors {
@@ -143,9 +146,9 @@ export class UmbMediaTypeWorkspaceEditorElement extends UmbLitElement {
}
#icon {
font-size: calc(var(--uui-size-layout-3) / 2);
margin-right: var(--uui-size-space-2);
margin-left: calc(var(--uui-size-space-4) * -1);
font-size: var(--uui-size-8);
height: 60px;
width: 60px;
}
`,
];

View File

@@ -135,7 +135,7 @@ export class UmbMediaWorkspaceViewInfoReferenceElement extends UmbLitElement {
}
#renderItems() {
if (!this._items?.length) return html`<p>${this.localize.term('references_DataTypeNoReferences')}</p>`;
if (!this._items?.length) return html`<p><umb-localize key="references_itemHasNoReferences">This item has no references.</umb-localize></p>`;
return html`
<uui-table>
<uui-table-head>

View File

@@ -22,10 +22,6 @@ export class UmbMemberTypeWorkspaceEditorElement extends UmbLitElement {
@state()
private _isNew?: boolean;
@state()
private _iconColorAlias?: string;
// TODO: Color should be using an alias, and look up in some dictionary/key/value) of project-colors.
#workspaceContext?: typeof UMB_MEMBER_TYPE_WORKSPACE_CONTEXT.TYPE;
constructor() {
@@ -82,14 +78,14 @@ export class UmbMemberTypeWorkspaceEditorElement extends UmbLitElement {
return html`
<umb-workspace-editor alias="Umb.Workspace.MemberType">
<div id="header" slot="header">
<uui-button id="icon" @click=${this._handleIconClick} label="icon" compact>
<uui-icon name="${ifDefined(this._icon)}" style="color: ${this._iconColorAlias}"></uui-icon>
<uui-button id="icon" compact label="icon" look="outline" @click=${this._handleIconClick}>
<umb-icon name=${ifDefined(this._icon)}></umb-icon>
</uui-button>
<div id="editors">
<umb-input-with-alias
id="name"
label="name"
label=${this.localize.term('placeholders_entername')}
value=${this._name}
alias=${this._alias}
?auto-generate-alias=${this._isNew}
@@ -120,6 +116,7 @@ export class UmbMemberTypeWorkspaceEditorElement extends UmbLitElement {
#header {
display: flex;
flex: 1 1 auto;
gap: var(--uui-size-space-2);
}
#editors {
@@ -156,9 +153,9 @@ export class UmbMemberTypeWorkspaceEditorElement extends UmbLitElement {
}
#icon {
font-size: calc(var(--uui-size-layout-3) / 2);
margin-right: var(--uui-size-space-2);
margin-left: calc(var(--uui-size-space-4) * -1);
font-size: var(--uui-size-8);
height: 60px;
width: 60px;
}
`,
];

View File

@@ -1,4 +1,4 @@
import type { UmbPropertyEditorConfigCollection } from '../../core/property-editor/config/index.js';
import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
import { html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UMB_DOCUMENT_COLLECTION_ALIAS } from '@umbraco-cms/backoffice/document';

View File

@@ -1,5 +1,5 @@
import { UmbPropertyValueChangeEvent } from '../../core/property-editor/index.js';
import type { UmbPropertyEditorConfigCollection } from '../../core/property-editor/index.js';
import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor';
import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
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';

View File

@@ -1,4 +1,4 @@
import { UmbPropertyValueChangeEvent } from '../../core/property-editor/index.js';
import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor';
import { customElement, html, property, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { UmbChangeEvent } from '@umbraco-cms/backoffice/event';

View File

@@ -1,4 +1,4 @@
import type { UmbInputNumberRangeElement } from '../../core/components/input-number-range/input-number-range.element.js';
import type { UmbInputNumberRangeElement } from '@umbraco-cms/backoffice/components';
import { customElement, html, property, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@@ -7,8 +7,6 @@ import type { UmbNumberRangeValueType } from '@umbraco-cms/backoffice/models';
import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry';
import '../../core/components/input-number-range/input-number-range.element.js';
/**
* @element umb-property-editor-ui-number-range
*/

View File

@@ -1,12 +1,10 @@
import type { UmbInputRadioButtonListElement } from '../../core/components/input-radio-button-list/input-radio-button-list.element.js';
import type { UmbInputRadioButtonListElement } from '@umbraco-cms/backoffice/components';
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 '../../core/components/input-radio-button-list/input-radio-button-list.element.js';
/**
* @element umb-property-editor-ui-radio-button-list
*/

View File

@@ -23,7 +23,7 @@ export const manifest: ManifestPropertyEditorSchema = {
],
defaultData: [
{ alias: 'minVal', value: 0 },
{ alias: 'maxVal', value: 0 },
{ alias: 'maxVal', value: 100 },
],
},
},

View File

@@ -50,7 +50,7 @@ export const manifests: Array<ManifestTypes> = [
},
{
alias: 'step',
value: 0,
value: 1,
},
],
},

View File

@@ -1,26 +1,28 @@
import type { UmbInputSliderElement } from '../../core/components/input-slider/input-slider.element.js';
import type { UmbInputSliderElement } from '@umbraco-cms/backoffice/components';
import { customElement, html, 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';
export type UmbSliderValue = { from: number; to: number } | undefined;
/**
* @element umb-property-editor-ui-slider
*/
@customElement('umb-property-editor-ui-slider')
export class UmbPropertyEditorUISliderElement extends UmbLitElement implements UmbPropertyEditorUiElement {
@property({ type: Object })
value: { to?: number; from?: number } | undefined = undefined;
value: UmbSliderValue | undefined;
@state()
_enableRange = false;
@state()
_initVal1?: number;
_initVal1: number = 0;
@state()
_initVal2?: number;
_initVal2: number = 1;
@state()
_step = 1;
@@ -35,11 +37,24 @@ export class UmbPropertyEditorUISliderElement extends UmbLitElement implements U
if (!config) return;
this._enableRange = Boolean(config.getValueByAlias('enableRange')) ?? false;
this._initVal1 = Number(config.getValueByAlias('initVal1'));
this._initVal2 = Number(config.getValueByAlias('initVal2'));
this._step = Number(config.getValueByAlias('step')) ?? 1;
// Make sure that step is higher than 0 (decimals ok).
const step = (config.getValueByAlias('step') ?? 1) as number;
this._step = step > 0 ? step : 1;
this._initVal1 = Number(config.getValueByAlias('initVal1')) ?? 0;
this._initVal2 = Number(config.getValueByAlias('initVal2')) ?? this._initVal1 + this._step;
this._min = Number(config.getValueByAlias('minVal')) ?? 0;
this._max = Number(config.getValueByAlias('maxVal')) ?? 100;
if (this._min === this._max) {
this._max = this._min + 100;
//TODO Maybe we want to show some kind of error element rather than trying to fix the mistake made by the user...?
throw new Error(
`Property Editor Slider: min and max are currently equal. Please change your data type configuration. To render the slider correctly, we changed this slider to: min = ${this._min}, max = ${this._max}`,
);
}
}
#getValueObject(value: string) {
@@ -55,8 +70,8 @@ export class UmbPropertyEditorUISliderElement extends UmbLitElement implements U
render() {
return html`
<umb-input-slider
.valueLow=${this.value?.from ?? this._initVal1 ?? 0}
.valueHigh=${this.value?.to ?? this._initVal2 ?? 0}
.valueLow=${this.value?.from ?? this._initVal1}
.valueHigh=${this.value?.to ?? this._initVal2}
.step=${this._step}
.min=${this._min}
.max=${this._max}

View File

@@ -1,4 +1,4 @@
import type { UmbInputToggleElement } from '../../core/components/input-toggle/input-toggle.element.js';
import type { UmbInputToggleElement } from '@umbraco-cms/backoffice/components';
import { customElement, html, property, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor';

View File

@@ -171,8 +171,8 @@ export class UmbPartialViewWorkspaceContext
if (error) {
throw new Error(error.message);
}
this.setIsNew(false);
this.#data.setValue(data);
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);

View File

@@ -1,9 +1,9 @@
import { loadManifestApi } from '../../../../libs/extension-api/functions/load-manifest-api.function.js';
import { availableLanguages } from './input-tiny-mce.languages.js';
import { defaultFallbackConfig } from './input-tiny-mce.defaults.js';
import { pastePreProcessHandler } from './input-tiny-mce.handlers.js';
import { uriAttributeSanitizer } from './input-tiny-mce.sanitizer.js';
import type { TinyMcePluginArguments, UmbTinyMcePluginBase } from './tiny-mce-plugin.js';
import { loadManifestApi } from '@umbraco-cms/backoffice/extension-api';
import { css, customElement, html, property, query, state } from '@umbraco-cms/backoffice/external/lit';
import { firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs';
import { getProcessedImageUrl } from '@umbraco-cms/backoffice/utils';

View File

@@ -105,4 +105,6 @@ export class UmbCurrentUserStore extends UmbContextBase<UmbCurrentUserStore> {
}
}
export default UmbCurrentUserStore;
export const UMB_CURRENT_USER_STORE_CONTEXT = new UmbContextToken<UmbCurrentUserStore>('UmbCurrentUserStore');

View File

@@ -1,4 +1,3 @@
import { UmbCurrentUserStore } from './current-user.store.js';
import type { ManifestRepository, ManifestStore, ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
export const UMB_CURRENT_USER_REPOSITORY_ALIAS = 'Umb.Repository.CurrentUser';
@@ -14,7 +13,7 @@ const store: ManifestStore = {
type: 'store',
alias: 'Umb.Store.CurrentUser',
name: 'Current User Store',
api: UmbCurrentUserStore,
api: () => import('./current-user.store.js'),
};
export const manifests: Array<ManifestTypes> = [repository, store];

View File

@@ -210,13 +210,13 @@ export class UmbUserGroupWorkspaceEditorElement extends UmbLitElement {
#renderHeader() {
return html`
<div id="header" slot="header">
<uui-button id="icon" @click=${this.#onIconClick} label="icon" compact>
<uui-button id="icon" compact label="icon" look="outline" @click=${this.#onIconClick}>
<umb-icon name=${this._icon || ''}></umb-icon>
</uui-button>
<umb-input-with-alias
id="name"
label=${this.localize.term('general_name')}
label=${this.localize.term('placeholders_entername')}
.value=${this._name}
alias=${ifDefined(this._alias)}
?auto-generate-alias=${this._isNew}
@@ -339,9 +339,12 @@ export class UmbUserGroupWorkspaceEditorElement extends UmbLitElement {
}
#renderRightColumn() {
return html` <uui-box headline="Actions">
<umb-entity-action-list .entityType=${UMB_USER_GROUP_ENTITY_TYPE} .unique=${this._unique}></umb-entity-action-list
></uui-box>`;
return html`
<uui-box headline="Actions">
<umb-entity-action-list .entityType=${UMB_USER_GROUP_ENTITY_TYPE} .unique=${this._unique}>
</umb-entity-action-list>
</uui-box>
`;
}
static styles = [
@@ -355,11 +358,14 @@ export class UmbUserGroupWorkspaceEditorElement extends UmbLitElement {
#header {
display: flex;
flex: 1 1 auto;
gap: var(--uui-size-space-3);
gap: var(--uui-size-space-2);
align-items: center;
}
#icon {
font-size: calc(var(--uui-size-layout-3) / 2);
font-size: var(--uui-size-5);
height: 30px;
width: 30px;
}
#name {

View File

@@ -34,7 +34,7 @@ export default {
},
}),
commonjs({
include: ['node_modules/base64-js/**/*', 'node_modules/tinymce/**/*']
include: ['node_modules/base64-js/**/*', 'node_modules/tinymce/**/*'],
}),
esbuildPlugin({ ts: true, tsconfig: './tsconfig.json', target: 'auto', json: true }),
],
@@ -53,6 +53,9 @@ export default {
<link rel="stylesheet" href="src/css/user-defined.css">
<link rel="stylesheet" href="node_modules/@umbraco-ui/uui-css/dist/uui-css.css">
<link rel="stylesheet" href="src/css/umb-css.css">
<script type="module">
import '@umbraco-cms/backoffice/components';
</script>
</head>
<body>
<script type="module" src="${testFramework}"></script>