From a933f71ed43abea2a1757885a2366aba40750531 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Fri, 10 Mar 2023 09:54:22 +0100 Subject: [PATCH 01/20] property editor tags --- .../src/backoffice/shared/components/index.ts | 1 + .../input-tags/input-tags.element.ts | 119 ++++++++++++++++++ .../input-tags/input-tags.stories.ts | 49 ++++++++ .../tags/property-editor-ui-tags.element.ts | 2 +- 4 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-tags/input-tags.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-tags/input-tags.stories.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/index.ts index cb63f61786..e00964ed16 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/index.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/index.ts @@ -24,6 +24,7 @@ import './input-media-picker/input-media-picker.element'; import './input-multi-url-picker/input-multi-url-picker.element'; import './input-slider/input-slider.element'; import './input-toggle/input-toggle.element'; +import './input-tags/input-tags.element'; import './property-type-based-property/property-type-based-property.element'; import './ref-property-editor-ui/ref-property-editor-ui.element'; import './section/section-main/section-main.element'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-tags/input-tags.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-tags/input-tags.element.ts new file mode 100644 index 0000000000..cfbac3ee3f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-tags/input-tags.element.ts @@ -0,0 +1,119 @@ +import { css, html, nothing } from 'lit'; +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { UUIInputElement } from '@umbraco-ui/uui'; +import { customElement, property, query } from 'lit/decorators.js'; +import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins'; +import { UmbLitElement } from '@umbraco-cms/element'; + +@customElement('umb-input-tags') +export class UmbInputTagsElement extends FormControlMixin(UmbLitElement) { + static styles = [ + UUITextStyles, + css` + #tags-wrapper { + margin-top: var(--uui-size-space-4); + display: flex; + gap: var(--uui-size-space-2); + flex-wrap: wrap; + } + + uui-tag uui-icon { + cursor: pointer; + margin-left: var(--uui-size-space-2); + } + + uui-tag uui-icon:hover, + uui-tag uui-icon:active { + color: var(--uui-color-selected-contrast); + } + `, + ]; + + @property({ type: String }) + group?: string; + + _tags: string[] = []; + @property({ type: Array }) + public set tags(newTags: string[]) { + this._tags = newTags; + super.value = this._tags.join(','); + } + public get tags(): string[] { + return this._tags; + } + + @query('#tag-input') + private _tagInput!: UUIInputElement; + + protected getFormElement() { + return undefined; + } + + constructor() { + super(); + } + + connectedCallback(): void { + super.connectedCallback(); + } + + #onKeypress(e: KeyboardEvent) { + if (e.key !== 'Enter') return; + const newTag = (this._tagInput.value as string).trim(); + + if (!newTag) return this.#inputError(); + + const tagExists = this.tags.find((tag) => tag === newTag); + + if (tagExists) return this.#inputError(); + + this._tagInput.error = false; + this.tags = [...this.tags, newTag]; + this._tagInput.value = ''; + this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true })); + } + + #inputError() { + this._tagInput.error = true; + } + + #delete(tag: string) { + this.tags.splice( + this.tags.findIndex((x) => x === tag), + 1 + ); + this.tags = [...this.tags]; + this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true })); + } + + render() { + return html` + ${this.#renderTags()}`; + } + + #renderTags() { + if (!this.tags.length) return nothing; + return html`
+ ${this.tags.map((tag) => { + return html` + + ${tag} + + + `; + })} +
`; + } +} + +export default UmbInputTagsElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-input-tags': UmbInputTagsElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-tags/input-tags.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-tags/input-tags.stories.ts new file mode 100644 index 0000000000..4b922ded32 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-tags/input-tags.stories.ts @@ -0,0 +1,49 @@ +import { Meta, StoryObj } from '@storybook/web-components'; +import './input-tags.element'; +import type { UmbInputTagsElement } from './input-tags.element'; + +const meta: Meta = { + title: 'Components/Inputs/Tags', + component: 'umb-input-tags', +}; + +export default meta; +type Story = StoryObj; + +export const Overview: Story = { + args: { + group: 'default', + tags: [], + }, +}; + +export const WithTags: Story = { + args: { + group: 'default', + tags: ['Flour', 'Eggs', 'Butter', 'Sugar', 'Salt', 'Milk'], + }, +}; + +export const WithTags2: Story = { + args: { + group: 'default', + tags: [ + 'Cranberry', + 'Kiwi', + 'Blueberries', + 'Watermelon', + 'Tomato', + 'Mango', + 'Strawberry', + 'Water Chestnut', + 'Papaya', + 'Orange Rind', + 'Olives', + 'Pear', + 'Sultana', + 'Mulberry', + 'Lychee', + 'Lemon', + ], + }, +}; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/tags/property-editor-ui-tags.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/tags/property-editor-ui-tags.element.ts index a7d922140f..5af1148097 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/tags/property-editor-ui-tags.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/tags/property-editor-ui-tags.element.ts @@ -17,7 +17,7 @@ export class UmbPropertyEditorUITagsElement extends UmbLitElement { public config = []; render() { - return html`
umb-property-editor-ui-tags
`; + return html``; } } From ba5aee520f08786d177036ea5ec6d14aa7217ded Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Fri, 10 Mar 2023 12:35:12 +0100 Subject: [PATCH 02/20] property editor --- .../input-tags/input-tags.element.ts | 26 +++++++++--------- .../input-tags/input-tags.stories.ts | 6 ++--- .../tags/property-editor-ui-tags.element.ts | 27 ++++++++++++++++--- .../src/core/mocks/data/data-type.data.ts | 11 +++++++- 4 files changed, 49 insertions(+), 21 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-tags/input-tags.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-tags/input-tags.element.ts index cfbac3ee3f..b726918854 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-tags/input-tags.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-tags/input-tags.element.ts @@ -32,14 +32,14 @@ export class UmbInputTagsElement extends FormControlMixin(UmbLitElement) { @property({ type: String }) group?: string; - _tags: string[] = []; + _items: string[] = []; @property({ type: Array }) - public set tags(newTags: string[]) { - this._tags = newTags; - super.value = this._tags.join(','); + public set items(newTags: string[]) { + this._items = newTags; + super.value = this._items.join(','); } - public get tags(): string[] { - return this._tags; + public get items(): string[] { + return this._items; } @query('#tag-input') @@ -63,12 +63,12 @@ export class UmbInputTagsElement extends FormControlMixin(UmbLitElement) { if (!newTag) return this.#inputError(); - const tagExists = this.tags.find((tag) => tag === newTag); + const tagExists = this.items.find((tag) => tag === newTag); if (tagExists) return this.#inputError(); this._tagInput.error = false; - this.tags = [...this.tags, newTag]; + this.items = [...this.items, newTag]; this._tagInput.value = ''; this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true })); } @@ -78,11 +78,11 @@ export class UmbInputTagsElement extends FormControlMixin(UmbLitElement) { } #delete(tag: string) { - this.tags.splice( - this.tags.findIndex((x) => x === tag), + this.items.splice( + this.items.findIndex((x) => x === tag), 1 ); - this.tags = [...this.tags]; + this.items = [...this.items]; this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true })); } @@ -96,9 +96,9 @@ export class UmbInputTagsElement extends FormControlMixin(UmbLitElement) { } #renderTags() { - if (!this.tags.length) return nothing; + if (!this.items.length) return nothing; return html`
- ${this.tags.map((tag) => { + ${this.items.map((tag) => { return html` ${tag} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-tags/input-tags.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-tags/input-tags.stories.ts index 4b922ded32..ea5bb6acb6 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-tags/input-tags.stories.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-tags/input-tags.stories.ts @@ -13,21 +13,21 @@ type Story = StoryObj; export const Overview: Story = { args: { group: 'default', - tags: [], + items: [], }, }; export const WithTags: Story = { args: { group: 'default', - tags: ['Flour', 'Eggs', 'Butter', 'Sugar', 'Salt', 'Milk'], + items: ['Flour', 'Eggs', 'Butter', 'Sugar', 'Salt', 'Milk'], }, }; export const WithTags2: Story = { args: { group: 'default', - tags: [ + items: [ 'Cranberry', 'Kiwi', 'Blueberries', diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/tags/property-editor-ui-tags.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/tags/property-editor-ui-tags.element.ts index 5af1148097..73b28bb5c4 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/tags/property-editor-ui-tags.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/tags/property-editor-ui-tags.element.ts @@ -1,7 +1,10 @@ import { html } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; -import { customElement, property } from 'lit/decorators.js'; +import { customElement, property, state } from 'lit/decorators.js'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import { UmbInputTagsElement } from '../../../../shared/components/input-tags/input-tags.element'; import { UmbLitElement } from '@umbraco-cms/element'; +import { DataTypePropertyModel } from '@umbraco-cms/backend-api'; /** * @element umb-property-editor-ui-tags @@ -11,13 +14,29 @@ export class UmbPropertyEditorUITagsElement extends UmbLitElement { static styles = [UUITextStyles]; @property() - value = ''; + value: string[] = []; + + @state() + private _group?: string; @property({ type: Array, attribute: false }) - public config = []; + public set config(config: Array) { + const group = config.find((x) => x.alias === 'group'); + if (group) this._group = group.value as string; + + const items = config.find((x) => x.alias === 'items'); + if (items) this.value = items.value as Array; + } + private _onChange(event: CustomEvent) { + this.value = ((event.target as UmbInputTagsElement).value as string).split(','); + this.dispatchEvent(new CustomEvent('property-value-change')); + } render() { - return html``; + return html``; } } diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/data/data-type.data.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/data/data-type.data.ts index fd0b0b01c0..5f6772a13a 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/data/data-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/data/data-type.data.ts @@ -364,7 +364,16 @@ export const data: Array = [ parentKey: null, propertyEditorAlias: 'Umbraco.Tags', propertyEditorUiAlias: 'Umb.PropertyEditorUI.Tags', - data: [], + data: [ + { + alias: 'group', + value: 'default', + }, + { + alias: 'items', + value: [], + }, + ], }, { $type: 'data-type', From 18a73a5606e178b463af9672500e735ddab087fc Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Mon, 13 Mar 2023 12:44:37 +0100 Subject: [PATCH 03/20] tags are written inside a tag --- .../input-tags/input-tags.element.ts | 179 ++++++++++++++---- .../input-tags/input-tags.stories.ts | 5 + 2 files changed, 151 insertions(+), 33 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-tags/input-tags.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-tags/input-tags.element.ts index b726918854..48c6810554 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-tags/input-tags.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-tags/input-tags.element.ts @@ -1,8 +1,8 @@ -import { css, html, nothing } from 'lit'; +import { css, html } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; -import { UUIInputElement } from '@umbraco-ui/uui'; -import { customElement, property, query } from 'lit/decorators.js'; +import { customElement, property, query, state } from 'lit/decorators.js'; import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins'; +import { UUIInputElement, UUITagElement } from '@umbraco-ui/uui'; import { UmbLitElement } from '@umbraco-cms/element'; @customElement('umb-input-tags') @@ -10,20 +10,104 @@ export class UmbInputTagsElement extends FormControlMixin(UmbLitElement) { static styles = [ UUITextStyles, css` - #tags-wrapper { - margin-top: var(--uui-size-space-4); + :host { + box-sizing: border-box; + } + + #wrapper { display: flex; gap: var(--uui-size-space-2); flex-wrap: wrap; + align-items: center; + padding: var(--uui-size-space-2); + border: 1px solid var(--uui-color-border); + background-color: var(--uui-input-background-color, var(--uui-color-surface)); + flex: 1; } uui-tag uui-icon { cursor: pointer; + } + + uui-tag { + max-width: 150px; + } + + uui-tag uui-icon { + min-width: 12.8px !important; + } + + uui-tag span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + #tag-add-wrapper { + padding: 3px 4px; + background-color: var(--uui-color-selected-contrast); + //transition: width 500ms ease-in; + width: 20px; + position: relative; + } + + #tag-add-wrapper:has(#tag-input:not(:focus)):hover { + cursor: pointer; + border: 1px solid var(--uui-color-selected-emphasis); + } + + #tag-add-wrapper:has(*:hover), + #tag-add-wrapper:has(*:active), + #tag-add-wrapper:has(*:focus) { + border: 1px solid var(--uui-color-selected-emphasis); + } + + #tag-add-wrapper #tag-input:not(:focus) { + opacity: 0; + } + + #tag-add-wrapper:has(#tag-input:focus), + #tag-add-wrapper:has(#tag-input:not(:placeholder-shown)) { + width: 150px; + } + + #tag-add-wrapper uui-icon { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } + + #tag-add-wrapper:hover uui-icon, + #tag-add-wrapper:active uui-icon { + color: var(--uui-color-selected); + } + + #tag-add-wrapper #tag-input { + box-sizing: border-box; + max-height: 25.8px; + background: none; + font: inherit; + color: var(--uui-color-selected); + line-height: reset; + padding: 0 var(--uui-size-space-2); + margin: 0.5px 0 -0.5px; + border: none; + outline: none; + width: 100%; + } + + #tag-add-wrapper #tag-input:focus ~ uui-icon, + #tag-add-wrapper #tag-input:not(:placeholder-shown) ~ uui-icon { + display: none; + } + + .tag uui-icon { margin-left: var(--uui-size-space-2); } - uui-tag uui-icon:hover, - uui-tag uui-icon:active { + .tag uui-icon:hover, + .tag uui-icon:active { color: var(--uui-color-selected-contrast); } `, @@ -45,6 +129,13 @@ export class UmbInputTagsElement extends FormControlMixin(UmbLitElement) { @query('#tag-input') private _tagInput!: UUIInputElement; + @query('#tag-add-wrapper') + private _tagWrapper!: UUITagElement; + + public focus() { + this._tagInput.focus(); + } + protected getFormElement() { return undefined; } @@ -57,24 +148,42 @@ export class UmbInputTagsElement extends FormControlMixin(UmbLitElement) { super.connectedCallback(); } - #onKeypress(e: KeyboardEvent) { - if (e.key !== 'Enter') return; - const newTag = (this._tagInput.value as string).trim(); + #onKeydown(e: KeyboardEvent) { + //Prevent tab away if there is a input + if (e.key === 'Tab' && (this._tagInput.value as string).trim().length) { + e.preventDefault(); + this.#createTag(); + return; + } + if (e.key === 'Enter') { + this.#createTag(); + return; + } + this.#inputError(false); + } - if (!newTag) return this.#inputError(); + #createTag() { + this.#inputError(false); + const newTag = (this._tagInput.value as string).trim(); + if (!newTag) return; const tagExists = this.items.find((tag) => tag === newTag); + if (tagExists) return this.#inputError(true); - if (tagExists) return this.#inputError(); - - this._tagInput.error = false; + this.#inputError(false); this.items = [...this.items, newTag]; this._tagInput.value = ''; this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true })); } - #inputError() { - this._tagInput.error = true; + #inputError(error: boolean) { + if (error) { + this._tagWrapper.style.border = '1px solid var(--uui-color-danger)'; + this._tagInput.style.color = 'var(--uui-color-danger)'; + return; + } + this._tagWrapper.style.border = ''; + this._tagInput.style.color = ''; } #delete(tag: string) { @@ -87,26 +196,30 @@ export class UmbInputTagsElement extends FormControlMixin(UmbLitElement) { } render() { - return html` - ${this.#renderTags()}`; + return html`
+ ${this.#renderTags()} + + + + +
`; } #renderTags() { - if (!this.items.length) return nothing; - return html`
- ${this.items.map((tag) => { - return html` - - ${tag} - - - `; - })} -
`; + return html` ${this.items.map((tag) => { + return html` + + ${tag} + + + `; + })}`; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-tags/input-tags.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-tags/input-tags.stories.ts index ea5bb6acb6..60bc4ecc89 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-tags/input-tags.stories.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-tags/input-tags.stories.ts @@ -44,6 +44,11 @@ export const WithTags2: Story = { 'Mulberry', 'Lychee', 'Lemon', + 'Apple', + 'Banana', + 'Dragonfruit', + 'Blackberry', + 'Raspberry', ], }, }; From 8228a137e2a79a7c8c8a3c325947f51222cf7c6f Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Fri, 14 Apr 2023 13:52:56 +0200 Subject: [PATCH 04/20] tag popover when it finds an already existing tag - UI --- .../input-tags/input-tags.element.ts | 94 +++++++++++++++---- 1 file changed, 75 insertions(+), 19 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-tags/input-tags.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-tags/input-tags.element.ts index 48c6810554..67dac94b02 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-tags/input-tags.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-tags/input-tags.element.ts @@ -1,9 +1,9 @@ -import { css, html } from 'lit'; +import { css, html, nothing } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, property, query, state } from 'lit/decorators.js'; import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins'; -import { UUIInputElement, UUITagElement } from '@umbraco-ui/uui'; -import { UmbLitElement } from '@umbraco-cms/element'; +import { UUIInputElement, UUIInputEvent, UUIPopoverElement, UUITagElement } from '@umbraco-ui/uui'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; @customElement('umb-input-tags') export class UmbInputTagsElement extends FormControlMixin(UmbLitElement) { @@ -14,6 +14,10 @@ export class UmbInputTagsElement extends FormControlMixin(UmbLitElement) { box-sizing: border-box; } + uui-popover { + width: auto; + } + #wrapper { display: flex; gap: var(--uui-size-space-2); @@ -44,7 +48,7 @@ export class UmbInputTagsElement extends FormControlMixin(UmbLitElement) { } #tag-add-wrapper { - padding: 3px 4px; + padding: 3px 3px; background-color: var(--uui-color-selected-contrast); //transition: width 500ms ease-in; width: 20px; @@ -110,6 +114,21 @@ export class UmbInputTagsElement extends FormControlMixin(UmbLitElement) { .tag uui-icon:active { color: var(--uui-color-selected-contrast); } + + #matching-tags button { + cursor: pointer; + text-align: left; + display: block; + width: 100%; + background: none; + border: none; + padding: 5px 7.5px; + } + + #matching-tags button:hover, + #matching-tags button:focus { + background: var(--uui-color-focus); + } `, ]; @@ -126,12 +145,21 @@ export class UmbInputTagsElement extends FormControlMixin(UmbLitElement) { return this._items; } + @state() + private _matches: Array = []; + + @state() + private _currentInput = ''; + @query('#tag-input') private _tagInput!: UUIInputElement; @query('#tag-add-wrapper') private _tagWrapper!: UUITagElement; + @query('#popover') + private _popover!: UUIPopoverElement; + public focus() { this._tagInput.focus(); } @@ -149,8 +177,8 @@ export class UmbInputTagsElement extends FormControlMixin(UmbLitElement) { } #onKeydown(e: KeyboardEvent) { - //Prevent tab away if there is a input - if (e.key === 'Tab' && (this._tagInput.value as string).trim().length) { + //Prevent tab away if there is a input, but no matches. + if (e.key === 'Tab' && (this._tagInput.value as string).trim().length && !this._matches.length) { e.preventDefault(); this.#createTag(); return; @@ -162,7 +190,21 @@ export class UmbInputTagsElement extends FormControlMixin(UmbLitElement) { this.#inputError(false); } + #onInput(e: UUIInputEvent) { + this._currentInput = e.target.value as string; + + //TODO: If match an existing tag + if (this._currentInput.length) this._popover.open = true; + //else this._popover.open = false; + } + + #onBlur() { + if (this._matches.length) return; + this.#createTag(); + } + #createTag() { + this._currentInput = ''; this.#inputError(false); const newTag = (this._tagInput.value as string).trim(); if (!newTag) return; @@ -196,19 +238,25 @@ export class UmbInputTagsElement extends FormControlMixin(UmbLitElement) { } render() { - return html`
- ${this.#renderTags()} - - - - -
`; + return html` +
+ ${this.#renderTags()} + + + + + +
${this.#renderTagOptions()}
+
+
+ `; } #renderTags() { @@ -221,6 +269,14 @@ export class UmbInputTagsElement extends FormControlMixin(UmbLitElement) { `; })}`; } + + #renderTagOptions() { + if (!this._currentInput.length) return nothing; + return html` + + + `; + } } export default UmbInputTagsElement; From 687453fac7b278ac0ccd73fa8131cb458e835513 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Fri, 14 Apr 2023 14:15:57 +0200 Subject: [PATCH 05/20] api generate --- .../libs/backend-api/src/index.ts | 8 ++++ .../src/models/CopyDocumentRequestModel.ts | 9 ++++ .../src/models/MoveDocumentRequestModel.ts | 7 ++++ .../src/models/MoveMediaRequestModel.ts | 7 ++++ .../src/models/PagedBooleanModel.ts | 8 ++++ .../src/models/PagedTagResponseModel.ts | 10 +++++ .../src/models/TagResponseModel.ts | 10 +++++ .../src/services/PropertyTypeResource.ts | 36 ++++++++++++++++ .../backend-api/src/services/TagResource.ts | 42 +++++++++++++++++++ 9 files changed, 137 insertions(+) create mode 100644 src/Umbraco.Web.UI.Client/libs/backend-api/src/models/CopyDocumentRequestModel.ts create mode 100644 src/Umbraco.Web.UI.Client/libs/backend-api/src/models/MoveDocumentRequestModel.ts create mode 100644 src/Umbraco.Web.UI.Client/libs/backend-api/src/models/MoveMediaRequestModel.ts create mode 100644 src/Umbraco.Web.UI.Client/libs/backend-api/src/models/PagedBooleanModel.ts create mode 100644 src/Umbraco.Web.UI.Client/libs/backend-api/src/models/PagedTagResponseModel.ts create mode 100644 src/Umbraco.Web.UI.Client/libs/backend-api/src/models/TagResponseModel.ts create mode 100644 src/Umbraco.Web.UI.Client/libs/backend-api/src/services/PropertyTypeResource.ts create mode 100644 src/Umbraco.Web.UI.Client/libs/backend-api/src/services/TagResource.ts diff --git a/src/Umbraco.Web.UI.Client/libs/backend-api/src/index.ts b/src/Umbraco.Web.UI.Client/libs/backend-api/src/index.ts index 9919e60217..d9b899a0ac 100644 --- a/src/Umbraco.Web.UI.Client/libs/backend-api/src/index.ts +++ b/src/Umbraco.Web.UI.Client/libs/backend-api/src/index.ts @@ -23,6 +23,7 @@ export type { ContentTypeResponseModelBaseMediaTypePropertyTypeResponseModelMedi export type { ContentTypeSortModel } from './models/ContentTypeSortModel'; export type { ContentUrlInfoModel } from './models/ContentUrlInfoModel'; export type { CopyDataTypeRequestModel } from './models/CopyDataTypeRequestModel'; +export type { CopyDocumentRequestModel } from './models/CopyDocumentRequestModel'; export type { CreateContentRequestModelBaseDocumentValueModelDocumentVariantRequestModel } from './models/CreateContentRequestModelBaseDocumentValueModelDocumentVariantRequestModel'; export type { CreateContentRequestModelBaseMediaValueModelMediaVariantRequestModel } from './models/CreateContentRequestModelBaseMediaValueModelMediaVariantRequestModel'; export type { CreateDataTypeRequestModel } from './models/CreateDataTypeRequestModel'; @@ -117,6 +118,8 @@ export type { ModelsBuilderResponseModel } from './models/ModelsBuilderResponseM export { ModelsModeModel } from './models/ModelsModeModel'; export type { MoveDataTypeRequestModel } from './models/MoveDataTypeRequestModel'; export type { MoveDictionaryRequestModel } from './models/MoveDictionaryRequestModel'; +export type { MoveDocumentRequestModel } from './models/MoveDocumentRequestModel'; +export type { MoveMediaRequestModel } from './models/MoveMediaRequestModel'; export type { ObjectTypeResponseModel } from './models/ObjectTypeResponseModel'; export type { OkResultModel } from './models/OkResultModel'; export { OperatorModel } from './models/OperatorModel'; @@ -128,6 +131,7 @@ export type { PackageMigrationStatusResponseModel } from './models/PackageMigrat export type { PackageModelBaseModel } from './models/PackageModelBaseModel'; export type { PagedAuditLogResponseModel } from './models/PagedAuditLogResponseModel'; export type { PagedAuditLogWithUsernameResponseModel } from './models/PagedAuditLogWithUsernameResponseModel'; +export type { PagedBooleanModel } from './models/PagedBooleanModel'; export type { PagedContentTreeItemResponseModel } from './models/PagedContentTreeItemResponseModel'; export type { PagedCultureReponseModel } from './models/PagedCultureReponseModel'; export type { PagedDictionaryOverviewResponseModel } from './models/PagedDictionaryOverviewResponseModel'; @@ -154,6 +158,7 @@ export type { PagedRelationResponseModel } from './models/PagedRelationResponseM export type { PagedSavedLogSearchResponseModel } from './models/PagedSavedLogSearchResponseModel'; export type { PagedSearcherResponseModel } from './models/PagedSearcherResponseModel'; export type { PagedSearchResultResponseModel } from './models/PagedSearchResultResponseModel'; +export type { PagedTagResponseModel } from './models/PagedTagResponseModel'; export type { PagedTelemetryResponseModel } from './models/PagedTelemetryResponseModel'; export type { PagedUserGroupPresentationModel } from './models/PagedUserGroupPresentationModel'; export type { PagedUserResponseModel } from './models/PagedUserResponseModel'; @@ -187,6 +192,7 @@ export type { SetAvatarRequestModel } from './models/SetAvatarRequestModel'; export type { StaticFileItemResponseModel } from './models/StaticFileItemResponseModel'; export { StatusResultTypeModel } from './models/StatusResultTypeModel'; export type { StylesheetItemResponseModel } from './models/StylesheetItemResponseModel'; +export type { TagResponseModel } from './models/TagResponseModel'; export { TelemetryLevelModel } from './models/TelemetryLevelModel'; export type { TelemetryRepresentationBaseModel } from './models/TelemetryRepresentationBaseModel'; export type { TelemetryRequestModel } from './models/TelemetryRequestModel'; @@ -259,6 +265,7 @@ export { ObjectTypesResource } from './services/ObjectTypesResource'; export { PackageResource } from './services/PackageResource'; export { PartialViewResource } from './services/PartialViewResource'; export { ProfilingResource } from './services/ProfilingResource'; +export { PropertyTypeResource } from './services/PropertyTypeResource'; export { PublishedCacheResource } from './services/PublishedCacheResource'; export { RedirectManagementResource } from './services/RedirectManagementResource'; export { RelationResource } from './services/RelationResource'; @@ -269,6 +276,7 @@ export { SecurityResource } from './services/SecurityResource'; export { ServerResource } from './services/ServerResource'; export { StaticFileResource } from './services/StaticFileResource'; export { StylesheetResource } from './services/StylesheetResource'; +export { TagResource } from './services/TagResource'; export { TelemetryResource } from './services/TelemetryResource'; export { TemplateResource } from './services/TemplateResource'; export { TemporaryFileResource } from './services/TemporaryFileResource'; diff --git a/src/Umbraco.Web.UI.Client/libs/backend-api/src/models/CopyDocumentRequestModel.ts b/src/Umbraco.Web.UI.Client/libs/backend-api/src/models/CopyDocumentRequestModel.ts new file mode 100644 index 0000000000..394602da1c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/backend-api/src/models/CopyDocumentRequestModel.ts @@ -0,0 +1,9 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type CopyDocumentRequestModel = { + targetId?: string | null; + relateToOriginal?: boolean; + includeDescendants?: boolean; +}; diff --git a/src/Umbraco.Web.UI.Client/libs/backend-api/src/models/MoveDocumentRequestModel.ts b/src/Umbraco.Web.UI.Client/libs/backend-api/src/models/MoveDocumentRequestModel.ts new file mode 100644 index 0000000000..7dd21a596a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/backend-api/src/models/MoveDocumentRequestModel.ts @@ -0,0 +1,7 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type MoveDocumentRequestModel = { + targetId?: string | null; +}; diff --git a/src/Umbraco.Web.UI.Client/libs/backend-api/src/models/MoveMediaRequestModel.ts b/src/Umbraco.Web.UI.Client/libs/backend-api/src/models/MoveMediaRequestModel.ts new file mode 100644 index 0000000000..d1b28688d6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/backend-api/src/models/MoveMediaRequestModel.ts @@ -0,0 +1,7 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type MoveMediaRequestModel = { + targetId?: string | null; +}; diff --git a/src/Umbraco.Web.UI.Client/libs/backend-api/src/models/PagedBooleanModel.ts b/src/Umbraco.Web.UI.Client/libs/backend-api/src/models/PagedBooleanModel.ts new file mode 100644 index 0000000000..4a2d76a9cb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/backend-api/src/models/PagedBooleanModel.ts @@ -0,0 +1,8 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type PagedBooleanModel = { + total: number; + items: Array; +}; diff --git a/src/Umbraco.Web.UI.Client/libs/backend-api/src/models/PagedTagResponseModel.ts b/src/Umbraco.Web.UI.Client/libs/backend-api/src/models/PagedTagResponseModel.ts new file mode 100644 index 0000000000..c969823a39 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/backend-api/src/models/PagedTagResponseModel.ts @@ -0,0 +1,10 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { TagResponseModel } from './TagResponseModel'; + +export type PagedTagResponseModel = { + total: number; + items: Array; +}; diff --git a/src/Umbraco.Web.UI.Client/libs/backend-api/src/models/TagResponseModel.ts b/src/Umbraco.Web.UI.Client/libs/backend-api/src/models/TagResponseModel.ts new file mode 100644 index 0000000000..e00932e13b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/backend-api/src/models/TagResponseModel.ts @@ -0,0 +1,10 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type TagResponseModel = { + id?: string; + text?: string | null; + group?: string | null; + nodeCount?: number; +}; diff --git a/src/Umbraco.Web.UI.Client/libs/backend-api/src/services/PropertyTypeResource.ts b/src/Umbraco.Web.UI.Client/libs/backend-api/src/services/PropertyTypeResource.ts new file mode 100644 index 0000000000..0ed21e9d3e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/backend-api/src/services/PropertyTypeResource.ts @@ -0,0 +1,36 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { PagedBooleanModel } from '../models/PagedBooleanModel'; + +import type { CancelablePromise } from '../core/CancelablePromise'; +import { OpenAPI } from '../core/OpenAPI'; +import { request as __request } from '../core/request'; + +export class PropertyTypeResource { + + /** + * @returns PagedBooleanModel Success + * @throws ApiError + */ + public static getPropertyTypeIsUsed({ +contentTypeId, +propertyAlias, +}: { +contentTypeId?: string, +propertyAlias?: string, +}): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/umbraco/management/api/v1/property-type/is-used', + query: { + 'contentTypeId': contentTypeId, + 'propertyAlias': propertyAlias, + }, + errors: { + 400: `Bad Request`, + }, + }); + } + +} diff --git a/src/Umbraco.Web.UI.Client/libs/backend-api/src/services/TagResource.ts b/src/Umbraco.Web.UI.Client/libs/backend-api/src/services/TagResource.ts new file mode 100644 index 0000000000..211c09f3da --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/backend-api/src/services/TagResource.ts @@ -0,0 +1,42 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { PagedTagResponseModel } from '../models/PagedTagResponseModel'; + +import type { CancelablePromise } from '../core/CancelablePromise'; +import { OpenAPI } from '../core/OpenAPI'; +import { request as __request } from '../core/request'; + +export class TagResource { + + /** + * @returns PagedTagResponseModel Success + * @throws ApiError + */ + public static getTag({ +query, +tagGroup, +culture, +skip, +take = 100, +}: { +query?: string, +tagGroup?: string, +culture?: string, +skip?: number, +take?: number, +}): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/umbraco/management/api/v1/tag', + query: { + 'query': query, + 'tagGroup': tagGroup, + 'culture': culture, + 'skip': skip, + 'take': take, + }, + }); + } + +} From 7b314d6a1d0a77f331998263ca0df5f31f6ac53c Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Wed, 19 Apr 2023 10:24:02 +0200 Subject: [PATCH 06/20] tag input rename, resource, ui fixes, dropdown suggestions --- .../src/backoffice/shared/components/index.ts | 2 +- .../input-tags/input-tags.element.ts | 288 ------------- .../components/tag-input/tag-input.element.ts | 407 ++++++++++++++++++ .../tag-input.stories.ts} | 12 +- .../tags/property-editor-ui-tags.element.ts | 4 +- .../src/core/mocks/browser-handlers.ts | 2 + .../src/core/mocks/data/data-type.data.ts | 2 +- .../src/core/mocks/domains/tag-handlers.ts | 195 +++++++++ 8 files changed, 614 insertions(+), 298 deletions(-) delete mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-tags/input-tags.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tag-input/tag-input.element.ts rename src/Umbraco.Web.UI.Client/src/backoffice/shared/components/{input-tags/input-tags.stories.ts => tag-input/tag-input.stories.ts} (75%) create mode 100644 src/Umbraco.Web.UI.Client/src/core/mocks/domains/tag-handlers.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/index.ts index 31a0c946df..200f4dd1fa 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/index.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/index.ts @@ -28,7 +28,7 @@ import './input-media-picker/input-media-picker.element'; import './input-multi-url-picker/input-multi-url-picker.element'; import './input-slider/input-slider.element'; import './input-toggle/input-toggle.element'; -import './input-tags/input-tags.element'; +import './tag-input/tag-input.element'; import './input-upload-field/input-upload-field.element'; import './input-template-picker/input-template-picker.element'; import './property-type-based-property/property-type-based-property.element'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-tags/input-tags.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-tags/input-tags.element.ts deleted file mode 100644 index 67dac94b02..0000000000 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-tags/input-tags.element.ts +++ /dev/null @@ -1,288 +0,0 @@ -import { css, html, nothing } from 'lit'; -import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; -import { customElement, property, query, state } from 'lit/decorators.js'; -import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins'; -import { UUIInputElement, UUIInputEvent, UUIPopoverElement, UUITagElement } from '@umbraco-ui/uui'; -import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; - -@customElement('umb-input-tags') -export class UmbInputTagsElement extends FormControlMixin(UmbLitElement) { - static styles = [ - UUITextStyles, - css` - :host { - box-sizing: border-box; - } - - uui-popover { - width: auto; - } - - #wrapper { - display: flex; - gap: var(--uui-size-space-2); - flex-wrap: wrap; - align-items: center; - padding: var(--uui-size-space-2); - border: 1px solid var(--uui-color-border); - background-color: var(--uui-input-background-color, var(--uui-color-surface)); - flex: 1; - } - - uui-tag uui-icon { - cursor: pointer; - } - - uui-tag { - max-width: 150px; - } - - uui-tag uui-icon { - min-width: 12.8px !important; - } - - uui-tag span { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - #tag-add-wrapper { - padding: 3px 3px; - background-color: var(--uui-color-selected-contrast); - //transition: width 500ms ease-in; - width: 20px; - position: relative; - } - - #tag-add-wrapper:has(#tag-input:not(:focus)):hover { - cursor: pointer; - border: 1px solid var(--uui-color-selected-emphasis); - } - - #tag-add-wrapper:has(*:hover), - #tag-add-wrapper:has(*:active), - #tag-add-wrapper:has(*:focus) { - border: 1px solid var(--uui-color-selected-emphasis); - } - - #tag-add-wrapper #tag-input:not(:focus) { - opacity: 0; - } - - #tag-add-wrapper:has(#tag-input:focus), - #tag-add-wrapper:has(#tag-input:not(:placeholder-shown)) { - width: 150px; - } - - #tag-add-wrapper uui-icon { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - } - - #tag-add-wrapper:hover uui-icon, - #tag-add-wrapper:active uui-icon { - color: var(--uui-color-selected); - } - - #tag-add-wrapper #tag-input { - box-sizing: border-box; - max-height: 25.8px; - background: none; - font: inherit; - color: var(--uui-color-selected); - line-height: reset; - padding: 0 var(--uui-size-space-2); - margin: 0.5px 0 -0.5px; - border: none; - outline: none; - width: 100%; - } - - #tag-add-wrapper #tag-input:focus ~ uui-icon, - #tag-add-wrapper #tag-input:not(:placeholder-shown) ~ uui-icon { - display: none; - } - - .tag uui-icon { - margin-left: var(--uui-size-space-2); - } - - .tag uui-icon:hover, - .tag uui-icon:active { - color: var(--uui-color-selected-contrast); - } - - #matching-tags button { - cursor: pointer; - text-align: left; - display: block; - width: 100%; - background: none; - border: none; - padding: 5px 7.5px; - } - - #matching-tags button:hover, - #matching-tags button:focus { - background: var(--uui-color-focus); - } - `, - ]; - - @property({ type: String }) - group?: string; - - _items: string[] = []; - @property({ type: Array }) - public set items(newTags: string[]) { - this._items = newTags; - super.value = this._items.join(','); - } - public get items(): string[] { - return this._items; - } - - @state() - private _matches: Array = []; - - @state() - private _currentInput = ''; - - @query('#tag-input') - private _tagInput!: UUIInputElement; - - @query('#tag-add-wrapper') - private _tagWrapper!: UUITagElement; - - @query('#popover') - private _popover!: UUIPopoverElement; - - public focus() { - this._tagInput.focus(); - } - - protected getFormElement() { - return undefined; - } - - constructor() { - super(); - } - - connectedCallback(): void { - super.connectedCallback(); - } - - #onKeydown(e: KeyboardEvent) { - //Prevent tab away if there is a input, but no matches. - if (e.key === 'Tab' && (this._tagInput.value as string).trim().length && !this._matches.length) { - e.preventDefault(); - this.#createTag(); - return; - } - if (e.key === 'Enter') { - this.#createTag(); - return; - } - this.#inputError(false); - } - - #onInput(e: UUIInputEvent) { - this._currentInput = e.target.value as string; - - //TODO: If match an existing tag - if (this._currentInput.length) this._popover.open = true; - //else this._popover.open = false; - } - - #onBlur() { - if (this._matches.length) return; - this.#createTag(); - } - - #createTag() { - this._currentInput = ''; - this.#inputError(false); - const newTag = (this._tagInput.value as string).trim(); - if (!newTag) return; - - const tagExists = this.items.find((tag) => tag === newTag); - if (tagExists) return this.#inputError(true); - - this.#inputError(false); - this.items = [...this.items, newTag]; - this._tagInput.value = ''; - this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true })); - } - - #inputError(error: boolean) { - if (error) { - this._tagWrapper.style.border = '1px solid var(--uui-color-danger)'; - this._tagInput.style.color = 'var(--uui-color-danger)'; - return; - } - this._tagWrapper.style.border = ''; - this._tagInput.style.color = ''; - } - - #delete(tag: string) { - this.items.splice( - this.items.findIndex((x) => x === tag), - 1 - ); - this.items = [...this.items]; - this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true })); - } - - render() { - return html` -
- ${this.#renderTags()} - - - - - -
${this.#renderTagOptions()}
-
-
- `; - } - - #renderTags() { - return html` ${this.items.map((tag) => { - return html` - - ${tag} - - - `; - })}`; - } - - #renderTagOptions() { - if (!this._currentInput.length) return nothing; - return html` - - - `; - } -} - -export default UmbInputTagsElement; - -declare global { - interface HTMLElementTagNameMap { - 'umb-input-tags': UmbInputTagsElement; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tag-input/tag-input.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tag-input/tag-input.element.ts new file mode 100644 index 0000000000..988f88d6b3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tag-input/tag-input.element.ts @@ -0,0 +1,407 @@ +import { css, html, nothing } from 'lit'; +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { customElement, property, query, queryAll, state } from 'lit/decorators.js'; +import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins'; +import { repeat } from 'lit/directives/repeat.js'; +import { UUIInputElement, UUIInputEvent, UUITagElement } from '@umbraco-ui/uui'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; +import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; +import { TagResource, TagResponseModel } from '@umbraco-cms/backoffice/backend-api'; + +@customElement('umb-tag-input') +export class UmbTagInputElement extends FormControlMixin(UmbLitElement) { + static styles = [ + UUITextStyles, + css` + #wrapper { + box-sizing: border-box; + display: flex; + gap: var(--uui-size-space-2); + flex-wrap: wrap; + align-items: center; + padding: var(--uui-size-space-2); + border: 1px solid var(--uui-color-border); + background-color: var(--uui-input-background-color, var(--uui-color-surface)); + flex: 1; + } + + #main-tag-wrapper { + position: relative; + } + + /** Tags */ + + uui-tag { + position: relative; + max-width: 200px; + } + + uui-tag uui-icon { + cursor: pointer; + min-width: 12.8px !important; + } + + uui-tag span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + /** Created tags */ + + .tag uui-icon { + margin-left: var(--uui-size-space-2); + } + + .tag uui-icon:hover, + .tag uui-icon:active { + color: var(--uui-color-selected-contrast); + } + + /** Main tag */ + + #main-tag { + padding: 3px; + background-color: var(--uui-color-selected-contrast); + min-width: 20px; + position: relative; + border-radius: var(--uui-size-5, 12px); + } + + #main-tag uui-icon { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } + + #main-tag:hover uui-icon, + #main-tag:active uui-icon { + color: var(--uui-color-selected); + } + + #main-tag #tag-input:focus ~ uui-icon, + #main-tag #tag-input:not(:placeholder-shown) ~ uui-icon { + display: none; + } + + #main-tag:has(*:hover), + #main-tag:has(*:active), + #main-tag:has(*:focus) { + border: 1px solid var(--uui-color-selected-emphasis); + } + + #main-tag:has(#tag-input:not(:focus)):hover { + cursor: pointer; + border: 1px solid var(--uui-color-selected-emphasis); + } + + #main-tag:not(:focus-within) #tag-input:placeholder-shown { + opacity: 0; + } + + #main-tag:has(#tag-input:focus), + #main-tag:has(#tag-input:not(:placeholder-shown)) { + min-width: 65px; + } + + #main-tag #tag-input { + box-sizing: border-box; + max-height: 25.8px; + background: none; + font: inherit; + color: var(--uui-color-selected); + line-height: reset; + padding: 0 var(--uui-size-space-2); + margin: 0.5px 0 -0.5px; + border: none; + outline: none; + width: 100%; + } + + /** Dropdown matchlist */ + + #matchlist input[type='radio'] { + -webkit-appearance: none; + appearance: none; + /* For iOS < 15 to remove gradient background */ + background-color: transparent; + /* Not removed via appearance */ + margin: 0; + } + + uui-tag:focus-within #matchlist { + display: flex; + } + + #matchlist { + display: none; + display: flex; + flex-direction: column; + background-color: var(--uui-color-surface); + position: absolute; + width: 150px; + left: 0; + top: var(--uui-size-space-6); + border-radius: var(--uui-border-radius); + border: 1px solid var(--uui-color-border); + } + + #matchlist label { + display: none; + cursor: pointer; + box-sizing: border-box; + display: block; + width: 100%; + background: none; + border: none; + text-align: left; + padding: 10px 12px; + + /** Overflow */ + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + #matchlist label:hover, + #matchlist label:focus, + #matchlist label:focus-within, + #matchlist input[type='radio']:focus + label { + display: block; + background-color: var(--uui-color-focus); + color: var(--uui-color-selected-contrast); + } + `, + ]; + + @property({ type: String }) + group?: string; + + _items: string[] = []; + @property({ type: Array }) + public set items(newTags: string[]) { + this._items = newTags; + super.value = this._items.join(','); + } + public get items(): string[] { + return this._items; + } + + @state() + private _matches: Array = []; + + @state() + private _currentInput = ''; + + @query('#main-tag') + private _mainTag!: UUITagElement; + + @query('#tag-input') + private _tagInput!: UUIInputElement; + + @query('#input-width-tracker') + private _widthTracker!: HTMLElement; + + @queryAll('.options') + private _optionCollection?: HTMLCollectionOf; + + public focus() { + this._tagInput.focus(); + } + + protected getFormElement() { + return undefined; + } + + async #getExistingTags(query: string) { + //TODO: Culture + const { data } = await tryExecuteAndNotify( + this, + TagResource.getTag({ query, skip: 0, take: 5, tagGroup: this.group }) + ); + if (!data) return; + this._matches = data.items; + } + + #onKeydown(e: KeyboardEvent) { + //Prevent tab away if there is a input. + if (e.key === 'Tab' && (this._tagInput.value as string).trim().length && !this._matches.length) { + e.preventDefault(); + this.#createTag(); + return; + } + if (e.key === 'Enter') { + this.#createTag(); + return; + } + if (e.key === 'ArrowDown' || e.key === 'Tab') { + e.preventDefault(); + this._currentInput = this._optionCollection?.item(0)?.value ?? this._currentInput; + this._optionCollection?.item(0)?.focus(); + return; + } + this.#inputError(false); + } + + #onInput(e: UUIInputEvent) { + this._currentInput = e.target.value as string; + if (!this._currentInput.length) this._matches = []; + else this.#getExistingTags(this._currentInput); + } + + protected updated(): void { + this._mainTag.style.width = `${this._widthTracker.offsetWidth - 4}px`; + } + + #onBlur() { + if (this._matches.length) return; + else this.#createTag(); + } + + #createTag(text?: string) { + if (text) { + this._tagInput.value = text; + } + this.#inputError(false); + const newTag = (this._tagInput.value as string).trim(); + if (!newTag) return; + + const tagExists = this.items.find((tag) => tag === newTag); + if (tagExists) return this.#inputError(true); + + this.#inputError(false); + this.items = [...this.items, newTag]; + this._tagInput.value = ''; + this._currentInput = ''; + this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true })); + } + + #inputError(error: boolean) { + if (error) { + this._mainTag.style.border = '1px solid var(--uui-color-danger)'; + this._tagInput.style.color = 'var(--uui-color-danger)'; + return; + } + this._mainTag.style.border = ''; + this._tagInput.style.color = ''; + } + + #delete(tag: string) { + this.items.splice( + this.items.findIndex((x) => x === tag), + 1 + ); + this.items = [...this.items]; + this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true })); + } + + /** Dropdown */ + + #optionClick(index: number) { + this._tagInput.value = this._optionCollection?.item(index)?.value ?? ''; + this.#createTag(); + this.focus(); + return; + } + + #optionKeydown(e: KeyboardEvent, index: number) { + if (e.key === 'Enter' || e.key === 'Tab') { + e.preventDefault(); + this._currentInput = this._optionCollection?.item(index)?.value ?? ''; + this.#createTag(); + this.focus(); + return; + } + + if (e.key === 'ArrowDown') { + e.preventDefault(); + if (!this._optionCollection?.item(index + 1)) return; + this._optionCollection?.item(index + 1)?.focus(); + this._currentInput = this._optionCollection?.item(index + 1)?.value ?? ''; + return; + } + + if (e.key === 'ArrowUp') { + e.preventDefault(); + if (!this._optionCollection?.item(index - 1)) return; + this._optionCollection?.item(index - 1)?.focus(); + this._currentInput = this._optionCollection?.item(index - 1)?.value ?? ''; + } + + if (e.key === 'Backspace') { + this.focus(); + } + } + + /** Render */ + + render() { + return html` +
+ ${this.#enteredTags()} + + + + + + ${this.#renderTagOptions()} + + +
+ `; + } + + #enteredTags() { + return html` ${this.items.map((tag) => { + return html` + + ${tag} + + + `; + })}`; + } + + #renderTagOptions() { + if (!this._currentInput.length || !this._matches.length) return nothing; + const matchfilter = this._matches.filter((tag) => tag.text !== this._items.find((x) => x === tag.text)); + if (!matchfilter.length) return; + return html` +
+ ${repeat( + matchfilter, + (tag: TagResponseModel) => tag.id, + (tag: TagResponseModel, index: number) => { + return html` + `; + } + )} +
+ `; + } +} + +export default UmbTagInputElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-tag-input': UmbTagInputElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-tags/input-tags.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tag-input/tag-input.stories.ts similarity index 75% rename from src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-tags/input-tags.stories.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tag-input/tag-input.stories.ts index 60bc4ecc89..2c0bc0ba61 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-tags/input-tags.stories.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tag-input/tag-input.stories.ts @@ -1,18 +1,18 @@ import { Meta, StoryObj } from '@storybook/web-components'; -import './input-tags.element'; -import type { UmbInputTagsElement } from './input-tags.element'; +import './tag-input.element'; +import type { UmbTagInputElement } from './tag-input.element'; -const meta: Meta = { +const meta: Meta = { title: 'Components/Inputs/Tags', - component: 'umb-input-tags', + component: 'umb-tag-input', }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Overview: Story = { args: { - group: 'default', + group: 'Fruits', items: [], }, }; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/tags/property-editor-ui-tags.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/tags/property-editor-ui-tags.element.ts index 5abb4b9df0..f11072295b 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/tags/property-editor-ui-tags.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/tags/property-editor-ui-tags.element.ts @@ -2,10 +2,10 @@ import { html } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, property, state } from 'lit/decorators.js'; import { ifDefined } from 'lit/directives/if-defined.js'; +import { UmbTagInputElement } from '../../../components/tag-input/tag-input.element'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; import { DataTypePropertyPresentationModel } from '@umbraco-cms/backoffice/backend-api'; import { UmbPropertyEditorElement } from '@umbraco-cms/backoffice/property-editor'; -import { UmbInputTagsElement } from '../../../../shared/components/input-tags/input-tags.element'; /** * @element umb-property-editor-ui-tags @@ -30,7 +30,7 @@ export class UmbPropertyEditorUITagsElement extends UmbLitElement implements Umb } private _onChange(event: CustomEvent) { - this.value = ((event.target as UmbInputTagsElement).value as string).split(','); + this.value = ((event.target as UmbTagInputElement).value as string).split(','); this.dispatchEvent(new CustomEvent('property-value-change')); } diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/browser-handlers.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/browser-handlers.ts index e899c2aaf6..2e709760e0 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/browser-handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/browser-handlers.ts @@ -29,6 +29,7 @@ import { handlers as logViewerHandlers } from './domains/log-viewer.handlers'; import { handlers as packageHandlers } from './domains/package.handlers'; import { handlers as rteEmbedHandlers } from './domains/rte-embed.handlers'; import { handlers as stylesheetHandlers } from './domains/stylesheet.handlers'; +import { handlers as tagHandlers } from './domains/tag-handlers'; const handlers = [ serverHandlers.serverVersionHandler, @@ -61,6 +62,7 @@ const handlers = [ ...packageHandlers, ...rteEmbedHandlers, ...stylesheetHandlers, + ...tagHandlers, ]; switch (import.meta.env.VITE_UMBRACO_INSTALL_STATUS) { diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/data/data-type.data.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/data/data-type.data.ts index 6b767f8d1d..8446bd7021 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/data/data-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/data/data-type.data.ts @@ -379,7 +379,7 @@ export const data: Array<(DataTypeResponseModel & { type: 'data-type' }) | Folde values: [ { alias: 'group', - value: 'default', + value: 'Fruits', }, { alias: 'items', diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/domains/tag-handlers.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/tag-handlers.ts new file mode 100644 index 0000000000..086270399e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/tag-handlers.ts @@ -0,0 +1,195 @@ +import { rest } from 'msw'; +import { umbracoPath } from '@umbraco-cms/backoffice/utils'; +import { PagedTagResponseModel, TagResponseModel } from '@umbraco-cms/backoffice/backend-api'; + +export const handlers = [ + rest.get(umbracoPath('/tag'), (_req, res, ctx) => { + // didnt add culture logic here + + const query = _req.url.searchParams.get('query'); + if (!query || !query.length) return; + + const tagGroup = _req.url.searchParams.get('tagGroup') ?? 'default'; + const skip = parseInt(_req.url.searchParams.get('skip') ?? '0', 10); + const take = parseInt(_req.url.searchParams.get('take') ?? '5', 10); + + const TagsByGroup = TagData.filter((tag) => tag.group?.toLocaleLowerCase() === tagGroup.toLocaleLowerCase()); + const TagsMatch = TagsByGroup.filter((tag) => tag.text?.toLocaleLowerCase().includes(query.toLocaleLowerCase())); + + const Tags = TagsMatch.slice(skip, skip + take); + + const PagedData: PagedTagResponseModel = { + total: Tags.length, + items: Tags, + }; + + return res(ctx.status(200), ctx.json(PagedData)); + }), +]; + +// Mock Data + +const TagData: TagResponseModel[] = [ + { + id: '1', + text: 'Cranberry', + group: 'Fruits', + nodeCount: 1, + }, + { + id: '2', + text: 'Kiwi', + group: 'Fruits', + nodeCount: 1, + }, + { + id: '3', + text: 'Blueberries', + group: 'Fruits', + nodeCount: 1, + }, + { + id: '4', + text: 'Watermelon', + group: 'Fruits', + nodeCount: 1, + }, + { + id: '5', + text: 'Tomato', + group: 'Fruits', + nodeCount: 1, + }, + { + id: '6', + text: 'Mango', + group: 'Fruits', + nodeCount: 1, + }, + { + id: '7', + text: 'Strawberry', + group: 'Fruits', + nodeCount: 1, + }, + { + id: '8', + text: 'Water Chestnut', + group: 'Fruits', + nodeCount: 1, + }, + { + id: '9', + text: 'Papaya', + group: 'Fruits', + nodeCount: 1, + }, + { + id: '10', + text: 'Orange Rind', + group: 'Fruits', + nodeCount: 1, + }, + { + id: '11', + text: 'Olives', + group: 'Fruits', + nodeCount: 1, + }, + { + id: '12', + text: 'Pear', + group: 'Fruits', + nodeCount: 1, + }, + { + id: '13', + text: 'Sultana', + group: 'Fruits', + nodeCount: 1, + }, + { + id: '14', + text: 'Mulberry', + group: 'Fruits', + nodeCount: 1, + }, + { + id: '15', + text: 'Lychee', + group: 'Fruits', + nodeCount: 1, + }, + { + id: '16', + text: 'Lemon', + group: 'Fruits', + nodeCount: 1, + }, + { + id: '17', + text: 'Apple', + group: 'Fruits', + nodeCount: 1, + }, + { + id: '18', + text: 'Banana', + group: 'Fruits', + nodeCount: 1, + }, + { + id: '19', + text: 'Dragonfruit', + group: 'Fruits', + nodeCount: 1, + }, + { + id: '20', + text: 'Blackberry', + group: 'Fruits', + nodeCount: 1, + }, + { + id: '21', + text: 'Raspberry', + group: 'Fruits', + nodeCount: 1, + }, + { + id: '22', + text: 'Flour', + group: 'Cake Ingredients', + nodeCount: 1, + }, + { + id: '23', + text: 'Eggs', + group: 'Cake Ingredients', + nodeCount: 1, + }, + { + id: '24', + text: 'Butter', + group: 'Cake Ingredients', + nodeCount: 1, + }, + { + id: '25', + text: 'Sugar', + group: 'Cake Ingredients', + nodeCount: 1, + }, + { + id: '26', + text: 'Salt', + group: 'Cake Ingredients', + nodeCount: 1, + }, + { + id: '26', + text: 'Milk', + group: 'Cake Ingredients', + nodeCount: 1, + }, +]; From 0e3fe76ea9a1f146a4c02331f848ae99f7a30c08 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Wed, 19 Apr 2023 10:49:36 +0200 Subject: [PATCH 07/20] rename --- .../src/backoffice/shared/components/index.ts | 2 +- .../tags-input.element.ts} | 8 ++++---- .../tags-input.stories.ts} | 10 +++++----- .../uis/tags/property-editor-ui-tags.element.ts | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) rename src/Umbraco.Web.UI.Client/src/backoffice/shared/components/{tag-input/tag-input.element.ts => tags-input/tags-input.element.ts} (98%) rename src/Umbraco.Web.UI.Client/src/backoffice/shared/components/{tag-input/tag-input.stories.ts => tags-input/tags-input.stories.ts} (77%) diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/index.ts index 200f4dd1fa..a94a422e2e 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/index.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/index.ts @@ -28,7 +28,7 @@ import './input-media-picker/input-media-picker.element'; import './input-multi-url-picker/input-multi-url-picker.element'; import './input-slider/input-slider.element'; import './input-toggle/input-toggle.element'; -import './tag-input/tag-input.element'; +import './tags-input/tags-input.element'; import './input-upload-field/input-upload-field.element'; import './input-template-picker/input-template-picker.element'; import './property-type-based-property/property-type-based-property.element'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tag-input/tag-input.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tags-input/tags-input.element.ts similarity index 98% rename from src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tag-input/tag-input.element.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tags-input/tags-input.element.ts index 988f88d6b3..dc3dc9b858 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tag-input/tag-input.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tags-input/tags-input.element.ts @@ -8,8 +8,8 @@ import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; import { TagResource, TagResponseModel } from '@umbraco-cms/backoffice/backend-api'; -@customElement('umb-tag-input') -export class UmbTagInputElement extends FormControlMixin(UmbLitElement) { +@customElement('umb-tags-input') +export class UmbTagsInputElement extends FormControlMixin(UmbLitElement) { static styles = [ UUITextStyles, css` @@ -398,10 +398,10 @@ export class UmbTagInputElement extends FormControlMixin(UmbLitElement) { } } -export default UmbTagInputElement; +export default UmbTagsInputElement; declare global { interface HTMLElementTagNameMap { - 'umb-tag-input': UmbTagInputElement; + 'umb-tags-input': UmbTagsInputElement; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tag-input/tag-input.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tags-input/tags-input.stories.ts similarity index 77% rename from src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tag-input/tag-input.stories.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tags-input/tags-input.stories.ts index 2c0bc0ba61..96a9cf4bd0 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tag-input/tag-input.stories.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tags-input/tags-input.stories.ts @@ -1,14 +1,14 @@ import { Meta, StoryObj } from '@storybook/web-components'; -import './tag-input.element'; -import type { UmbTagInputElement } from './tag-input.element'; +import './tags-input.element'; +import type { UmbTagsInputElement } from './tags-input.element'; -const meta: Meta = { +const meta: Meta = { title: 'Components/Inputs/Tags', - component: 'umb-tag-input', + component: 'umb-tags-input', }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Overview: Story = { args: { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/tags/property-editor-ui-tags.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/tags/property-editor-ui-tags.element.ts index f11072295b..334ffaf078 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/tags/property-editor-ui-tags.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/property-editors/uis/tags/property-editor-ui-tags.element.ts @@ -2,7 +2,7 @@ import { html } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, property, state } from 'lit/decorators.js'; import { ifDefined } from 'lit/directives/if-defined.js'; -import { UmbTagInputElement } from '../../../components/tag-input/tag-input.element'; +import { UmbTagsInputElement } from '../../../components/tags-input/tags-input.element'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; import { DataTypePropertyPresentationModel } from '@umbraco-cms/backoffice/backend-api'; import { UmbPropertyEditorElement } from '@umbraco-cms/backoffice/property-editor'; @@ -30,7 +30,7 @@ export class UmbPropertyEditorUITagsElement extends UmbLitElement implements Umb } private _onChange(event: CustomEvent) { - this.value = ((event.target as UmbTagInputElement).value as string).split(','); + this.value = ((event.target as UmbTagsInputElement).value as string).split(','); this.dispatchEvent(new CustomEvent('property-value-change')); } From 8642fd0cd671e6c42db692e58cf57983f3dd3ee1 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Thu, 20 Apr 2023 15:50:00 +0200 Subject: [PATCH 08/20] repository --- .../backoffice/tags/repository/manifests.ts | 23 ++++++++ .../repository/sources/tag.server.data.ts | 44 ++++++++++++++++ .../tags/repository/tag.repository.ts | 48 +++++++++++++++++ .../backoffice/tags/repository/tag.store.ts | 52 +++++++++++++++++++ 4 files changed, 167 insertions(+) create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/sources/tag.server.data.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/tag.repository.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/tag.store.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/manifests.ts new file mode 100644 index 0000000000..4eb7b6b7f3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/manifests.ts @@ -0,0 +1,23 @@ +import { UmbTagRepository } from './tag.repository'; +import { UmbTagStore } from './tag.store'; +import type { ManifestStore, ManifestRepository } from '@umbraco-cms/backoffice/extensions-registry'; + +export const TAG_REPOSITORY_ALIAS = 'Umb.Repository.Tags'; + +const repository: ManifestRepository = { + type: 'repository', + alias: TAG_REPOSITORY_ALIAS, + name: 'Tags Repository', + class: UmbTagRepository, +}; + +export const TAG_STORE_ALIAS = 'Umb.Store.Tags'; + +const store: ManifestStore = { + type: 'store', + alias: TAG_STORE_ALIAS, + name: 'Tags Store', + class: UmbTagStore, +}; + +export const manifests = [repository, store]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/sources/tag.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/sources/tag.server.data.ts new file mode 100644 index 0000000000..a9d60e0fb5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/sources/tag.server.data.ts @@ -0,0 +1,44 @@ +import { v4 as uuidv4 } from 'uuid'; +import { TagResource } from '@umbraco-cms/backoffice/backend-api'; +import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; +import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; + +/** + * A data source for the Tag that fetches data from the server + * @export + * @class UmbTagServerDataSource + * @implements {RepositoryDetailDataSource} + */ +export class UmbTagServerDataSource { + #host: UmbControllerHostElement; + + /** + * Creates an instance of UmbTagServerDataSource. + * @param {UmbControllerHostElement} host + * @memberof UmbTagServerDataSource + */ + constructor(host: UmbControllerHostElement) { + this.#host = host; + } + + /** + * Get a list of tags on the server + * @return {*} + * @memberof UmbTagServerDataSource + */ + async getCollection({ + query, + skip, + take, + tagGroup, + culture, + }: { + query: string; + skip: number; + take: number; + tagGroup?: string; + culture?: string; + }) { + return tryExecuteAndNotify(this.#host, TagResource.getTag({ query, skip, take, tagGroup, culture })); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/tag.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/tag.repository.ts new file mode 100644 index 0000000000..4c876de37f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/tag.repository.ts @@ -0,0 +1,48 @@ +import { UmbTagServerDataSource } from './sources/tag.server.data'; +import { UmbTagStore, UMB_TAG_STORE_CONTEXT_TOKEN } from './tag.store'; +import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; +import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; +import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/notification'; +import { TagResponseModel, ProblemDetailsModel } from '@umbraco-cms/backoffice/backend-api'; + +export class UmbTagRepository { + #init!: Promise; + + #host: UmbControllerHostElement; + + #dataSource: UmbTagServerDataSource; + #tagStore?: UmbTagStore; + + #notificationContext?: UmbNotificationContext; + + constructor(host: UmbControllerHostElement) { + this.#host = host; + + this.#dataSource = new UmbTagServerDataSource(this.#host); + + this.#init = Promise.all([ + new UmbContextConsumerController(this.#host, UMB_NOTIFICATION_CONTEXT_TOKEN, (instance) => { + this.#notificationContext = instance; + }), + + new UmbContextConsumerController(this.#host, UMB_TAG_STORE_CONTEXT_TOKEN, (instance) => { + this.#tagStore = instance; + }).asPromise(), + ]); + } + + async requestTags( + { query, skip, take, tagGroup, culture } = { query: '', skip: 0, take: 1000, tagGroup: '', culture: '' } + ) { + await this.#init; + + const { data, error } = await this.#dataSource.getCollection({ query, skip, take, tagGroup, culture }); + + if (data) { + // TODO: allow to append an array of items to the store + data.items.forEach((x) => this.#tagStore?.append(x)); + } + + return { data, error, asObservable: () => this.#tagStore!.data }; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/tag.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/tag.store.ts new file mode 100644 index 0000000000..d69b24df5c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/tag.store.ts @@ -0,0 +1,52 @@ +import type { PagedTagResponseModel, TagResponseModel } from '@umbraco-cms/backoffice/backend-api'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; +import { ArrayState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbStoreBase } from '@umbraco-cms/backoffice/store'; +import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; + +export const UMB_TAG_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbTAGStore'); +/** + * @export + * @class UmbTagStore + * @extends {UmbStoreBase} + * @description - Data Store for Template Details + */ +export class UmbTagStore extends UmbStoreBase { + #data = new ArrayState([], (x) => x.id); + + /** + * Creates an instance of UmbTagStore. + * @param {UmbControllerHostElement} host + * @memberof UmbTagStore + */ + constructor(host: UmbControllerHostElement) { + super(host, UMB_TAG_STORE_CONTEXT_TOKEN.toString()); + } + + /** + * Append a tag to the store + * @param {TagResponseModel} TAG + * @memberof UmbTagStore + */ + append(TAG: TagResponseModel) { + this.#data.append([TAG]); + } + + /** + * Append a tag to the store + * @param {id} TAGResponseModel id. + * @memberof UmbTagStore + */ + byId(id: TagResponseModel['id']) { + return this.#data.getObservablePart((x) => x.find((y) => y.id === id)); + } + + /** + * Removes tag in the store with the given uniques + * @param {string[]} uniques + * @memberof UmbTagStore + */ + remove(uniques: Array) { + this.#data.remove(uniques); + } +} From 7e1b0d2cd0515c38d158e580de6e61f95507a3a0 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Fri, 21 Apr 2023 15:07:54 +0200 Subject: [PATCH 09/20] tag store and repo --- .../tags/repository/tag.repository.ts | 7 ------ .../backoffice/tags/repository/tag.store.ts | 23 +++++++++++++++---- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/tag.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/tag.repository.ts index 4c876de37f..7409042f6f 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/tag.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/tag.repository.ts @@ -13,18 +13,12 @@ export class UmbTagRepository { #dataSource: UmbTagServerDataSource; #tagStore?: UmbTagStore; - #notificationContext?: UmbNotificationContext; - constructor(host: UmbControllerHostElement) { this.#host = host; this.#dataSource = new UmbTagServerDataSource(this.#host); this.#init = Promise.all([ - new UmbContextConsumerController(this.#host, UMB_NOTIFICATION_CONTEXT_TOKEN, (instance) => { - this.#notificationContext = instance; - }), - new UmbContextConsumerController(this.#host, UMB_TAG_STORE_CONTEXT_TOKEN, (instance) => { this.#tagStore = instance; }).asPromise(), @@ -39,7 +33,6 @@ export class UmbTagRepository { const { data, error } = await this.#dataSource.getCollection({ query, skip, take, tagGroup, culture }); if (data) { - // TODO: allow to append an array of items to the store data.items.forEach((x) => this.#tagStore?.append(x)); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/tag.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/tag.store.ts index d69b24df5c..b9bbcefced 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/tag.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/tag.store.ts @@ -1,10 +1,10 @@ -import type { PagedTagResponseModel, TagResponseModel } from '@umbraco-cms/backoffice/backend-api'; +import type { TagResponseModel } from '@umbraco-cms/backoffice/backend-api'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; import { ArrayState } from '@umbraco-cms/backoffice/observable-api'; import { UmbStoreBase } from '@umbraco-cms/backoffice/store'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; -export const UMB_TAG_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbTAGStore'); +export const UMB_TAG_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbTagStore'); /** * @export * @class UmbTagStore @@ -13,6 +13,7 @@ export const UMB_TAG_STORE_CONTEXT_TOKEN = new UmbContextToken('Umb */ export class UmbTagStore extends UmbStoreBase { #data = new ArrayState([], (x) => x.id); + data = this.#data.asObservable(); /** * Creates an instance of UmbTagStore. @@ -28,19 +29,31 @@ export class UmbTagStore extends UmbStoreBase { * @param {TagResponseModel} TAG * @memberof UmbTagStore */ - append(TAG: TagResponseModel) { - this.#data.append([TAG]); + append(tag: TagResponseModel) { + this.#data.append([tag]); } /** * Append a tag to the store - * @param {id} TAGResponseModel id. + * @param {id} TagResponseModel id. * @memberof UmbTagStore */ byId(id: TagResponseModel['id']) { return this.#data.getObservablePart((x) => x.find((y) => y.id === id)); } + // TODO + byGroup(group: TagResponseModel['group']) { + return this.#data.getObservablePart((x) => x.filter((y) => y.group === group)); + } + + // TODO + byText(text: string) { + return this.#data.getObservablePart((items) => + items.filter((item) => item.text?.toLocaleLowerCase().includes(text.toLocaleLowerCase())) + ); + } + /** * Removes tag in the store with the given uniques * @param {string[]} uniques From 1fd5cc9e258b3802cc2ba079acc94bcc778adbe9 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Wed, 3 May 2023 13:19:07 +0200 Subject: [PATCH 10/20] move files --- .../{shared => core}/components/tags-input/tags-input.element.ts | 0 .../{shared => core}/components/tags-input/tags-input.stories.ts | 0 .../property-editors/uis/tags/property-editor-ui-tags.element.ts | 1 + 3 files changed, 1 insertion(+) rename src/Umbraco.Web.UI.Client/src/backoffice/{shared => core}/components/tags-input/tags-input.element.ts (100%) rename src/Umbraco.Web.UI.Client/src/backoffice/{shared => core}/components/tags-input/tags-input.stories.ts (100%) diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tags-input/tags-input.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/core/components/tags-input/tags-input.element.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tags-input/tags-input.element.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/core/components/tags-input/tags-input.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tags-input/tags-input.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/core/components/tags-input/tags-input.stories.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/backoffice/shared/components/tags-input/tags-input.stories.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/core/components/tags-input/tags-input.stories.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/core/property-editors/uis/tags/property-editor-ui-tags.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/core/property-editors/uis/tags/property-editor-ui-tags.element.ts index 7a50d6ac7c..17247e9524 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/core/property-editors/uis/tags/property-editor-ui-tags.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/core/property-editors/uis/tags/property-editor-ui-tags.element.ts @@ -2,6 +2,7 @@ import { html } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, property, state } from 'lit/decorators.js'; import { ifDefined } from 'lit/directives/if-defined.js'; +import { UmbTagsInputElement } from '../../../components/tags-input/tags-input.element'; import { UmbPropertyEditorExtensionElement } from '@umbraco-cms/backoffice/extensions-registry'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; import { DataTypePropertyPresentationModel } from '@umbraco-cms/backoffice/backend-api'; From 576473197f70f35011a3620db19c8e698a67594e Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Wed, 3 May 2023 15:14:16 +0200 Subject: [PATCH 11/20] tags, move styling, update repo --- .../tags-input/tags-input.element.ts | 330 +++++++++--------- .../tags/property-editor-ui-tags.element.ts | 4 +- .../tags/repository/tag.repository.ts | 6 +- .../backoffice/tags/repository/tag.store.ts | 17 +- 4 files changed, 177 insertions(+), 180 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/core/components/tags-input/tags-input.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/core/components/tags-input/tags-input.element.ts index dc3dc9b858..a90a6c5e0f 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/core/components/tags-input/tags-input.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/core/components/tags-input/tags-input.element.ts @@ -10,171 +10,6 @@ import { TagResource, TagResponseModel } from '@umbraco-cms/backoffice/backend-a @customElement('umb-tags-input') export class UmbTagsInputElement extends FormControlMixin(UmbLitElement) { - static styles = [ - UUITextStyles, - css` - #wrapper { - box-sizing: border-box; - display: flex; - gap: var(--uui-size-space-2); - flex-wrap: wrap; - align-items: center; - padding: var(--uui-size-space-2); - border: 1px solid var(--uui-color-border); - background-color: var(--uui-input-background-color, var(--uui-color-surface)); - flex: 1; - } - - #main-tag-wrapper { - position: relative; - } - - /** Tags */ - - uui-tag { - position: relative; - max-width: 200px; - } - - uui-tag uui-icon { - cursor: pointer; - min-width: 12.8px !important; - } - - uui-tag span { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - /** Created tags */ - - .tag uui-icon { - margin-left: var(--uui-size-space-2); - } - - .tag uui-icon:hover, - .tag uui-icon:active { - color: var(--uui-color-selected-contrast); - } - - /** Main tag */ - - #main-tag { - padding: 3px; - background-color: var(--uui-color-selected-contrast); - min-width: 20px; - position: relative; - border-radius: var(--uui-size-5, 12px); - } - - #main-tag uui-icon { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - } - - #main-tag:hover uui-icon, - #main-tag:active uui-icon { - color: var(--uui-color-selected); - } - - #main-tag #tag-input:focus ~ uui-icon, - #main-tag #tag-input:not(:placeholder-shown) ~ uui-icon { - display: none; - } - - #main-tag:has(*:hover), - #main-tag:has(*:active), - #main-tag:has(*:focus) { - border: 1px solid var(--uui-color-selected-emphasis); - } - - #main-tag:has(#tag-input:not(:focus)):hover { - cursor: pointer; - border: 1px solid var(--uui-color-selected-emphasis); - } - - #main-tag:not(:focus-within) #tag-input:placeholder-shown { - opacity: 0; - } - - #main-tag:has(#tag-input:focus), - #main-tag:has(#tag-input:not(:placeholder-shown)) { - min-width: 65px; - } - - #main-tag #tag-input { - box-sizing: border-box; - max-height: 25.8px; - background: none; - font: inherit; - color: var(--uui-color-selected); - line-height: reset; - padding: 0 var(--uui-size-space-2); - margin: 0.5px 0 -0.5px; - border: none; - outline: none; - width: 100%; - } - - /** Dropdown matchlist */ - - #matchlist input[type='radio'] { - -webkit-appearance: none; - appearance: none; - /* For iOS < 15 to remove gradient background */ - background-color: transparent; - /* Not removed via appearance */ - margin: 0; - } - - uui-tag:focus-within #matchlist { - display: flex; - } - - #matchlist { - display: none; - display: flex; - flex-direction: column; - background-color: var(--uui-color-surface); - position: absolute; - width: 150px; - left: 0; - top: var(--uui-size-space-6); - border-radius: var(--uui-border-radius); - border: 1px solid var(--uui-color-border); - } - - #matchlist label { - display: none; - cursor: pointer; - box-sizing: border-box; - display: block; - width: 100%; - background: none; - border: none; - text-align: left; - padding: 10px 12px; - - /** Overflow */ - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - #matchlist label:hover, - #matchlist label:focus, - #matchlist label:focus-within, - #matchlist input[type='radio']:focus + label { - display: block; - background-color: var(--uui-color-focus); - color: var(--uui-color-selected-contrast); - } - `, - ]; - @property({ type: String }) group?: string; @@ -396,6 +231,171 @@ export class UmbTagsInputElement extends FormControlMixin(UmbLitElement) {
`; } + + static styles = [ + UUITextStyles, + css` + #wrapper { + box-sizing: border-box; + display: flex; + gap: var(--uui-size-space-2); + flex-wrap: wrap; + align-items: center; + padding: var(--uui-size-space-2); + border: 1px solid var(--uui-color-border); + background-color: var(--uui-input-background-color, var(--uui-color-surface)); + flex: 1; + } + + #main-tag-wrapper { + position: relative; + } + + /** Tags */ + + uui-tag { + position: relative; + max-width: 200px; + } + + uui-tag uui-icon { + cursor: pointer; + min-width: 12.8px !important; + } + + uui-tag span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + /** Created tags */ + + .tag uui-icon { + margin-left: var(--uui-size-space-2); + } + + .tag uui-icon:hover, + .tag uui-icon:active { + color: var(--uui-color-selected-contrast); + } + + /** Main tag */ + + #main-tag { + padding: 3px; + background-color: var(--uui-color-selected-contrast); + min-width: 20px; + position: relative; + border-radius: var(--uui-size-5, 12px); + } + + #main-tag uui-icon { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } + + #main-tag:hover uui-icon, + #main-tag:active uui-icon { + color: var(--uui-color-selected); + } + + #main-tag #tag-input:focus ~ uui-icon, + #main-tag #tag-input:not(:placeholder-shown) ~ uui-icon { + display: none; + } + + #main-tag:has(*:hover), + #main-tag:has(*:active), + #main-tag:has(*:focus) { + border: 1px solid var(--uui-color-selected-emphasis); + } + + #main-tag:has(#tag-input:not(:focus)):hover { + cursor: pointer; + border: 1px solid var(--uui-color-selected-emphasis); + } + + #main-tag:not(:focus-within) #tag-input:placeholder-shown { + opacity: 0; + } + + #main-tag:has(#tag-input:focus), + #main-tag:has(#tag-input:not(:placeholder-shown)) { + min-width: 65px; + } + + #main-tag #tag-input { + box-sizing: border-box; + max-height: 25.8px; + background: none; + font: inherit; + color: var(--uui-color-selected); + line-height: reset; + padding: 0 var(--uui-size-space-2); + margin: 0.5px 0 -0.5px; + border: none; + outline: none; + width: 100%; + } + + /** Dropdown matchlist */ + + #matchlist input[type='radio'] { + -webkit-appearance: none; + appearance: none; + /* For iOS < 15 to remove gradient background */ + background-color: transparent; + /* Not removed via appearance */ + margin: 0; + } + + uui-tag:focus-within #matchlist { + display: flex; + } + + #matchlist { + display: none; + display: flex; + flex-direction: column; + background-color: var(--uui-color-surface); + position: absolute; + width: 150px; + left: 0; + top: var(--uui-size-space-6); + border-radius: var(--uui-border-radius); + border: 1px solid var(--uui-color-border); + } + + #matchlist label { + display: none; + cursor: pointer; + box-sizing: border-box; + display: block; + width: 100%; + background: none; + border: none; + text-align: left; + padding: 10px 12px; + + /** Overflow */ + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + #matchlist label:hover, + #matchlist label:focus, + #matchlist label:focus-within, + #matchlist input[type='radio']:focus + label { + display: block; + background-color: var(--uui-color-focus); + color: var(--uui-color-selected-contrast); + } + `, + ]; } export default UmbTagsInputElement; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/core/property-editors/uis/tags/property-editor-ui-tags.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/core/property-editors/uis/tags/property-editor-ui-tags.element.ts index 17247e9524..2b097b857c 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/core/property-editors/uis/tags/property-editor-ui-tags.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/core/property-editors/uis/tags/property-editor-ui-tags.element.ts @@ -33,10 +33,10 @@ export class UmbPropertyEditorUITagsElement extends UmbLitElement implements Umb } render() { - return html``; + @change="${this._onChange}">`; } static styles = [UUITextStyles]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/tag.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/tag.repository.ts index 7409042f6f..53ed6acf9b 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/tag.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/tag.repository.ts @@ -26,16 +26,16 @@ export class UmbTagRepository { } async requestTags( - { query, skip, take, tagGroup, culture } = { query: '', skip: 0, take: 1000, tagGroup: '', culture: '' } + { query, skip, take, tagGroup, culture } = { query: '', skip: 0, take: 1000, tagGroup: 'default', culture: '' } ) { await this.#init; const { data, error } = await this.#dataSource.getCollection({ query, skip, take, tagGroup, culture }); if (data) { - data.items.forEach((x) => this.#tagStore?.append(x)); + this.#tagStore?.appendItems(data.items); } - return { data, error, asObservable: () => this.#tagStore!.data }; + return { data, error, asObservable: () => this.#tagStore?.byGroup(tagGroup) }; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/tag.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/tag.store.ts index b9bbcefced..e0b3e5b3d8 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/tag.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/tag.store.ts @@ -1,6 +1,6 @@ import type { TagResponseModel } from '@umbraco-cms/backoffice/backend-api'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import { ArrayState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; import { UmbStoreBase } from '@umbraco-cms/backoffice/store'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; @@ -12,16 +12,13 @@ export const UMB_TAG_STORE_CONTEXT_TOKEN = new UmbContextToken('Umb * @description - Data Store for Template Details */ export class UmbTagStore extends UmbStoreBase { - #data = new ArrayState([], (x) => x.id); - data = this.#data.asObservable(); - /** * Creates an instance of UmbTagStore. * @param {UmbControllerHostElement} host * @memberof UmbTagStore */ constructor(host: UmbControllerHostElement) { - super(host, UMB_TAG_STORE_CONTEXT_TOKEN.toString()); + super(host, UMB_TAG_STORE_CONTEXT_TOKEN.toString(), new UmbArrayState([], (x) => x.id)); } /** @@ -30,7 +27,7 @@ export class UmbTagStore extends UmbStoreBase { * @memberof UmbTagStore */ append(tag: TagResponseModel) { - this.#data.append([tag]); + this._data.append([tag]); } /** @@ -39,17 +36,17 @@ export class UmbTagStore extends UmbStoreBase { * @memberof UmbTagStore */ byId(id: TagResponseModel['id']) { - return this.#data.getObservablePart((x) => x.find((y) => y.id === id)); + return this._data.getObservablePart((x) => x.find((y) => y.id === id)); } // TODO byGroup(group: TagResponseModel['group']) { - return this.#data.getObservablePart((x) => x.filter((y) => y.group === group)); + return this._data.getObservablePart((x) => x.filter((y) => y.group === group)); } // TODO byText(text: string) { - return this.#data.getObservablePart((items) => + return this._data.getObservablePart((items) => items.filter((item) => item.text?.toLocaleLowerCase().includes(text.toLocaleLowerCase())) ); } @@ -60,6 +57,6 @@ export class UmbTagStore extends UmbStoreBase { * @memberof UmbTagStore */ remove(uniques: Array) { - this.#data.remove(uniques); + this._data.remove(uniques); } } From 6bfb660d3f9c9a7c2730ca0dab9bed0c69104e89 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Mon, 8 May 2023 12:52:18 +0200 Subject: [PATCH 12/20] tags repository and store --- .../src/backoffice/backoffice.element.ts | 1 + .../tags-input/tags-input.element.ts | 16 +++++----- .../tags/property-editor-ui-tags.element.ts | 21 +++++++++++-- .../src/backoffice/tags/index.ts | 10 +++++++ .../tags/repository/tag.repository.ts | 30 +++++++++++++++---- .../backoffice/tags/repository/tag.store.ts | 22 ++++++++++---- .../src/backoffice/tags/umbraco-package.ts | 10 +++++++ 7 files changed, 89 insertions(+), 21 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/tags/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/tags/umbraco-package.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts index 3e90e86cf2..36f83f663c 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts @@ -25,6 +25,7 @@ const CORE_PACKAGES = [ import('./search/umbraco-package'), import('./templating/umbraco-package'), import('./umbraco-news/umbraco-package'), + import('./tags/umbraco-package'), ]; @defineElement('umb-backoffice') diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/core/components/tags-input/tags-input.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/core/components/tags-input/tags-input.element.ts index a90a6c5e0f..2ee6389f8a 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/core/components/tags-input/tags-input.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/core/components/tags-input/tags-input.element.ts @@ -4,15 +4,18 @@ import { customElement, property, query, queryAll, state } from 'lit/decorators. import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins'; import { repeat } from 'lit/directives/repeat.js'; import { UUIInputElement, UUIInputEvent, UUITagElement } from '@umbraco-ui/uui'; +import { UmbTagRepository } from '../../../tags/repository/tag.repository'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; -import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; -import { TagResource, TagResponseModel } from '@umbraco-cms/backoffice/backend-api'; +import { TagResponseModel } from '@umbraco-cms/backoffice/backend-api'; @customElement('umb-tags-input') export class UmbTagsInputElement extends FormControlMixin(UmbLitElement) { @property({ type: String }) group?: string; + @property({ type: String }) + culture?: string | null; + _items: string[] = []; @property({ type: Array }) public set items(newTags: string[]) { @@ -41,6 +44,8 @@ export class UmbTagsInputElement extends FormControlMixin(UmbLitElement) { @queryAll('.options') private _optionCollection?: HTMLCollectionOf; + #repository = new UmbTagRepository(this); + public focus() { this._tagInput.focus(); } @@ -50,11 +55,8 @@ export class UmbTagsInputElement extends FormControlMixin(UmbLitElement) { } async #getExistingTags(query: string) { - //TODO: Culture - const { data } = await tryExecuteAndNotify( - this, - TagResource.getTag({ query, skip: 0, take: 5, tagGroup: this.group }) - ); + if (!this.group || this.culture === undefined) return; + const { data } = await this.#repository.queryTags(this.group, this.culture, query); if (!data) return; this._matches = data.items; } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/core/property-editors/uis/tags/property-editor-ui-tags.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/core/property-editors/uis/tags/property-editor-ui-tags.element.ts index 2b097b857c..f557538fbb 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/core/property-editors/uis/tags/property-editor-ui-tags.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/core/property-editors/uis/tags/property-editor-ui-tags.element.ts @@ -3,6 +3,7 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, property, state } from 'lit/decorators.js'; import { ifDefined } from 'lit/directives/if-defined.js'; import { UmbTagsInputElement } from '../../../components/tags-input/tags-input.element'; +import { UMB_WORKSPACE_PROPERTY_CONTEXT_TOKEN } from '../../../components/workspace-property/workspace-property.context'; import { UmbPropertyEditorExtensionElement } from '@umbraco-cms/backoffice/extensions-registry'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; import { DataTypePropertyPresentationModel } from '@umbraco-cms/backoffice/backend-api'; @@ -18,6 +19,10 @@ export class UmbPropertyEditorUITagsElement extends UmbLitElement implements Umb @state() private _group?: string; + @state() + private _culture?: string | null; + //TODO: Use type from VariantID + @property({ type: Array, attribute: false }) public set config(config: Array) { const group = config.find((x) => x.alias === 'group'); @@ -27,6 +32,17 @@ export class UmbPropertyEditorUITagsElement extends UmbLitElement implements Umb if (items) this.value = items.value as Array; } + constructor() { + super(); + this.consumeContext(UMB_WORKSPACE_PROPERTY_CONTEXT_TOKEN, (context) => { + this.observe(context.variantId, (id) => { + if (id && id.culture !== undefined) { + this._culture = id.culture; + } + }); + }); + } + private _onChange(event: CustomEvent) { this.value = ((event.target as UmbTagsInputElement).value as string).split(','); this.dispatchEvent(new CustomEvent('property-value-change')); @@ -35,8 +51,9 @@ export class UmbPropertyEditorUITagsElement extends UmbLitElement implements Umb render() { return html``; + .culture=${this._culture} + .items=${this.value} + @change=${this._onChange}>`; } static styles = [UUITextStyles]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/tags/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/tags/index.ts new file mode 100644 index 0000000000..ca1066dcb4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/tags/index.ts @@ -0,0 +1,10 @@ +import { manifests as repositoryManifests } from './repository/manifests'; + +import { UmbEntrypointOnInit } from '@umbraco-cms/backoffice/extensions-api'; + +export const manifests = [...repositoryManifests]; + +export const onInit: UmbEntrypointOnInit = (host, extensionRegistry) => { + console.log('tags registrer'); + extensionRegistry.registerMany(manifests); +}; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/tag.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/tag.repository.ts index 53ed6acf9b..37efa072a2 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/tag.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/tag.repository.ts @@ -2,8 +2,6 @@ import { UmbTagServerDataSource } from './sources/tag.server.data'; import { UmbTagStore, UMB_TAG_STORE_CONTEXT_TOKEN } from './tag.store'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; -import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/notification'; -import { TagResponseModel, ProblemDetailsModel } from '@umbraco-cms/backoffice/backend-api'; export class UmbTagRepository { #init!: Promise; @@ -26,16 +24,36 @@ export class UmbTagRepository { } async requestTags( - { query, skip, take, tagGroup, culture } = { query: '', skip: 0, take: 1000, tagGroup: 'default', culture: '' } + tagGroupName: string, + culture: string | null, + { skip, take, query } = { skip: 0, take: 1000, query: '' } ) { await this.#init; - const { data, error } = await this.#dataSource.getCollection({ query, skip, take, tagGroup, culture }); + const requestCulture = culture || ''; + + const { data, error } = await this.#dataSource.getCollection({ + skip, + take, + tagGroup: tagGroupName, + culture: requestCulture, + query, + }); if (data) { - this.#tagStore?.appendItems(data.items); + // TODO: allow to append an array of items to the store + data.items.forEach((x) => this.#tagStore?.append(x)); } - return { data, error, asObservable: () => this.#tagStore?.byGroup(tagGroup) }; + return { data, error, asObservable: () => this.#tagStore!.byQuery(tagGroupName, requestCulture, query) }; + } + + async queryTags( + tagGroupName: string, + culture: string | null, + query: string, + { skip, take } = { skip: 0, take: 1000 } + ) { + return this.requestTags(tagGroupName, culture, { skip, take, query }); } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/tag.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/tag.store.ts index e0b3e5b3d8..5ddf7e15c9 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/tag.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/tag.store.ts @@ -12,6 +12,8 @@ export const UMB_TAG_STORE_CONTEXT_TOKEN = new UmbContextToken('Umb * @description - Data Store for Template Details */ export class UmbTagStore extends UmbStoreBase { + public readonly data = this._data.asObservable(); + /** * Creates an instance of UmbTagStore. * @param {UmbControllerHostElement} host @@ -19,6 +21,7 @@ export class UmbTagStore extends UmbStoreBase { */ constructor(host: UmbControllerHostElement) { super(host, UMB_TAG_STORE_CONTEXT_TOKEN.toString(), new UmbArrayState([], (x) => x.id)); + console.log('Store is open'); } /** @@ -39,15 +42,22 @@ export class UmbTagStore extends UmbStoreBase { return this._data.getObservablePart((x) => x.find((y) => y.id === id)); } - // TODO - byGroup(group: TagResponseModel['group']) { - return this._data.getObservablePart((x) => x.filter((y) => y.group === group)); + items(group: TagResponseModel['group'], culture: string) { + return this._data.getObservablePart((items) => + items.filter((item) => item.group === group && item.culture === culture) + ); } - // TODO - byText(text: string) { + //TODO Skriv god kommentar til filter/exclude + + byQuery(group: TagResponseModel['group'], culture: string, query: string) { return this._data.getObservablePart((items) => - items.filter((item) => item.text?.toLocaleLowerCase().includes(text.toLocaleLowerCase())) + items.filter( + (item) => + item.group === group && + item.culture === culture && + item.query?.toLocaleLowerCase().includes(query.toLocaleLowerCase()) + ) ); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/tags/umbraco-package.ts b/src/Umbraco.Web.UI.Client/src/backoffice/tags/umbraco-package.ts new file mode 100644 index 0000000000..6f6c4e8434 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/tags/umbraco-package.ts @@ -0,0 +1,10 @@ +export const name = 'Umbraco.Core.UserManagement'; +export const version = '0.0.1'; +export const extensions = [ + { + name: 'Tags Management Entry Point', + alias: 'Umb.EntryPoint.TagsManagement', + type: 'entryPoint', + loader: () => import('./index'), + }, +]; From 7c99ba3784b0627bc2e70e68ba0b5d8f7ef9d071 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Mon, 8 May 2023 16:16:07 +0200 Subject: [PATCH 13/20] fix tag delete, todo on repository, removed console logs --- .../tags-input/tags-input.element.ts | 28 +++++++++---------- .../src/backoffice/tags/index.ts | 1 - .../backoffice/tags/repository/tag.store.ts | 7 +++-- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/core/components/tags-input/tags-input.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/core/components/tags-input/tags-input.element.ts index 2ee6389f8a..edacc9e74d 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/core/components/tags-input/tags-input.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/core/components/tags-input/tags-input.element.ts @@ -19,7 +19,8 @@ export class UmbTagsInputElement extends FormControlMixin(UmbLitElement) { _items: string[] = []; @property({ type: Array }) public set items(newTags: string[]) { - this._items = newTags; + const newItems = newTags.filter((x) => x !== ''); + this._items = newItems; super.value = this._items.join(','); } public get items(): string[] { @@ -55,7 +56,7 @@ export class UmbTagsInputElement extends FormControlMixin(UmbLitElement) { } async #getExistingTags(query: string) { - if (!this.group || this.culture === undefined) return; + if (!this.group || this.culture === undefined || !query) return; const { data } = await this.#repository.queryTags(this.group, this.culture, query); if (!data) return; this._matches = data.items; @@ -83,8 +84,11 @@ export class UmbTagsInputElement extends FormControlMixin(UmbLitElement) { #onInput(e: UUIInputEvent) { this._currentInput = e.target.value as string; - if (!this._currentInput.length) this._matches = []; - else this.#getExistingTags(this._currentInput); + if (!this._currentInput || !this._currentInput.length) { + this._matches = []; + } else { + this.#getExistingTags(this._currentInput); + } } protected updated(): void { @@ -96,10 +100,7 @@ export class UmbTagsInputElement extends FormControlMixin(UmbLitElement) { else this.#createTag(); } - #createTag(text?: string) { - if (text) { - this._tagInput.value = text; - } + #createTag() { this.#inputError(false); const newTag = (this._tagInput.value as string).trim(); if (!newTag) return; @@ -125,11 +126,10 @@ export class UmbTagsInputElement extends FormControlMixin(UmbLitElement) { } #delete(tag: string) { - this.items.splice( - this.items.findIndex((x) => x === tag), - 1 - ); - this.items = [...this.items]; + const currentItems = [...this.items]; + const index = currentItems.findIndex((x) => x === tag); + currentItems.splice(index, 1); + currentItems.length ? (this.items = [...currentItems]) : (this.items = []); this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true })); } @@ -216,7 +216,7 @@ export class UmbTagsInputElement extends FormControlMixin(UmbLitElement) { return html`
${repeat( - matchfilter, + matchfilter.slice(0, 5), (tag: TagResponseModel) => tag.id, (tag: TagResponseModel, index: number) => { return html` { - console.log('tags registrer'); extensionRegistry.registerMany(manifests); }; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/tag.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/tag.store.ts index 5ddf7e15c9..7fc8a4aa97 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/tag.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/tag.store.ts @@ -21,7 +21,6 @@ export class UmbTagStore extends UmbStoreBase { */ constructor(host: UmbControllerHostElement) { super(host, UMB_TAG_STORE_CONTEXT_TOKEN.toString(), new UmbArrayState([], (x) => x.id)); - console.log('Store is open'); } /** @@ -48,7 +47,11 @@ export class UmbTagStore extends UmbStoreBase { ); } - //TODO Skriv god kommentar til filter/exclude + // TODO + // There isnt really any way to exclude certain tags when searching for suggestions. + // This is important for the skip/take in the endpoint. We do not want to get the tags from database that we already have picked. + // Forexample: we have 10 different tags that includes "berry" (and searched for "berry") and we have a skip of 0 and take of 5. + // If we already has picked lets say 4 of them, the list will only show 1 more, even though there is more remaining in the database. byQuery(group: TagResponseModel['group'], culture: string, query: string) { return this._data.getObservablePart((items) => From d0b285ba9a1f7e0d0974aeb79c112f83c37fa5b5 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Tue, 9 May 2023 10:44:43 +0200 Subject: [PATCH 14/20] culture todo --- .../src/backoffice/tags/repository/tag.repository.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/tag.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/tag.repository.ts index 37efa072a2..c244f5ce90 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/tag.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/tags/repository/tag.repository.ts @@ -42,10 +42,15 @@ export class UmbTagRepository { if (data) { // TODO: allow to append an array of items to the store + // TODO: append culture? "Invariant" if null. data.items.forEach((x) => this.#tagStore?.append(x)); } - return { data, error, asObservable: () => this.#tagStore!.byQuery(tagGroupName, requestCulture, query) }; + return { + data, + error, + asObservable: () => this.#tagStore!.byQuery(tagGroupName, requestCulture, query), + }; } async queryTags( From b43baf928712ea77307936cd622ccb2731e83188 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Tue, 9 May 2023 12:02:04 +0200 Subject: [PATCH 15/20] move tags --- .../src/backoffice/core/components/index.ts | 1 - .../core/property-editors/uis/manifests.ts | 2 -- .../src/backoffice/tags/components/index.ts | 1 + .../tags-input/tags-input.element.ts | 7 ++++++- .../tags-input/tags-input.stories.ts | 0 .../src/backoffice/tags/index.ts | 6 ++++-- .../tags/property-editors/Umbraco.Tags.ts | 19 +++++++++++++++++++ .../tags/property-editors/manifests.ts | 4 ++++ .../tags/config/storage-type/manifests.ts | 0 ...rty-editor-ui-tags-storage-type.element.ts | 0 ...rty-editor-ui-tags-storage-type.stories.ts | 0 ...operty-editor-ui-tags-storage-type.test.ts | 0 .../property-editors}/tags/manifests.ts | 0 .../tags/property-editor-ui-tags.element.ts | 5 +++-- .../tags/property-editor-ui-tags.stories.ts | 0 .../tags/property-editor-ui-tags.test.ts | 0 16 files changed, 37 insertions(+), 8 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/tags/components/index.ts rename src/Umbraco.Web.UI.Client/src/backoffice/{core => tags}/components/tags-input/tags-input.element.ts (98%) rename src/Umbraco.Web.UI.Client/src/backoffice/{core => tags}/components/tags-input/tags-input.stories.ts (100%) create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/tags/property-editors/Umbraco.Tags.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/tags/property-editors/manifests.ts rename src/Umbraco.Web.UI.Client/src/backoffice/{core/property-editors/uis => tags/property-editors}/tags/config/storage-type/manifests.ts (100%) rename src/Umbraco.Web.UI.Client/src/backoffice/{core/property-editors/uis => tags/property-editors}/tags/config/storage-type/property-editor-ui-tags-storage-type.element.ts (100%) rename src/Umbraco.Web.UI.Client/src/backoffice/{core/property-editors/uis => tags/property-editors}/tags/config/storage-type/property-editor-ui-tags-storage-type.stories.ts (100%) rename src/Umbraco.Web.UI.Client/src/backoffice/{core/property-editors/uis => tags/property-editors}/tags/config/storage-type/property-editor-ui-tags-storage-type.test.ts (100%) rename src/Umbraco.Web.UI.Client/src/backoffice/{core/property-editors/uis => tags/property-editors}/tags/manifests.ts (100%) rename src/Umbraco.Web.UI.Client/src/backoffice/{core/property-editors/uis => tags/property-editors}/tags/property-editor-ui-tags.element.ts (92%) rename src/Umbraco.Web.UI.Client/src/backoffice/{core/property-editors/uis => tags/property-editors}/tags/property-editor-ui-tags.stories.ts (100%) rename src/Umbraco.Web.UI.Client/src/backoffice/{core/property-editors/uis => tags/property-editors}/tags/property-editor-ui-tags.test.ts (100%) diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/core/components/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/core/components/index.ts index 594c53e7f4..a485d7735e 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/core/components/index.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/core/components/index.ts @@ -36,7 +36,6 @@ import './input-eye-dropper/input-eye-dropper.element'; import './input-multi-url-picker/input-multi-url-picker.element'; import './input-slider/input-slider.element'; import './input-toggle/input-toggle.element'; -import './tags-input/tags-input.element'; import './input-upload-field/input-upload-field.element'; import './property-type-based-property/property-type-based-property.element'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/core/property-editors/uis/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/core/property-editors/uis/manifests.ts index 8ee7084153..d0975419e2 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/core/property-editors/uis/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/core/property-editors/uis/manifests.ts @@ -10,7 +10,6 @@ import { manifest as multipleTextString } from './multiple-text-string/manifests import { manifest as textArea } from './textarea/manifests'; import { manifest as slider } from './slider/manifests'; import { manifest as toggle } from './toggle/manifests'; -import { manifests as tags } from './tags/manifests'; import { manifest as markdownEditor } from './markdown-editor/manifests'; import { manifest as radioButtonList } from './radio-button-list/manifests'; import { manifest as checkboxList } from './checkbox-list/manifests'; @@ -66,7 +65,6 @@ export const manifests: Array = [ ...blockGrid, ...collectionView, ...tinyMCE, - ...tags, { type: 'propertyEditorUI', alias: 'Umb.PropertyEditorUI.Number', diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/tags/components/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/tags/components/index.ts new file mode 100644 index 0000000000..a4fa49584b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/tags/components/index.ts @@ -0,0 +1 @@ +export * from './tags-input/tags-input.element'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/core/components/tags-input/tags-input.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/tags/components/tags-input/tags-input.element.ts similarity index 98% rename from src/Umbraco.Web.UI.Client/src/backoffice/core/components/tags-input/tags-input.element.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/tags/components/tags-input/tags-input.element.ts index edacc9e74d..0d0b0fd37c 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/core/components/tags-input/tags-input.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/tags/components/tags-input/tags-input.element.ts @@ -4,7 +4,7 @@ import { customElement, property, query, queryAll, state } from 'lit/decorators. import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins'; import { repeat } from 'lit/directives/repeat.js'; import { UUIInputElement, UUIInputEvent, UUITagElement } from '@umbraco-ui/uui'; -import { UmbTagRepository } from '../../../tags/repository/tag.repository'; +import { UmbTagRepository } from '../../repository/tag.repository'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; import { TagResponseModel } from '@umbraco-cms/backoffice/backend-api'; @@ -47,6 +47,11 @@ export class UmbTagsInputElement extends FormControlMixin(UmbLitElement) { #repository = new UmbTagRepository(this); + constructor() { + super(); + console.log('tags-input'); + } + public focus() { this._tagInput.focus(); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/core/components/tags-input/tags-input.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/tags/components/tags-input/tags-input.stories.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/backoffice/core/components/tags-input/tags-input.stories.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/tags/components/tags-input/tags-input.stories.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/tags/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/tags/index.ts index 98398a7edb..8b556a6664 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/tags/index.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/tags/index.ts @@ -1,8 +1,10 @@ import { manifests as repositoryManifests } from './repository/manifests'; - +import { manifests as propertyEditorManifests } from './property-editors/manifests'; import { UmbEntrypointOnInit } from '@umbraco-cms/backoffice/extensions-api'; -export const manifests = [...repositoryManifests]; +import './components'; + +export const manifests = [...repositoryManifests, ...propertyEditorManifests]; export const onInit: UmbEntrypointOnInit = (host, extensionRegistry) => { extensionRegistry.registerMany(manifests); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/tags/property-editors/Umbraco.Tags.ts b/src/Umbraco.Web.UI.Client/src/backoffice/tags/property-editors/Umbraco.Tags.ts new file mode 100644 index 0000000000..5385d679b3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/tags/property-editors/Umbraco.Tags.ts @@ -0,0 +1,19 @@ +import type { ManifestPropertyEditorModel } from '@umbraco-cms/backoffice/extensions-registry'; + +export const manifest: ManifestPropertyEditorModel = { + type: 'propertyEditorModel', + name: 'Tags', + alias: 'Umbraco.Tags', + meta: { + config: { + properties: [ + { + alias: 'startNodeId', + label: 'Start node', + description: '', + propertyEditorUI: 'Umb.PropertyEditorUI.Tags', + }, + ], + }, + }, +}; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/tags/property-editors/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/tags/property-editors/manifests.ts new file mode 100644 index 0000000000..1f987be412 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/tags/property-editors/manifests.ts @@ -0,0 +1,4 @@ +import { manifests as tagsUI } from './tags/manifests'; +import type { ManifestTypes } from '@umbraco-cms/backoffice/extensions-registry'; + +export const manifests: Array = [...tagsUI]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/core/property-editors/uis/tags/config/storage-type/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/tags/property-editors/tags/config/storage-type/manifests.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/backoffice/core/property-editors/uis/tags/config/storage-type/manifests.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/tags/property-editors/tags/config/storage-type/manifests.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/core/property-editors/uis/tags/config/storage-type/property-editor-ui-tags-storage-type.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/tags/property-editors/tags/config/storage-type/property-editor-ui-tags-storage-type.element.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/backoffice/core/property-editors/uis/tags/config/storage-type/property-editor-ui-tags-storage-type.element.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/tags/property-editors/tags/config/storage-type/property-editor-ui-tags-storage-type.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/core/property-editors/uis/tags/config/storage-type/property-editor-ui-tags-storage-type.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/tags/property-editors/tags/config/storage-type/property-editor-ui-tags-storage-type.stories.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/backoffice/core/property-editors/uis/tags/config/storage-type/property-editor-ui-tags-storage-type.stories.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/tags/property-editors/tags/config/storage-type/property-editor-ui-tags-storage-type.stories.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/core/property-editors/uis/tags/config/storage-type/property-editor-ui-tags-storage-type.test.ts b/src/Umbraco.Web.UI.Client/src/backoffice/tags/property-editors/tags/config/storage-type/property-editor-ui-tags-storage-type.test.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/backoffice/core/property-editors/uis/tags/config/storage-type/property-editor-ui-tags-storage-type.test.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/tags/property-editors/tags/config/storage-type/property-editor-ui-tags-storage-type.test.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/core/property-editors/uis/tags/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/tags/property-editors/tags/manifests.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/backoffice/core/property-editors/uis/tags/manifests.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/tags/property-editors/tags/manifests.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/core/property-editors/uis/tags/property-editor-ui-tags.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/tags/property-editors/tags/property-editor-ui-tags.element.ts similarity index 92% rename from src/Umbraco.Web.UI.Client/src/backoffice/core/property-editors/uis/tags/property-editor-ui-tags.element.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/tags/property-editors/tags/property-editor-ui-tags.element.ts index f557538fbb..87fe03e2e4 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/core/property-editors/uis/tags/property-editor-ui-tags.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/tags/property-editors/tags/property-editor-ui-tags.element.ts @@ -2,8 +2,8 @@ import { html } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, property, state } from 'lit/decorators.js'; import { ifDefined } from 'lit/directives/if-defined.js'; -import { UmbTagsInputElement } from '../../../components/tags-input/tags-input.element'; -import { UMB_WORKSPACE_PROPERTY_CONTEXT_TOKEN } from '../../../components/workspace-property/workspace-property.context'; +import { UmbTagsInputElement } from '../../components/tags-input/tags-input.element'; +import { UMB_WORKSPACE_PROPERTY_CONTEXT_TOKEN } from '../../../core/components/workspace-property/workspace-property.context'; import { UmbPropertyEditorExtensionElement } from '@umbraco-cms/backoffice/extensions-registry'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; import { DataTypePropertyPresentationModel } from '@umbraco-cms/backoffice/backend-api'; @@ -34,6 +34,7 @@ export class UmbPropertyEditorUITagsElement extends UmbLitElement implements Umb constructor() { super(); + console.log('ui tags'); this.consumeContext(UMB_WORKSPACE_PROPERTY_CONTEXT_TOKEN, (context) => { this.observe(context.variantId, (id) => { if (id && id.culture !== undefined) { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/core/property-editors/uis/tags/property-editor-ui-tags.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/tags/property-editors/tags/property-editor-ui-tags.stories.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/backoffice/core/property-editors/uis/tags/property-editor-ui-tags.stories.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/tags/property-editors/tags/property-editor-ui-tags.stories.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/core/property-editors/uis/tags/property-editor-ui-tags.test.ts b/src/Umbraco.Web.UI.Client/src/backoffice/tags/property-editors/tags/property-editor-ui-tags.test.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/backoffice/core/property-editors/uis/tags/property-editor-ui-tags.test.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/tags/property-editors/tags/property-editor-ui-tags.test.ts From 2843181cc793a62708151eca4838ed732a9f1a2f Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Wed, 10 May 2023 10:00:42 +0200 Subject: [PATCH 16/20] set backoffice path to slash --- src/Umbraco.Web.UI.Client/src/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/app.ts b/src/Umbraco.Web.UI.Client/src/app.ts index 67bd2eb16a..7c299b310f 100644 --- a/src/Umbraco.Web.UI.Client/src/app.ts +++ b/src/Umbraco.Web.UI.Client/src/app.ts @@ -40,7 +40,7 @@ export class UmbAppElement extends UmbLitElement { */ @property({ type: String }) // TODO: get from server config - private backofficePath = import.meta.env.DEV ? '' : '/umbraco'; + private backofficePath = import.meta.env.DEV ? '/' : '/umbraco'; private _routes: UmbRoute[] = [ { From 0535051d04ee5234feabea3b3b4ed9e0f8d413a4 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Wed, 10 May 2023 09:36:58 +0200 Subject: [PATCH 17/20] add docs for authentication --- .../docs/authentication.md | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/Umbraco.Web.UI.Client/docs/authentication.md diff --git a/src/Umbraco.Web.UI.Client/docs/authentication.md b/src/Umbraco.Web.UI.Client/docs/authentication.md new file mode 100644 index 0000000000..91f3b38e5e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/docs/authentication.md @@ -0,0 +1,65 @@ +# Authentication + +## What is this? + +You can now authorize against the Management API using OpenID Connect. Most endpoints will soon require a token, albeit they are open for now. + +## How does it work? + +You need to authorize against the Management API using OpenID Connect if you want to access protected endpoints running on a real Umbraco instance. This will give you a token that you can use to access the API. The token is stored in local storage and will be used for all subsequent requests. + +If you are running the backoffice locally, you can use the `VITE_UMBRACO_USE_MSW` environment variable to bypass the OpenID Connect flow and use mocked responses instead by setting it to `on` in the `.env.local` file. + +## How to use + +There are two ways to use this: + +### Running directly in the Umbraco-CMS repository + +1. Checkout the `v13/dev` branch of [Umbraco-CMS](https://github.com/umbraco/Umbraco-cms/tree/v13/dev) +2. Run `git submodule update --init` to initialize and pull down the backoffice repository + 1. If you are using a Git GUI client, you might need to do this manually +3. Go to src/Umbraco.Web.UI.New or switch default startup project to "Umbraco.Web.UI.New" +4. Start the backend server: `dotnet run` or run the project from your IDE +5. Access https://localhost:44339/umbraco and complete the installation of Umbraco +6. You should see the log in screen after installation +7. Log in using the credentials you provided during installation + +### Running with Vite + +1. Perform steps 1 to 5 from before +2. Open this file in an editor: `src/Umbraco.Web.UI.New/appsettings.Development.json` +3. Add this to the Umbraco.CMS section to override the backoffice host: + +```json +"Umbraco": { + "CMS": { + "NewBackOffice":{ + "BackOfficeHost": "http://localhost:5173" + }, + }, + [...] +} +``` + +4. Set Vite to use Umbraco API by copying the ".env" file to ".env.local" and setting the following: + +``` +VITE_UMBRACO_USE_MSW=off +VITE_UMBRACO_API_URL=https://localhost:44339 +``` + +5. Start the vite server: `npm run dev` in your backoffice folder +6. Check that you are sent to the login page +7. Log in + +## To test a secure endpoint + +If you want to mark an endpoint as secure, you can add the `[Authorize]` attribute to the controller or action. This will require you to be logged in to access the endpoint. + +## What does not work yet + +- You cannot log out through the UI + - Clear your local storage to log out for now +- If your session expires or your token is revoked, you will start getting 401 network errors, which for now only will be shown as a notification in the UI - we need to figure out how to send you back to log in +- We do not _yet_ poll to see if the token is still valid or check how long before you are logged out, so you won't be notified before trying to perfor actions that require a token From c8f53b63b86ee6e9f97f2af6c71b2a73ce94341c Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Wed, 10 May 2023 09:37:10 +0200 Subject: [PATCH 18/20] add a link from the readme to authentication.md --- src/Umbraco.Web.UI.Client/.github/README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/.github/README.md b/src/Umbraco.Web.UI.Client/.github/README.md index 5d50e21a29..52bcd87919 100644 --- a/src/Umbraco.Web.UI.Client/.github/README.md +++ b/src/Umbraco.Web.UI.Client/.github/README.md @@ -27,10 +27,15 @@ The development environment is the default environment and is used when running ### Run against a local Umbraco instance +> **Note** +> Make sure you have followed the [Authentication guide](../docs/authentication.md) before continuing. + +If you have a local Umbraco instance running, you can use the development environment to run against it by overriding the API URL and bypassing the mock-service-worker in the frontend client. + Create a `.env.local` file and set the following variables: ```bash -VITE_UMBRACO_API_URL=http://localhost:5000 # This will be the URL to your Umbraco instance +VITE_UMBRACO_API_URL=https://localhost:44339 # This will be the URL to your Umbraco instance VITE_UMBRACO_USE_MSW=off # Indicate that you want all API calls to bypass MSW (mock-service-worker) ``` From cb3c84e4c09e5e0191bfb5f8af49d0be1a9cf580 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Wed, 10 May 2023 10:05:20 +0200 Subject: [PATCH 19/20] add new appsetting to docs --- src/Umbraco.Web.UI.Client/docs/authentication.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/docs/authentication.md b/src/Umbraco.Web.UI.Client/docs/authentication.md index 91f3b38e5e..0ab31d4be6 100644 --- a/src/Umbraco.Web.UI.Client/docs/authentication.md +++ b/src/Umbraco.Web.UI.Client/docs/authentication.md @@ -35,7 +35,8 @@ There are two ways to use this: "Umbraco": { "CMS": { "NewBackOffice":{ - "BackOfficeHost": "http://localhost:5173" + "BackOfficeHost": "http://localhost:5173", + "AuthorizeCallbackPathName": "/" }, }, [...] From fe23177893467218dd42ffaa6b89d4b97560932c Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Wed, 10 May 2023 10:21:52 +0200 Subject: [PATCH 20/20] remove console.log --- .../property-editors/tags/property-editor-ui-tags.element.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/tags/property-editors/tags/property-editor-ui-tags.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/tags/property-editors/tags/property-editor-ui-tags.element.ts index 87fe03e2e4..e46ba4b81e 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/tags/property-editors/tags/property-editor-ui-tags.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/tags/property-editors/tags/property-editor-ui-tags.element.ts @@ -34,7 +34,6 @@ export class UmbPropertyEditorUITagsElement extends UmbLitElement implements Umb constructor() { super(); - console.log('ui tags'); this.consumeContext(UMB_WORKSPACE_PROPERTY_CONTEXT_TOKEN, (context) => { this.observe(context.variantId, (id) => { if (id && id.culture !== undefined) {