= [];
+
+ #detailRepo = new UmbDictionaryRepository(this);
+
+ async #importDictionary() {
+ if (!this._uploadedDictionary?.fileName) return;
+
+ this.modalHandler?.close({
+ fileName: this._uploadedDictionary.fileName,
+ parentKey: this._selection[0],
+ });
+ }
+
+ #handleClose() {
+ this.modalHandler?.close({});
+ }
+
+ #submitForm() {
+ this._form?.requestSubmit();
+ }
+
+ async #handleSubmit(e: SubmitEvent) {
+ e.preventDefault();
+
+ if (!this._form.checkValidity()) return;
+
+ const formData = new FormData(this._form);
+ const { data } = await this.#detailRepo.upload(formData);
+
+ this._uploadedDictionary = data;
+
+ if (!this._uploadedDictionary) {
+ this._showErrorView = true;
+ this._showImportView = false;
+ return;
+ }
+
+ this._showErrorView = false;
+ this._showUploadView = false;
+ this._showImportView = true;
+ }
+
+ #handleSelectionChange(e: CustomEvent) {
+ e.stopPropagation();
+ const element = e.target as UmbTreeElement;
+ this._selection = element.selection;
+ }
+
+ #renderUploadView() {
+ return html`
+ To import a dictionary item, find the ".udt" file on your computer by clicking the "Import" button (you'll be
+ asked for confirmation on the next screen)
+
+
+
+
+
+ `;
+ }
+
+ /// TODO => Tree view needs isolation and single-select option
+ #renderImportView() {
+ if (!this._uploadedDictionary?.dictionaryItems) return;
+
+ return html`
+ Dictionary items
+
+ ${repeat(
+ this._uploadedDictionary.dictionaryItems,
+ (item) => item.name,
+ (item) => html`- ${item.name}
`
+ )}
+
+
+ Choose where to import dictionary items (optional)
+
+
+
+
+ `;
+ }
+
+ // TODO => Determine what to display when dictionary import/upload fails
+ #renderErrorView() {
+ return html`Something went wrong`;
+ }
+
+ render() {
+ return html`
+ ${when(this._showUploadView, () => this.#renderUploadView())}
+ ${when(this._showImportView, () => this.#renderImportView())}
+ ${when(this._showErrorView, () => this.#renderErrorView())}
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'umb-import-dictionary-modal-layout': UmbImportDictionaryModalLayoutElement;
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/import/import.action.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/import/import.action.ts
new file mode 100644
index 0000000000..b091cc0e36
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/import/import.action.ts
@@ -0,0 +1,42 @@
+import { UUITextStyles } from '@umbraco-ui/uui-css';
+import { UmbEntityActionBase } from '../../../../shared/entity-actions';
+import { UmbDictionaryRepository } from '../../repository/dictionary.repository';
+import type { UmbImportDictionaryModalResultData } from './import-dictionary-modal-layout.element';
+import { UmbControllerHostInterface } from '@umbraco-cms/controller';
+import { UmbContextConsumerController } from '@umbraco-cms/context-api';
+import { UmbModalService, UMB_MODAL_SERVICE_CONTEXT_TOKEN } from '@umbraco-cms/modal';
+
+import './import-dictionary-modal-layout.element';
+
+export default class UmbImportDictionaryEntityAction extends UmbEntityActionBase {
+ static styles = [UUITextStyles];
+
+ #modalService?: UmbModalService;
+
+ constructor(host: UmbControllerHostInterface, repositoryAlias: string, unique: string) {
+ super(host, repositoryAlias, unique);
+
+ new UmbContextConsumerController(this.host, UMB_MODAL_SERVICE_CONTEXT_TOKEN, (instance) => {
+ this.#modalService = instance;
+ });
+ }
+
+ async execute() {
+ // TODO: what to do if modal service is not available?
+ if (!this.#modalService) return;
+
+ const modalHandler = this.#modalService?.open('umb-import-dictionary-modal-layout', {
+ type: 'sidebar',
+ data: { unique: this.unique },
+ });
+
+ // TODO: get type from modal result
+ const { fileName, parentKey }: UmbImportDictionaryModalResultData = await modalHandler.onClose();
+ if (!fileName) return;
+
+ const result = await this.repository?.import(fileName, parentKey);
+
+ // TODO => get location header to route to new item
+ console.log(result);
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/manifests.ts
new file mode 100644
index 0000000000..3449a3787a
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/manifests.ts
@@ -0,0 +1,93 @@
+import { UmbDeleteEntityAction } from '../../../../backoffice/shared/entity-actions/delete/delete.action';
+import { UmbMoveEntityAction } from '../../../../backoffice/shared/entity-actions/move/move.action';
+import UmbReloadDictionaryEntityAction from './reload.action';
+import UmbImportDictionaryEntityAction from './import/import.action';
+import UmbExportDictionaryEntityAction from './export/export.action';
+import UmbCreateDictionaryEntityAction from './create/create.action';
+import type { ManifestEntityAction } from '@umbraco-cms/models';
+
+const entityType = 'dictionary-item';
+const repositoryAlias = 'Umb.Repository.Dictionary';
+
+const entityActions: Array = [
+ {
+ type: 'entityAction',
+ alias: 'Umb.EntityAction.Dictionary.Create',
+ name: 'Create Dictionary Entity Action',
+ weight: 600,
+ meta: {
+ entityType,
+ icon: 'umb:add',
+ label: 'Create',
+ repositoryAlias,
+ api: UmbCreateDictionaryEntityAction,
+ },
+ },
+ {
+ type: 'entityAction',
+ alias: 'Umb.EntityAction.Dictionary.Move',
+ name: 'Move Dictionary Entity Action',
+ weight: 500,
+ meta: {
+ entityType,
+ icon: 'umb:enter',
+ label: 'Move',
+ repositoryAlias,
+ api: UmbMoveEntityAction,
+ },
+ },
+ {
+ type: 'entityAction',
+ alias: 'Umb.EntityAction.Dictionary.Export',
+ name: 'Export Dictionary Entity Action',
+ weight: 400,
+ meta: {
+ entityType,
+ icon: 'umb:download-alt',
+ label: 'Export',
+ repositoryAlias,
+ api: UmbExportDictionaryEntityAction,
+ },
+ },
+ {
+ type: 'entityAction',
+ alias: 'Umb.EntityAction.Dictionary.Import',
+ name: 'Import Dictionary Entity Action',
+ weight: 300,
+ meta: {
+ entityType,
+ icon: 'umb:page-up',
+ label: 'Import',
+ repositoryAlias,
+ api: UmbImportDictionaryEntityAction,
+ },
+ },
+ {
+ type: 'entityAction',
+ alias: 'Umb.EntityAction.Dictionary.Reload',
+ name: 'Reload Dictionary Entity Action',
+ weight: 200,
+ meta: {
+ entityType,
+ icon: 'umb:refresh',
+ label: 'Reload',
+ repositoryAlias,
+ api: UmbReloadDictionaryEntityAction,
+ },
+ },
+ {
+ type: 'entityAction',
+ alias: 'Umb.EntityAction.Dictionary.Delete',
+ name: 'Delete Dictionary Entity Action',
+ weight: 100,
+ meta: {
+ entityType,
+ icon: 'umb:trash',
+ label: 'Delete',
+ repositoryAlias,
+ api: UmbDeleteEntityAction,
+ },
+ },
+];
+
+export const manifests = [...entityActions];
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/reload.action.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/reload.action.ts
new file mode 100644
index 0000000000..19f607129d
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/reload.action.ts
@@ -0,0 +1,16 @@
+import { UUITextStyles } from '@umbraco-ui/uui-css';
+import { UmbEntityActionBase } from '../../../shared/entity-actions';
+import { UmbDictionaryRepository } from '../repository/dictionary.repository';
+import { UmbControllerHostInterface } from '@umbraco-cms/controller';
+
+export default class UmbReloadDictionaryEntityAction extends UmbEntityActionBase {
+ static styles = [UUITextStyles];
+
+ constructor(host: UmbControllerHostInterface, repositoryAlias: string, unique: string) {
+ super(host, repositoryAlias, unique);
+ }
+
+ async execute() {
+ alert('refresh')
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/manifests.ts
index a4edc8b4f1..6c4d56a671 100644
--- a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/manifests.ts
+++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/manifests.ts
@@ -1,5 +1,13 @@
import { manifests as sidebarMenuItemManifests } from './sidebar-menu-item/manifests';
import { manifests as treeManifests } from './tree/manifests';
+import { manifests as repositoryManifests } from './repository/manifests';
import { manifests as workspaceManifests } from './workspace/manifests';
+import { manifests as entityActionManifests } from './entity-actions/manifests';
-export const manifests = [...sidebarMenuItemManifests, ...treeManifests, ...workspaceManifests];
+export const manifests = [
+ ...sidebarMenuItemManifests,
+ ...treeManifests,
+ ...repositoryManifests,
+ ...workspaceManifests,
+ ...entityActionManifests,
+];
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.detail.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.detail.store.ts
new file mode 100644
index 0000000000..4b1ab81c72
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.detail.store.ts
@@ -0,0 +1,33 @@
+import { UmbContextToken } from '@umbraco-cms/context-api';
+import { UmbStoreBase } from '@umbraco-cms/store';
+import { UmbControllerHostInterface } from '@umbraco-cms/controller';
+import { ArrayState } from '@umbraco-cms/observable-api';
+import type { DictionaryDetails } from '@umbraco-cms/models';
+
+/**
+ * @export
+ * @class UmbDictionaryDetailStore
+ * @extends {UmbStoreBase}
+ * @description - Details Data Store for Data Types
+ */
+export class UmbDictionaryDetailStore
+ extends UmbStoreBase
+{
+ #data = new ArrayState([], (x) => x.key);
+
+ constructor(host: UmbControllerHostInterface) {
+ super(host, UmbDictionaryDetailStore.name);
+ }
+
+ append(dictionary: DictionaryDetails) {
+ this.#data.append([dictionary]);
+ }
+
+ remove(uniques: string[]) {
+ this.#data.remove(uniques);
+ }
+}
+
+export const UMB_DICTIONARY_DETAIL_STORE_CONTEXT_TOKEN = new UmbContextToken(
+ UmbDictionaryDetailStore.name
+);
\ No newline at end of file
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.repository.ts
new file mode 100644
index 0000000000..6315120503
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.repository.ts
@@ -0,0 +1,247 @@
+import { DictionaryTreeServerDataSource } from './sources/dictionary.tree.server.data';
+import { UmbDictionaryTreeStore, UMB_DICTIONARY_TREE_STORE_CONTEXT_TOKEN } from './dictionary.tree.store';
+import { UmbDictionaryDetailStore, UMB_DICTIONARY_DETAIL_STORE_CONTEXT_TOKEN } from './dictionary.detail.store';
+import { UmbDictionaryDetailServerDataSource } from './sources/dictionary.detail.server.data';
+import { UmbControllerHostInterface } from '@umbraco-cms/controller';
+import { UmbContextConsumerController } from '@umbraco-cms/context-api';
+import { RepositoryTreeDataSource, UmbTreeRepository } from '@umbraco-cms/repository';
+import { ProblemDetailsModel } from '@umbraco-cms/backend-api';
+import { UmbNotificationService, UMB_NOTIFICATION_SERVICE_CONTEXT_TOKEN } from '@umbraco-cms/notification';
+import type { DictionaryDetails } from '@umbraco-cms/models';
+
+export class UmbDictionaryRepository implements UmbTreeRepository {
+ #init!: Promise;
+
+ #host: UmbControllerHostInterface;
+
+ #treeSource: RepositoryTreeDataSource;
+ #treeStore?: UmbDictionaryTreeStore;
+
+ #detailSource: UmbDictionaryDetailServerDataSource;
+ #detailStore?: UmbDictionaryDetailStore;
+
+ #notificationService?: UmbNotificationService;
+
+ constructor(host: UmbControllerHostInterface) {
+ this.#host = host;
+
+ // TODO: figure out how spin up get the correct data source
+ this.#treeSource = new DictionaryTreeServerDataSource(this.#host);
+ this.#detailSource = new UmbDictionaryDetailServerDataSource(this.#host);
+
+ this.#init = Promise.all([
+ new UmbContextConsumerController(this.#host, UMB_DICTIONARY_DETAIL_STORE_CONTEXT_TOKEN, (instance) => {
+ this.#detailStore = instance;
+ }),
+
+ new UmbContextConsumerController(this.#host, UMB_DICTIONARY_TREE_STORE_CONTEXT_TOKEN, (instance) => {
+ this.#treeStore = instance;
+ }),
+
+ new UmbContextConsumerController(this.#host, UMB_NOTIFICATION_SERVICE_CONTEXT_TOKEN, (instance) => {
+ this.#notificationService = instance;
+ }),
+ ]);
+ }
+
+ async requestRootTreeItems() {
+ await this.#init;
+
+ const { data, error } = await this.#treeSource.getRootItems();
+
+ if (data) {
+ this.#treeStore?.appendItems(data.items);
+ }
+
+ return { data, error, asObservable: () => this.#treeStore!.rootItems };
+ }
+
+ async requestTreeItemsOf(parentKey: string | null) {
+ await this.#init;
+
+ if (!parentKey) {
+ const error: ProblemDetailsModel = { title: 'Parent key is missing' };
+ return { data: undefined, error };
+ }
+
+ const { data, error } = await this.#treeSource.getChildrenOf(parentKey);
+
+ if (data) {
+ this.#treeStore?.appendItems(data.items);
+ }
+
+ return { data, error, asObservable: () => this.#treeStore!.childrenOf(parentKey) };
+ }
+
+ async requestTreeItems(keys: Array) {
+ await this.#init;
+
+ if (!keys) {
+ const error: ProblemDetailsModel = { title: 'Keys are missing' };
+ return { data: undefined, error };
+ }
+
+ const { data, error } = await this.#treeSource.getItems(keys);
+
+ return { data, error, asObservable: () => this.#treeStore!.items(keys) };
+ }
+
+ async rootTreeItems() {
+ await this.#init;
+ return this.#treeStore!.rootItems;
+ }
+
+ async treeItemsOf(parentKey: string | null) {
+ await this.#init;
+ return this.#treeStore!.childrenOf(parentKey);
+ }
+
+ async treeItems(keys: Array) {
+ await this.#init;
+ return this.#treeStore!.items(keys);
+ }
+
+ // DETAILS
+
+ async createDetailsScaffold(parentKey: string | null) {
+ await this.#init;
+
+ if (!parentKey) {
+ const error: ProblemDetailsModel = { title: 'Parent key is missing' };
+ return { data: undefined, error };
+ }
+
+ return this.#detailSource.createScaffold(parentKey);
+ }
+
+ async requestDetails(key: string) {
+ await this.#init;
+
+ // TODO: should we show a notification if the key is missing?
+ // Investigate what is best for Acceptance testing, cause in that perspective a thrown error might be the best choice?
+ if (!key) {
+ const error: ProblemDetailsModel = { title: 'Key is missing' };
+ return { error };
+ }
+ const { data, error } = await this.#detailSource.get(key);
+
+ if (data) {
+ this.#detailStore?.append(data);
+ }
+ return { data, error };
+ }
+
+ async list(skip = 0, take = 1000) {
+ await this.#init;
+ return this.#detailSource.list(skip, take);
+ }
+
+ async delete(key: string) {
+ await this.#init;
+ return this.#detailSource.delete(key);
+ }
+
+ async saveDetail(dictionary: DictionaryDetails) {
+ await this.#init;
+
+ // TODO: should we show a notification if the dictionary is missing?
+ // Investigate what is best for Acceptance testing, cause in that perspective a thrown error might be the best choice?
+ if (!dictionary || !dictionary.key) {
+ const error: ProblemDetailsModel = { title: 'Dictionary is missing' };
+ return { error };
+ }
+
+ const { error } = await this.#detailSource.update(dictionary);
+
+ if (!error) {
+ const notification = { data: { message: `Dictionary '${dictionary.name}' saved` } };
+ this.#notificationService?.peek('positive', notification);
+ }
+
+ // TODO: we currently don't use the detail store for anything.
+ // Consider to look up the data before fetching from the server
+ // Consider notify a workspace if a dictionary is updated in the store while someone is editing it.
+ this.#detailStore?.append(dictionary);
+ this.#treeStore?.updateItem(dictionary.key, { name: dictionary.name });
+ // TODO: would be nice to align the stores on methods/methodNames.
+
+ return { error };
+ }
+
+ async createDetail(detail: DictionaryDetails) {
+ await this.#init;
+
+ if (!detail.name) {
+ const error: ProblemDetailsModel = { title: 'Name is missing' };
+ return { error };
+ }
+
+ const { data, error } = await this.#detailSource.insert(detail);
+
+ if (!error) {
+ const notification = { data: { message: `Dictionary '${detail.name}' created` } };
+ this.#notificationService?.peek('positive', notification);
+ }
+
+ return { data, error };
+ }
+
+ async export(key: string, includeChildren = false) {
+ await this.#init;
+
+ if (!key) {
+ const error: ProblemDetailsModel = { title: 'Key is missing' };
+ return { error };
+ }
+
+ return this.#detailSource.export(key, includeChildren);
+ }
+
+ async import(fileName: string, parentKey?: string) {
+ await this.#init;
+
+ if (!fileName) {
+ const error: ProblemDetailsModel = { title: 'File is missing' };
+ return { error };
+ }
+
+ return this.#detailSource.import(fileName, parentKey);
+ }
+
+ async upload(formData: FormData) {
+ await this.#init;
+
+ if (!formData) {
+ const error: ProblemDetailsModel = { title: 'Form data is missing' };
+ return { error };
+ }
+
+ return this.#detailSource.upload(formData);
+ }
+
+ // TODO => temporary only, until languages data source exists, or might be
+ // ok to keep, as it reduces downstream dependencies
+ async getLanguages() {
+ await this.#init;
+
+ const { data } = await this.#detailSource.getLanguages();
+
+ // default first, then sorted by name
+ // easier to unshift than conditionally sorting by bool and string
+ const languages =
+ data?.items.sort((a, b) => {
+ a.name = a.name ?? '';
+ b.name = b.name ?? '';
+ return a.name > b.name ? 1 : b.name > a.name ? -1 : 0;
+ }) ?? [];
+
+ const defaultIndex = languages.findIndex((x) => x.isDefault);
+ languages.unshift(...languages.splice(defaultIndex, 1));
+
+ return languages;
+ }
+
+ async move() {
+ alert('move me!');
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.tree.store.ts
new file mode 100644
index 0000000000..e0df7e7b23
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.tree.store.ts
@@ -0,0 +1,25 @@
+import { UmbContextToken } from '@umbraco-cms/context-api';
+import { UmbTreeStoreBase } from '@umbraco-cms/store';
+import { UmbControllerHostInterface } from '@umbraco-cms/controller';
+
+/**
+ * @export
+ * @class UmbDictionaryTreeStore
+ * @extends {UmbTreeStoreBase}
+ * @description - Tree Data Store for Data Types
+ */
+export class UmbDictionaryTreeStore extends UmbTreeStoreBase {
+
+ /**
+ * Creates an instance of UmbDictionaryTreeStore.
+ * @param {UmbControllerHostInterface} host
+ * @memberof UmbDictionaryTreeStore
+ */
+ constructor(host: UmbControllerHostInterface) {
+ super(host, UMB_DICTIONARY_TREE_STORE_CONTEXT_TOKEN.toString());
+ }
+}
+
+export const UMB_DICTIONARY_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken(
+ UmbDictionaryTreeStore.name
+);
\ No newline at end of file
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/manifests.ts
new file mode 100644
index 0000000000..3e900b29a5
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/manifests.ts
@@ -0,0 +1,13 @@
+import { UmbDictionaryRepository } from '../repository/dictionary.repository';
+import { ManifestRepository } from 'libs/extensions-registry/repository.models';
+
+export const DICTIONARY_REPOSITORY_ALIAS = 'Umb.Repository.Dictionary';
+
+const repository: ManifestRepository = {
+ type: 'repository',
+ alias: DICTIONARY_REPOSITORY_ALIAS,
+ name: 'Dictionary Repository',
+ class: UmbDictionaryRepository,
+};
+
+export const manifests = [repository];
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/sources/dictionary.detail.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/sources/dictionary.detail.server.data.ts
new file mode 100644
index 0000000000..ba30ab03fb
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/sources/dictionary.detail.server.data.ts
@@ -0,0 +1,153 @@
+import { DictionaryDetailDataSource } from './dictionary.details.server.data.interface';
+import { UmbControllerHostInterface } from '@umbraco-cms/controller';
+import { tryExecuteAndNotify } from '@umbraco-cms/resources';
+import {
+ DictionaryItemCreateModel,
+ DictionaryResource,
+ LanguageResource,
+ ProblemDetailsModel,
+} from '@umbraco-cms/backend-api';
+import type { DictionaryDetails } from '@umbraco-cms/models';
+
+/**
+ * @description - A data source for the Dictionary detail that fetches data from the server
+ * @export
+ * @class UmbDictionaryDetailServerDataSource
+ * @implements {DictionaryDetailDataSource}
+ */
+export class UmbDictionaryDetailServerDataSource implements DictionaryDetailDataSource {
+ #host: UmbControllerHostInterface;
+
+ constructor(host: UmbControllerHostInterface) {
+ this.#host = host;
+ }
+
+ /**
+ * @description - Creates a new Dictionary scaffold
+ * @param {string} parentKey
+ * @return {*}
+ * @memberof UmbDictionaryDetailServerDataSource
+ */
+ async createScaffold(parentKey: string) {
+ const data: DictionaryDetails = {
+ name: '',
+ parentKey,
+ } as DictionaryDetails;
+
+ return { data };
+ }
+
+ /**
+ * @description - Fetches a Dictionary with the given key from the server
+ * @param {string} key
+ * @return {*}
+ * @memberof UmbDictionaryDetailServerDataSource
+ */
+ get(key: string) {
+ return tryExecuteAndNotify(this.#host, DictionaryResource.getDictionaryByKey({ key })) as any;
+ }
+
+ /**
+ * @description - Get the dictionary overview
+ * @param {number?} skip
+ * @param {number?} take
+ * @returns {*}
+ */
+ list(skip = 0, take = 1000) {
+ return tryExecuteAndNotify(this.#host, DictionaryResource.getDictionary({ skip, take }));
+ }
+
+ /**
+ * @description - Updates a Dictionary on the server
+ * @param {DictionaryDetails} dictionary
+ * @return {*}
+ * @memberof UmbDictionaryDetailServerDataSource
+ */
+ async update(dictionary: DictionaryDetails) {
+ if (!dictionary.key) {
+ const error: ProblemDetailsModel = { title: 'Dictionary key is missing' };
+ return { error };
+ }
+
+ const payload = { key: dictionary.key, requestBody: dictionary };
+ return tryExecuteAndNotify(this.#host, DictionaryResource.putDictionaryByKey(payload));
+ }
+
+ /**
+ * @description - Inserts a new Dictionary on the server
+ * @param {DictionaryDetails} data
+ * @return {*}
+ * @memberof UmbDictionaryDetailServerDataSource
+ */
+ async insert(data: DictionaryDetails) {
+ const requestBody: DictionaryItemCreateModel = {
+ parentKey: data.parentKey,
+ name: data.name,
+ };
+
+ return tryExecuteAndNotify(this.#host, DictionaryResource.postDictionary({ requestBody }));
+ }
+
+ /**
+ * @description - Deletes a Dictionary on the server
+ * @param {string} key
+ * @return {*}
+ * @memberof UmbDictionaryDetailServerDataSource
+ */
+ async delete(key: string) {
+ if (!key) {
+ const error: ProblemDetailsModel = { title: 'Key is missing' };
+ return { error };
+ }
+
+ return await tryExecuteAndNotify(this.#host, DictionaryResource.deleteDictionaryByKey({ key }));
+ }
+
+ /**
+ * @description - Import a dictionary
+ * @param {string} fileName
+ * @param {string?} parentKey
+ * @returns {*}
+ * @memberof UmbDictionaryDetailServerDataSource
+ */
+ async import(fileName: string, parentKey?: string) {
+ // TODO => parentKey will be a guid param once #13786 is merged and API regenerated
+ return await tryExecuteAndNotify(
+ this.#host,
+ DictionaryResource.postDictionaryImport({ requestBody: { fileName, parentKey } })
+ );
+ }
+
+ /**
+ * @description - Upload a Dictionary
+ * @param {FormData} formData
+ * @return {*}
+ * @memberof UmbDictionaryDetailServerDataSource
+ */
+ async upload(formData: FormData) {
+ return await tryExecuteAndNotify(
+ this.#host,
+ DictionaryResource.postDictionaryUpload({
+ requestBody: formData,
+ })
+ );
+ }
+
+ /**
+ * @description - Export a Dictionary, optionally including child items.
+ * @param {string} key
+ * @param {boolean} includeChildren
+ * @return {*}
+ * @memberof UmbDictionaryDetailServerDataSource
+ */
+ async export(key: string, includeChildren: boolean) {
+ return await tryExecuteAndNotify(this.#host, DictionaryResource.getDictionaryByKeyExport({ key, includeChildren }));
+ }
+
+ async getLanguages() {
+ // TODO => temp until language service exists. Need languages as the dictionary response
+ // includes the translated iso codes only, no friendly names and no way to tell if a dictionary
+ // is missing a translation
+ return await tryExecuteAndNotify(this.#host, LanguageResource.getLanguage({ skip: 0, take: 1000 }));
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/sources/dictionary.details.server.data.interface.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/sources/dictionary.details.server.data.interface.ts
new file mode 100644
index 0000000000..238702d0bc
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/sources/dictionary.details.server.data.interface.ts
@@ -0,0 +1,21 @@
+import {
+ DictionaryItemModel,
+ DictionaryUploadModel,
+ PagedDictionaryOverviewModel,
+ PagedLanguageModel,
+} from '@umbraco-cms/backend-api';
+import type { DataSourceResponse, DictionaryDetails } from '@umbraco-cms/models';
+
+export interface DictionaryDetailDataSource {
+ createScaffold(parentKey: string): Promise>;
+ list(skip?: number, take?: number): Promise>;
+ get(key: string): Promise>;
+ insert(data: DictionaryDetails): Promise;
+ update(dictionary: DictionaryItemModel): Promise;
+ delete(key: string): Promise;
+ export(key: string, includeChildren: boolean): Promise>;
+ import(fileName: string, parentKey?: string): Promise>;
+ upload(formData: FormData): Promise>;
+ // TODO - temp only
+ getLanguages(): Promise>;
+}
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/sources/dictionary.tree.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/sources/dictionary.tree.server.data.ts
new file mode 100644
index 0000000000..faacf9512b
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/sources/dictionary.tree.server.data.ts
@@ -0,0 +1,72 @@
+import { DictionaryResource, ProblemDetailsModel } from '@umbraco-cms/backend-api';
+import { UmbControllerHostInterface } from '@umbraco-cms/controller';
+import { RepositoryTreeDataSource } from '@umbraco-cms/repository';
+import { tryExecuteAndNotify } from '@umbraco-cms/resources';
+
+/**
+ * A data source for the Dictionary tree that fetches data from the server
+ * @export
+ * @class DictionaryTreeServerDataSource
+ * @implements {DictionaryTreeDataSource}
+ */
+export class DictionaryTreeServerDataSource implements RepositoryTreeDataSource {
+ #host: UmbControllerHostInterface;
+
+ /**
+ * Creates an instance of DictionaryTreeDataSource.
+ * @param {UmbControllerHostInterface} host
+ * @memberof DictionaryTreeDataSource
+ */
+ constructor(host: UmbControllerHostInterface) {
+ this.#host = host;
+ }
+
+ /**
+ * Fetches the root items for the tree from the server
+ * @return {*}
+ * @memberof DictionaryTreeServerDataSource
+ */
+ async getRootItems() {
+ return tryExecuteAndNotify(this.#host, DictionaryResource.getTreeDictionaryRoot({}));
+ }
+
+ /**
+ * Fetches the children of a given parent key from the server
+ * @param {(string | null)} parentKey
+ * @return {*}
+ * @memberof DictionaryTreeServerDataSource
+ */
+ async getChildrenOf(parentKey: string | null) {
+ if (!parentKey) {
+ const error: ProblemDetailsModel = { title: 'Parent key is missing' };
+ return { error };
+ }
+
+ return tryExecuteAndNotify(
+ this.#host,
+ DictionaryResource.getTreeDictionaryChildren({
+ parentKey,
+ })
+ );
+ }
+
+ /**
+ * Fetches the items for the given keys from the server
+ * @param {Array} keys
+ * @return {*}
+ * @memberof DictionaryTreeServerDataSource
+ */
+ async getItems(keys: Array) {
+ if (!keys || keys.length === 0) {
+ const error: ProblemDetailsModel = { title: 'Keys are missing' };
+ return { error };
+ }
+
+ return tryExecuteAndNotify(
+ this.#host,
+ DictionaryResource.getTreeDictionaryItem({
+ key: keys,
+ })
+ );
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/sidebar-menu-item/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/sidebar-menu-item/manifests.ts
index c172609271..214391bf88 100644
--- a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/sidebar-menu-item/manifests.ts
+++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/sidebar-menu-item/manifests.ts
@@ -8,8 +8,9 @@ const sidebarMenuItem: ManifestSidebarMenuItem = {
loader: () => import('./dictionary-sidebar-menu-item.element'),
meta: {
label: 'Dictionary',
- icon: 'umb:folder',
+ icon: 'umb:book-alt',
sections: ['Umb.Section.Translation'],
+ entityType: 'dictionary-item'
},
};
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/tree/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/tree/manifests.ts
index 8ba7f7d41e..2d1129154a 100644
--- a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/tree/manifests.ts
+++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/tree/manifests.ts
@@ -1,17 +1,13 @@
-import { UMB_DICTIONARY_TREE_STORE_CONTEXT_TOKEN } from '../dictionary.tree.store';
-import type { ManifestTree, ManifestTreeItemAction } from '@umbraco-cms/models';
-
-const treeAlias = 'Umb.Tree.Dictionary';
+import { UmbDictionaryRepository } from '../repository/dictionary.repository';
+import type { ManifestTree } from '@umbraco-cms/models';
const tree: ManifestTree = {
type: 'tree',
- alias: treeAlias,
+ alias: 'Umb.Tree.Dictionary',
name: 'Dictionary Tree',
meta: {
- storeAlias: UMB_DICTIONARY_TREE_STORE_CONTEXT_TOKEN.toString(),
+ repository: UmbDictionaryRepository,
},
};
-const treeItemActions: Array = [];
-
-export const manifests = [tree, ...treeItemActions];
+export const manifests = [tree];
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/workspace/dictionary-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/workspace/dictionary-workspace.context.ts
new file mode 100644
index 0000000000..36824bb5a0
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/workspace/dictionary-workspace.context.ts
@@ -0,0 +1,84 @@
+import { UmbDictionaryRepository } from '../repository/dictionary.repository';
+import { UmbWorkspaceContext } from '../../../../backoffice/shared/components/workspace/workspace-context/workspace-context';
+import { UmbWorkspaceEntityContextInterface } from '../../../../backoffice/shared/components/workspace/workspace-context/workspace-entity-context.interface';
+import { UmbControllerHostInterface } from '@umbraco-cms/controller';
+import { ObjectState } from '@umbraco-cms/observable-api';
+import type { DictionaryDetails } from '@umbraco-cms/models';
+
+type EntityType = DictionaryDetails;
+export class UmbWorkspaceDictionaryContext
+ extends UmbWorkspaceContext
+ implements UmbWorkspaceEntityContextInterface
+{
+ #host: UmbControllerHostInterface;
+ #repo: UmbDictionaryRepository;
+
+ #data = new ObjectState(undefined);
+ data = this.#data.asObservable();
+ name = this.#data.getObservablePart((data) => data?.name);
+ dictionary = this.#data.getObservablePart((data) => data);
+
+ constructor(host: UmbControllerHostInterface) {
+ super(host);
+ this.#host = host;
+ this.#repo = new UmbDictionaryRepository(this.#host);
+ }
+
+ getData() {
+ return this.#data.getValue();
+ }
+
+ getEntityKey() {
+ return this.getData()?.key || '';
+ }
+
+ getEntityType() {
+ return 'dictionary-item';
+ }
+
+ setName(name: string) {
+ this.#data.update({ name });
+ }
+
+ setPropertyValue(isoCode: string, translation: string) {
+ if (!this.#data.value) return;
+
+ // update if the code already exists
+ const updatedValue =
+ this.#data.value.translations?.map((translationItem) => {
+ if (translationItem.isoCode === isoCode) {
+ return { ...translationItem, translation};
+ }
+ return translationItem;
+ }) ?? [];
+
+ // if code doesn't exist, add it to the new value set
+ if (!updatedValue?.find((x) => x.isoCode === isoCode)) {
+ updatedValue?.push({ isoCode, translation });
+ }
+
+ this.#data.next({ ...this.#data.value, translations: updatedValue });
+ }
+
+ async load(entityKey: string) {
+ const { data } = await this.#repo.requestDetails(entityKey);
+ if (data) {
+ this.#data.next(data);
+ }
+ }
+
+ async createScaffold(parentKey: string | null) {
+ const { data } = await this.#repo.createDetailsScaffold(parentKey);
+ if (!data) return;
+ this.#data.next(data);
+ }
+
+ async save() {
+ if (!this.#data.value) return;
+ this.#repo.saveDetail(this.#data.value);
+ }
+
+ public destroy(): void {
+ this.#data.complete();
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/workspace/dictionary-workspace.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/workspace/dictionary-workspace.element.ts
index 1ee69d6156..74e6564ca6 100644
--- a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/workspace/dictionary-workspace.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/workspace/dictionary-workspace.element.ts
@@ -1,26 +1,74 @@
+import { UUIInputElement, UUIInputEvent } from '@umbraco-ui/uui';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
-import { css, html, LitElement } from 'lit';
-import { customElement, property } from 'lit/decorators.js';
+import { css, html } from 'lit';
+import { customElement, state } from 'lit/decorators.js';
+import { UmbWorkspaceEntityElement } from '../../../../backoffice/shared/components/workspace/workspace-entity-element.interface';
+import { UmbWorkspaceDictionaryContext } from './dictionary-workspace.context';
+import { UmbLitElement } from '@umbraco-cms/element';
-@customElement('umb-workspace-dictionary')
-export class UmbWorkspaceDictionaryElement extends LitElement {
+@customElement('umb-dictionary-workspace')
+export class UmbWorkspaceDictionaryElement extends UmbLitElement implements UmbWorkspaceEntityElement {
static styles = [
UUITextStyles,
css`
- :host {
- display: block;
+ #header {
+ display: flex;
+ padding: 0 var(--uui-size-space-6);
+ gap: var(--uui-size-space-4);
+ width: 100%;
+ }
+ uui-input {
width: 100%;
- height: 100%;
}
`,
];
- @property()
- id!: string;
+ @state()
+ _unique?: string;
+
+ public load(entityKey: string) {
+ this.#workspaceContext.load(entityKey);
+ this._unique = entityKey;
+ }
+
+ public create(parentKey: string | null) {
+ this.#workspaceContext.createScaffold(parentKey);
+ }
+
+ @state()
+ private _name?: string | null = '';
+
+ #workspaceContext = new UmbWorkspaceDictionaryContext(this);
+
+ async connectedCallback() {
+ super.connectedCallback();
+
+ this.observe(this.#workspaceContext.name, (name) => {
+ this._name = name;
+ });
+ }
+
+ // TODO. find a way where we don't have to do this for all workspaces.
+ #handleInput(event: UUIInputEvent) {
+ if (event instanceof UUIInputEvent) {
+ const target = event.composedPath()[0] as UUIInputElement;
+
+ if (typeof target?.value === 'string') {
+ this.#workspaceContext?.setName(target.value);
+ }
+ }
+ }
render() {
return html`
- Dictionary Workspace
+
+
+
`;
}
}
@@ -29,6 +77,6 @@ export default UmbWorkspaceDictionaryElement;
declare global {
interface HTMLElementTagNameMap {
- 'umb-workspace-dictionary': UmbWorkspaceDictionaryElement;
+ 'umb-dictionary-workspace': UmbWorkspaceDictionaryElement;
}
}
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/workspace/dictionary-workspace.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/workspace/dictionary-workspace.stories.ts
new file mode 100644
index 0000000000..71a1059e42
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/workspace/dictionary-workspace.stories.ts
@@ -0,0 +1,16 @@
+import './dictionary-workspace.element';
+import { Meta, Story } from '@storybook/web-components';
+import { html } from 'lit-html';
+import { data } from '../../../../core/mocks/data/dictionary.data';
+import type { UmbWorkspaceDictionaryElement } from './dictionary-workspace.element';
+
+export default {
+ title: 'Workspaces/Dictionary',
+ component: 'umb-dictionary-workspace',
+ id: 'umb-dictionary-workspace',
+} as Meta;
+
+export const AAAOverview: Story = () =>
+ html` `;
+
+AAAOverview.storyName = 'Overview';
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/workspace/manifests.ts
index c354084f1c..55b41e8202 100644
--- a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/workspace/manifests.ts
+++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/workspace/manifests.ts
@@ -1,4 +1,6 @@
-import type { ManifestWorkspace } from '@umbraco-cms/models';
+import { DICTIONARY_REPOSITORY_ALIAS } from '../repository/manifests';
+import { UmbSaveWorkspaceAction } from '../../../../backoffice/shared/workspace-actions/save.action';
+import type { ManifestWorkspace, ManifestWorkspaceAction, ManifestWorkspaceView } from '@umbraco-cms/models';
const workspaceAlias = 'Umb.Workspace.Dictionary';
@@ -8,8 +10,41 @@ const workspace: ManifestWorkspace = {
name: 'Dictionary Workspace',
loader: () => import('./dictionary-workspace.element'),
meta: {
- entityType: 'dictionary',
+ entityType: 'dictionary-item',
},
};
-export const manifests = [workspace];
+const workspaceViews: Array = [
+ {
+ type: 'workspaceView',
+ alias: 'Umb.WorkspaceView.Dictionary.Edit',
+ name: 'Dictionary Workspace Edit View',
+ loader: () => import('./views/edit/workspace-view-dictionary-edit.element'),
+ weight: 100,
+ meta: {
+ workspaces: [workspaceAlias],
+ label: 'Edit',
+ pathname: 'edit',
+ icon: 'edit',
+ },
+ },
+];
+
+const workspaceActions: Array = [
+ {
+ type: 'workspaceAction',
+ alias: 'Umb.WorkspaceAction.Dictionary.Save',
+ name: 'Save Dictionary Workspace Action',
+ weight: 90,
+ meta: {
+ workspaces: ['Umb.Workspace.Dictionary'],
+ label: 'Save',
+ look: 'primary',
+ color: 'positive',
+ repositoryAlias: DICTIONARY_REPOSITORY_ALIAS,
+ api: UmbSaveWorkspaceAction,
+ },
+ },
+];
+
+export const manifests = [workspace, ...workspaceViews, ...workspaceActions];
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/workspace/views/edit/workspace-view-dictionary-edit.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/workspace/views/edit/workspace-view-dictionary-edit.element.ts
new file mode 100644
index 0000000000..04152ba8a9
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/workspace/views/edit/workspace-view-dictionary-edit.element.ts
@@ -0,0 +1,98 @@
+import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
+import { css, html } from 'lit';
+import { customElement, state } from 'lit/decorators.js';
+import { repeat } from 'lit/directives/repeat.js';
+import { ifDefined } from 'lit-html/directives/if-defined.js';
+import { UUITextareaElement, UUITextareaEvent } from '@umbraco-ui/uui';
+import { UmbWorkspaceDictionaryContext } from '../../dictionary-workspace.context';
+import { UmbDictionaryRepository } from '../../../repository/dictionary.repository';
+import { UmbLitElement } from '@umbraco-cms/element';
+import { DictionaryItemModel, LanguageModel } from '@umbraco-cms/backend-api';
+
+@customElement('umb-workspace-view-dictionary-edit')
+export class UmbWorkspaceViewDictionaryEditElement extends UmbLitElement {
+ static styles = [
+ UUITextStyles,
+ css`
+ :host {
+ display: block;
+ margin: var(--uui-size-layout-1);
+ }
+ `,
+ ];
+
+ @state()
+ private _dictionary?: DictionaryItemModel;
+
+ #repo!: UmbDictionaryRepository;
+
+ @state()
+ private _languages: Array = [];
+
+ #workspaceContext!: UmbWorkspaceDictionaryContext;
+
+ async connectedCallback() {
+ super.connectedCallback();
+
+ this.#repo = new UmbDictionaryRepository(this);
+ this._languages = await this.#repo.getLanguages();
+
+ this.consumeContext('umbWorkspaceContext', (_instance) => {
+ this.#workspaceContext = _instance;
+ this.#observeDictionary();
+ });
+ }
+
+ #observeDictionary() {
+ this.observe(this.#workspaceContext.dictionary, (dictionary) => {
+ this._dictionary = dictionary;
+ });
+ }
+
+ #renderTranslation(language: LanguageModel) {
+ if (!language.isoCode) return;
+
+ const translation = this._dictionary?.translations?.find((x) => x.isoCode === language.isoCode);
+
+ return html`
+
+ `;
+ }
+
+ #onTextareaChange(e: Event) {
+ if (e instanceof UUITextareaEvent) {
+ const target = e.composedPath()[0] as UUITextareaElement;
+ const translation = target.value.toString();
+ const isoCode = target.getAttribute('name')!;
+
+ this.#workspaceContext.setPropertyValue(isoCode, translation);
+ }
+ }
+
+ render() {
+ return html`
+
+ Edit the different language versions for the dictionary item '${this._dictionary?.name}' below.
+
+ ${repeat(
+ this._languages,
+ (item) => item.isoCode,
+ (item) => this.#renderTranslation(item)
+ )}
+
+ `;
+ }
+}
+
+export default UmbWorkspaceViewDictionaryEditElement;
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'umb-workspace-view-dictionary-edit': UmbWorkspaceViewDictionaryEditElement;
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/workspace/views/edit/workspace-view-dictionary-edit.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/workspace/views/edit/workspace-view-dictionary-edit.stories.ts
new file mode 100644
index 0000000000..806bd714e5
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/workspace/views/edit/workspace-view-dictionary-edit.stories.ts
@@ -0,0 +1,25 @@
+import { Meta, Story } from '@storybook/web-components';
+import { html } from 'lit-html';
+//import { data } from '../../../../../core/mocks/data/dictionary.data';
+import type { UmbWorkspaceViewDictionaryEditElement } from './workspace-view-dictionary-edit.element';
+import './workspace-view-dictionary-edit.element';
+//import { UmbWorkspaceDictionaryContext } from '../../workspace-dictionary.context';
+
+export default {
+ title: 'Workspaces/Dictionary/Views/Edit',
+ component: 'umb-workspace-view-dictionary-edit',
+ id: 'umb-workspace-view-dictionary-edit',
+ decorators: [
+ (story) => {
+ return html`TODO: make use of mocked workspace context??`;
+ /*html`
+ ${story()}
+ `,*/
+ }
+ ],
+} as Meta;
+
+export const AAAOverview: Story = () =>
+ html` `;
+
+AAAOverview.storyName = 'Overview';
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/section.manifest.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/section.manifest.ts
index 0b0deaadd9..6e60134aa8 100644
--- a/src/Umbraco.Web.UI.Client/src/backoffice/translation/section.manifest.ts
+++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/section.manifest.ts
@@ -1,4 +1,4 @@
-import type { ManifestSection } from '@umbraco-cms/models';
+import type { ManifestDashboard, ManifestSection } from '@umbraco-cms/models';
const sectionAlias = 'Umb.Section.Translation';
@@ -13,4 +13,20 @@ const section: ManifestSection = {
},
};
-export const manifests = [section];
+const dashboards: Array = [
+ {
+ type: 'dashboard',
+ alias: 'Umb.Dashboard.TranslationDictionary',
+ name: 'Dictionary Translation Dashboard',
+ elementName: 'umb-dashboard-translation-dictionary',
+ loader: () => import('./dashboards/dictionary/dashboard-translation-dictionary.element'),
+ meta: {
+ label: 'Dictionary overview',
+ sections: [sectionAlias],
+ pathname: '',
+ },
+ },
+];
+
+
+export const manifests = [section, ...dashboards];
diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/data/dictionary.data.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/data/dictionary.data.ts
index e2d8f9fec5..4c4ea991be 100644
--- a/src/Umbraco.Web.UI.Client/src/core/mocks/data/dictionary.data.ts
+++ b/src/Umbraco.Web.UI.Client/src/core/mocks/data/dictionary.data.ts
@@ -7,20 +7,36 @@ export const data: Array = [
{
parentKey: null,
name: 'Hello',
- key: 'b7e7d0ab-53ba-485d-b8bd-12537f9925cb',
+ key: 'aae7d0ab-53ba-485d-b8bd-12537f9925cb',
hasChildren: true,
- type: 'dictionary',
+ type: 'dictionary-item',
isContainer: false,
- icon: 'umb:icon-book-alt',
+ icon: 'umb:book-alt',
+ translations: [{
+ isoCode: 'en',
+ translation: 'hello in en-US'
+ },
+ {
+ isoCode: 'fr',
+ translation: '',
+ }],
},
{
- parentKey: 'b7e7d0ab-53ba-485d-b8bd-12537f9925cb',
- name: 'Hello',
- key: 'b7e7d0ab-53bb-485d-b8bd-12537f9925cb',
+ parentKey: 'aae7d0ab-53ba-485d-b8bd-12537f9925cb',
+ name: 'Hello again',
+ key: 'bbe7d0ab-53bb-485d-b8bd-12537f9925cb',
hasChildren: false,
- type: 'dictionary',
+ type: 'dictionary-item',
isContainer: false,
- icon: 'umb:icon-book-alt',
+ icon: 'umb:book-alt',
+ translations: [{
+ isoCode: 'en',
+ translation: 'Hello again in en-US'
+ },
+ {
+ isoCode: 'fr',
+ translation: 'Hello in fr'
+ }],
},
];
diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/data/entity.data.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/data/entity.data.ts
index 700be3b625..29d9fc8f09 100644
--- a/src/Umbraco.Web.UI.Client/src/core/mocks/data/entity.data.ts
+++ b/src/Umbraco.Web.UI.Client/src/core/mocks/data/entity.data.ts
@@ -7,6 +7,10 @@ export class UmbEntityData extends UmbData {
super(data);
}
+ getList(skip: number, take: number) {
+ return this.data.slice(skip, skip + take);
+ }
+
getByKey(key: string) {
return this.data.find((item) => item.key === key);
}
diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/domains/dictionary.handlers.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/dictionary.handlers.ts
index f73e8c29cf..d3b99f6c88 100644
--- a/src/Umbraco.Web.UI.Client/src/core/mocks/domains/dictionary.handlers.ts
+++ b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/dictionary.handlers.ts
@@ -1,13 +1,124 @@
import { rest } from 'msw';
+import { v4 as uuidv4 } from 'uuid';
import { umbDictionaryData } from '../data/dictionary.data';
+import { CreatedResultModel, DictionaryImportModel, DictionaryOverviewModel } from '@umbraco-cms/backend-api';
+import type { DictionaryDetails } from '@umbraco-cms/models';
+
+const uploadResponse: DictionaryImportModel = {
+ fileName: 'c:/path/to/tempfilename.udt',
+ parentKey: 'b7e7d0ab-53ba-485d-dddd-12537f9925aa',
+};
+
+///
+const importResponse: DictionaryDetails = {
+ parentKey: null,
+ name: 'Uploaded dictionary',
+ key: 'b7e7d0ab-53ba-485d-dddd-12537f9925cb',
+ hasChildren: false,
+ type: 'dictionary-item',
+ isContainer: false,
+ icon: 'umb:book-alt',
+ translations: [{
+ isoCode: 'en',
+ translation: 'I am an imported US value'
+ },
+ {
+ isoCode: 'fr',
+ translation: 'I am an imported French value',
+ }],
+};
+
+
+// alternate data for dashboard view
+const overviewData: Array = [
+ {
+ name: 'Hello',
+ key: 'aae7d0ab-53ba-485d-b8bd-12537f9925cb',
+ translatedIsoCodes: ['en'],
+ },
+ {
+ name: 'Hello again',
+ key: 'bbe7d0ab-53bb-485d-b8bd-12537f9925cb',
+ translatedIsoCodes: ['en', 'fr'],
+ },
+];
// TODO: add schema
export const handlers = [
- rest.get('/umbraco/management/api/v1/tree/dictionary/root', (req, res, ctx) => {
- const rootItems = umbDictionaryData.getTreeRoot();
+ rest.get('/umbraco/management/api/v1/dictionary/:key', (req, res, ctx) => {
+ const key = req.params.key as string;
+ if (!key) return;
+
+ const dictionary = umbDictionaryData.getByKey(key);
+ console.log(dictionary);
+ return res(ctx.status(200), ctx.json(dictionary));
+ }),
+
+ rest.get('/umbraco/management/api/v1/dictionary', (req, res, ctx) => {
+ const skip = req.url.searchParams.get('skip');
+ const take = req.url.searchParams.get('take');
+ if (!skip || !take) return;
+
+ // overview is DictionaryOverview[], umbDictionaryData provides DictionaryDetails[]
+ // which are incompatible types to mock, so we can do a filthy replacement here
+ //const items = umbDictionaryData.getList(parseInt(skip), parseInt(take));
+ const items = overviewData;
+
const response = {
- total: rootItems.length,
- items: rootItems,
+ total: items.length,
+ items,
+ };
+
+ return res(ctx.status(200), ctx.json(response));
+ }),
+
+ rest.post('/umbraco/management/api/v1/dictionary', async (req, res, ctx) => {
+ const data = await req.json();
+ if (!data) return;
+
+ data.parentKey = data.parentId;
+ data.icon = 'umb:book-alt';
+ data.hasChildren = false;
+ data.type = 'dictionary-item';
+ data.translations = [
+ {
+ isoCode: 'en-US',
+ translation: '',
+ },
+ {
+ isoCode: 'fr',
+ translation: '',
+ },
+ ];
+
+ const value = umbDictionaryData.save([data])[0];
+
+ const createdResult: CreatedResultModel = {
+ value,
+ statusCode: 200,
+ };
+
+ return res(ctx.status(200), ctx.json(createdResult));
+ }),
+
+ rest.patch('/umbraco/management/api/v1/dictionary/:key', async (req, res, ctx) => {
+ const data = await req.json();
+ if (!data) return;
+
+ const key = req.params.key as string;
+ if (!key) return;
+
+ const dataToSave = JSON.parse(data[0].value);
+ const saved = umbDictionaryData.save([dataToSave]);
+
+ return res(ctx.status(200), ctx.json(saved));
+ }),
+
+ rest.get('/umbraco/management/api/v1/tree/dictionary/root', (req, res, ctx) => {
+ const items = umbDictionaryData.getTreeRoot();
+ const response = {
+ total: items.length,
+ items,
};
return res(ctx.status(200), ctx.json(response));
}),
@@ -16,11 +127,11 @@ export const handlers = [
const parentKey = req.url.searchParams.get('parentKey');
if (!parentKey) return;
- const children = umbDictionaryData.getTreeItemChildren(parentKey);
+ const items = umbDictionaryData.getTreeItemChildren(parentKey);
const response = {
- total: children.length,
- items: children,
+ total: items.length,
+ items,
};
return res(ctx.status(200), ctx.json(response));
@@ -34,4 +145,53 @@ export const handlers = [
return res(ctx.status(200), ctx.json(items));
}),
+
+ rest.delete('/umbraco/management/api/v1/dictionary/:key', (req, res, ctx) => {
+ const key = req.params.key as string;
+ if (!key) return;
+
+ const deletedKeys = umbDictionaryData.delete([key]);
+
+ return res(ctx.status(200), ctx.json(deletedKeys));
+ }),
+
+ // TODO => handle properly, querystring breaks handler
+ rest.get('/umbraco/management/api/v1/dictionary/:key/export', (req, res, ctx) => {
+ const key = req.params.key as string;
+ if (!key) return;
+
+ const includeChildren = req.url.searchParams.get('includeChildren');
+ const item = umbDictionaryData.getByKey(key);
+
+ alert(`Downloads file for dictionary "${item?.name}", ${includeChildren === 'true' ? 'with' : 'without'} children.`)
+ return res(ctx.status(200));
+ }),
+
+ rest.post('/umbraco/management/api/v1/dictionary/upload', async (req, res, ctx) => {
+ if (!req.arrayBuffer()) return;
+
+ return res(ctx.status(200), ctx.json(uploadResponse));
+ }),
+
+ rest.post('/umbraco/management/api/v1/dictionary/import', async (req, res, ctx) => {
+ const file = req.url.searchParams.get('file');
+
+ if (!file) return;
+
+ importResponse.parentKey = req.url.searchParams.get('parentId') ?? null;
+ umbDictionaryData.save([importResponse]);
+
+ // build the path to the new item => reflects the expected server response
+ const path = ['-1'];
+ if (importResponse.parentKey) path.push(importResponse.parentKey);
+
+ path.push(importResponse.key);
+
+ const contentResult = {
+ content: path.join(','),
+ statusCode: 200,
+ };
+
+ return res(ctx.status(200), ctx.json(contentResult));
+ }),
];