Merge branch 'main' into feature/debug-component
This commit is contained in:
@@ -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
|
||||
>
|
||||
|
||||
@@ -45,4 +45,3 @@ export type ConstructorInfoModel = {
|
||||
readonly isSecurityTransparent?: boolean;
|
||||
memberType?: MemberTypesModel;
|
||||
};
|
||||
|
||||
|
||||
@@ -6,4 +6,3 @@ export type CultureModel = {
|
||||
name?: string;
|
||||
englishName?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -10,4 +10,3 @@ export type DataTypeModelBaseModel = {
|
||||
propertyEditorUiAlias?: string | null;
|
||||
data?: Array<DataTypePropertyModel>;
|
||||
};
|
||||
|
||||
|
||||
@@ -6,4 +6,3 @@ export type DataTypePropertyModel = {
|
||||
alias?: string;
|
||||
value?: any;
|
||||
};
|
||||
|
||||
|
||||
@@ -6,4 +6,3 @@ export type DataTypePropertyReferenceModel = {
|
||||
name?: string;
|
||||
alias?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -12,4 +12,3 @@ export type DatabaseInstallModel = {
|
||||
useIntegratedAuthentication?: boolean;
|
||||
connectionString?: string | null;
|
||||
};
|
||||
|
||||
|
||||
@@ -15,4 +15,3 @@ export type DatabaseSettingsModel = {
|
||||
supportsIntegratedAuthentication?: boolean;
|
||||
requiresConnectionTest?: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -8,4 +8,3 @@ export type DictionaryItemModelBaseModel = {
|
||||
name?: string;
|
||||
translations?: Array<DictionaryItemTranslationModel>;
|
||||
};
|
||||
|
||||
|
||||
@@ -6,4 +6,3 @@ export type DictionaryItemTranslationModel = {
|
||||
isoCode?: string;
|
||||
translation?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -7,4 +7,3 @@ export type DictionaryItemsImportModel = {
|
||||
name?: string | null;
|
||||
parentKey?: string | null;
|
||||
};
|
||||
|
||||
|
||||
@@ -8,4 +8,3 @@ export type DictionaryOverviewModel = {
|
||||
parentKey?: string | null;
|
||||
translatedIsoCodes?: Array<string>;
|
||||
};
|
||||
|
||||
|
||||
@@ -37,4 +37,3 @@ export type FieldInfoModel = {
|
||||
readonly isSecurityTransparent?: boolean;
|
||||
fieldHandle?: RuntimeFieldHandleModel;
|
||||
};
|
||||
|
||||
|
||||
@@ -6,4 +6,3 @@ export type FieldModel = {
|
||||
name?: string;
|
||||
values?: Array<string>;
|
||||
};
|
||||
|
||||
|
||||
@@ -12,4 +12,3 @@ export type HealthCheckActionModel = {
|
||||
providedValueValidation?: string | null;
|
||||
providedValueValidationRegex?: string | null;
|
||||
};
|
||||
|
||||
|
||||
@@ -8,4 +8,3 @@ export type HelpPageModel = {
|
||||
url?: string | null;
|
||||
type?: string | null;
|
||||
};
|
||||
|
||||
|
||||
@@ -13,4 +13,3 @@ export type IndexModel = {
|
||||
fieldCount: number;
|
||||
providerProperties?: Record<string, any> | null;
|
||||
};
|
||||
|
||||
|
||||
@@ -4,4 +4,3 @@
|
||||
|
||||
export type IntPtrModel = {
|
||||
};
|
||||
|
||||
|
||||
@@ -4,4 +4,3 @@
|
||||
|
||||
export type JsonNamingPolicyModel = {
|
||||
};
|
||||
|
||||
|
||||
@@ -8,4 +8,3 @@ export type LanguageModelBaseModel = {
|
||||
isMandatory?: boolean;
|
||||
fallbackIsoCode?: string | null;
|
||||
};
|
||||
|
||||
|
||||
@@ -6,4 +6,3 @@ export type LogMessagePropertyModel = {
|
||||
name?: string;
|
||||
value?: string | null;
|
||||
};
|
||||
|
||||
|
||||
@@ -6,4 +6,3 @@ export type LogTemplateModel = {
|
||||
messageTemplate?: string | null;
|
||||
count?: number;
|
||||
};
|
||||
|
||||
|
||||
@@ -45,4 +45,3 @@ export type MethodBaseModel = {
|
||||
readonly isSecuritySafeCritical?: boolean;
|
||||
readonly isSecurityTransparent?: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -13,4 +13,3 @@ export type ModelsBuilderModel = {
|
||||
modelsNamespace?: string | null;
|
||||
trackingOutOfDateModels?: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -5,4 +5,3 @@
|
||||
export type ModuleHandleModel = {
|
||||
readonly mdStreamVersion?: number;
|
||||
};
|
||||
|
||||
|
||||
@@ -5,4 +5,3 @@
|
||||
export type NotFoundResultModel = {
|
||||
statusCode?: number;
|
||||
};
|
||||
|
||||
|
||||
@@ -5,4 +5,3 @@
|
||||
export type OkResultModel = {
|
||||
statusCode?: number;
|
||||
};
|
||||
|
||||
|
||||
@@ -5,4 +5,3 @@
|
||||
export type ProfilingStatusModel = {
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -11,4 +11,3 @@ export type RecycleBinItemModel = {
|
||||
isContainer?: boolean;
|
||||
parentKey?: string | null;
|
||||
};
|
||||
|
||||
|
||||
@@ -10,4 +10,3 @@ export type RedirectUrlModel = {
|
||||
contentKey?: string;
|
||||
culture?: string | null;
|
||||
};
|
||||
|
||||
|
||||
@@ -4,4 +4,3 @@
|
||||
|
||||
export type ReferenceHandlerModel = {
|
||||
};
|
||||
|
||||
|
||||
@@ -13,4 +13,3 @@ export type RelationItemModel = {
|
||||
relationTypeIsBidirectional?: boolean;
|
||||
relationTypeIsDependency?: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -10,4 +10,3 @@ export type RelationModel = {
|
||||
createDate?: string;
|
||||
comment?: string | null;
|
||||
};
|
||||
|
||||
|
||||
@@ -6,4 +6,3 @@ export type SavedLogSearchModel = {
|
||||
name?: string;
|
||||
query?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -10,4 +10,3 @@ export type SearchResultModel = {
|
||||
readonly fieldCount?: number;
|
||||
fields?: Array<FieldModel>;
|
||||
};
|
||||
|
||||
|
||||
@@ -5,4 +5,3 @@
|
||||
export type SearcherModel = {
|
||||
name?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -7,4 +7,3 @@ export type TemplateModelBaseModel = {
|
||||
alias?: string;
|
||||
content?: string | null;
|
||||
};
|
||||
|
||||
|
||||
@@ -9,4 +9,3 @@ export type TemplateQueryExecuteFilterModel = {
|
||||
constraintValue?: string;
|
||||
operator?: OperatorModel;
|
||||
};
|
||||
|
||||
|
||||
@@ -12,4 +12,3 @@ export type TemplateQueryExecuteModel = {
|
||||
sort?: TemplateQueryExecuteSortModel | null;
|
||||
take?: number;
|
||||
};
|
||||
|
||||
|
||||
@@ -6,4 +6,3 @@ export type TemplateQueryExecuteSortModel = {
|
||||
propertyAlias?: string;
|
||||
direction?: string | null;
|
||||
};
|
||||
|
||||
|
||||
@@ -6,4 +6,3 @@ export type TemplateQueryResultItemModel = {
|
||||
icon?: string;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -5,4 +5,3 @@
|
||||
export type TemplateScaffoldModel = {
|
||||
content?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -8,4 +8,3 @@ export type TreeItemModel = {
|
||||
icon?: string;
|
||||
hasChildren?: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -101,4 +101,3 @@ export type TypeInfoModel = {
|
||||
readonly declaredProperties?: Array<PropertyInfoModel>;
|
||||
readonly implementedInterfaces?: Array<TypeModel>;
|
||||
};
|
||||
|
||||
|
||||
@@ -86,4 +86,3 @@ export type TypeModel = {
|
||||
readonly containsGenericParameters?: boolean;
|
||||
readonly isVisible?: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -9,4 +9,3 @@ export type UpgradeSettingsModel = {
|
||||
oldVersion?: string;
|
||||
readonly reportUrl?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -8,4 +8,3 @@ export type UserInstallModel = {
|
||||
password: string;
|
||||
readonly subscribeToNewsletter?: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -9,4 +9,3 @@ export type UserSettingsModel = {
|
||||
minNonAlphaNumericLength?: number;
|
||||
consentLevels?: Array<ConsentLevelModel>;
|
||||
};
|
||||
|
||||
|
||||
@@ -5,4 +5,3 @@
|
||||
export type VersionModel = {
|
||||
version?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -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}',
|
||||
|
||||
@@ -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}',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}',
|
||||
|
||||
@@ -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}',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
@@ -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}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ export class UmbSectionSidebarElement extends UmbLitElement {
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index:10;
|
||||
}
|
||||
|
||||
h3 {
|
||||
|
||||
@@ -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>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 ?? '')));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
@@ -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
|
||||
);
|
||||
@@ -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!');
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
@@ -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];
|
||||
@@ -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 }));
|
||||
}
|
||||
}
|
||||
@@ -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>>;
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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];
|
||||
|
||||
@@ -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'
|
||||
}],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}),
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user