Merge branch 'main' into feature/debug-component

This commit is contained in:
Warren Buckley
2023-02-14 15:00:57 +00:00
committed by GitHub
92 changed files with 1979 additions and 371 deletions

View File

@@ -11,11 +11,11 @@ import { html } from 'lit-html';
import { initialize, mswDecorator } from 'msw-storybook-addon';
import { setCustomElements } from '@storybook/web-components';
import { UmbDataTypeStore } from '../src/backoffice/settings/data-types/data-type.store';
import { UMB_DATA_TYPE_STORE_CONTEXT_TOKEN, UmbDataTypeStore } from '../src/backoffice/settings/data-types/repository/data-type.store.ts';
import {
UMB_DOCUMENT_TYPE_DETAIL_STORE_CONTEXT_TOKEN,
UMB_DOCUMENT_TYPE_STORE_CONTEXT_TOKEN,
UmbDocumentTypeStore,
} from '../src/backoffice/documents/document-types/document-type.detail.store';
} from '../src/backoffice/documents/document-types/repository/document-type.store.ts';
import customElementManifests from '../custom-elements.json';
import { UmbIconStore } from '../libs/store/icon/icon.store';
@@ -69,7 +69,7 @@ const dataTypeStoreProvider = (story) => html`
const documentTypeStoreProvider = (story) => html`
<umb-context-provider
key=${UMB_DOCUMENT_TYPE_DETAIL_STORE_CONTEXT_TOKEN.toString()}
key=${UMB_DOCUMENT_TYPE_STORE_CONTEXT_TOKEN.toString()}
.create=${(host) => new UmbDocumentTypeStore(host)}
>${story()}</umb-context-provider
>

View File

@@ -45,4 +45,3 @@ export type ConstructorInfoModel = {
readonly isSecurityTransparent?: boolean;
memberType?: MemberTypesModel;
};

View File

@@ -6,4 +6,3 @@ export type CultureModel = {
name?: string;
englishName?: string;
};

View File

@@ -10,4 +10,3 @@ export type DataTypeModelBaseModel = {
propertyEditorUiAlias?: string | null;
data?: Array<DataTypePropertyModel>;
};

View File

@@ -6,4 +6,3 @@ export type DataTypePropertyModel = {
alias?: string;
value?: any;
};

View File

@@ -6,4 +6,3 @@ export type DataTypePropertyReferenceModel = {
name?: string;
alias?: string;
};

View File

@@ -12,4 +12,3 @@ export type DatabaseInstallModel = {
useIntegratedAuthentication?: boolean;
connectionString?: string | null;
};

View File

@@ -15,4 +15,3 @@ export type DatabaseSettingsModel = {
supportsIntegratedAuthentication?: boolean;
requiresConnectionTest?: boolean;
};

View File

@@ -8,4 +8,3 @@ export type DictionaryItemModelBaseModel = {
name?: string;
translations?: Array<DictionaryItemTranslationModel>;
};

View File

@@ -6,4 +6,3 @@ export type DictionaryItemTranslationModel = {
isoCode?: string;
translation?: string;
};

View File

@@ -7,4 +7,3 @@ export type DictionaryItemsImportModel = {
name?: string | null;
parentKey?: string | null;
};

View File

@@ -8,4 +8,3 @@ export type DictionaryOverviewModel = {
parentKey?: string | null;
translatedIsoCodes?: Array<string>;
};

View File

@@ -37,4 +37,3 @@ export type FieldInfoModel = {
readonly isSecurityTransparent?: boolean;
fieldHandle?: RuntimeFieldHandleModel;
};

View File

@@ -6,4 +6,3 @@ export type FieldModel = {
name?: string;
values?: Array<string>;
};

View File

@@ -12,4 +12,3 @@ export type HealthCheckActionModel = {
providedValueValidation?: string | null;
providedValueValidationRegex?: string | null;
};

View File

@@ -8,4 +8,3 @@ export type HelpPageModel = {
url?: string | null;
type?: string | null;
};

View File

@@ -13,4 +13,3 @@ export type IndexModel = {
fieldCount: number;
providerProperties?: Record<string, any> | null;
};

View File

@@ -4,4 +4,3 @@
export type IntPtrModel = {
};

View File

@@ -4,4 +4,3 @@
export type JsonNamingPolicyModel = {
};

View File

@@ -8,4 +8,3 @@ export type LanguageModelBaseModel = {
isMandatory?: boolean;
fallbackIsoCode?: string | null;
};

View File

@@ -6,4 +6,3 @@ export type LogMessagePropertyModel = {
name?: string;
value?: string | null;
};

View File

@@ -6,4 +6,3 @@ export type LogTemplateModel = {
messageTemplate?: string | null;
count?: number;
};

View File

@@ -45,4 +45,3 @@ export type MethodBaseModel = {
readonly isSecuritySafeCritical?: boolean;
readonly isSecurityTransparent?: boolean;
};

View File

@@ -13,4 +13,3 @@ export type ModelsBuilderModel = {
modelsNamespace?: string | null;
trackingOutOfDateModels?: boolean;
};

View File

@@ -5,4 +5,3 @@
export type ModuleHandleModel = {
readonly mdStreamVersion?: number;
};

View File

@@ -5,4 +5,3 @@
export type NotFoundResultModel = {
statusCode?: number;
};

View File

@@ -5,4 +5,3 @@
export type OkResultModel = {
statusCode?: number;
};

View File

@@ -5,4 +5,3 @@
export type ProfilingStatusModel = {
enabled?: boolean;
};

View File

@@ -11,4 +11,3 @@ export type RecycleBinItemModel = {
isContainer?: boolean;
parentKey?: string | null;
};

View File

@@ -10,4 +10,3 @@ export type RedirectUrlModel = {
contentKey?: string;
culture?: string | null;
};

View File

@@ -4,4 +4,3 @@
export type ReferenceHandlerModel = {
};

View File

@@ -13,4 +13,3 @@ export type RelationItemModel = {
relationTypeIsBidirectional?: boolean;
relationTypeIsDependency?: boolean;
};

View File

@@ -10,4 +10,3 @@ export type RelationModel = {
createDate?: string;
comment?: string | null;
};

View File

@@ -6,4 +6,3 @@ export type SavedLogSearchModel = {
name?: string;
query?: string;
};

View File

@@ -10,4 +10,3 @@ export type SearchResultModel = {
readonly fieldCount?: number;
fields?: Array<FieldModel>;
};

View File

@@ -5,4 +5,3 @@
export type SearcherModel = {
name?: string;
};

View File

@@ -7,4 +7,3 @@ export type TemplateModelBaseModel = {
alias?: string;
content?: string | null;
};

View File

@@ -9,4 +9,3 @@ export type TemplateQueryExecuteFilterModel = {
constraintValue?: string;
operator?: OperatorModel;
};

View File

@@ -12,4 +12,3 @@ export type TemplateQueryExecuteModel = {
sort?: TemplateQueryExecuteSortModel | null;
take?: number;
};

View File

@@ -6,4 +6,3 @@ export type TemplateQueryExecuteSortModel = {
propertyAlias?: string;
direction?: string | null;
};

View File

@@ -6,4 +6,3 @@ export type TemplateQueryResultItemModel = {
icon?: string;
name?: string;
};

View File

@@ -5,4 +5,3 @@
export type TemplateScaffoldModel = {
content?: string;
};

View File

@@ -8,4 +8,3 @@ export type TreeItemModel = {
icon?: string;
hasChildren?: boolean;
};

View File

@@ -101,4 +101,3 @@ export type TypeInfoModel = {
readonly declaredProperties?: Array<PropertyInfoModel>;
readonly implementedInterfaces?: Array<TypeModel>;
};

View File

@@ -86,4 +86,3 @@ export type TypeModel = {
readonly containsGenericParameters?: boolean;
readonly isVisible?: boolean;
};

View File

@@ -9,4 +9,3 @@ export type UpgradeSettingsModel = {
oldVersion?: string;
readonly reportUrl?: string;
};

View File

@@ -8,4 +8,3 @@ export type UserInstallModel = {
password: string;
readonly subscribeToNewsletter?: boolean;
};

View File

@@ -9,4 +9,3 @@ export type UserSettingsModel = {
minNonAlphaNumericLength?: number;
consentLevels?: Array<ConsentLevelModel>;
};

View File

@@ -5,4 +5,3 @@
export type VersionModel = {
version?: string;
};

View File

@@ -25,10 +25,10 @@ export class DataTypeResource {
* @throws ApiError
*/
public static postDataType({
requestBody,
}: {
requestBody?: DataTypeCreateModel,
}): CancelablePromise<any> {
requestBody,
}: {
requestBody?: DataTypeCreateModel,
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'POST',
url: '/umbraco/management/api/v1/data-type',
@@ -67,10 +67,10 @@ export class DataTypeResource {
* @throws ApiError
*/
public static deleteDataTypeByKey({
key,
}: {
key: string,
}): CancelablePromise<any> {
key,
}: {
key: string,
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/umbraco/management/api/v1/data-type/{key}',
@@ -89,12 +89,12 @@ export class DataTypeResource {
* @throws ApiError
*/
public static putDataTypeByKey({
key,
requestBody,
}: {
key: string,
requestBody?: DataTypeUpdateModel,
}): CancelablePromise<any> {
key,
requestBody,
}: {
key: string,
requestBody?: DataTypeUpdateModel,
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'PUT',
url: '/umbraco/management/api/v1/data-type/{key}',
@@ -186,10 +186,10 @@ export class DataTypeResource {
* @throws ApiError
*/
public static postDataTypeFolder({
requestBody,
}: {
requestBody?: FolderCreateModel,
}): CancelablePromise<any> {
requestBody,
}: {
requestBody?: FolderCreateModel,
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'POST',
url: '/umbraco/management/api/v1/data-type/folder',
@@ -224,10 +224,10 @@ export class DataTypeResource {
* @throws ApiError
*/
public static deleteDataTypeFolderByKey({
key,
}: {
key: string,
}): CancelablePromise<any> {
key,
}: {
key: string,
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/umbraco/management/api/v1/data-type/folder/{key}',
@@ -245,12 +245,12 @@ export class DataTypeResource {
* @throws ApiError
*/
public static putDataTypeFolderByKey({
key,
requestBody,
}: {
key: string,
requestBody?: FolderUpdateModel,
}): CancelablePromise<any> {
key,
requestBody,
}: {
key: string,
requestBody?: FolderUpdateModel,
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'PUT',
url: '/umbraco/management/api/v1/data-type/folder/{key}',

View File

@@ -44,10 +44,10 @@ export class DictionaryResource {
* @throws ApiError
*/
public static postDictionary({
requestBody,
}: {
requestBody?: DictionaryItemCreateModel,
}): CancelablePromise<any> {
requestBody,
}: {
requestBody?: DictionaryItemCreateModel,
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'POST',
url: '/umbraco/management/api/v1/dictionary',
@@ -87,10 +87,10 @@ export class DictionaryResource {
* @throws ApiError
*/
public static deleteDictionaryByKey({
key,
}: {
key: string,
}): CancelablePromise<any> {
key,
}: {
key: string,
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/umbraco/management/api/v1/dictionary/{key}',
@@ -109,12 +109,12 @@ export class DictionaryResource {
* @throws ApiError
*/
public static putDictionaryByKey({
key,
requestBody,
}: {
key: string,
requestBody?: DictionaryItemUpdateModel,
}): CancelablePromise<any> {
key,
requestBody,
}: {
key: string,
requestBody?: DictionaryItemUpdateModel,
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'PUT',
url: '/umbraco/management/api/v1/dictionary/{key}',

View File

@@ -41,12 +41,12 @@ export class LogViewerResource {
* @throws ApiError
*/
public static getLogViewerLevelCount({
startDate,
endDate,
}: {
startDate?: string,
endDate?: string,
}): CancelablePromise<any> {
startDate,
endDate,
}: {
startDate?: string,
endDate?: string,
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'GET',
url: '/umbraco/management/api/v1/log-viewer/level-count',
@@ -193,10 +193,10 @@ export class LogViewerResource {
* @throws ApiError
*/
public static deleteLogViewerSavedSearchByName({
name,
}: {
name: string,
}): CancelablePromise<any> {
name,
}: {
name: string,
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/umbraco/management/api/v1/log-viewer/saved-search/{name}',
@@ -214,12 +214,12 @@ export class LogViewerResource {
* @throws ApiError
*/
public static getLogViewerValidateLogsSize({
startDate,
endDate,
}: {
startDate?: string,
endDate?: string,
}): CancelablePromise<any> {
startDate,
endDate,
}: {
startDate?: string,
endDate?: string,
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'GET',
url: '/umbraco/management/api/v1/log-viewer/validate-logs-size',

View File

@@ -69,10 +69,10 @@ export class RedirectManagementResource {
* @throws ApiError
*/
public static deleteRedirectManagementByKey({
key,
}: {
key: string,
}): CancelablePromise<any> {
key,
}: {
key: string,
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/umbraco/management/api/v1/redirect-management/{key}',

View File

@@ -27,10 +27,10 @@ export class TemplateResource {
* @throws ApiError
*/
public static postTemplate({
requestBody,
}: {
requestBody?: TemplateCreateModel,
}): CancelablePromise<any> {
requestBody,
}: {
requestBody?: TemplateCreateModel,
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'POST',
url: '/umbraco/management/api/v1/template',
@@ -69,10 +69,10 @@ export class TemplateResource {
* @throws ApiError
*/
public static deleteTemplateByKey({
key,
}: {
key: string,
}): CancelablePromise<any> {
key,
}: {
key: string,
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/umbraco/management/api/v1/template/{key}',
@@ -91,12 +91,12 @@ export class TemplateResource {
* @throws ApiError
*/
public static putTemplateByKey({
key,
requestBody,
}: {
key: string,
requestBody?: TemplateUpdateModel,
}): CancelablePromise<any> {
key,
requestBody,
}: {
key: string,
requestBody?: TemplateUpdateModel,
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'PUT',
url: '/umbraco/management/api/v1/template/{key}',

View File

@@ -1,5 +1,6 @@
import {
ContentTreeItemModel,
DictionaryItemTranslationModel,
EntityTreeItemModel,
FolderTreeItemModel,
ProblemDetailsModel,
@@ -127,6 +128,7 @@ export interface MemberDetails extends EntityTreeItemModel {
// Dictionary
export interface DictionaryDetails extends EntityTreeItemModel {
key: string; // TODO: Remove this when the backend is fixed
translations: DictionaryItemTranslationModel[];
}
// Document Blueprint

View File

@@ -29,8 +29,8 @@ import { UmbMemberGroupDetailStore } from './members/member-groups/member-group.
import { UmbMemberGroupTreeStore } from './members/member-groups/repository/member-group.tree.store';
import { UmbMemberDetailStore } from './members/members/member.detail.store';
import { UmbMemberTreeStore } from './members/members/repository/member.tree.store';
import { UmbDictionaryDetailStore } from './translation/dictionary/dictionary.detail.store';
import { UmbDictionaryTreeStore } from './translation/dictionary/dictionary.tree.store';
import { UmbDictionaryDetailStore } from './translation/dictionary/repository/dictionary.detail.store';
import { UmbDictionaryTreeStore } from './translation/dictionary/repository/dictionary.tree.store';
import { UmbDocumentBlueprintDetailStore } from './documents/document-blueprints/document-blueprint.detail.store';
import { UmbDocumentBlueprintTreeStore } from './documents/document-blueprints/document-blueprint.tree.store';
import { UmbDataTypeStore } from './settings/data-types/repository/data-type.store';

View File

@@ -29,7 +29,6 @@ import './input-media-picker/input-media-picker.element';
import './input-document-picker/input-document-picker.element';
import './empty-state/empty-state.element';
import './color-picker/color-picker.element';
import './debug/debug.element';

View File

@@ -37,6 +37,19 @@ export class UmbSectionDashboardsElement extends UmbLitElement {
display: block;
padding: var(--uui-size-5);
}
#header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
min-height: 60px;
box-sizing: border-box;
margin:0;
padding:0 var(--uui-size-5);
background-color:var(--uui-color-surface);
border-bottom:1px solid var(--uui-color-border);
}
`,
];
@@ -140,6 +153,8 @@ export class UmbSectionDashboardsElement extends UmbLitElement {
)}
</uui-tab-group>
`
: this._dashboards?.length === 1
? html`<h3 id="header">${this._dashboards[0].meta.label || this._dashboards[0].name}</h3>`
: nothing}
`;
}

View File

@@ -21,6 +21,7 @@ export class UmbSectionSidebarElement extends UmbLitElement {
font-weight: 500;
display: flex;
flex-direction: column;
z-index:10;
}
h3 {

View File

@@ -221,8 +221,8 @@ export class UmbTableElement extends LitElement {
private _renderHeaderCell(column: UmbTableColumn) {
return html`
<uui-table-head-cell style="--uui-table-cell-padding: 0">
${column.allowSorting ? html`${this._renderSortingUI(column)}` : nothing}
<uui-table-head-cell style="--uui-table-cell-padding: 0 var(--uui-size-5)">
${column.allowSorting ? html`${this._renderSortingUI(column)}` : column.name}
</uui-table-head-cell>
`;
}
@@ -284,9 +284,8 @@ export class UmbTableElement extends LitElement {
}
private _renderRowCell(column: UmbTableColumn, item: UmbTableItem) {
return html`<uui-table-cell style="width: ${column.width || 'auto'}"
>${this._renderCellContent(column, item)}</uui-table-cell
>
return html`<uui-table-cell style="--uui-table-cell-padding: 0 var(--uui-size-5); width: ${column.width || 'auto'}"
>${this._renderCellContent(column, item)}</uui-table-cell>
</uui-table-cell>`;
}

View File

@@ -27,6 +27,10 @@ export class UmbWorkspacePropertyElement extends UmbLitElement {
display: block;
}
:host(:last-child) umb-workspace-property-layout {
border-bottom:0;
}
p {
color: var(--uui-color-text-alt);
}

View File

@@ -0,0 +1,203 @@
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { when } from 'lit-html/directives/when.js';
import { UmbTableConfig, UmbTableColumn, UmbTableItem } from '../../../../backoffice/shared/components/table';
import { UmbDictionaryRepository } from '../../dictionary/repository/dictionary.repository';
import { UmbLitElement } from '@umbraco-cms/element';
import { DictionaryOverviewModel, LanguageModel } from '@umbraco-cms/backend-api';
import { UmbModalService, UMB_MODAL_SERVICE_CONTEXT_TOKEN } from '@umbraco-cms/modal';
import { UmbContextConsumerController } from '@umbraco-cms/context-api';
import { UmbCreateDictionaryModalResultData } from '../../dictionary/entity-actions/create/create-dictionary-modal-layout.element';
@customElement('umb-dashboard-translation-dictionary')
export class UmbDashboardTranslationDictionaryElement extends UmbLitElement {
static styles = [
UUITextStyles,
css`
:host {
display: flex;
flex-direction: column;
height: 100%;
}
#dictionary-top-bar {
margin-bottom: var(--uui-size-space-5);
display: flex;
justify-content: space-between;
}
umb-table {
display: inline;
padding: 0;
}
umb-empty-state {
margin: auto;
font-size: var(--uui-size-6);
}
`,
];
@state()
private _tableConfig: UmbTableConfig = {
allowSelection: false,
};
@state()
private _tableItemsFiltered: Array<UmbTableItem> = [];
#dictionaryItems: DictionaryOverviewModel[] = [];
#repo!: UmbDictionaryRepository;
#modalService!: UmbModalService;
#tableItems: Array<UmbTableItem> = [];
#tableColumns: Array<UmbTableColumn> = [];
#languages: Array<LanguageModel> = [];
constructor() {
super();
new UmbContextConsumerController(this, UMB_MODAL_SERVICE_CONTEXT_TOKEN, (instance) => {
this.#modalService = instance;
});
}
async connectedCallback() {
super.connectedCallback();
this.#repo = new UmbDictionaryRepository(this);
this.#languages = await this.#repo.getLanguages();
await this.#getDictionaryItems();
}
async #getDictionaryItems() {
if (!this.#repo) return;
const { data } = await this.#repo.list(0, 1000);
this.#dictionaryItems = data?.items ?? [];
this.#setTableColumns();
this.#setTableItems();
}
/**
* We don't know how many translation items exist for each dictionary until the data arrives
* so can not generate the columns in advance.
* @returns
*/
#setTableColumns() {
this.#tableColumns = [
{
name: 'Name',
alias: 'name',
},
];
this.#languages.forEach((l) => {
if (!l.name) return;
this.#tableColumns.push({
name: l.name ?? '',
alias: l.isoCode ?? '',
});
});
}
#setTableItems() {
this.#tableItems = this.#dictionaryItems.map((dictionary) => {
// key is name to allow filtering on the displayed value
const tableItem: UmbTableItem = {
key: dictionary.name ?? '',
icon: 'umb:book-alt',
data: [
{
columnAlias: 'name',
value: html`<a style="font-weight:bold" href="/section/translation/dictionary-item/edit/${dictionary.key}">
${dictionary.name}</a
> `,
},
],
};
this.#languages.forEach((l) => {
if (!l.isoCode) return;
tableItem.data.push({
columnAlias: l.isoCode,
value: dictionary.translatedIsoCodes?.includes(l.isoCode)
? html`<uui-icon
name="check"
title="Translation exists for ${l.name}"
style="color:var(--uui-color-positive-standalone);display:inline-block"></uui-icon>`
: html`<uui-icon
name="alert"
title="Translation does not exist for ${l.name}"
style="color:var(--uui-color-danger-standalone);display:inline-block"></uui-icon>`,
});
});
return tableItem;
});
this._tableItemsFiltered = this.#tableItems;
}
#filter(e: { target: HTMLInputElement }) {
this._tableItemsFiltered = e.target.value
? this.#tableItems.filter((t) => t.key.includes(e.target.value))
: this.#tableItems;
}
async #create() {
// TODO: what to do if modal service is not available?
if (!this.#modalService) return;
const modalHandler = this.#modalService?.open('umb-create-dictionary-modal-layout', {
type: 'sidebar',
data: { unique: null },
});
// TODO: get type from modal result
const { name }: UmbCreateDictionaryModalResultData = await modalHandler.onClose();
if (!name) return;
const result = await this.#repo?.createDetail({ name, parentKey: null, translations: [], key: '' });
// TODO => get location header to route to new item
console.log(result);
}
render() {
return html` <div id="dictionary-top-bar">
<uui-button type="button" look="outline" @click=${this.#create}>Create dictionary item</uui-button>
<uui-input
@keyup="${this.#filter}"
placeholder="Type to filter..."
label="Type to filter dictionary"
id="searchbar">
<div slot="prepend">
<uui-icon name="search" id="searchbar_icon"></uui-icon>
</div>
</uui-input>
</div>
${when(
this._tableItemsFiltered.length,
() => html` <umb-table
.config=${this._tableConfig}
.columns=${this.#tableColumns}
.items=${this._tableItemsFiltered}></umb-table>`,
() => html`<umb-empty-state>There were no dictionary items found.</umb-empty-state>`
)}`;
}
}
export default UmbDashboardTranslationDictionaryElement;
declare global {
interface HTMLElementTagNameMap {
'umb-dashboard-translation-dictionary': UmbDashboardTranslationDictionaryElement;
}
}

View File

@@ -1,100 +0,0 @@
import type { DictionaryDetails } from '@umbraco-cms/models';
import { UmbContextToken } from '@umbraco-cms/context-api';
import { ArrayState } from '@umbraco-cms/observable-api';
import { UmbEntityDetailStore, UmbStoreBase } from '@umbraco-cms/store';
import type { UmbControllerHostInterface } from '@umbraco-cms/controller';
import type { EntityTreeItemModel } from '@umbraco-cms/backend-api';
export const UMB_DICTIONARY_DETAIL_STORE_CONTEXT_TOKEN = new UmbContextToken<UmbDictionaryDetailStore>(
'UmbDictionaryDetailStore'
);
/**
* @export
* @class UmbDictionaryDetailStore
* @extends {UmbStoreBase}
* @description - Details Data Store for Data Types
*/
// TODO: use the right type for dictionary:
export class UmbDictionaryDetailStore extends UmbStoreBase implements UmbEntityDetailStore<EntityTreeItemModel> {
// TODO: use the right type:
#data = new ArrayState<EntityTreeItemModel>([], (x) => x.key);
constructor(host: UmbControllerHostInterface) {
super(host, UMB_DICTIONARY_DETAIL_STORE_CONTEXT_TOKEN.toString());
}
getScaffold(entityType: string, parentKey: string | null) {
return {} as EntityTreeItemModel;
}
/**
* @description - Request a Data Type by key. The Data Type is added to the store and is returned as an Observable.
* @param {string} key
* @return {*} {(Observable<DictionaryDetails | undefined>)}
* @memberof UmbDictionaryDetailStore
*/
getByKey(key: string) {
// TODO: use backend cli when available.
fetch(`/umbraco/management/api/v1/dictionary/details/${key}`)
.then((res) => res.json())
.then((data) => {
this.#data.append(data);
});
return this.#data.getObservablePart((documents) => documents.find((document) => document.key === key));
}
// TODO: make sure UI somehow can follow the status of this action.
/**
* @description - Save a Dictionary.
* @param {Array<DictionaryDetails>} Dictionaries
* @memberof UmbDictionaryDetailStore
* @return {*} {Promise<void>}
*/
save(data: DictionaryDetails[]) {
// fetch from server and update store
// TODO: use Fetcher API.
let body: string;
try {
body = JSON.stringify(data);
} catch (error) {
console.error(error);
return Promise.reject();
}
// TODO: use backend cli when available.
return fetch('/umbraco/management/api/v1/dictionary/save', {
method: 'POST',
body: body,
headers: {
'Content-Type': 'application/json',
},
})
.then((res) => res.json())
.then((data: Array<DictionaryDetails>) => {
this.#data.append(data);
});
}
// TODO: How can we avoid having this in both stores?
/**
* @description - Delete a Data Type.
* @param {string[]} keys
* @memberof UmbDictionaryDetailStore
* @return {*} {Promise<void>}
*/
async delete(keys: string[]) {
// TODO: use backend cli when available.
await fetch('/umbraco/backoffice/dictionary/delete', {
method: 'POST',
body: JSON.stringify(keys),
headers: {
'Content-Type': 'application/json',
},
});
this.#data.remove(keys);
}
}

View File

@@ -1,93 +0,0 @@
import { DictionaryResource, DocumentTreeItemModel } from '@umbraco-cms/backend-api';
import { tryExecuteAndNotify } from '@umbraco-cms/resources';
import { UmbContextToken } from '@umbraco-cms/context-api';
import { ArrayState } from '@umbraco-cms/observable-api';
import { UmbStoreBase } from '@umbraco-cms/store';
import { UmbControllerHostInterface } from '@umbraco-cms/controller';
export const UMB_DICTIONARY_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken<UmbDictionaryTreeStore>(
'UmbDictionaryTreeStore'
);
/**
* @export
* @class UmbDictionaryTreeStore
* @extends {UmbStoreBase}
* @description - Tree Data Store for Data Types
*/
export class UmbDictionaryTreeStore extends UmbStoreBase {
#data = new ArrayState<DocumentTreeItemModel>([], (x) => x.key);
constructor(host: UmbControllerHostInterface) {
super(host, UMB_DICTIONARY_TREE_STORE_CONTEXT_TOKEN.toString());
}
// TODO: How can we avoid having this in both stores?
/**
* @description - Delete a Data Type.
* @param {string[]} keys
* @memberof UmbDictionarysStore
* @return {*} {Promise<void>}
*/
async delete(keys: string[]) {
// TODO: use backend cli when available.
await fetch('/umbraco/backoffice/data-type/delete', {
method: 'POST',
body: JSON.stringify(keys),
headers: {
'Content-Type': 'application/json',
},
});
this.#data.remove(keys);
}
getTreeRoot() {
tryExecuteAndNotify(this._host, DictionaryResource.getTreeDictionaryRoot({})).then(({ data }) => {
if (data) {
// TODO: how do we handle if an item has been removed during this session(like in another tab or by another user)?
this.#data.append(data.items);
}
});
// TODO: how do we handle trashed items?
// TODO: remove ignore when we know how to handle trashed items.
return this.#data.getObservablePart((items) => items.filter((item) => item.parentKey === null && !item.isTrashed));
}
getTreeItemChildren(key: string) {
tryExecuteAndNotify(
this._host,
DictionaryResource.getTreeDictionaryChildren({
parentKey: key,
})
).then(({ data }) => {
if (data) {
// TODO: how do we handle if an item has been removed during this session(like in another tab or by another user)?
this.#data.append(data.items);
}
});
// TODO: how do we handle trashed items?
// TODO: remove ignore when we know how to handle trashed items.
return this.#data.getObservablePart((items) => items.filter((item) => item.parentKey === key && !item.isTrashed));
}
getTreeItems(keys: Array<string>) {
if (keys?.length > 0) {
tryExecuteAndNotify(
this._host,
DictionaryResource.getTreeDictionaryItem({
key: keys,
})
).then(({ data }) => {
if (data) {
// TODO: how do we handle if an item has been removed during this session(like in another tab or by another user)?
this.#data.append(data);
}
});
}
return this.#data.getObservablePart((items) => items.filter((item) => keys.includes(item.key ?? '')));
}
}

View File

@@ -0,0 +1,84 @@
import { html } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement, query } from 'lit/decorators.js';
import { Observable } from 'rxjs';
import { when } from 'lit-html/directives/when.js';
import { UmbModalLayoutElement } from '@umbraco-cms/modal';
export interface UmbCreateDictionaryModalData {
unique: string | null;
parentName: Observable<string | undefined>
}
export interface UmbCreateDictionaryModalResultData {
name?: string;
}
@customElement('umb-create-dictionary-modal-layout')
export class UmbCreateDictionaryModalLayoutElement extends UmbModalLayoutElement<UmbCreateDictionaryModalData> {
static styles = [UUITextStyles];
@query('#form')
private _form!: HTMLFormElement;
#parentName?: string;
connectedCallback() {
super.connectedCallback();
if (this.data?.parentName) {
this.observe(this.data.parentName, (value) => this.#parentName = value);
}
}
#handleCancel() {
this.modalHandler?.close({});
}
#submitForm() {
this._form?.requestSubmit();
}
async #handleSubmit(e: SubmitEvent) {
e.preventDefault();
const form = e.target as HTMLFormElement;
if (!form || !form.checkValidity()) return;
const formData = new FormData(form);
this.modalHandler?.close({
name: formData.get('name') as string,
});
}
render() {
return html` <umb-body-layout headline="Create">
${when(this.#parentName, () => html`<p>Create a dictionary item under <b>${this.#parentName}</b></p>`)}
<uui-form>
<form id="form" name="form" @submit=${this.#handleSubmit}>
<uui-form-layout-item>
<uui-label for="nameinput" slot="label" required>Name</uui-label>
<div>
<uui-input
type="text"
id="nameinput"
name="name"
label="name"
required
required-message="Name is required"></uui-input>
</div>
</uui-form-layout-item>
</form>
</uui-form>
<uui-button slot="actions" type="button" label="Close" @click=${this.#handleCancel}></uui-button>
<uui-button slot="actions" type="button" label="Create" look="primary" @click=${this.#submitForm}></uui-button>
</umb-body-layout>`;
}
}
declare global {
interface HTMLElementTagNameMap {
'umb-create-dictionary-modal-layout': UmbCreateDictionaryModalLayoutElement;
}
}

View File

@@ -0,0 +1,55 @@
import { UUITextStyles } from '@umbraco-ui/uui-css';
import { UmbEntityActionBase } from '../../../../shared/entity-actions';
import { UmbDictionaryRepository } from '../../repository/dictionary.repository';
import {
UmbSectionSidebarContext,
UMB_SECTION_SIDEBAR_CONTEXT_TOKEN,
} from '../../../../../backoffice/shared/components/section/section-sidebar/section-sidebar.context';
import type { UmbCreateDictionaryModalResultData } from './create-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';
// TODO: temp import
import './create-dictionary-modal-layout.element';
export default class UmbCreateDictionaryEntityAction extends UmbEntityActionBase<UmbDictionaryRepository> {
static styles = [UUITextStyles];
#modalService?: UmbModalService;
#sectionSidebarContext!: UmbSectionSidebarContext;
constructor(host: UmbControllerHostInterface, repositoryAlias: string, unique: string) {
super(host, repositoryAlias, unique);
new UmbContextConsumerController(this.host, UMB_MODAL_SERVICE_CONTEXT_TOKEN, (instance) => {
this.#modalService = instance;
});
new UmbContextConsumerController(this.host, UMB_SECTION_SIDEBAR_CONTEXT_TOKEN, (instance) => {
this.#sectionSidebarContext = instance;
});
}
async execute() {
// TODO: what to do if modal service is not available?
if (!this.#modalService) return;
// TODO: how can we get the current entity detail in the modal? Passing the observable
// feels a bit hacky. Works, but hacky.
const modalHandler = this.#modalService?.open('umb-create-dictionary-modal-layout', {
type: 'sidebar',
data: { unique: this.unique, parentName: this.#sectionSidebarContext.headline },
});
// TODO: get type from modal result
const { name }: UmbCreateDictionaryModalResultData = await modalHandler.onClose();
if (!name) return;
const result = await this.repository?.createDetail({ name, parentKey: this.unique, translations: [], key: ''});
// TODO => get location header to route to new item
console.log(result);
}
}

View File

@@ -0,0 +1,60 @@
import { html } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement, query } from 'lit/decorators.js';
import { UmbModalLayoutElement } from '@umbraco-cms/modal';
export interface UmbExportDictionaryModalData {
unique: string | null;
}
export interface UmbExportDictionaryModalResultData {
includeChildren?: boolean;
}
@customElement('umb-export-dictionary-modal-layout')
export class UmbExportDictionaryModalLayoutElement extends UmbModalLayoutElement<UmbExportDictionaryModalData> {
static styles = [UUITextStyles];
@query('#form')
private _form!: HTMLFormElement;
#handleClose() {
this.modalHandler?.close({});
}
#submitForm() {
this._form?.requestSubmit();
}
async #handleSubmit(e: SubmitEvent) {
e.preventDefault();
const form = e.target as HTMLFormElement;
if (!form) return;
const formData = new FormData(form);
this.modalHandler?.close({ includeChildren: (formData.get('includeDescendants') as string) === 'on' });
}
render() {
return html` <umb-body-layout headline="Export">
<uui-form>
<form id="form" name="form" @submit=${this.#handleSubmit}>
<uui-form-layout-item>
<uui-label for="includeDescendants" slot="label">Include descendants</uui-label>
<uui-toggle id="includeDescendants" name="includeDescendants"></uui-toggle>
</uui-form-layout-item>
</form>
</uui-form>
<uui-button slot="actions" type="button" label="Cancel" look="secondary" @click=${this.#handleClose}></uui-button>
<uui-button slot="actions" type="button" label="Export" look="primary" @click=${this.#submitForm}></uui-button>
</umb-body-layout>`;
}
}
declare global {
interface HTMLElementTagNameMap {
'umb-export-dictionary-modal-layout': UmbExportDictionaryModalLayoutElement;
}
}

View File

@@ -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 { UmbExportDictionaryModalResultData } from './export-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 './export-dictionary-modal-layout.element';
export default class UmbExportDictionaryEntityAction extends UmbEntityActionBase<UmbDictionaryRepository> {
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-export-dictionary-modal-layout', {
type: 'sidebar',
data: { unique: this.unique },
});
// TODO: get type from modal result
const { includeChildren }: UmbExportDictionaryModalResultData = await modalHandler.onClose();
if (includeChildren === undefined) return;
const result = await this.repository?.export(this.unique, includeChildren);
// TODO => get location header to route to new item
console.log(result);
}
}

View File

@@ -0,0 +1,163 @@
import { css, html } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement, query, state } from 'lit/decorators.js';
import { when } from 'lit-html/directives/when.js';
import { repeat } from 'lit/directives/repeat.js';
import { UmbTreeElement } from '../../../../shared/components/tree/tree.element';
import { UmbDictionaryRepository } from '../../repository/dictionary.repository';
import { DictionaryUploadModel } from '@umbraco-cms/backend-api';
import { UmbModalLayoutElement } from '@umbraco-cms/modal';
export interface UmbImportDictionaryModalData {
unique: string | null;
}
export interface UmbImportDictionaryModalResultData {
fileName?: string;
parentKey?: string;
}
@customElement('umb-import-dictionary-modal-layout')
export class UmbImportDictionaryModalLayoutElement extends UmbModalLayoutElement<UmbImportDictionaryModalData> {
static styles = [
UUITextStyles,
css`
uui-input {
width: 100%;
}
`,
];
@query('#form')
private _form!: HTMLFormElement;
@state()
private _uploadedDictionary?: DictionaryUploadModel;
@state()
private _showUploadView = true;
@state()
private _showImportView = false;
@state()
private _showErrorView = false;
@state()
private _selection: Array<string> = [];
#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`<p>
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)
</p>
<uui-form>
<form id="form" name="form" @submit=${this.#handleSubmit}>
<uui-form-layout-item>
<uui-label for="file" slot="label" required>File</uui-label>
<div>
<uui-input-file
accept=".udt"
name="file"
id="file"
required
required-message="File is required"></uui-input-file>
</div>
</uui-form-layout-item>
</form>
</uui-form>
<uui-button slot="actions" type="button" label="Cancel" @click=${this.#handleClose}></uui-button>
<uui-button slot="actions" type="button" label="Import" look="primary" @click=${this.#submitForm}></uui-button>`;
}
/// TODO => Tree view needs isolation and single-select option
#renderImportView() {
if (!this._uploadedDictionary?.dictionaryItems) return;
return html`
<b>Dictionary items</b>
<ul>
${repeat(
this._uploadedDictionary.dictionaryItems,
(item) => item.name,
(item) => html`<li>${item.name}</li>`
)}
</ul>
<hr />
<b>Choose where to import dictionary items (optional)</b>
<umb-tree
alias="Umb.Tree.Dictionary"
@selected=${this.#handleSelectionChange}
.selection=${this._selection}
selectable></umb-tree>
<uui-button slot="actions" type="button" label="Cancel" @click=${this.#handleClose}></uui-button>
<uui-button slot="actions" type="button" label="Import" look="primary" @click=${this.#importDictionary}></uui-button>
`;
}
// TODO => Determine what to display when dictionary import/upload fails
#renderErrorView() {
return html`Something went wrong`;
}
render() {
return html` <umb-body-layout headline="Import">
${when(this._showUploadView, () => this.#renderUploadView())}
${when(this._showImportView, () => this.#renderImportView())}
${when(this._showErrorView, () => this.#renderErrorView())}
</umb-body-layout>`;
}
}
declare global {
interface HTMLElementTagNameMap {
'umb-import-dictionary-modal-layout': UmbImportDictionaryModalLayoutElement;
}
}

View File

@@ -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<UmbDictionaryRepository> {
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);
}
}

View File

@@ -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<ManifestEntityAction> = [
{
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];

View File

@@ -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<UmbDictionaryRepository> {
static styles = [UUITextStyles];
constructor(host: UmbControllerHostInterface, repositoryAlias: string, unique: string) {
super(host, repositoryAlias, unique);
}
async execute() {
alert('refresh')
}
}

View File

@@ -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,
];

View File

@@ -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<DictionaryDetails>([], (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>(
UmbDictionaryDetailStore.name
);

View File

@@ -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<unknown>;
#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<string>) {
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<string>) {
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!');
}
}

View File

@@ -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>(
UmbDictionaryTreeStore.name
);

View File

@@ -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];

View File

@@ -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 }));
}
}

View File

@@ -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<DataSourceResponse<DictionaryItemModel>>;
list(skip?: number, take?: number): Promise<DataSourceResponse<PagedDictionaryOverviewModel>>;
get(key: string): Promise<DataSourceResponse<DictionaryItemModel>>;
insert(data: DictionaryDetails): Promise<DataSourceResponse>;
update(dictionary: DictionaryItemModel): Promise<DataSourceResponse>;
delete(key: string): Promise<DataSourceResponse>;
export(key: string, includeChildren: boolean): Promise<DataSourceResponse<Blob>>;
import(fileName: string, parentKey?: string): Promise<DataSourceResponse<any>>;
upload(formData: FormData): Promise<DataSourceResponse<DictionaryUploadModel>>;
// TODO - temp only
getLanguages(): Promise<DataSourceResponse<PagedLanguageModel>>;
}

View File

@@ -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<string>} keys
* @return {*}
* @memberof DictionaryTreeServerDataSource
*/
async getItems(keys: Array<string>) {
if (!keys || keys.length === 0) {
const error: ProblemDetailsModel = { title: 'Keys are missing' };
return { error };
}
return tryExecuteAndNotify(
this.#host,
DictionaryResource.getTreeDictionaryItem({
key: keys,
})
);
}
}

View File

@@ -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'
},
};

View File

@@ -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<ManifestTreeItemAction> = [];
export const manifests = [tree, ...treeItemActions];
export const manifests = [tree];

View File

@@ -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<EntityType | undefined>
{
#host: UmbControllerHostInterface;
#repo: UmbDictionaryRepository;
#data = new ObjectState<DictionaryDetails | undefined>(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();
}
}

View File

@@ -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`
<umb-workspace-layout alias="Umb.Workspace.Dictionary">Dictionary Workspace</umb-workspace-layout>
<umb-workspace-layout alias="Umb.Workspace.Dictionary">
<div id="header" slot="header">
<uui-button href="/section/translation/dashboard" compact>
<uui-icon name="umb:arrow-left"></uui-icon>
</uui-button>
<uui-input .value=${this._name} @input="${this.#handleInput}"></uui-input>
</div>
</umb-workspace-layout>
`;
}
}
@@ -29,6 +77,6 @@ export default UmbWorkspaceDictionaryElement;
declare global {
interface HTMLElementTagNameMap {
'umb-workspace-dictionary': UmbWorkspaceDictionaryElement;
'umb-dictionary-workspace': UmbWorkspaceDictionaryElement;
}
}

View File

@@ -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<UmbWorkspaceDictionaryElement> = () =>
html` <umb-dictionary-workspace id="${data[0].key}"></umb-dictionary-workspace>`;
AAAOverview.storyName = 'Overview';

View File

@@ -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<ManifestWorkspaceView> = [
{
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<ManifestWorkspaceAction> = [
{
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];

View File

@@ -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<LanguageModel> = [];
#workspaceContext!: UmbWorkspaceDictionaryContext;
async connectedCallback() {
super.connectedCallback();
this.#repo = new UmbDictionaryRepository(this);
this._languages = await this.#repo.getLanguages();
this.consumeContext<UmbWorkspaceDictionaryContext>('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` <umb-workspace-property-layout label=${language.name ?? language.isoCode}>
<uui-textarea
slot="editor"
name=${language.isoCode}
label="translation"
@change=${this.#onTextareaChange}
value=${ifDefined(translation?.translation)}></uui-textarea>
</umb-workspace-property-layout>`;
}
#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`
<uui-box>
<p>Edit the different language versions for the dictionary item '<em>${this._dictionary?.name}</em>' below.</p>
${repeat(
this._languages,
(item) => item.isoCode,
(item) => this.#renderTranslation(item)
)}
</uui-box>
`;
}
}
export default UmbWorkspaceViewDictionaryEditElement;
declare global {
interface HTMLElementTagNameMap {
'umb-workspace-view-dictionary-edit': UmbWorkspaceViewDictionaryEditElement;
}
}

View File

@@ -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` <umb-context-provider key="umbDataTypeContext" .value=${new UmbWorkspaceDictionaryContext(data[0])}>
${story()}
</umb-context-provider>`,*/
}
],
} as Meta;
export const AAAOverview: Story<UmbWorkspaceViewDictionaryEditElement> = () =>
html` <umb-workspace-view-dictionary-edit></umb-workspace-view-dictionary-edit>`;
AAAOverview.storyName = 'Overview';

View File

@@ -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<ManifestDashboard> = [
{
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];

View File

@@ -7,20 +7,36 @@ export const data: Array<DictionaryDetails> = [
{
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'
}],
},
];

View File

@@ -7,6 +7,10 @@ export class UmbEntityData<T extends Entity> extends UmbData<T> {
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);
}

View File

@@ -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<DictionaryOverviewModel> = [
{
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));
}),
];