Merge branch 'main' into v14/feature/readonly-media-picker-property-editor-ui
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
import { UmbSorterController } from './sorter.controller.js';
|
||||
import { aTimeout, expect, fixture, html } from '@open-wc/testing';
|
||||
import { customElement } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '../lit-element/lit-element.element.js';
|
||||
|
||||
@customElement('test-my-sorter')
|
||||
class UmbSorterTestElement extends UmbLitElement {
|
||||
sorter = new UmbSorterController<string, HTMLElement>(this, {
|
||||
getUniqueOfElement: (element) => {
|
||||
return element.id;
|
||||
},
|
||||
getUniqueOfModel: (modelEntry) => {
|
||||
return modelEntry;
|
||||
},
|
||||
identifier: 'Umb.SorterIdentifier.Test',
|
||||
itemSelector: '.item',
|
||||
containerSelector: '#container',
|
||||
disabledItemSelector: '.disabled',
|
||||
onChange: ({ model }) => {
|
||||
this.model = model;
|
||||
},
|
||||
});
|
||||
|
||||
getAllItems() {
|
||||
return Array.from(this.shadowRoot!.querySelectorAll('.item')) as HTMLElement[];
|
||||
}
|
||||
|
||||
getSortableItems() {
|
||||
return Array.from(this.shadowRoot!.querySelectorAll('.item:not(.disabled')) as HTMLElement[];
|
||||
}
|
||||
|
||||
getDisabledItems() {
|
||||
return Array.from(this.shadowRoot!.querySelectorAll('.item.disabled')) as HTMLElement[];
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`<div id="container">
|
||||
<div id="1" class="item">Item 1</div>
|
||||
<div id="2" class="item">Item 2</div>
|
||||
<div id="3" class="item disabled">Item 3</div>
|
||||
<div id="4" class="item">Item 4</div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
describe('UmbSorterController', () => {
|
||||
let element: UmbSorterTestElement;
|
||||
|
||||
beforeEach(async () => {
|
||||
element = await fixture(html`<test-my-sorter></test-my-sorter>`);
|
||||
await aTimeout(10);
|
||||
});
|
||||
|
||||
it('is defined with its own instance', () => {
|
||||
expect(element).to.be.instanceOf(UmbSorterTestElement);
|
||||
expect(element.sorter).to.be.instanceOf(UmbSorterController);
|
||||
});
|
||||
|
||||
describe('Public API', () => {
|
||||
describe('methods', () => {
|
||||
it('has a enable method', () => {
|
||||
expect(element.sorter).to.have.property('enable').that.is.a('function');
|
||||
});
|
||||
|
||||
it('has a disable method', () => {
|
||||
expect(element.sorter).to.have.property('disable').that.is.a('function');
|
||||
});
|
||||
|
||||
it('has a setModel method', () => {
|
||||
expect(element.sorter).to.have.property('setModel').that.is.a('function');
|
||||
});
|
||||
|
||||
it('has a hasItem method', () => {
|
||||
expect(element.sorter).to.have.property('hasItem').that.is.a('function');
|
||||
});
|
||||
|
||||
it('has a getItem method', () => {
|
||||
expect(element.sorter).to.have.property('getItem').that.is.a('function');
|
||||
});
|
||||
|
||||
it('has a setupItem method', () => {
|
||||
expect(element.sorter).to.have.property('setupItem').that.is.a('function');
|
||||
});
|
||||
|
||||
it('has a destroyItem method', () => {
|
||||
expect(element.sorter).to.have.property('destroyItem').that.is.a('function');
|
||||
});
|
||||
|
||||
it('has a hasOtherItemsThan method', () => {
|
||||
expect(element.sorter).to.have.property('hasOtherItemsThan').that.is.a('function');
|
||||
});
|
||||
|
||||
it('has a moveItemInModel method', () => {
|
||||
expect(element.sorter).to.have.property('moveItemInModel').that.is.a('function');
|
||||
});
|
||||
|
||||
it('has a updateAllowIndication method', () => {
|
||||
expect(element.sorter).to.have.property('updateAllowIndication').that.is.a('function');
|
||||
});
|
||||
|
||||
it('has a removeAllowIndication method', () => {
|
||||
expect(element.sorter).to.have.property('removeAllowIndication').that.is.a('function');
|
||||
});
|
||||
|
||||
it('has a notifyDisallowed method', () => {
|
||||
expect(element.sorter).to.have.property('notifyDisallowed').that.is.a('function');
|
||||
});
|
||||
|
||||
it('has a notifyRequestDrop method', () => {
|
||||
expect(element.sorter).to.have.property('notifyRequestDrop').that.is.a('function');
|
||||
});
|
||||
|
||||
it('has a destroy method', () => {
|
||||
expect(element.sorter).to.have.property('destroy').that.is.a('function');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Init', () => {
|
||||
it('should find all items', () => {
|
||||
const items = element.getAllItems();
|
||||
expect(items.length).to.equal(4);
|
||||
});
|
||||
|
||||
it('sets all allowed draggable items to draggable', () => {
|
||||
const items = element.getSortableItems();
|
||||
expect(items.length).to.equal(3);
|
||||
items.forEach((item) => {
|
||||
expect(item.draggable).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
it('sets all disabled items non draggable', () => {
|
||||
const items = element.getDisabledItems();
|
||||
expect(items.length).to.equal(1);
|
||||
items.forEach((item) => {
|
||||
expect(item.draggable).to.be.false;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('disable', () => {
|
||||
it('sets all items to non draggable', () => {
|
||||
element.sorter.disable();
|
||||
const items = element.getAllItems();
|
||||
items.forEach((item) => {
|
||||
expect(item.draggable).to.be.false;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('enable', () => {
|
||||
it('sets all allowed items to draggable', () => {
|
||||
const items = element.getSortableItems();
|
||||
expect(items.length).to.equal(3);
|
||||
items.forEach((item) => {
|
||||
expect(item.draggable).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
it('sets all disabled items non draggable', () => {
|
||||
const items = element.getDisabledItems();
|
||||
expect(items.length).to.equal(1);
|
||||
items.forEach((item) => {
|
||||
expect(item.draggable).to.be.false;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setModel & getModel', () => {
|
||||
it('it sets the model', () => {
|
||||
const model = ['1', '2', '3', '4'];
|
||||
element.sorter.setModel(model);
|
||||
expect(element.sorter.getModel()).to.deep.equal(model);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasItem', () => {
|
||||
beforeEach(() => {
|
||||
element.sorter.setModel(['1', '2', '3', '4']);
|
||||
});
|
||||
|
||||
it('returns true if item exists', () => {
|
||||
expect(element.sorter.hasItem('1')).to.be.true;
|
||||
});
|
||||
|
||||
it('returns false if item does not exist', () => {
|
||||
expect(element.sorter.hasItem('5')).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('getItem', () => {
|
||||
beforeEach(() => {
|
||||
element.sorter.setModel(['1', '2', '3', '4']);
|
||||
});
|
||||
|
||||
it('returns the item if it exists', () => {
|
||||
expect(element.sorter.getItem('1')).to.equal('1');
|
||||
});
|
||||
|
||||
it('returns undefined if item does not exist', () => {
|
||||
expect(element.sorter.getItem('5')).to.be.undefined;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -260,6 +260,8 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
|
||||
#dragX = 0;
|
||||
#dragY = 0;
|
||||
|
||||
#items = Array<ElementType>();
|
||||
|
||||
public get identifier() {
|
||||
return this.#config.identifier;
|
||||
}
|
||||
@@ -294,6 +296,11 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables the sorter, this will allow sorting to happen.
|
||||
* @return {*} {void}
|
||||
* @memberof UmbSorterController
|
||||
*/
|
||||
enable(): void {
|
||||
if (this.#enabled) return;
|
||||
this.#enabled = true;
|
||||
@@ -301,6 +308,12 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
|
||||
this.#initialize();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the sorter, this will prevent any sorting to happen.
|
||||
* @return {*} {void}
|
||||
* @memberof UmbSorterController
|
||||
*/
|
||||
disable(): void {
|
||||
if (!this.#enabled) return;
|
||||
this.#enabled = false;
|
||||
@@ -316,6 +329,15 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the model of the sorter.
|
||||
* @return {Array<T>}
|
||||
* @memberof UmbSorterController
|
||||
*/
|
||||
getModel(): Array<T> {
|
||||
return this.#model;
|
||||
}
|
||||
|
||||
hasItem(unique: UniqueType) {
|
||||
return this.#model.find((x) => this.#config.getUniqueOfModel(x) === unique) !== undefined;
|
||||
}
|
||||
@@ -330,12 +352,14 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
|
||||
requestAnimationFrame(this.#initialize);
|
||||
}
|
||||
}
|
||||
|
||||
override hostDisconnected() {
|
||||
this.#isConnected = false;
|
||||
if (this.#enabled) {
|
||||
this.#uninitialize();
|
||||
}
|
||||
}
|
||||
|
||||
#initialize = () => {
|
||||
const containerEl =
|
||||
(this.#config.containerSelector
|
||||
@@ -365,6 +389,7 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
|
||||
subtree: false,
|
||||
});
|
||||
};
|
||||
|
||||
#uninitialize() {
|
||||
// TODO: Is there more clean up to do??
|
||||
this.#observer.disconnect();
|
||||
@@ -377,6 +402,8 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
|
||||
containerElement.removeEventListener('dragover', this._itemDraggedOver as unknown as EventListener);
|
||||
(this.#containerElement as unknown) = undefined;
|
||||
}
|
||||
|
||||
this.#items.forEach((item) => this.destroyItem(item));
|
||||
}
|
||||
|
||||
_itemDraggedOver = (e: DragEvent) => {
|
||||
@@ -442,6 +469,9 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.#items.push(element);
|
||||
this.#items = Array.from(new Set(this.#items));
|
||||
}
|
||||
|
||||
destroyItem(element: HTMLElement) {
|
||||
@@ -453,6 +483,10 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
|
||||
draggableElement.removeEventListener('dragstart', this.#handleDragStart);
|
||||
// We are not ready to remove the dragend or drop, as this is might be the active one just moving container:
|
||||
//draggableElement.removeEventListener('dragend', this.#handleDragEnd);
|
||||
|
||||
(draggableElement as HTMLElement).draggable = false;
|
||||
|
||||
this.#items = this.#items.filter((x) => x !== element);
|
||||
}
|
||||
|
||||
#setupPlaceholderStyle() {
|
||||
|
||||
@@ -225,6 +225,7 @@ export class UmbWorkspaceSplitViewVariantSelectorElement extends UmbLitElement {
|
||||
.value=${this._name ?? ''}
|
||||
@input=${this.#handleInput}
|
||||
required
|
||||
?readonly=${this.#isReadOnly(this._activeVariant?.culture ?? null)}
|
||||
${umbBindToValidation(this, `$.variants[${UmbDataPathVariantQuery(this._variantId)}].name`, this._name ?? '')}
|
||||
${umbFocus()}
|
||||
>
|
||||
|
||||
@@ -5,26 +5,50 @@ import { UUITextareaEvent } from '@umbraco-cms/backoffice/external/uui';
|
||||
import { css, html, customElement, state, repeat, ifDefined, unsafeHTML } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { UmbLanguageCollectionRepository, type UmbLanguageDetailModel } from '@umbraco-cms/backoffice/language';
|
||||
import { UMB_CURRENT_USER_CONTEXT } from '@umbraco-cms/backoffice/current-user';
|
||||
|
||||
@customElement('umb-workspace-view-dictionary-editor')
|
||||
export class UmbWorkspaceViewDictionaryEditorElement extends UmbLitElement {
|
||||
@state()
|
||||
private _dictionary?: UmbDictionaryDetailModel;
|
||||
|
||||
#languageCollectionRepository = new UmbLanguageCollectionRepository(this);
|
||||
|
||||
@state()
|
||||
private _languages: Array<UmbLanguageDetailModel> = [];
|
||||
|
||||
#workspaceContext!: typeof UMB_DICTIONARY_WORKSPACE_CONTEXT.TYPE;
|
||||
@state()
|
||||
private _currentUserLanguageAccess?: Array<string> = [];
|
||||
|
||||
override async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
@state()
|
||||
private _currentUserHasAccessToAllLanguages?: boolean = false;
|
||||
|
||||
#languageCollectionRepository = new UmbLanguageCollectionRepository(this);
|
||||
#workspaceContext!: typeof UMB_DICTIONARY_WORKSPACE_CONTEXT.TYPE;
|
||||
#currentUserContext?: typeof UMB_CURRENT_USER_CONTEXT.TYPE;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.consumeContext(UMB_DICTIONARY_WORKSPACE_CONTEXT, (_instance) => {
|
||||
this.#workspaceContext = _instance;
|
||||
this.#observeDictionary();
|
||||
});
|
||||
|
||||
this.consumeContext(UMB_CURRENT_USER_CONTEXT, (context) => {
|
||||
this.#currentUserContext = context;
|
||||
this.#observeCurrentUserLanguageAccess();
|
||||
});
|
||||
}
|
||||
|
||||
#observeCurrentUserLanguageAccess() {
|
||||
if (!this.#currentUserContext) return;
|
||||
|
||||
this.observe(this.#currentUserContext.languages, (languages) => {
|
||||
this._currentUserLanguageAccess = languages;
|
||||
});
|
||||
|
||||
this.observe(this.#currentUserContext.hasAccessToAllLanguages, (hasAccess) => {
|
||||
this._currentUserHasAccessToAllLanguages = hasAccess;
|
||||
});
|
||||
}
|
||||
|
||||
override async firstUpdated() {
|
||||
@@ -40,19 +64,11 @@ export class UmbWorkspaceViewDictionaryEditorElement extends UmbLitElement {
|
||||
});
|
||||
}
|
||||
|
||||
#renderTranslation(language: UmbLanguageDetailModel) {
|
||||
if (!language.unique) return;
|
||||
|
||||
const translation = this._dictionary?.translations?.find((x) => x.isoCode === language.unique);
|
||||
|
||||
return html` <umb-property-layout label=${language.name ?? language.unique}>
|
||||
<uui-textarea
|
||||
slot="editor"
|
||||
name=${language.unique}
|
||||
label="translation"
|
||||
@change=${this.#onTextareaChange}
|
||||
value=${ifDefined(translation?.translation)}></uui-textarea>
|
||||
</umb-property-layout>`;
|
||||
#isReadOnly(culture: string | null) {
|
||||
if (!this.#currentUserContext) return true;
|
||||
if (!culture) return false;
|
||||
if (this._currentUserHasAccessToAllLanguages) return false;
|
||||
return !this._currentUserLanguageAccess?.includes(culture);
|
||||
}
|
||||
|
||||
#onTextareaChange(e: Event) {
|
||||
@@ -78,6 +94,22 @@ export class UmbWorkspaceViewDictionaryEditorElement extends UmbLitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
#renderTranslation(language: UmbLanguageDetailModel) {
|
||||
if (!language.unique) return;
|
||||
|
||||
const translation = this._dictionary?.translations?.find((x) => x.isoCode === language.unique);
|
||||
|
||||
return html` <umb-property-layout label=${language.name ?? language.unique}>
|
||||
<uui-textarea
|
||||
slot="editor"
|
||||
name=${language.unique}
|
||||
label="translation"
|
||||
@change=${this.#onTextareaChange}
|
||||
value=${ifDefined(translation?.translation)}
|
||||
?readonly=${this.#isReadOnly(language.unique)}></uui-textarea>
|
||||
</umb-property-layout>`;
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
css`
|
||||
:host {
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import { UmbDocumentPickerContext } from './input-document.context.js';
|
||||
import { classMap, css, customElement, html, property, repeat, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
import {
|
||||
classMap,
|
||||
css,
|
||||
customElement,
|
||||
html,
|
||||
nothing,
|
||||
property,
|
||||
repeat,
|
||||
state,
|
||||
} from '@umbraco-cms/backoffice/external/lit';
|
||||
import { splitStringToArray } from '@umbraco-cms/backoffice/utils';
|
||||
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
|
||||
import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation';
|
||||
@@ -103,6 +112,27 @@ export class UmbInputDocumentElement extends UmbFormControlMixin<string | undefi
|
||||
return this.selection.length > 0 ? this.selection.join(',') : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the input to readonly mode, meaning value cannot be changed but still able to read and select its content.
|
||||
* @type {boolean}
|
||||
* @attr
|
||||
* @default false
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public get readonly() {
|
||||
return this.#readonly;
|
||||
}
|
||||
public set readonly(value) {
|
||||
this.#readonly = value;
|
||||
|
||||
if (this.#readonly) {
|
||||
this.#sorter.disable();
|
||||
} else {
|
||||
this.#sorter.enable();
|
||||
}
|
||||
}
|
||||
#readonly = false;
|
||||
|
||||
@state()
|
||||
private _editDocumentPath = '';
|
||||
|
||||
@@ -173,7 +203,8 @@ export class UmbInputDocumentElement extends UmbFormControlMixin<string | undefi
|
||||
id="btn-add"
|
||||
look="placeholder"
|
||||
@click=${this.#openPicker}
|
||||
label=${this.localize.term('general_choose')}></uui-button>
|
||||
label=${this.localize.term('general_choose')}
|
||||
?disabled=${this.readonly}></uui-button>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -193,11 +224,14 @@ export class UmbInputDocumentElement extends UmbFormControlMixin<string | undefi
|
||||
#renderItem(item: UmbDocumentItemModel) {
|
||||
if (!item.unique) return;
|
||||
return html`
|
||||
<uui-ref-node name=${item.name} id=${item.unique} class=${classMap({ draft: this.#isDraft(item) })}>
|
||||
<uui-ref-node
|
||||
name=${item.name}
|
||||
id=${item.unique}
|
||||
class=${classMap({ draft: this.#isDraft(item) })}
|
||||
?readonly=${this.readonly}>
|
||||
${this.#renderIcon(item)} ${this.#renderIsTrashed(item)}
|
||||
<uui-action-bar slot="actions">
|
||||
${this.#renderOpenButton(item)}
|
||||
<uui-button @click=${() => this.#onRemove(item)} label=${this.localize.term('general_remove')}></uui-button>
|
||||
${this.#renderOpenButton(item)} ${this.#renderRemoveButton(item)}
|
||||
</uui-action-bar>
|
||||
</uui-ref-node>
|
||||
`;
|
||||
@@ -213,8 +247,16 @@ export class UmbInputDocumentElement extends UmbFormControlMixin<string | undefi
|
||||
return html`<uui-tag size="s" slot="tag" color="danger">Trashed</uui-tag>`;
|
||||
}
|
||||
|
||||
#renderRemoveButton(item: UmbDocumentItemModel) {
|
||||
if (this.readonly) return nothing;
|
||||
return html`
|
||||
<uui-button @click=${() => this.#onRemove(item)} label=${this.localize.term('general_remove')}></uui-button>
|
||||
`;
|
||||
}
|
||||
|
||||
#renderOpenButton(item: UmbDocumentItemModel) {
|
||||
if (!this.showOpenButton) return;
|
||||
if (this.readonly) return nothing;
|
||||
if (!this.showOpenButton) return nothing;
|
||||
return html`
|
||||
<uui-button
|
||||
href="${this._editDocumentPath}edit/${item.unique}"
|
||||
|
||||
@@ -12,6 +12,7 @@ export const manifests: Array<ManifestTypes> = [
|
||||
propertyEditorSchemaAlias: 'Umbraco.ContentPicker',
|
||||
icon: 'icon-document',
|
||||
group: 'pickers',
|
||||
supportsReadOnly: true,
|
||||
settings: {
|
||||
properties: [
|
||||
{
|
||||
|
||||
@@ -26,6 +26,15 @@ export class UmbPropertyEditorUIDocumentPickerElement extends UmbLitElement impl
|
||||
this._showOpenButton = config.getValueByAlias('showOpenButton') ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the input to readonly mode, meaning value cannot be changed but still able to read and select its content.
|
||||
* @type {boolean}
|
||||
* @attr
|
||||
* @default false
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true })
|
||||
readonly = false;
|
||||
|
||||
@state()
|
||||
private _min = 0;
|
||||
|
||||
@@ -57,7 +66,8 @@ export class UmbPropertyEditorUIDocumentPickerElement extends UmbLitElement impl
|
||||
.startNode=${startNode}
|
||||
.value=${this.value}
|
||||
?showOpenButton=${this._showOpenButton}
|
||||
@change=${this.#onChange}>
|
||||
@change=${this.#onChange}
|
||||
?readonly=${this.readonly}>
|
||||
</umb-input-document>
|
||||
`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user