diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/import-dictionary-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/import-dictionary-modal.token.ts index aefea44ec1..32e628e5a9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/import-dictionary-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/import-dictionary-modal.token.ts @@ -1,3 +1,4 @@ +import { EntityTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; export interface UmbImportDictionaryModalData { @@ -5,7 +6,7 @@ export interface UmbImportDictionaryModalData { } export interface UmbImportDictionaryModalValue { - temporaryFileId: string; + entityItems: Array; parentId?: string; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/dictionary/components/dictionary-item-input/dictionary-item-input.context.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/dictionary/components/dictionary-item-input/dictionary-item-input.context.ts deleted file mode 100644 index 87b963ab7d..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/dictionary/dictionary/components/dictionary-item-input/dictionary-item-input.context.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input'; -import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; -import { UMB_DICTIONARY_ITEM_PICKER_MODAL } from '@umbraco-cms/backoffice/modal'; -import { DictionaryItemItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; - -export class UmbDictionaryItemPickerContext extends UmbPickerInputContext { - constructor(host: UmbControllerHostElement) { - super(host, 'Umb.Repository.Dictionary', UMB_DICTIONARY_ITEM_PICKER_MODAL); - } -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/dictionary/components/dictionary-item-input/dictionary-item-input.element.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/dictionary/components/dictionary-item-input/dictionary-item-input.element.ts deleted file mode 100644 index b0860583b5..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/dictionary/dictionary/components/dictionary-item-input/dictionary-item-input.element.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { UmbDictionaryItemPickerContext } from './dictionary-item-input.context.js'; -import { css, html, customElement, property, state, ifDefined, repeat } from '@umbraco-cms/backoffice/external/lit'; -import { FormControlMixin } from '@umbraco-cms/backoffice/external/uui'; -import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; -import type { DictionaryItemItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; - -@customElement('umb-dictionary-item-input') -export class UmbDictionaryItemInputElement extends FormControlMixin(UmbLitElement) { - /** - * This is a minimum amount of selected items in this input. - * @type {number} - * @attr - * @default 0 - */ - @property({ type: Number }) - public get min(): number { - return this.#pickerContext.min; - } - public set min(value: number) { - this.#pickerContext.min = value; - } - - /** - * Min validation message. - * @type {boolean} - * @attr - * @default - */ - @property({ type: String, attribute: 'min-message' }) - minMessage = 'This field need more items'; - - /** - * This is a maximum amount of selected items in this input. - * @type {number} - * @attr - * @default Infinity - */ - @property({ type: Number }) - public get max(): number { - return this.#pickerContext.max; - } - public set max(value: number) { - this.#pickerContext.max = value; - } - - /** - * Max validation message. - * @type {boolean} - * @attr - * @default - */ - @property({ type: String, attribute: 'min-message' }) - maxMessage = 'This field exceeds the allowed amount of items'; - - public get selectedIds(): Array { - return this.#pickerContext.getSelection(); - } - public set selectedIds(ids: Array) { - this.#pickerContext.setSelection(ids); - } - - @property() - public set value(idsString: string) { - // Its with full purpose we don't call super.value, as thats being handled by the observation of the context selection. - this.selectedIds = idsString.split(/[ ,]+/); - } - - @state() - private _items?: Array; - - #pickerContext = new UmbDictionaryItemPickerContext(this); - - constructor() { - super(); - - this.addValidator( - 'rangeUnderflow', - () => this.minMessage, - () => !!this.min && this.#pickerContext.getSelection().length < this.min, - ); - - this.addValidator( - 'rangeOverflow', - () => this.maxMessage, - () => !!this.max && this.#pickerContext.getSelection().length > this.max, - ); - - this.observe(this.#pickerContext.selection, (selection) => (super.value = selection.join(','))); - this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems)); - } - - protected getFormElement() { - return undefined; - } - - render() { - return html` - ${this._items - ? html` ${repeat( - this._items, - (item) => item.id, - (item) => this._renderItem(item), - )} - ` - : ''} - ${this.#renderAddButton()} - `; - } - - #renderAddButton() { - if (this.max > 0 && this.selectedIds.length >= this.max) return; - return html` this.#pickerContext.openPicker()} - label=${this.localize.term('general_add')}>`; - } - - private _renderItem(item: DictionaryItemItemResponseModel) { - if (!item.id) return; - return html` - - - - this.#pickerContext.requestRemoveItem(item.id!)} - label=${this.localize.term('actions_remove')}> - - - `; - } - - static styles = [ - css` - #add-button { - width: 100%; - } - `, - ]; -} - -export default UmbDictionaryItemInputElement; - -declare global { - interface HTMLElementTagNameMap { - 'umb-dictionary-item-input': UmbDictionaryItemInputElement; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/dictionary/components/index.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/dictionary/components/index.ts deleted file mode 100644 index 2cf0fea3fc..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/dictionary/dictionary/components/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './dictionary-item-input/dictionary-item-input.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/dictionary/entity-actions/import/import-dictionary-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/dictionary/entity-actions/import/import-dictionary-modal.element.ts index e054fc78de..b13c922855 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/dictionary/dictionary/entity-actions/import/import-dictionary-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/dictionary/entity-actions/import/import-dictionary-modal.element.ts @@ -1,5 +1,5 @@ import '../../components/dictionary-item-input/dictionary-item-input.element.js'; -import UmbDictionaryItemInputElement from '../../components/dictionary-item-input/dictionary-item-input.element.js'; +import { UMB_DICTIONARY_TREE_ALIAS } from '../../tree/manifests.js'; import { UmbDictionaryRepository } from '../../repository/dictionary.repository.js'; import { css, html, customElement, query, state, when } from '@umbraco-cms/backoffice/external/lit'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; @@ -8,11 +8,13 @@ import { UmbImportDictionaryModalValue, UmbModalBaseElement, } from '@umbraco-cms/backoffice/modal'; -import { ImportDictionaryRequestModel } from '@umbraco-cms/backoffice/backend-api'; import { UmbId } from '@umbraco-cms/backoffice/id'; +import { EntityTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; +import { UmbTreeElement } from '@umbraco-cms/backoffice/tree'; interface DictionaryItemPreview { name: string; + id: string; children: Array; } @@ -25,32 +27,64 @@ export class UmbImportDictionaryModalLayout extends UmbModalBaseElement< private _parentId?: string; @state() - private _temporaryFileId?: string; + private _temporaryFileId = ''; @query('#form') private _form!: HTMLFormElement; + @query('umb-tree') + private _treeElement?: UmbTreeElement; + #fileReader; + #fileNodes!: NodeListOf; + #fileContent: Array = []; + #dictionaryRepository: UmbDictionaryRepository; + #handleClose() { this.modalContext?.reject(); } - #submit() { - // TODO: Gotta do a temp file upload before submitting, so that the server can use it - console.log('submit:', this._temporaryFileId, this._parentId); - //this.modalContext?.submit({ temporaryFileId: this._temporaryFileId, parentId: this._parentId }); + #createTreeEntitiesFromTempFile(): Array { + const data: Array = []; + + const list = this.#dictionaryPreviewItemBuilder(this.#fileNodes); + const scaffold = (items: Array, parentId?: string) => { + items.forEach((item) => { + data.push({ + id: item.id, + name: item.name, + type: 'dictionary-item', + hasChildren: item.children.length ? true : false, + parentId: parentId, + }); + scaffold(item.children, item.id); + }); + }; + + scaffold(list, this._parentId); + return data; + } + + async #submit() { + const { error } = await this.#dictionaryRepository.import(this._temporaryFileId, this._parentId); + if (error) return; + + this.modalContext?.submit({ entityItems: this.#createTreeEntitiesFromTempFile(), parentId: this._parentId }); } constructor() { super(); + this.#dictionaryRepository = new UmbDictionaryRepository(this); + this.#fileReader = new FileReader(); + this.#fileReader.onload = (e) => { if (typeof e.target?.result === 'string') { const fileContent = e.target.result; - this.#dictionaryItemBuilder(fileContent); + this.#dictionaryPreviewBuilder(fileContent); } }; } @@ -60,16 +94,17 @@ export class UmbImportDictionaryModalLayout extends UmbModalBaseElement< this._parentId = this.data?.unique ?? undefined; } - #dictionaryItemBuilder(htmlString: string) { + #dictionaryPreviewBuilder(htmlString: string) { const parser = new DOMParser(); const doc = parser.parseFromString(htmlString, 'text/xml'); const elements = doc.childNodes; + this.#fileNodes = elements; - this.#fileContent = this.#makeDictionaryItems(elements); + this.#fileContent = this.#dictionaryPreviewItemBuilder(elements); this.requestUpdate(); } - #makeDictionaryItems(nodeList: NodeListOf): Array { + #dictionaryPreviewItemBuilder(nodeList: NodeListOf): Array { const items: Array = []; const list: Array = []; nodeList.forEach((node) => { @@ -81,25 +116,29 @@ export class UmbImportDictionaryModalLayout extends UmbModalBaseElement< list.forEach((item) => { items.push({ name: item.getAttribute('Name') ?? '', - children: this.#makeDictionaryItems(item.childNodes) ?? undefined, + id: item.getAttribute('Key') ?? '', + children: this.#dictionaryPreviewItemBuilder(item.childNodes) ?? undefined, }); }); return items; } - #onUpload(e: Event) { + async #onUpload(e: Event) { e.preventDefault(); const formData = new FormData(this._form); - const file = formData.get('file') as Blob; + const file = formData.get('file') as File; + + if (!file) throw new Error('File is missing'); this.#fileReader.readAsText(file); - this._temporaryFileId = file ? UmbId.new() : undefined; + this._temporaryFileId = UmbId.new(); + + this.#dictionaryRepository.upload(this._temporaryFileId, file); } - #onParentChange(event: CustomEvent) { - this._parentId = (event.target as UmbDictionaryItemInputElement).selectedIds[0] || undefined; - //console.log((event.target as UmbDictionaryItemInputElement).selectedIds[0] || undefined); + #onParentChange() { + this._parentId = this._treeElement?.selection[0] ?? undefined; } async #onFileInput() { @@ -145,16 +184,15 @@ export class UmbImportDictionaryModalLayout extends UmbModalBaseElement<
Choose where to import: - Work in progress
- ${ - this._parentId - // TODO - // - // - } +
parentId: + +
${this.#renderNavigate()} diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/dictionary/entity-actions/import/import.action.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/dictionary/entity-actions/import/import.action.ts index 264f28715d..634b0e5c91 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/dictionary/dictionary/entity-actions/import/import.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/dictionary/entity-actions/import/import.action.ts @@ -7,11 +7,13 @@ import { UMB_MODAL_MANAGER_CONTEXT_TOKEN, UMB_IMPORT_DICTIONARY_MODAL, } from '@umbraco-cms/backoffice/modal'; +import { UMB_DICTIONARY_TREE_STORE_CONTEXT, UmbDictionaryTreeStore } from '@umbraco-cms/backoffice/dictionary'; export default class UmbImportDictionaryEntityAction extends UmbEntityActionBase { static styles = [UmbTextStyles]; #modalContext?: UmbModalManagerContext; + #treeStore?: UmbDictionaryTreeStore; constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) { super(host, repositoryAlias, unique); @@ -19,6 +21,9 @@ export default class UmbImportDictionaryEntityAction extends UmbEntityActionBase this.consumeContext(UMB_MODAL_MANAGER_CONTEXT_TOKEN, (instance) => { this.#modalContext = instance; }); + this.consumeContext(UMB_DICTIONARY_TREE_STORE_CONTEXT, (instance) => { + this.#treeStore = instance; + }); } async execute() { @@ -26,8 +31,12 @@ export default class UmbImportDictionaryEntityAction extends UmbEntityActionBase const modalContext = this.#modalContext?.open(UMB_IMPORT_DICTIONARY_MODAL, { unique: this.unique }); - const { parentId, temporaryFileId } = await modalContext.onSubmit(); + const { entityItems, parentId } = await modalContext.onSubmit(); - await this.repository?.import(temporaryFileId, parentId); + if (!entityItems?.length) return; + + this.#treeStore?.appendItems(entityItems); + + if (parentId) this.#treeStore?.updateItem(parentId, { hasChildren: true }); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/dictionary/index.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/dictionary/index.ts index a24a0f6363..5c342dd939 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/dictionary/dictionary/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/dictionary/index.ts @@ -1,3 +1,2 @@ export * from './repository/index.js'; export * from './tree/index.js'; -export * from './components/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/dictionary/repository/dictionary.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/dictionary/repository/dictionary.repository.ts index 0601c1ffe5..cb6f0171d4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/dictionary/dictionary/repository/dictionary.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/dictionary/repository/dictionary.repository.ts @@ -6,11 +6,11 @@ import { UmbDetailRepository } from '@umbraco-cms/backoffice/repository'; import { CreateDictionaryItemRequestModel, DictionaryOverviewResponseModel, - ImportDictionaryRequestModel, UpdateDictionaryItemRequestModel, } from '@umbraco-cms/backoffice/backend-api'; import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/notification'; import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; +import { UmbTemporaryFileRepository } from '@umbraco-cms/backoffice/temporary-file'; export class UmbDictionaryRepository extends UmbBaseController @@ -30,6 +30,8 @@ export class UmbDictionaryRepository #detailSource: UmbDictionaryDetailServerDataSource; #detailStore?: UmbDictionaryStore; + #temporaryFileRepository: UmbTemporaryFileRepository; + #notificationContext?: UmbNotificationContext; constructor(host: UmbControllerHostElement) { @@ -37,6 +39,7 @@ export class UmbDictionaryRepository // TODO: figure out how spin up get the correct data source this.#detailSource = new UmbDictionaryDetailServerDataSource(this); + this.#temporaryFileRepository = new UmbTemporaryFileRepository(host); this.#init = Promise.all([ this.consumeContext(UMB_DICTIONARY_STORE_CONTEXT_TOKEN, (instance) => { @@ -92,6 +95,7 @@ export class UmbDictionaryRepository async delete(id: string) { await this.#init; + await this.#treeStore?.removeItem(id); return this.#detailSource.delete(id); } @@ -154,14 +158,12 @@ export class UmbDictionaryRepository return this.#detailSource.import(temporaryFileId, parentId); } - async upload(formData: ImportDictionaryRequestModel) { + async upload(UmbId: string, file: File) { await this.#init; + if (!UmbId) throw new Error('UmbId is missing'); + if (!file) throw new Error('File is missing'); - if (!formData) { - throw new Error('Form data is missing'); - } - - return this.#detailSource.upload(formData); + return this.#temporaryFileRepository.upload(UmbId, file); } // TODO => temporary only, until languages data source exists, or might be