Merge remote-tracking branch 'origin/main' into feature/easy-way-to-produce-a-set-of-properties

This commit is contained in:
Niels Lyngsø
2023-12-04 15:41:22 +01:00
29 changed files with 867 additions and 1172 deletions

View File

@@ -52,7 +52,7 @@ jobs:
registry-url: https://registry.npmjs.org/
scope: '@umbraco-cms'
- run: npm ci
- run: npm run build:for:npm
- run: npm run build
- name: Calculate version
run: |
if [ -z "${{inputs.version}}" ]; then

File diff suppressed because it is too large Load Diff

View File

@@ -97,7 +97,6 @@
"backoffice:test:e2e": "npx playwright test",
"build-storybook": "npm run wc-analyze && storybook build",
"build:for:cms": "npm run build && node ./devops/build/copy-to-cms.js",
"build:for:npm": "npm run build && tsc-alias -f -p src/tsconfig.build.json && npm run generate:jsonschema:dist && npm run wc-analyze && npm run wc-analyze:vscode",
"build:for:static": "vite build",
"build:vite": "tsc && vite build --mode staging",
"build": "tsc --project ./src/tsconfig.build.json && rollup -c ./src/rollup.config.js",
@@ -116,7 +115,7 @@
"lint:fix": "npm run lint -- --fix",
"lint": "eslint src",
"new-extension": "plop --plopfile ./devops/plop/plop.js",
"prepublishOnly": "node ./devops/publish/cleanse-pkg.js",
"prepack": "tsc-alias -f -p src/tsconfig.build.json && npm run generate:jsonschema:dist && npm run wc-analyze && npm run wc-analyze:vscode && node ./devops/publish/cleanse-pkg.js",
"preview": "vite preview --open",
"storybook:build": "npm run wc-analyze && storybook build",
"storybook": "npm run wc-analyze && storybook dev -p 6006",
@@ -141,7 +140,7 @@
"lit": "^2.8.0",
"lodash-es": "4.17.21",
"marked": "^9.1.0",
"monaco-editor": "^0.41.0",
"monaco-editor": "^0.44.0",
"rxjs": "^7.8.1",
"tinymce-i18n": "^23.8.7",
"tinymce": "^6.7.3",
@@ -155,13 +154,13 @@
"@rollup/plugin-commonjs": "^25.0.4",
"@rollup/plugin-json": "^6.0.1",
"@rollup/plugin-node-resolve": "^15.2.1",
"@storybook/addon-a11y": "7.5.3",
"@storybook/addon-actions": "7.5.3",
"@storybook/addon-essentials": "7.5.3",
"@storybook/addon-links": "7.5.3",
"@storybook/addon-a11y": "7.6.3",
"@storybook/addon-actions": "7.6.3",
"@storybook/addon-essentials": "7.6.3",
"@storybook/addon-links": "7.6.3",
"@storybook/mdx2-csf": "^1.1.0",
"@storybook/web-components-vite": "7.5.3",
"@storybook/web-components": "7.5.3",
"@storybook/web-components-vite": "7.6.3",
"@storybook/web-components": "7.6.3",
"@types/chai": "^4.3.5",
"@types/lodash-es": "^4.17.8",
"@types/mocha": "^10.0.1",
@@ -180,7 +179,7 @@
"eslint-plugin-lit": "^1.10.1",
"eslint-plugin-local-rules": "^1.3.2",
"eslint-plugin-storybook": "^0.6.15",
"eslint-plugin-wc": "^1.5.0",
"eslint-plugin-wc": "^2.0.4",
"eslint": "^8.46.0",
"lucide-static": "^0.290.0",
"msw": "^1.2.3",
@@ -196,7 +195,7 @@
"rollup-plugin-import-css": "^3.3.4",
"rollup-plugin-web-worker-loader": "^1.6.1",
"rollup": "^3.27.2",
"storybook": "7.5.3",
"storybook": "7.6.3",
"tiny-glob": "^0.2.9",
"tsc-alias": "^1.8.8",
"typescript-json-schema": "^0.62.0",

View File

@@ -1,4 +1,4 @@
import { UmbTextStyles } from "@umbraco-cms/backoffice/style";
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import {
css,
html,
@@ -54,14 +54,11 @@ export class UmbCodeBlockElement extends LitElement {
: ''}
</div>`
: ''}
<pre style="${this.language ? 'border-top: 1px solid var(--uui-color-divider-emphasis);' : ''}">
<uui-scroll-container>
<code>
<slot></slot>
</code>
</uui-scroll-container>
</pre>
`;
<pre
style="${this.language
? 'border-top: 1px solid var(--uui-color-divider-emphasis);'
: ''}"><uui-scroll-container><code><slot></slot></code></uui-scroll-container></pre>
`; // Avoid breaks between elements of <pre></pre>
}
static styles = [
@@ -84,7 +81,12 @@ export class UmbCodeBlockElement extends LitElement {
background-color: var(--uui-color-surface-alt);
color: #303033;
display: block;
font-family: Lato, Helvetica Neue, Helvetica, Arial, sans-serif;
font-family:
Lato,
Helvetica Neue,
Helvetica,
Arial,
sans-serif;
margin: 0;
overflow-x: auto;
padding: 9.5px;

View File

@@ -1,4 +1,5 @@
import { css, html, nothing, until, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import { UMB_APP_CONTEXT } from '@umbraco-cms/backoffice/app';
import { html, until, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
type FileItem = {
@@ -8,12 +9,15 @@ type FileItem = {
@customElement('umb-input-upload-field-file')
export class UmbInputUploadFieldFileElement extends UmbLitElement {
@property({ type: String })
path = '';
/**
* @description The file to be rendered.
* @type {File}
* @required
*/
@property({ type: File, attribute: false })
@property({ attribute: false })
set file(value: File) {
this.#fileItem = new Promise((resolve) => {
/**
@@ -40,15 +44,35 @@ export class UmbInputUploadFieldFileElement extends UmbLitElement {
}
#fileItem!: Promise<FileItem>;
#serverUrl = '';
render = () => until(this.#renderFileItem(), html`<uui-loader></uui-loader>`);
constructor() {
super();
this.consumeContext(UMB_APP_CONTEXT, (instance) => {
this.#serverUrl = instance.getServerUrl();
});
}
// TODO Better way to do this....
render = () => {
if (this.path) {
return html`<uui-symbol-file-thumbnail
src=${this.#serverUrl + this.path}
title=${this.path}
alt=${this.path}></uui-symbol-file-thumbnail>`;
} else {
return until(this.#renderFileItem(), html`<uui-loader></uui-loader>`);
}
};
// render = () => until(this.#renderFileItem(), html`<uui-loader></uui-loader>`);
async #renderFileItem() {
const fileItem = await this.#fileItem;
return html`<uui-symbol-file-thumbnail
src=${fileItem.src}
title=${fileItem.name}
alt=${fileItem.name}></uui-symbol-file-thumbnail>`;
alt=${fileItem.name}></uui-symbol-file-thumbnail> `;
}
}

View File

@@ -1,19 +1,22 @@
import { TemporaryFileQueueItem, UmbTemporaryFileManager } from '../../temporary-file/temporary-file-manager.class.js';
import { UmbId } from '@umbraco-cms/backoffice/id';
import {
css,
html,
nothing,
map,
ifDefined,
customElement,
property,
query,
state,
repeat,
} from '@umbraco-cms/backoffice/external/lit';
import { FormControlMixin } from '@umbraco-cms/backoffice/external/uui';
import type { UUIFileDropzoneElement, UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import './input-upload-field-file.element.js';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
@customElement('umb-input-upload-field')
export class UmbInputUploadFieldElement extends FormControlMixin(UmbLitElement) {
@@ -23,10 +26,13 @@ export class UmbInputUploadFieldElement extends FormControlMixin(UmbLitElement)
* @type {Array<String>}
* @default []
*/
@property({ type: Array<string> })
@property({ type: Array })
public set keys(fileKeys: Array<string>) {
this._keys = fileKeys;
super.value = this._keys.join(',');
fileKeys.forEach((key) => {
if (!UmbId.validate(key) && key.startsWith('/')) this._filePaths.push(key);
});
}
public get keys(): Array<string> {
return this._keys;
@@ -37,7 +43,7 @@ export class UmbInputUploadFieldElement extends FormControlMixin(UmbLitElement)
* @type {Array<String>}
* @default undefined
*/
@property({ type: Array<string> })
@property({ type: Array })
fileExtensions?: Array<string>;
/**
@@ -50,7 +56,10 @@ export class UmbInputUploadFieldElement extends FormControlMixin(UmbLitElement)
multiple = false;
@state()
_currentFiles: File[] = [];
_currentFiles: Array<TemporaryFileQueueItem> = [];
@state()
_filePaths: Array<string> = [];
@state()
extensions?: string[];
@@ -58,10 +67,20 @@ export class UmbInputUploadFieldElement extends FormControlMixin(UmbLitElement)
@query('#dropzone')
private _dropzone?: UUIFileDropzoneElement;
#manager;
protected getFormElement() {
return undefined;
}
constructor() {
super();
this.#manager = new UmbTemporaryFileManager(this);
this.observe(this.#manager.isReady, (value) => (this.error = !value));
this.observe(this.#manager.queue, (value) => (this._currentFiles = value));
}
connectedCallback(): void {
super.connectedCallback();
this.#setExtensions();
@@ -85,12 +104,19 @@ export class UmbInputUploadFieldElement extends FormControlMixin(UmbLitElement)
}
#setFiles(files: File[]) {
this._currentFiles = [...this._currentFiles, ...files];
const items = files.map(
(file): TemporaryFileQueueItem => ({
unique: UmbId.new(),
file,
status: 'waiting',
}),
);
this.#manager.upload(items);
//TODO: set keys when possible, not names
this.keys = this._currentFiles.map((file) => file.name);
this.dispatchEvent(new CustomEvent('change', { bubbles: true }));
this.keys = items.map((item) => item.unique);
this.value = this.keys.join(',');
this.dispatchEvent(new UmbChangeEvent());
}
#handleBrowse() {
@@ -99,11 +125,14 @@ export class UmbInputUploadFieldElement extends FormControlMixin(UmbLitElement)
}
render() {
return html`${this.#renderFiles()} ${this.#renderDropzone()}`;
return html`<div id="wrapper">${this.#renderFilesWithPath()} ${this.#renderFilesUploaded()}</div>
${this.#renderDropzone()}${this.#renderButtonRemove()}`;
}
//TODO When the property editor gets saved, it seems that the property editor gets the file path from the server rather than key/id.
// This however does not work when there is multiple files. Can the server not handle multiple files uploaded into one property editor?
#renderDropzone() {
if (!this.multiple && this._currentFiles.length) return nothing;
if (!this.multiple && (this._currentFiles.length || this._filePaths.length)) return nothing;
return html`
<uui-file-dropzone
id="dropzone"
@@ -116,21 +145,41 @@ export class UmbInputUploadFieldElement extends FormControlMixin(UmbLitElement)
`;
}
#renderFiles() {
#renderFilesWithPath() {
if (!this._filePaths.length) return nothing;
return html`${this._filePaths.map(
(path) => html`<umb-input-upload-field-file .path=${path}></umb-input-upload-field-file>`,
)}`;
}
#renderFilesUploaded() {
if (!this._currentFiles.length) return nothing;
return html` <div id="wrapper">
${map(this._currentFiles, (file) => {
return html`<umb-input-upload-field-file .file=${file}></umb-input-upload-field-file>`;
})}
</div>
<uui-button compact @click=${this.#handleRemove} label="Remove files">
<uui-icon name="icon-trash"></uui-icon> Remove file(s)
</uui-button>`;
return html`
${repeat(
this._currentFiles,
(item) => item.unique + item.status,
(item) =>
html`<div style="position:relative;">
<umb-input-upload-field-file .file=${item.file as any}></umb-input-upload-field-file>
${item.status === 'waiting' ? html`<umb-temporary-file-badge></umb-temporary-file-badge>` : nothing}
</div> `,
)}
</div>`;
}
#renderButtonRemove() {
if (!this._currentFiles.length && !this._filePaths.length) return;
return html`<uui-button compact @click=${this.#handleRemove} label="Remove files">
<uui-icon name="icon-trash"></uui-icon> Remove file(s)
</uui-button>`;
}
#handleRemove() {
// Remove via endpoint?
this._currentFiles = [];
this._filePaths = [];
const uniques = this._currentFiles.map((item) => item.unique) as string[];
this.#manager.remove(uniques);
this.dispatchEvent(new UmbChangeEvent());
}
static styles = [
@@ -156,6 +205,10 @@ export class UmbInputUploadFieldElement extends FormControlMixin(UmbLitElement)
grid-template-columns: repeat(auto-fit, 200px);
gap: var(--uui-size-space-4);
}
uui-file-dropzone {
padding: 3px; /** Dropzone background is blurry and covers slightly into other elements. Hack to avoid this */
}
`,
];
}

View File

@@ -53,10 +53,10 @@ export class UmbDataTypeCreateOptionsModalElement extends UmbLitElement {
href=${`section/settings/workspace/data-type/create/${this.data?.parentKey || null}`}
label="New Data Type..."
@click=${this.#onNavigate}>
<uui-icon slot="icon" name="icon-autofill"></uui-icon>}
<uui-icon slot="icon" name="icon-autofill"></uui-icon>
</uui-menu-item>
<uui-menu-item @click=${this.#onClick} label="New Folder...">
<uui-icon slot="icon" name="icon-folder"></uui-icon>}
<uui-icon slot="icon" name="icon-folder"></uui-icon>
</uui-menu-item>
</uui-box>
<uui-button slot="actions" id="cancel" label="Cancel" @click="${this.#onCancel}">Cancel</uui-button>

View File

@@ -1,5 +1,5 @@
import { type UmbTreeElement } from '../../../tree/tree.element.js';
import { html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { html, customElement, state, ifDefined } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbTreePickerModalData, UmbPickerModalValue, UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
import { TreeItemPresentationModel } from '@umbraco-cms/backoffice/backend-api';
@@ -43,7 +43,8 @@ export class UmbTreePickerModalElement<TreeItemType extends TreeItemPresentation
<umb-body-layout headline="Select">
<uui-box>
<umb-tree
alias=${this.data?.treeAlias}
?hide-tree-root=${this.data?.hideTreeRoot}
alias=${ifDefined(this.data?.treeAlias)}
@selection-change=${this.#onSelectionChange}
.selection=${this._selection}
selectable

View File

@@ -1,6 +1,7 @@
export interface UmbPickerModalData<ItemType> {
multiple?: boolean;
selection?: Array<string | null>;
hideTreeRoot?: boolean;
filter?: (item: ItemType) => boolean;
pickableFilter?: (item: ItemType) => boolean;
}

View File

@@ -1,3 +1,4 @@
import { EntityTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
export interface UmbImportDictionaryModalData {
@@ -5,7 +6,7 @@ export interface UmbImportDictionaryModalData {
}
export interface UmbImportDictionaryModalValue {
temporaryFileId: string;
entityItems: Array<EntityTreeItemResponseModel>;
parentId?: string;
}

View File

@@ -1,6 +1,6 @@
import { UmbInputUploadFieldElement } from '../../../components/input-upload-field/input-upload-field.element.js';
import { html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from "@umbraco-cms/backoffice/style";
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
@@ -34,7 +34,8 @@ export class UmbPropertyEditorUIUploadFieldElement extends UmbLitElement impleme
return html`<umb-input-upload-field
@change="${this._onChange}"
?multiple="${this._multiple}"
.fileExtensions="${this._fileExtensions}"></umb-input-upload-field>`;
.fileExtensions="${this._fileExtensions}"
.keys=${(this.value as string)?.split(',') ?? []}></umb-input-upload-field>`;
}
static styles = [UmbTextStyles];

View File

@@ -0,0 +1,73 @@
import { css, customElement, html, property } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { clamp } from '@umbraco-cms/backoffice/external/uui';
@customElement('umb-temporary-file-badge')
export class UmbTemporaryFileBadgeElement extends UmbLitElement {
private _progress = 0;
@property({ type: Number })
public set progress(v: number) {
const oldVal = this._progress;
const p = clamp(v, 0, 100);
this._progress = p;
this.requestUpdate('progress', oldVal);
}
public get progress(): number {
return this._progress;
}
render() {
return html`<uui-badge>
<div id="wrapper">
<uui-loader-circle progress=${this.progress}></uui-loader-circle>
<uui-icon name="icon-arrow-up"></uui-icon>
</div>
</uui-badge>`;
}
static styles = css`
:host {
display: block;
}
#wrapper {
box-sizing: border-box;
box-shadow: inset 0px 0px 0px 6px var(--uui-color-surface);
background-color: var(--uui-color-selected);
position: relative;
border-radius: 100%;
font-size: var(--uui-size-6);
}
uui-loader-circle {
display: absolute;
inset: 0;
color: var(--uui-color-focus);
font-size: var(--uui-size-12);
}
uui-badge {
padding: 0;
background-color: transparent;
}
uui-icon {
font-size: var(--uui-size-6);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
'umb-temporary-file-badge': UmbTemporaryFileBadgeElement;
}
}
export default UmbTemporaryFileBadgeElement;

View File

@@ -1 +1,2 @@
export * from './temporary-file.repository.js';
export * from './components/temporary-file-badge.element.js';

View File

@@ -0,0 +1,74 @@
import { UmbTemporaryFileRepository } from './temporary-file.repository.js';
import { UmbArrayState, UmbBooleanState } from '@umbraco-cms/backoffice/observable-api';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import { UmbBaseController } from '@umbraco-cms/backoffice/class-api';
export type TemporaryFileStatus = 'complete' | 'waiting' | 'error';
export interface TemporaryFileQueueItem {
unique: string;
file: File;
status?: TemporaryFileStatus;
}
export class UmbTemporaryFileManager extends UmbBaseController {
#temporaryFileRepository;
#queue = new UmbArrayState<TemporaryFileQueueItem>([], (item) => item.unique);
public readonly queue = this.#queue.asObservable();
#isReady = new UmbBooleanState(true);
public readonly isReady = this.#isReady.asObservable();
constructor(host: UmbControllerHostElement) {
super(host);
this.#temporaryFileRepository = new UmbTemporaryFileRepository(host);
}
uploadOne(unique: string, file: File, status: TemporaryFileStatus = 'waiting') {
this.#queue.appendOne({ unique, file, status });
this.handleQueue();
}
upload(queueItems: Array<TemporaryFileQueueItem>) {
this.#queue.append(queueItems);
this.handleQueue();
}
removeOne(unique: string) {
this.#queue.removeOne(unique);
}
remove(uniques: Array<string>) {
this.#queue.remove(uniques);
}
private async handleQueue() {
const queue = this.#queue.getValue();
if (!queue.length && this.getIsReady()) return;
this.#isReady.next(false);
queue.forEach(async (item) => {
if (item.status !== 'waiting') return;
const { error } = await this.#temporaryFileRepository.upload(item.unique, item.file);
await new Promise((resolve) => setTimeout(resolve, (Math.random() + 0.5) * 1000)); // simulate small delay so that the upload badge is properly shown
if (error) {
this.#queue.updateOne(item.unique, { ...item, status: 'error' });
} else {
this.#queue.updateOne(item.unique, { ...item, status: 'complete' });
}
});
if (!queue.find((item) => item.status === 'waiting') && !this.getIsReady()) {
this.#isReady.next(true);
}
}
getIsReady() {
return this.#queue.getValue();
}
}

View File

@@ -1,10 +0,0 @@
import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import { UMB_DICTIONARY_ITEM_PICKER_MODAL } from '@umbraco-cms/backoffice/modal';
import { DictionaryItemItemResponseModel } from '@umbraco-cms/backoffice/backend-api';
export class UmbDictionaryItemPickerContext extends UmbPickerInputContext<DictionaryItemItemResponseModel> {
constructor(host: UmbControllerHostElement) {
super(host, 'Umb.Repository.Dictionary', UMB_DICTIONARY_ITEM_PICKER_MODAL);
}
}

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
import '../../components/dictionary-item-input/dictionary-item-input.element.js';
import UmbDictionaryItemInputElement from '../../components/dictionary-item-input/dictionary-item-input.element.js';
import { UMB_DICTIONARY_TREE_ALIAS } from '../../tree/manifests.js';
import { UmbDictionaryRepository } from '../../repository/dictionary.repository.js';
import { css, html, customElement, query, state, when } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
@@ -8,11 +7,13 @@ import {
UmbImportDictionaryModalValue,
UmbModalBaseElement,
} from '@umbraco-cms/backoffice/modal';
import { ImportDictionaryRequestModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbId } from '@umbraco-cms/backoffice/id';
import { EntityTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbTreeElement } from '@umbraco-cms/backoffice/tree';
interface DictionaryItemPreview {
name: string;
id: string;
children: Array<DictionaryItemPreview>;
}
@@ -25,32 +26,64 @@ export class UmbImportDictionaryModalLayout extends UmbModalBaseElement<
private _parentId?: string;
@state()
private _temporaryFileId?: string;
private _temporaryFileId = '';
@query('#form')
private _form!: HTMLFormElement;
@query('umb-tree')
private _treeElement?: UmbTreeElement;
#fileReader;
#fileNodes!: NodeListOf<ChildNode>;
#fileContent: Array<DictionaryItemPreview> = [];
#dictionaryRepository: UmbDictionaryRepository;
#handleClose() {
this.modalContext?.reject();
}
#submit() {
// TODO: Gotta do a temp file upload before submitting, so that the server can use it
console.log('submit:', this._temporaryFileId, this._parentId);
//this.modalContext?.submit({ temporaryFileId: this._temporaryFileId, parentId: this._parentId });
#createTreeEntitiesFromTempFile(): Array<EntityTreeItemResponseModel> {
const data: Array<EntityTreeItemResponseModel> = [];
const list = this.#dictionaryPreviewItemBuilder(this.#fileNodes);
const scaffold = (items: Array<DictionaryItemPreview>, parentId?: string) => {
items.forEach((item) => {
data.push({
id: item.id,
name: item.name,
type: 'dictionary-item',
hasChildren: item.children.length ? true : false,
parentId: parentId,
});
scaffold(item.children, item.id);
});
};
scaffold(list, this._parentId);
return data;
}
async #submit() {
const { error } = await this.#dictionaryRepository.import(this._temporaryFileId, this._parentId);
if (error) return;
this.modalContext?.submit({ entityItems: this.#createTreeEntitiesFromTempFile(), parentId: this._parentId });
}
constructor() {
super();
this.#dictionaryRepository = new UmbDictionaryRepository(this);
this.#fileReader = new FileReader();
this.#fileReader.onload = (e) => {
if (typeof e.target?.result === 'string') {
const fileContent = e.target.result;
this.#dictionaryItemBuilder(fileContent);
this.#dictionaryPreviewBuilder(fileContent);
}
};
}
@@ -60,16 +93,17 @@ export class UmbImportDictionaryModalLayout extends UmbModalBaseElement<
this._parentId = this.data?.unique ?? undefined;
}
#dictionaryItemBuilder(htmlString: string) {
#dictionaryPreviewBuilder(htmlString: string) {
const parser = new DOMParser();
const doc = parser.parseFromString(htmlString, 'text/xml');
const elements = doc.childNodes;
this.#fileNodes = elements;
this.#fileContent = this.#makeDictionaryItems(elements);
this.#fileContent = this.#dictionaryPreviewItemBuilder(elements);
this.requestUpdate();
}
#makeDictionaryItems(nodeList: NodeListOf<ChildNode>): Array<DictionaryItemPreview> {
#dictionaryPreviewItemBuilder(nodeList: NodeListOf<ChildNode>): Array<DictionaryItemPreview> {
const items: Array<DictionaryItemPreview> = [];
const list: Array<Element> = [];
nodeList.forEach((node) => {
@@ -81,25 +115,29 @@ export class UmbImportDictionaryModalLayout extends UmbModalBaseElement<
list.forEach((item) => {
items.push({
name: item.getAttribute('Name') ?? '',
children: this.#makeDictionaryItems(item.childNodes) ?? undefined,
id: item.getAttribute('Key') ?? '',
children: this.#dictionaryPreviewItemBuilder(item.childNodes) ?? undefined,
});
});
return items;
}
#onUpload(e: Event) {
async #onUpload(e: Event) {
e.preventDefault();
const formData = new FormData(this._form);
const file = formData.get('file') as Blob;
const file = formData.get('file') as File;
if (!file) throw new Error('File is missing');
this.#fileReader.readAsText(file);
this._temporaryFileId = file ? UmbId.new() : undefined;
this._temporaryFileId = UmbId.new();
this.#dictionaryRepository.upload(this._temporaryFileId, file);
}
#onParentChange(event: CustomEvent) {
this._parentId = (event.target as UmbDictionaryItemInputElement).selectedIds[0] || undefined;
//console.log((event.target as UmbDictionaryItemInputElement).selectedIds[0] || undefined);
#onParentChange() {
this._parentId = this._treeElement?.selection[0] ?? undefined;
}
async #onFileInput() {
@@ -145,16 +183,15 @@ export class UmbImportDictionaryModalLayout extends UmbModalBaseElement<
</div>
<div>
<strong><umb-localize key="actions_chooseWhereToImport">Choose where to import</umb-localize>:</strong>
Work in progress<br />
${
this._parentId
// TODO
// <umb-dictionary-item-input
// @change=${this.#onParentChange}
// .selectedIds=${this._parentId ? [this._parentId] : []}
// max="1">
// </umb-dictionary-item-input>
}
<br />parentId:
<umb-tree
?hide-tree-root=${true}
?multiple=${false}
alias=${UMB_DICTIONARY_TREE_ALIAS}
@selection-change=${this.#onParentChange}
.selection=${[this._parentId ?? '']}
selectable></umb-tree>
</div>
${this.#renderNavigate()}

View File

@@ -7,11 +7,13 @@ import {
UMB_MODAL_MANAGER_CONTEXT_TOKEN,
UMB_IMPORT_DICTIONARY_MODAL,
} from '@umbraco-cms/backoffice/modal';
import { UMB_DICTIONARY_TREE_STORE_CONTEXT, UmbDictionaryTreeStore } from '@umbraco-cms/backoffice/dictionary';
export default class UmbImportDictionaryEntityAction extends UmbEntityActionBase<UmbDictionaryRepository> {
static styles = [UmbTextStyles];
#modalContext?: UmbModalManagerContext;
#treeStore?: UmbDictionaryTreeStore;
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) {
super(host, repositoryAlias, unique);
@@ -19,6 +21,9 @@ export default class UmbImportDictionaryEntityAction extends UmbEntityActionBase
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT_TOKEN, (instance) => {
this.#modalContext = instance;
});
this.consumeContext(UMB_DICTIONARY_TREE_STORE_CONTEXT, (instance) => {
this.#treeStore = instance;
});
}
async execute() {
@@ -26,8 +31,12 @@ export default class UmbImportDictionaryEntityAction extends UmbEntityActionBase
const modalContext = this.#modalContext?.open(UMB_IMPORT_DICTIONARY_MODAL, { unique: this.unique });
const { parentId, temporaryFileId } = await modalContext.onSubmit();
const { entityItems, parentId } = await modalContext.onSubmit();
await this.repository?.import(temporaryFileId, parentId);
if (!entityItems?.length) return;
this.#treeStore?.appendItems(entityItems);
if (parentId) this.#treeStore?.updateItem(parentId, { hasChildren: true });
}
}

View File

@@ -1,3 +1,2 @@
export * from './repository/index.js';
export * from './tree/index.js';
export * from './components/index.js';

View File

@@ -7,11 +7,11 @@ import { UmbDetailRepository } from '@umbraco-cms/backoffice/repository';
import {
CreateDictionaryItemRequestModel,
DictionaryOverviewResponseModel,
ImportDictionaryRequestModel,
UpdateDictionaryItemRequestModel,
} from '@umbraco-cms/backoffice/backend-api';
import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/notification';
import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
import { UmbTemporaryFileRepository } from '@umbraco-cms/backoffice/temporary-file';
export class UmbDictionaryRepository
extends UmbBaseController
@@ -31,6 +31,8 @@ export class UmbDictionaryRepository
#detailSource: UmbDictionaryDetailServerDataSource;
#detailStore?: UmbDictionaryStore;
#temporaryFileRepository: UmbTemporaryFileRepository;
#notificationContext?: UmbNotificationContext;
constructor(host: UmbControllerHostElement) {
@@ -38,6 +40,7 @@ export class UmbDictionaryRepository
// TODO: figure out how spin up get the correct data source
this.#detailSource = new UmbDictionaryDetailServerDataSource(this);
this.#temporaryFileRepository = new UmbTemporaryFileRepository(host);
this.#init = Promise.all([
this.consumeContext(UMB_DICTIONARY_STORE_CONTEXT_TOKEN, (instance) => {
@@ -93,6 +96,7 @@ export class UmbDictionaryRepository
async delete(id: string) {
await this.#init;
await this.#treeStore?.removeItem(id);
return this.#detailSource.delete(id);
}
@@ -155,14 +159,12 @@ export class UmbDictionaryRepository
return this.#detailSource.import(temporaryFileId, parentId);
}
async upload(formData: ImportDictionaryRequestModel) {
async upload(UmbId: string, file: File) {
await this.#init;
if (!UmbId) throw new Error('UmbId is missing');
if (!file) throw new Error('File is missing');
if (!formData) {
throw new Error('Form data is missing');
}
return this.#detailSource.upload(formData);
return this.#temporaryFileRepository.upload(UmbId, file);
}
// TODO => temporary only, until languages data source exists, or might be

View File

@@ -1,5 +1,5 @@
import { UMB_MEDIA_WORKSPACE_CONTEXT } from './media-workspace.context.js';
import { UmbTextStyles } from "@umbraco-cms/backoffice/style";
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, nothing, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
@customElement('umb-media-workspace-editor')

View File

@@ -1,81 +0,0 @@
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, customElement, html, property } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
@customElement('umb-template-alias-input')
export class UmbTemplateAliasInputElement extends UmbLitElement {
render() {
return html`
<uui-button compact @click=${this.#handleClick} label="unlock alias input">
<uui-symbol-lock .open=${this.isOpen} ></uui-symbol-lock>
</uui-button>
<input placeholder="Enter alias..." .value=${this.value} ?disabled=${!this.isOpen} @input=${
this.#setValue
}></input>
`;
}
@property({ type: String, attribute: 'value' })
value = '';
@property({ type: Boolean })
isOpen = false;
#setValue(event: Event) {
event.stopPropagation();
this.value = (event.target as HTMLInputElement).value;
}
#handleClick() {
this.isOpen = !this.isOpen;
if (!this.isOpen) {
this.dispatchEvent(new UmbChangeEvent());
}
}
static styles = [
UmbTextStyles,
css`
:host {
display: inline-flex;
align-items: center;
}
:host(:focus-within) {
border-color: var(--uui-input-border-color-focus, var(--uui-color-border-emphasis, #a1a1a1));
outline: calc(2px * var(--uui-show-focus-outline, 1)) solid var(--uui-color-focus, #3879ff);
}
input {
background: transparent;
border-color: transparent;
font-family: inherit;
padding: var(--uui-size-1, 3px) var(--uui-size-space-3, 9px);
font-size: inherit;
color: inherit;
border-radius: 0px;
box-sizing: border-box;
border: none;
background: none;
width: 100%;
height: 100%;
text-align: inherit;
outline: none;
}
input:disabled {
color: #a2a1a6;
}
`,
];
}
export default UmbTemplateAliasInputElement;
declare global {
interface HTMLElementTagNameMap {
'umb-template-alias-input': UmbTemplateAliasInputElement;
}
}

View File

@@ -1,3 +1,2 @@
export * from './template-card/template-card.element.js';
export * from './input-template/input-template.element.js';
export * from './alias-input/alias-input.js';

View File

@@ -93,13 +93,13 @@ export class UmbQueryBuilderFilterElement extends UmbLitElement {
<uui-combobox-list slot="dropdown" @change=${this.#setOperator} class="options-list">
${this.settings?.operators
?.filter((operator) =>
this.currentPropertyType ? operator.applicableTypes?.includes(this.currentPropertyType) : true
this.currentPropertyType ? operator.applicableTypes?.includes(this.currentPropertyType) : true,
)
.map(
(operator) =>
html`<uui-combobox-list-option .value=${(operator.operator as string) ?? ''}
>${operator.operator}</uui-combobox-list-option
>`
>`,
)}</uui-combobox-list
>
</umb-button-with-dropdown>`;
@@ -120,27 +120,27 @@ export class UmbQueryBuilderFilterElement extends UmbLitElement {
render() {
return html`
<span>${this.unremovable ? 'where' : 'and'}</span>
<span>${this.unremovable ? this.localize.term('template_where') : this.localize.term('template_and')}</span>
<umb-button-with-dropdown look="outline" id="property-alias-dropdown" label="Property alias"
>${this.filter?.propertyAlias ?? ''}
<uui-combobox-list slot="dropdown" @change=${this.#setPropertyAlias} class="options-list">
${this.settings?.properties?.map(
(property) =>
html`<uui-combobox-list-option tabindex="0" .value=${property.alias ?? ''}
>${property.alias}</uui-combobox-list-option
>`
html`<uui-combobox-list-option tabindex="0" .value=${property.alias ?? ''}>
${property.alias}
</uui-combobox-list-option>`,
)}
</uui-combobox-list></umb-button-with-dropdown
>
${this.filter?.propertyAlias ? this._renderOperatorsDropdown() : ''}
${this.filter?.operator ? this._renderConstraintValueInput() : ''}
<uui-button-group>
<uui-button title="Add filter" label="Add filter" compact @click=${this.#addFilter}
><uui-icon name="add"></uui-icon
></uui-button>
<uui-button title="Remove filter" label="Remove filter" compact @click=${this.#removeOrReset}
><uui-icon name="delete"></uui-icon
></uui-button>
<uui-button title="Add filter" label="Add filter" compact @click=${this.#addFilter}>
<uui-icon name="icon-add"></uui-icon>
</uui-button>
<uui-button title="Remove filter" label="Remove filter" compact @click=${this.#removeOrReset}>
<uui-icon name="delete"></uui-icon>
</uui-button>
</uui-button-group>
`;
}

View File

@@ -57,10 +57,10 @@ export default class UmbChooseInsertTypeModalElement extends UmbModalBaseElement
private _queryBuilderSettings?: TemplateQuerySettingsResponseModel;
@state()
private _selectedRootContentName? = 'all pages';
private _selectedRootContentName? = this.localize.term('template_websiteRoot');
@state()
private _defaultSortDirection: SortOrder = SortOrder.Descending;
private _defaultSortDirection: SortOrder = SortOrder.Ascending;
#documentRepository: UmbDocumentRepository;
#modalManagerContext?: UmbModalManagerContext;
@@ -111,7 +111,7 @@ export default class UmbChooseInsertTypeModalElement extends UmbModalBaseElement
#openDocumentPicker = () => {
this.#modalManagerContext
?.open(UMB_DOCUMENT_PICKER_MODAL)
?.open(UMB_DOCUMENT_PICKER_MODAL, { hideTreeRoot: true })
.onSubmit()
.then((result) => {
this.#updateQueryRequest({ rootContentId: result.selection[0] });
@@ -252,7 +252,7 @@ export default class UmbChooseInsertTypeModalElement extends UmbModalBaseElement
ms</span
>
</div>
<umb-code-block language="C#" copy> ${this._templateQuery?.queryExpression ?? ''} </umb-code-block>
<umb-code-block language="C#" copy>${this._templateQuery?.queryExpression ?? ''}</umb-code-block>
</uui-box>
</div>

View File

@@ -10,6 +10,9 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
export class UmbTemplateQueryBuilderServerDataSource {
#host: UmbControllerHost;
// TODO: When we map the server models to our own models, we need to have a localization property.
// For example, the OperatorModel.NOT_EQUALS need to use the localization key "template_doesNotEqual"
/**
* Creates an instance of UmbTemplateQueryBuilderServerDataSource.
* @param {UmbControllerHost} host

View File

@@ -6,7 +6,7 @@ import { UMB_TEMPLATE_WORKSPACE_CONTEXT } from './template-workspace.context.js'
import type { UmbCodeEditorElement } from '@umbraco-cms/backoffice/code-editor';
import { camelCase } from '@umbraco-cms/backoffice/external/lodash';
import { UUIInputElement } from '@umbraco-cms/backoffice/external/uui';
import { css, html, customElement, query, state, nothing } from '@umbraco-cms/backoffice/external/lit';
import { css, html, customElement, query, state, nothing, ifDefined } from '@umbraco-cms/backoffice/external/lit';
import {
UMB_MODAL_MANAGER_CONTEXT_TOKEN,
UMB_TEMPLATE_PICKER_MODAL,
@@ -179,12 +179,10 @@ export class UmbTemplateWorkspaceEditorElement extends UmbLitElement {
slot="header"
.value=${this._name}
@input=${this.#onNameInput}
label="template name"
><umb-template-alias-input
slot="append"
.value=${this._alias ?? ''}
@change=${this.#onAliasInput}></umb-template-alias-input
></uui-input>
label="template name">
<uui-input-lock slot="append" value=${ifDefined(this._alias!)} @change=${this.#onAliasInput}></uui-input-lock>
</uui-input>
<uui-box>
<div slot="header" id="code-editor-menu-container">
${this.#renderMasterTemplatePicker()}

View File

@@ -49,6 +49,9 @@ export class UmbUmbracoNewsDashboardElement extends UmbLitElement {
display: block;
padding: var(--uui-size-layout-1);
}
p {
position: relative;
}
`,
];
}