This commit is contained in:
Niels Lyngsø
2023-03-29 15:55:57 +02:00
158 changed files with 3480 additions and 750 deletions

View File

@@ -1,6 +1,6 @@
{
"cssVariables.lookupFiles": ["node_modules/@umbraco-ui/uui-css/dist/custom-properties.css"],
"cSpell.words": ["combobox", "variantable"],
"cSpell.words": ["combobox", "templating", "variantable"],
"exportall.config.folderListener": [],
"exportall.config.relExclusion": []
}

View File

@@ -1,4 +1,4 @@
import { UmbContextToken } from '../context-token';
import { UmbContextToken } from '../token/context-token';
import { UmbContextConsumer } from './context-consumer';
import { UmbContextCallback } from './context-request.event';
import type { UmbControllerHostInterface, UmbControllerInterface } from '@umbraco-cms/backoffice/controller';

View File

@@ -1,4 +1,4 @@
import { UmbContextToken } from '../context-token';
import { UmbContextToken } from '../token/context-token';
import { isUmbContextProvideEventType, umbContextProvideEventType } from '../provide/context-provide.event';
import { UmbContextRequestEventImplementation, UmbContextCallback } from './context-request.event';

View File

@@ -1,4 +1,4 @@
import { UmbContextToken } from '../context-token';
import { UmbContextToken } from '../token/context-token';
export const umbContextRequestEventType = 'umb:context-request';
export const umbDebugContextEventType = 'umb:debug-contexts';
@@ -33,9 +33,8 @@ export const isUmbContextRequestEvent = (event: Event): event is UmbContextReque
return event.type === umbContextRequestEventType;
};
export class UmbContextDebugRequest extends Event {
public constructor(public readonly callback:any) {
public constructor(public readonly callback: any) {
super(umbDebugContextEventType, { bubbles: true, composed: true, cancelable: false });
}
}
}

View File

@@ -4,4 +4,4 @@ export * from './consume/context-request.event';
export * from './provide/context-provider.controller';
export * from './provide/context-provider';
export * from './provide/context-provide.event';
export * from './context-token';
export * from './token/context-token';

View File

@@ -1,4 +1,4 @@
import { UmbContextToken } from '../context-token';
import { UmbContextToken } from '../token/context-token';
export const umbContextProvideEventType = 'umb:context-provide';

View File

@@ -1,4 +1,4 @@
import { UmbContextToken } from '../context-token';
import { UmbContextToken } from '../token/context-token';
import { UmbContextProvider } from './context-provider';
import type { UmbControllerHostInterface, UmbControllerInterface } from '@umbraco-cms/backoffice/controller';

View File

@@ -1,5 +1,9 @@
import { umbContextRequestEventType, isUmbContextRequestEvent, umbDebugContextEventType } from '../consume/context-request.event';
import { UmbContextToken } from '../context-token';
import {
umbContextRequestEventType,
isUmbContextRequestEvent,
umbDebugContextEventType,
} from '../consume/context-request.event';
import { UmbContextToken } from '../token/context-token';
import { UmbContextProvideEventImplementation } from './context-provide.event';
/**
@@ -68,14 +72,14 @@ export class UmbContextProvider<HostType extends EventTarget = EventTarget> {
private _handleDebugContextRequest = (event: any) => {
// If the event doesn't have an instances property, create it.
if(!event.instances){
if (!event.instances) {
event.instances = new Map();
}
// If the event doesn't have an instance for this context, add it.
// Nearest to the DOM element of <umb-debug> will be added first
// as contexts can change/override deeper in the DOM
if(!event.instances.has(this._contextAlias)){
if (!event.instances.has(this._contextAlias)) {
event.instances.set(this._contextAlias, this.#instance);
}
};

View File

@@ -1,7 +1,7 @@
import { expect } from '@open-wc/testing';
import { UmbContextConsumer } from './consume/context-consumer';
import { UmbContextConsumer } from '../consume/context-consumer';
import { UmbContextProvider } from '../provide/context-provider';
import { UmbContextToken } from './context-token';
import { UmbContextProvider } from './provide/context-provider';
const testContextAlias = 'my-test-context';

View File

@@ -47,7 +47,7 @@ export class UmbExtensionRegistry {
const nextData = this._kinds
.getValue()
.filter(
(k) => k.matchType !== (kind as ManifestKind).matchType && k.matchKind !== (kind as ManifestKind).matchKind
(k) => !(k.matchType === (kind as ManifestKind).matchType && k.matchKind === (kind as ManifestKind).matchKind)
);
nextData.push(kind as ManifestKind);
this._kinds.next(nextData);
@@ -125,7 +125,10 @@ export class UmbExtensionRegistry {
T extends ManifestBase = SpecificManifestTypeOrManifestBase<Key>
>(type: Key, alias: string) {
return combineLatest([
this.extensions.pipe(map((exts) => exts.find((ext) => ext.type === type && ext.alias === alias))),
this.extensions.pipe(
map((exts) => exts.find((ext) => ext.type === type && ext.alias === alias)),
distinctUntilChanged(extensionSingleMemoization)
),
this._kindsOfType(type),
]).pipe(
map(([ext, kinds]) => {

View File

@@ -16,6 +16,7 @@ import type { ManifestMenu } from './menu.models';
import type { ManifestMenuItem, ManifestMenuItemTreeKind } from './menu-item.models';
import type { ManifestTheme } from './theme.models';
import type { ManifestTree } from './tree.models';
import type { ManifestTreeItem } from './tree-item.models';
import type { ManifestUserDashboard } from './user-dashboard.models';
import type { ManifestWorkspace } from './workspace.models';
import type { ManifestWorkspaceAction } from './workspace-action.models';
@@ -44,6 +45,7 @@ export * from './menu.models';
export * from './menu-item.models';
export * from './theme.models';
export * from './tree.models';
export * from './tree-item.models';
export * from './user-dashboard.models';
export * from './workspace-action.models';
export * from './workspace-view-collection.models';
@@ -78,6 +80,7 @@ export type ManifestTypes =
| ManifestMenuItemTreeKind
| ManifestTheme
| ManifestTree
| ManifestTreeItem
| ManifestUserDashboard
| ManifestWorkspace
| ManifestWorkspaceAction

View File

@@ -1,10 +1,10 @@
import type { ManifestClass } from './models';
import { UmbStoreBase, UmbTreeStoreBase } from '@umbraco-cms/backoffice/store';
import { UmbStoreBase, UmbTreeStore } from '@umbraco-cms/backoffice/store';
export interface ManifestStore extends ManifestClass<UmbStoreBase> {
type: 'store';
}
export interface ManifestTreeStore extends ManifestClass<UmbTreeStoreBase> {
export interface ManifestTreeStore extends ManifestClass<UmbTreeStore> {
type: 'treeStore';
}

View File

@@ -0,0 +1,10 @@
import type { ManifestElement } from './models';
export interface ManifestTreeItem extends ManifestElement {
type: 'treeItem';
conditions: ConditionsTreeItem;
}
export interface ConditionsTreeItem {
entityType: string;
}

View File

@@ -1,5 +1,4 @@
import type { ManifestBase } from './models';
import type { ClassConstructor } from '@umbraco-cms/backoffice/models';
export interface ManifestTree extends ManifestBase {
type: 'tree';
@@ -7,6 +6,5 @@ export interface ManifestTree extends ManifestBase {
}
export interface MetaTree {
storeAlias?: string;
repository?: ClassConstructor<unknown>;
repositoryAlias: string;
}

View File

@@ -0,0 +1,9 @@
import type { DataSourceResponse } from '@umbraco-cms/backoffice/repository';
export interface UmbDataSource<T> {
createScaffold(parentKey: string | null): Promise<DataSourceResponse<T>>;
get(key: string): Promise<DataSourceResponse<T>>;
insert(data: T): Promise<DataSourceResponse<T>>;
update(data: T): Promise<DataSourceResponse<T>>;
delete(key: string): Promise<DataSourceResponse<T>>;
}

View File

@@ -0,0 +1,3 @@
export * from './data-source-response.interface';
export * from './data-source.interface';
export * from './tree-data-source.interface';

View File

@@ -0,0 +1,7 @@
import type { DataSourceResponse } from '@umbraco-cms/backoffice/repository';
export interface UmbTreeDataSource<PagedItemsType = any, ItemsType = any> {
getRootItems(): Promise<DataSourceResponse<PagedItemsType>>;
getChildrenOf(parentUnique: string): Promise<DataSourceResponse<PagedItemsType>>;
getItems(unique: Array<string>): Promise<DataSourceResponse<Array<ItemsType>>>;
}

View File

@@ -1,5 +1,3 @@
export * from './data-source-response.interface';
export * from './data-source';
export * from './detail-repository.interface';
export * from './tree-repository.interface';
export * from './repository-tree-data-source.interface';
export * from './repository-detail-data-source.interface';

View File

@@ -1,9 +0,0 @@
import type { DataSourceResponse } from '@umbraco-cms/backoffice/repository';
export interface RepositoryDetailDataSource<DetailType> {
createScaffold(parentKey: string | null): Promise<DataSourceResponse<DetailType>>;
get(key: string): Promise<DataSourceResponse<DetailType>>;
insert(data: DetailType): Promise<DataSourceResponse<DetailType>>;
update(data: DetailType): Promise<DataSourceResponse<DetailType>>;
delete(key: string): Promise<DataSourceResponse<DetailType>>;
}

View File

@@ -1,8 +0,0 @@
import { EntityTreeItemResponseModel, PagedEntityTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api';
import type { DataSourceResponse } from '@umbraco-cms/backoffice/repository';
export interface RepositoryTreeDataSource {
getRootItems(): Promise<DataSourceResponse<PagedEntityTreeItemResponseModel>>;
getChildrenOf(parentKey: string): Promise<DataSourceResponse<PagedEntityTreeItemResponseModel>>;
getItems(key: Array<string>): Promise<DataSourceResponse<EntityTreeItemResponseModel[]>>;
}

View File

@@ -1,24 +1,29 @@
import type { Observable } from 'rxjs';
import { EntityTreeItemResponseModel, PagedEntityTreeItemResponseModel, ProblemDetailsModel } from '@umbraco-cms/backoffice/backend-api';
import { ProblemDetailsModel } from '@umbraco-cms/backoffice/backend-api';
export interface UmbTreeRepository {
requestRootTreeItems: () => Promise<{
data: PagedEntityTreeItemResponseModel | undefined;
error: ProblemDetailsModel | undefined;
asObservable?: () => Observable<EntityTreeItemResponseModel[]>;
}>;
requestTreeItemsOf: (parentKey: string | null) => Promise<{
data: PagedEntityTreeItemResponseModel | undefined;
error: ProblemDetailsModel | undefined;
asObservable?: () => Observable<EntityTreeItemResponseModel[]>;
}>;
requestTreeItems: (keys: string[]) => Promise<{
data: Array<EntityTreeItemResponseModel> | undefined;
error: ProblemDetailsModel | undefined;
asObservable?: () => Observable<EntityTreeItemResponseModel[]>;
}>;
rootTreeItems: () => Promise<Observable<EntityTreeItemResponseModel[]>>;
treeItemsOf: (parentKey: string | null) => Promise<Observable<EntityTreeItemResponseModel[]>>;
treeItems: (keys: string[]) => Promise<Observable<EntityTreeItemResponseModel[]>>;
export interface UmbPagedData<T> {
total: number;
items: Array<T>;
}
export interface UmbTreeRepository<ItemType = any, PagedItemType = UmbPagedData<ItemType>> {
requestRootTreeItems: () => Promise<{
data: PagedItemType | undefined;
error: ProblemDetailsModel | undefined;
asObservable?: () => Observable<ItemType[]>;
}>;
requestTreeItemsOf: (parentUnique: string | null) => Promise<{
data: PagedItemType | undefined;
error: ProblemDetailsModel | undefined;
asObservable?: () => Observable<ItemType[]>;
}>;
requestTreeItems: (uniques: string[]) => Promise<{
data: Array<ItemType> | undefined;
error: ProblemDetailsModel | undefined;
asObservable?: () => Observable<ItemType[]>;
}>;
rootTreeItems: () => Promise<Observable<ItemType[]>>;
treeItemsOf: (parentUnique: string | null) => Promise<Observable<ItemType[]>>;
treeItems: (uniques: string[]) => Promise<Observable<ItemType[]>>;
}

View File

@@ -1,21 +1,20 @@
import { EntityTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api';
import { ArrayState, partialUpdateFrozenArray } from '@umbraco-cms/backoffice/observable-api';
import { UmbStoreBase } from '@umbraco-cms/backoffice/store';
import { UmbStoreBase, UmbTreeStore } from '@umbraco-cms/backoffice/store';
/**
* @export
* @class UmbTreeStoreBase
* @class UmbEntityTreeStore
* @extends {UmbStoreBase}
* @description - General Tree Data Store
*/
// TODO: consider if tree store could be turned into a general EntityTreeStore class?
export class UmbTreeStoreBase extends UmbStoreBase {
export class UmbEntityTreeStore extends UmbStoreBase implements UmbTreeStore<EntityTreeItemResponseModel> {
#data = new ArrayState<EntityTreeItemResponseModel>([], (x) => x.key);
/**
* Appends items to the store
* @param {Array<EntityTreeItemModel>} items
* @memberof UmbTreeStoreBase
* @param {Array<EntityTreeItemResponseModel>} items
* @memberof UmbEntityTreeStore
*/
appendItems(items: Array<EntityTreeItemResponseModel>) {
this.#data.append(items);
@@ -24,8 +23,8 @@ export class UmbTreeStoreBase extends UmbStoreBase {
/**
* Updates an item in the store
* @param {string} key
* @param {Partial<EntityTreeItemModel>} data
* @memberof UmbTreeStoreBase
* @param {Partial<EntityTreeItemResponseModel>} data
* @memberof UmbEntityTreeStore
*/
updateItem(key: string, data: Partial<EntityTreeItemResponseModel>) {
this.#data.next(partialUpdateFrozenArray(this.#data.getValue(), data, (entry) => entry.key === key));
@@ -34,7 +33,7 @@ export class UmbTreeStoreBase extends UmbStoreBase {
/**
* Removes an item from the store
* @param {string} key
* @memberof UmbTreeStoreBase
* @memberof UmbEntityTreeStore
*/
removeItem(key: string) {
this.#data.removeOne(key);
@@ -42,7 +41,7 @@ export class UmbTreeStoreBase extends UmbStoreBase {
/**
* An observable to observe the root items
* @memberof UmbTreeStoreBase
* @memberof UmbEntityTreeStore
*/
rootItems = this.#data.getObservablePart((items) => items.filter((item) => item.parentKey === null));
@@ -50,7 +49,7 @@ export class UmbTreeStoreBase extends UmbStoreBase {
* Returns an observable to observe the children of a given parent
* @param {(string | null)} parentKey
* @return {*}
* @memberof UmbTreeStoreBase
* @memberof UmbEntityTreeStore
*/
childrenOf(parentKey: string | null) {
return this.#data.getObservablePart((items) => items.filter((item) => item.parentKey === parentKey));
@@ -60,7 +59,7 @@ export class UmbTreeStoreBase extends UmbStoreBase {
* Returns an observable to observe the items with the given keys
* @param {Array<string>} keys
* @return {*}
* @memberof UmbTreeStoreBase
* @memberof UmbEntityTreeStore
*/
items(keys: Array<string>) {
return this.#data.getObservablePart((items) => items.filter((item) => keys.includes(item.key ?? '')));

View File

@@ -0,0 +1,67 @@
import { FileSystemTreeItemPresentationModel } from '@umbraco-cms/backoffice/backend-api';
import { ArrayState, partialUpdateFrozenArray } from '@umbraco-cms/backoffice/observable-api';
import { UmbStoreBase, UmbTreeStore } from '@umbraco-cms/backoffice/store';
/**
* @export
* @class UmbFileSystemTreeStore
* @extends {UmbStoreBase}
* @description - General Tree Data Store
*/
export class UmbFileSystemTreeStore extends UmbStoreBase implements UmbTreeStore<FileSystemTreeItemPresentationModel> {
#data = new ArrayState<FileSystemTreeItemPresentationModel>([], (x) => x.path);
/**
* Appends items to the store
* @param {Array<FileSystemTreeItemPresentationModel>} items
* @memberof UmbFileSystemTreeStore
*/
appendItems(items: Array<FileSystemTreeItemPresentationModel>) {
this.#data.append(items);
}
/**
* Updates an item in the store
* @param {string} path
* @param {Partial<FileSystemTreeItemPresentationModel>} data
* @memberof UmbFileSystemTreeStore
*/
updateItem(path: string, data: Partial<FileSystemTreeItemPresentationModel>) {
this.#data.appendOne(data)
}
/**
* Removes an item from the store
* @param {string} path
* @memberof UmbFileSystemTreeStore
*/
removeItem(path: string) {
this.#data.removeOne(path);
}
/**
* An observable to observe the root items
* @memberof UmbFileSystemTreeStore
*/
rootItems = this.#data.getObservablePart((items) => items.filter((item) => item.path?.includes('/') === false));
/**
* Returns an observable to observe the children of a given parent
* @param {(string | null)} parentPath
* @return {*}
* @memberof UmbFileSystemTreeStore
*/
childrenOf(parentPath: string | null) {
return this.#data.getObservablePart((items) => items.filter((item) => item.path?.startsWith(parentPath + '/')));
}
/**
* Returns an observable to observe the items with the given keys
* @param {Array<string>} paths
* @return {*}
* @memberof UmbFileSystemTreeStore
*/
items(paths: Array<string>) {
return this.#data.getObservablePart((items) => items.filter((item) => paths.includes(item.path ?? '')));
}
}

View File

@@ -1,3 +1,5 @@
export * from './store';
export * from './store-base';
export * from './tree-store-base';
export * from './entity-tree-store';
export * from './file-system-tree.store';
export * from './tree-store.interface';

View File

@@ -9,33 +9,7 @@ export interface UmbDataStore {
readonly storeAlias: string;
}
export interface UmbTreeStore<T> extends UmbDataStore {
getTreeRoot(): Observable<Array<T>>;
getTreeItemChildren(key: string): Observable<Array<T>>;
// Notice: this might not be right to put here as only some content items has ability to be trashed.
/**
* @description - Trash data.
* @param {string[]} keys
* @return {*} {(Promise<void>)}
* @memberof UmbTreeStore
*/
trash(keys: string[]): Promise<void>;
// Notice: this might not be right to put here as only some content items has ability to be moved.
/**
* @description - Move data.
* @param {string[]} keys
* @return {*} {(Promise<void>)}
* @memberof UmbTreeStore
*/
move(keys: string[], destination: string): Promise<void>;
}
export interface UmbEntityDetailStore<T> extends UmbDataStore {
/**
* @description - Request scaffold data by entityType and . The data is added to the store and is returned as an Observable.
* @param {string} key
@@ -61,10 +35,7 @@ export interface UmbEntityDetailStore<T> extends UmbDataStore {
save(data: T[]): Promise<void>;
}
export interface UmbContentStore<T> extends UmbEntityDetailStore<T> {
// TODO: make something that is specific for UmbContentStore, or then we should get rid of it. But for now i kept it as we might want this for rollback or other things specific to Content types.
save(data: T[]): Promise<void>;
}

View File

@@ -0,0 +1,12 @@
import type { Observable } from 'rxjs';
import { TreeItemPresentationModel } from '../backend-api';
export interface UmbTreeStore<T extends TreeItemPresentationModel = any> {
appendItems: (items: Array<T>) => void;
updateItem: (unique: string, item: Partial<T>) => void;
removeItem: (unique: string) => void;
rootItems: Observable<Array<T>>;
childrenOf: (parentUnique: string | null) => Observable<Array<T>>;
items: (uniques: Array<string>) => Observable<Array<T>>;
}

View File

@@ -1,4 +1,4 @@
import { UmbWorkspaceContextInterface } from '../../../../src/backoffice/shared/components/workspace/workspace-context/workspace-context.interface';
import { UmbWorkspaceContextInterface } from '../../context/workspace-context.interface';
import { UmbWorkspaceActionBase } from '../workspace-action-base';
import type { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller';

View File

@@ -0,0 +1 @@
export * from './workspace-context.interface';

View File

@@ -7,6 +7,7 @@ export interface UmbWorkspaceContextInterface<T = unknown> {
isNew: Observable<boolean>;
getIsNew(): boolean;
setIsNew(value: boolean): void;
// TODO: should we consider another name than entity type. File system files are not entities but still have this type.
getEntityType(): string;
getData(): T;
destroy(): void;

View File

@@ -1 +1,2 @@
export * from './actions';
export * from './context';

View File

@@ -1,5 +1,5 @@
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UmbTreeStoreBase } from '@umbraco-cms/backoffice/store';
import { UmbEntityTreeStore } from '@umbraco-cms/backoffice/store';
import type { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller';
export const UMB_DOCUMENT_BLUEPRINT_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken<UmbDocumentBlueprintTreeStore>(
@@ -12,7 +12,7 @@ export const UMB_DOCUMENT_BLUEPRINT_TREE_STORE_CONTEXT_TOKEN = new UmbContextTok
* @extends {UmbStoreBase}
* @description - Tree Data Store for Document Blueprints
*/
export class UmbDocumentBlueprintTreeStore extends UmbTreeStoreBase {
export class UmbDocumentBlueprintTreeStore extends UmbEntityTreeStore {
constructor(host: UmbControllerHostInterface) {
super(host, UMB_DOCUMENT_BLUEPRINT_TREE_STORE_CONTEXT_TOKEN.toString());
}

View File

@@ -2,11 +2,7 @@ import { DocumentTypeTreeServerDataSource } from './sources/document-type.tree.s
import { UmbDocumentTypeServerDataSource } from './sources/document-type.server.data';
import { UmbDocumentTypeTreeStore, UMB_DOCUMENT_TYPE_TREE_STORE_CONTEXT_TOKEN } from './document-type.tree.store';
import { UmbDocumentTypeStore, UMB_DOCUMENT_TYPE_STORE_CONTEXT_TOKEN } from './document-type.store';
import type {
RepositoryTreeDataSource,
UmbTreeRepository,
UmbDetailRepository,
} from '@umbraco-cms/backoffice/repository';
import type { UmbTreeDataSource, UmbTreeRepository, UmbDetailRepository } from '@umbraco-cms/backoffice/repository';
import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller';
import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api';
import { ProblemDetailsModel, DocumentTypeResponseModel } from '@umbraco-cms/backoffice/backend-api';
@@ -14,12 +10,12 @@ import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco
type ItemType = DocumentTypeResponseModel;
export class UmbDocumentTypeRepository implements UmbTreeRepository, UmbDetailRepository<ItemType> {
export class UmbDocumentTypeRepository implements UmbTreeRepository<ItemType>, UmbDetailRepository<ItemType> {
#init!: Promise<unknown>;
#host: UmbControllerHostInterface;
#treeSource: RepositoryTreeDataSource;
#treeSource: UmbTreeDataSource;
#treeStore?: UmbDocumentTypeTreeStore;
#detailDataSource: UmbDocumentTypeServerDataSource;

View File

@@ -1,5 +1,5 @@
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UmbTreeStoreBase } from '@umbraco-cms/backoffice/store';
import { UmbEntityTreeStore } from '@umbraco-cms/backoffice/store';
import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller';
/**
@@ -9,7 +9,7 @@ import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller';
* @description - Tree Data Store for Document-Types
*/
// TODO: consider if tree store could be turned into a general EntityTreeStore class?
export class UmbDocumentTypeTreeStore extends UmbTreeStoreBase {
export class UmbDocumentTypeTreeStore extends UmbEntityTreeStore {
/**
* Creates an instance of UmbDocumentTypeTreeStore.
* @param {UmbControllerHostInterface} host

View File

@@ -1,5 +1,9 @@
import { RepositoryDetailDataSource } from '@umbraco-cms/backoffice/repository';
import { DocumentTypeResource, ProblemDetailsModel, DocumentTypeResponseModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbDataSource } from '@umbraco-cms/backoffice/repository';
import {
DocumentTypeResource,
ProblemDetailsModel,
DocumentTypeResponseModel,
} from '@umbraco-cms/backoffice/backend-api';
import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
@@ -9,7 +13,7 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
* @class UmbDocumentTypeServerDataSource
* @implements {RepositoryDetailDataSource}
*/
export class UmbDocumentTypeServerDataSource implements RepositoryDetailDataSource<DocumentTypeResponseModel> {
export class UmbDocumentTypeServerDataSource implements UmbDataSource<DocumentTypeResponseModel> {
#host: UmbControllerHostInterface;
/**

View File

@@ -1,4 +1,4 @@
import type { RepositoryTreeDataSource } from '@umbraco-cms/backoffice/repository';
import type { UmbTreeDataSource } from '@umbraco-cms/backoffice/repository';
import { ProblemDetailsModel, DocumentTypeResource } from '@umbraco-cms/backoffice/backend-api';
import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
@@ -9,7 +9,7 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
* @class DocumentTreeServerDataSource
* @implements {DocumentTreeDataSource}
*/
export class DocumentTypeTreeServerDataSource implements RepositoryTreeDataSource {
export class DocumentTypeTreeServerDataSource implements UmbTreeDataSource {
#host: UmbControllerHostInterface;
// TODO: how do we handle trashed items?

View File

@@ -1,13 +1,23 @@
import { UmbDocumentTypeRepository } from '../repository/document-type.repository';
import type { ManifestTree } from '@umbraco-cms/backoffice/extensions-registry';
import { DOCUMENT_TYPE_REPOSITORY_ALIAS } from '../repository/manifests';
import type { ManifestTree, ManifestTreeItem } from '@umbraco-cms/backoffice/extensions-registry';
const tree: ManifestTree = {
type: 'tree',
alias: 'Umb.Tree.DocumentTypes',
name: 'Document Types Tree',
meta: {
repository: UmbDocumentTypeRepository,
repositoryAlias: DOCUMENT_TYPE_REPOSITORY_ALIAS,
},
};
export const manifests = [tree];
const treeItem: ManifestTreeItem = {
type: 'treeItem',
kind: 'entity',
alias: 'Umb.TreeItem.DocumentType',
name: 'Document Type Tree Item',
conditions: {
entityType: 'document-type',
},
};
export const manifests = [tree, treeItem];

View File

@@ -105,7 +105,18 @@ export class UmbDocumentTypeWorkspaceEditElement extends UmbLitElement {
</uui-input>
</div>
<div slot="footer">Keyboard Shortcuts</div>
<div slot="footer">
<uui-button label="Show keyboard shortcuts">
Keyboard Shortcuts
<uui-keyboard-shortcut>
<uui-key>ALT</uui-key>
+
<uui-key>shift</uui-key>
+
<uui-key>k</uui-key>
</uui-keyboard-shortcut>
</uui-button>
</div>
</umb-workspace-layout>
`;
}

View File

@@ -25,7 +25,52 @@ const workspaceViews: Array<ManifestWorkspaceView> = [
meta: {
label: 'Design',
pathname: 'design',
icon: 'edit',
icon: 'umb:document-dashed-line',
},
conditions: {
workspaces: ['Umb.Workspace.DocumentType'],
},
},
{
type: 'workspaceView',
alias: 'Umb.WorkspaceView.DocumentType.ListView',
name: 'Document Type Workspace List View',
loader: () => import('./views/listview/workspace-view-document-type-listview.element'),
weight: 100,
meta: {
label: 'Listview',
pathname: 'listview',
icon: 'umb:list',
},
conditions: {
workspaces: ['Umb.Workspace.DocumentType'],
},
},
{
type: 'workspaceView',
alias: 'Umb.WorkspaceView.DocumentType.Permissions',
name: 'Document Type Workspace Permissions View',
loader: () => import('./views/permissions/workspace-view-document-type-permissions.element'),
weight: 100,
meta: {
label: 'Permissions',
pathname: 'permissions',
icon: 'umb:keychain',
},
conditions: {
workspaces: ['Umb.Workspace.DocumentType'],
},
},
{
type: 'workspaceView',
alias: 'Umb.WorkspaceView.DocumentType.Templates',
name: 'Document Type Workspace Templates View',
loader: () => import('./views/templates/workspace-view-document-type-templates.element'),
weight: 100,
meta: {
label: 'Templates',
pathname: 'templates',
icon: 'umb:layout',
},
conditions: {
workspaces: ['Umb.Workspace.DocumentType'],

View File

@@ -1,6 +1,7 @@
import { css, html } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement, state } from 'lit/decorators.js';
import { customElement, query, state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import { UmbWorkspaceDocumentTypeContext } from '../../document-type-workspace.context';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import type { DocumentTypeResponseModel } from '@umbraco-cms/backoffice/backend-api';
@@ -13,8 +14,66 @@ export class UmbWorkspaceViewDocumentTypeDesignElement extends UmbLitElement {
css`
:host {
display: block;
}
/* TODO: This should be replaced with a general workspace bar — naming is hard */
#workspace-tab-bar {
padding: 0 var(--uui-size-layout-1);
display: flex;
align-items: center;
justify-content: space-between;
background-color: var(--uui-color-surface);
flex-wrap: nowrap;
}
.tab-actions {
display: flex;
gap: var(--uui-size-space-4);
}
.tab-actions uui-button uui-icon {
padding-right: calc(-1 * var(--uui-size-space-4));
}
uui-tab {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
}
uui-tab .trash {
display: flex;
align-items: stretch;
}
uui-tab uui-input {
flex-grow: 1;
}
uui-input:not(:focus) {
border: 1px solid transparent;
}
uui-input:not(:hover, :focus) .trash {
opacity: 0;
}
/** Property Group Wrapper */
#wrapper {
margin: var(--uui-size-layout-1);
}
#add-group {
margin-top: var(--uui-size-layout-1);
width: 100%;
--uui-button-height: var(--uui-size-layout-4);
}
.group-headline {
display: flex;
gap: var(--uui-size-space-4);
}
.group-headline uui-input {
flex-grow: 1;
}
`,
];
@@ -23,6 +82,9 @@ export class UmbWorkspaceViewDocumentTypeDesignElement extends UmbLitElement {
private _workspaceContext?: UmbWorkspaceDocumentTypeContext;
@state()
private _tabs: any[] = [];
constructor() {
super();
@@ -42,12 +104,70 @@ export class UmbWorkspaceViewDocumentTypeDesignElement extends UmbLitElement {
}
render() {
return html` Design of ${this._documentType?.name}
<uui-box headline=${this._documentType?.name ?? ''}>
<div>
<umb-property-creator></umb-property-creator>
return html`
<div id="workspace-tab-bar">
${this.renderTabBar()}
<div class="tab-actions">
<uui-button label="Compositions" look="outline" compact>
<uui-icon name="umb:merge"></uui-icon>
Compositions
</uui-button>
<uui-button label="Recorder" look="outline" compact>
<uui-icon name="umb:navigation"></uui-icon>
Recorder
</uui-button>
</div>
</uui-box>`;
</div>
<div id="wrapper">
<uui-box class="group-wrapper">
<div class="group-headline" slot="headline">
<uui-input label="Group name" value="${this._documentType?.name ?? ''}" size="10">
<uui-button slot="append" label="Delete group" compact><uui-icon name="umb:trash"></uui-icon></uui-button>
</uui-input>
</div>
<umb-property-creator></umb-property-creator>
</uui-box>
<uui-button label="Add group" id="add-group" look="placeholder">Add group</uui-button>
</div>
`;
}
#remove(index: number) {
this._tabs.splice(index, 1);
this.requestUpdate();
}
async #addTab() {
this._tabs = [...this._tabs, { name: 'Test' }];
}
renderTabBar() {
return html`<uui-tab-group>
${repeat(
this._tabs,
(tab) => tab.name,
(tab, index) => {
//TODO: Should these tabs be part of routing?
return html`<uui-tab label=${tab.name!} .active="${false}">
<div>
<uui-input label="${tab.name}" look="placeholder" value="" placeholder="Enter a name">
<uui-button
label="Remove tab"
class="trash"
slot="append"
@click="${() => this.#remove(index)}"
compact>
<uui-icon name="umb:trash"></uui-icon>
</uui-button>
</uui-input>
</div>
</uui-tab>`;
}
)}
<uui-button id="add-tab" @click="${this.#addTab}" label="Add tab" compact>
<uui-icon name="umb:add"></uui-icon>
Add tab
</uui-button>
</uui-tab-group>`;
}
}

View File

@@ -0,0 +1,46 @@
import { css, html } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement, state } from 'lit/decorators.js';
import { UmbWorkspaceDocumentTypeContext } from '../../document-type-workspace.context';
import type { DocumentTypeResponseModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
@customElement('umb-workspace-view-document-type-listview')
export class UmbWorkspaceViewDocumentTypeListviewElement extends UmbLitElement {
static styles = [UUITextStyles, css``];
@state()
_documentType?: DocumentTypeResponseModel;
private _workspaceContext?: UmbWorkspaceDocumentTypeContext;
constructor() {
super();
// TODO: Figure out if this is the best way to consume the context or if it can be strongly typed with an UmbContextToken
this.consumeContext<UmbWorkspaceDocumentTypeContext>('umbWorkspaceContext', (documentTypeContext) => {
this._workspaceContext = documentTypeContext;
this._observeDocumentType();
});
}
private _observeDocumentType() {
if (!this._workspaceContext) return;
this.observe(this._workspaceContext.data, (documentType) => {
this._documentType = documentType;
});
}
render() {
return html` Listview `;
}
}
export default UmbWorkspaceViewDocumentTypeListviewElement;
declare global {
interface HTMLElementTagNameMap {
'umb-workspace-view-document-type-listview': UmbWorkspaceViewDocumentTypeListviewElement;
}
}

View File

@@ -0,0 +1,100 @@
import { css, html } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement, state } from 'lit/decorators.js';
import { UmbWorkspaceDocumentTypeContext } from '../../document-type-workspace.context';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import type { DocumentTypeResponseModel } from '@umbraco-cms/backoffice/backend-api';
@customElement('umb-workspace-view-document-type-permissions')
export class UmbWorkspaceViewDocumentTypePermissionsElement extends UmbLitElement {
static styles = [
UUITextStyles,
css`
:host {
display: block;
margin: var(--uui-size-layout-1);
}
uui-label,
umb-property-editor-ui-number {
display: block;
}
uui-toggle {
display: flex;
}
`,
];
@state()
_documentType?: DocumentTypeResponseModel;
private _workspaceContext?: UmbWorkspaceDocumentTypeContext;
constructor() {
super();
// TODO: Figure out if this is the best way to consume the context or if it can be strongly typed with an UmbContextToken
this.consumeContext<UmbWorkspaceDocumentTypeContext>('umbWorkspaceContext', (documentTypeContext) => {
this._workspaceContext = documentTypeContext;
this._observeDocumentType();
});
}
private _observeDocumentType() {
if (!this._workspaceContext) return;
this.observe(this._workspaceContext.data, (documentType) => {
this._documentType = documentType;
});
}
render() {
return html`
<uui-box headline="Permissions">
<umb-workspace-property-layout alias="Root" label="Allow as Root">
<div slot="description">Allow editors to create content of this type in the root of the content tree.</div>
<div slot="editor"><uui-toggle label="Allow as root"></uui-toggle></div>
</umb-workspace-property-layout>
<umb-workspace-property-layout alias="ChildNodeType" label="Allowed child node types">
<div slot="description">
Allow content of the specified types to be created underneath content of this type.
</div>
<div slot="editor">
<umb-input-document-type-picker
.currentDocumentType="${this._documentType}"></umb-input-document-type-picker>
</div>
</umb-workspace-property-layout>
<umb-workspace-property-layout alias="VaryByNature" label="Allow vary by culture">
<div slot="description">Allow editors to create content of different languages.</div>
<div slot="editor"><uui-toggle label="Vary by culture"></uui-toggle></div>
</umb-workspace-property-layout>
<umb-workspace-property-layout alias="ElementType" label="Is an Element Type">
<div slot="description">
An Element Type is meant to be used for instance in Nested Content, and not in the tree.<br />
A Document Type cannot be changed to an Element Type once it has been used to create one or more content
items.
</div>
<div slot="editor"><uui-toggle label="Element type"></uui-toggle></div>
</umb-workspace-property-layout>
<umb-workspace-property-layout alias="HistoryCleanup" label="History cleanup">
<div slot="description">Allow overriding the global history cleanup settings.</div>
<div slot="editor">
<uui-toggle .checked="${true}" label="Auto cleanup"></uui-toggle>
<uui-label for="versions-newer-than-days">Keep all versions newer than days</uui-label>
<umb-property-editor-ui-number id="versions-newer-than-days"></umb-property-editor-ui-number>
<uui-label for="latest-version-per-day-days">Keep latest version per day for days</uui-label>
<umb-property-editor-ui-number id="latest-version-per-day-days"></umb-property-editor-ui-number>
</div>
</umb-workspace-property-layout>
</uui-box>
`;
}
}
export default UmbWorkspaceViewDocumentTypePermissionsElement;
declare global {
interface HTMLElementTagNameMap {
'umb-workspace-view-document-type-permissions': UmbWorkspaceViewDocumentTypePermissionsElement;
}
}

View File

@@ -0,0 +1,91 @@
import { css, html } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement, state } from 'lit/decorators.js';
import { UmbWorkspaceDocumentTypeContext } from '../../document-type-workspace.context';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { DocumentTypeResponseModel } from '@umbraco-cms/backoffice/backend-api';
@customElement('umb-workspace-view-document-type-templates')
export class UmbWorkspaceViewDocumentTypeTemplatesElement extends UmbLitElement {
static styles = [
UUITextStyles,
css`
:host {
display: block;
margin: var(--uui-size-layout-1);
}
#templates {
text-align: center;
}
#template-card-wrapper {
display: flex;
gap: var(--uui-size-space-4);
align-items: stretch;
}
umb-workspace-property-layout {
border-top: 1px solid var(--uui-color-border);
}
umb-workspace-property-layout:first-child {
padding-top: 0;
border: none;
}
`,
];
@state()
_documentType?: DocumentTypeResponseModel;
private _workspaceContext?: UmbWorkspaceDocumentTypeContext;
constructor() {
super();
this.consumeContext<UmbWorkspaceDocumentTypeContext>('umbWorkspaceContext', (documentTypeContext) => {
this._workspaceContext = documentTypeContext;
this._observeDocumentType();
});
}
private _observeDocumentType() {
if (!this._workspaceContext) return;
this.observe(this._workspaceContext.data, (documentType) => {
this._documentType = documentType;
});
}
async #changeDefaultKey(e: CustomEvent) {
// save new default key
console.log('workspace: default template key', e);
}
#changeAllowedKeys(e: CustomEvent) {
// save new allowed keys
console.log('workspace: allowed templates changed', e);
}
render() {
return html`<uui-box headline="Templates">
<umb-workspace-property-layout alias="Templates" label="Allowed Templates">
<div slot="description">Choose which templates editors are allowed to use on content of this type</div>
<div id="templates" slot="editor">
<umb-input-template-picker
.defaultKey="${this._documentType?.defaultTemplateKey ?? ''}"
.allowedKeys="${this._documentType?.allowedTemplateKeys ?? []}"
@change-default="${this.#changeDefaultKey}"
@change-allowed="${this.#changeAllowedKeys}"></umb-input-template-picker>
</div>
</umb-workspace-property-layout>
</uui-box>`;
}
}
export default UmbWorkspaceViewDocumentTypeTemplatesElement;
declare global {
interface HTMLElementTagNameMap {
'umb-workspace-view-document-type-templates': UmbWorkspaceViewDocumentTypeTemplatesElement;
}
}

View File

@@ -1,5 +1,5 @@
import { manifests as collectionManifests } from './collection/manifests';
import { manifests as menuItemManifests } from './sidebar-menu-item/manifests';
import { manifests as menuItemManifests } from './menu-item/manifests';
import { manifests as repositoryManifests } from './repository/manifests';
import { manifests as treeManifests } from './tree/manifests';
import { manifests as workspaceManifests } from './workspace/manifests';

View File

@@ -2,17 +2,17 @@ import { html } from 'lit';
import { customElement } from 'lit/decorators.js';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
@customElement('umb-document-sidebar-menu-item')
export class UmbDocumentSidebarMenuItemElement extends UmbLitElement {
@customElement('umb-document-menu-item')
export class UmbDocumentMenuItemElement extends UmbLitElement {
render() {
return html`<umb-tree alias="Umb.Tree.Documents"></umb-tree>`;
}
}
export default UmbDocumentSidebarMenuItemElement;
export default UmbDocumentMenuItemElement;
declare global {
interface HTMLElementTagNameMap {
'umb-document-sidebar-menu-item': UmbDocumentSidebarMenuItemElement;
'umb-document-menu-item': UmbDocumentMenuItemElement;
}
}

View File

@@ -5,7 +5,7 @@ const menuItem: ManifestMenuItem = {
alias: 'Umb.MenuItem.Documents',
name: 'Documents Menu Item',
weight: 100,
loader: () => import('./document-sidebar-menu-item.element'),
loader: () => import('./document-menu-item.element'),
meta: {
label: 'Documents',
icon: 'umb:folder',

View File

@@ -0,0 +1,103 @@
import { css, html } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement, state } from 'lit/decorators.js';
import type { UmbTreeElement } from '../../../../shared/components/tree/tree.element';
import { UmbDocumentTypePickerModalData, UmbDocumentTypePickerModalResult } from '.';
import { UmbModalBaseElement } from '@umbraco-cms/internal/modal';
// TODO: make use of UmbPickerLayoutBase
@customElement('umb-document-type-picker-modal')
export class UmbDocumentTypePickerModalElement extends UmbModalBaseElement<
UmbDocumentTypePickerModalData,
UmbDocumentTypePickerModalResult
> {
static styles = [
UUITextStyles,
css`
h3 {
margin-left: var(--uui-size-space-5);
margin-right: var(--uui-size-space-5);
}
uui-input {
width: 100%;
}
hr {
border: none;
border-bottom: 1px solid var(--uui-color-divider);
margin: 16px 0;
}
#content-list {
display: flex;
flex-direction: column;
gap: var(--uui-size-space-3);
}
.content-item {
cursor: pointer;
}
.content-item.selected {
background-color: var(--uui-color-selected);
color: var(--uui-color-selected-contrast);
}
`,
];
@state()
_selection: Array<string> = [];
@state()
_multiple = true;
connectedCallback() {
super.connectedCallback();
this._selection = this.data?.selection ?? [];
this._multiple = this.data?.multiple ?? true;
}
private _handleSelectionChange(e: CustomEvent) {
e.stopPropagation();
const element = e.target as UmbTreeElement;
//TODO: Should multiple property be implemented here or be passed down into umb-tree?
this._selection = this._multiple ? element.selection : [element.selection[element.selection.length - 1]];
}
private _submit() {
this.modalHandler?.submit({ selection: this._selection });
}
private _close() {
this.modalHandler?.reject();
}
render() {
return html`
<umb-workspace-layout headline="Select Content">
<uui-box>
<uui-input></uui-input>
<hr />
<umb-tree
alias="Umb.Tree.DocumentTypes"
@selected=${this._handleSelectionChange}
.selection=${this._selection}
selectable></umb-tree>
</uui-box>
<div slot="actions">
<uui-button label="Close" @click=${this._close}></uui-button>
<uui-button label="Submit" look="primary" color="positive" @click=${this._submit}></uui-button>
</div>
</umb-workspace-layout>
`;
}
}
export default UmbDocumentTypePickerModalElement;
declare global {
interface HTMLElementTagNameMap {
'umb-document-type-picker-modal': UmbDocumentTypePickerModalElement;
}
}

View File

@@ -0,0 +1,26 @@
import '../../../../shared/components/body-layout/body-layout.element';
import './document-type-picker-modal.element';
import { Meta, Story } from '@storybook/web-components';
import { html } from 'lit';
import type { UmbDocumentTypePickerModalElement } from './document-type-picker-modal.element';
import type { UmbDocumentTypePickerModalData } from './index';
export default {
title: 'API/Modals/Layouts/Content Picker',
component: 'umb-document-type-picker-modal',
id: 'umb-document-type-picker-modal',
} as Meta;
const data: UmbDocumentTypePickerModalData = {
multiple: true,
selection: [],
};
export const Overview: Story<UmbDocumentTypePickerModalElement> = () => html`
<!-- TODO: figure out if generics are allowed for properties:
https://github.com/runem/lit-analyzer/issues/149
https://github.com/runem/lit-analyzer/issues/163 -->
<umb-document-picker-modal .data=${data as any}></umb-document-picker-modal>
`;

View File

@@ -0,0 +1,18 @@
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
export interface UmbDocumentTypePickerModalData {
multiple?: boolean;
selection?: Array<string>;
}
export interface UmbDocumentTypePickerModalResult {
selection: Array<string>;
}
export const UMB_DOCUMENT_TYPE_PICKER_MODAL_TOKEN = new UmbModalToken<
UmbDocumentTypePickerModalData,
UmbDocumentTypePickerModalResult
>('Umb.Modal.DocumentTypePicker', {
type: 'sidebar',
size: 'small',
});

View File

@@ -7,6 +7,12 @@ const modals: Array<ManifestModal> = [
name: 'Document Picker Modal',
loader: () => import('./document-picker/document-picker-modal.element'),
},
{
type: 'modal',
alias: 'Umb.Modal.DocumentTypePicker',
name: 'Document Type Picker Modal',
loader: () => import('./document-type-picker/document-type-picker-modal.element'),
},
];
export const manifests = [...modals];

View File

@@ -1,12 +1,8 @@
import { DocumentTreeServerDataSource } from './sources/document.tree.server.data';
import { UmbDocumentTreeStore, UMB_DOCUMENT_TREE_STORE_CONTEXT_TOKEN } from './document.tree.store';
import { UmbDocumentStore, UMB_DOCUMENT_STORE_CONTEXT_TOKEN } from './document.store';
import { UmbDocumentServerDataSource } from './sources/document.server.data';
import type {
RepositoryTreeDataSource,
UmbTreeRepository,
UmbDetailRepository,
} from '@umbraco-cms/backoffice/repository';
import { UmbDocumentStore, UMB_DOCUMENT_STORE_CONTEXT_TOKEN } from './document.store';
import { UmbDocumentTreeStore, UMB_DOCUMENT_TREE_STORE_CONTEXT_TOKEN } from './document.tree.store';
import { DocumentTreeServerDataSource } from './sources/document.tree.server.data';
import type { UmbTreeDataSource, UmbTreeRepository, UmbDetailRepository } from '@umbraco-cms/backoffice/repository';
import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller';
import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api';
import { ProblemDetailsModel, DocumentResponseModel } from '@umbraco-cms/backoffice/backend-api';
@@ -14,16 +10,12 @@ import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco
type ItemType = DocumentResponseModel;
// Move to documentation / JSdoc
/* We need to create a new instance of the repository from within the element context. We want the notifications to be displayed in the right context. */
// element -> context -> repository -> (store) -> data source
// All methods should be async and return a promise. Some methods might return an observable as part of the promise response.
export class UmbDocumentRepository implements UmbTreeRepository, UmbDetailRepository<ItemType> {
export class UmbDocumentRepository implements UmbTreeRepository<ItemType>, UmbDetailRepository<ItemType> {
#init!: Promise<unknown>;
#host: UmbControllerHostInterface;
#treeSource: RepositoryTreeDataSource;
#treeSource: UmbTreeDataSource;
#treeStore?: UmbDocumentTreeStore;
#detailDataSource: UmbDocumentServerDataSource;

View File

@@ -1,14 +1,14 @@
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UmbTreeStoreBase } from '@umbraco-cms/backoffice/store';
import { UmbEntityTreeStore } from '@umbraco-cms/backoffice/store';
import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller';
/**
* @export
* @class UmbDocumentTreeStore
* @extends {UmbTreeStoreBase}
* @extends {UmbEntityTreeStore}
* @description - Tree Data Store for Templates
*/
export class UmbDocumentTreeStore extends UmbTreeStoreBase {
export class UmbDocumentTreeStore extends UmbEntityTreeStore {
/**
* Creates an instance of UmbDocumentTreeStore.
* @param {UmbControllerHostInterface} host
@@ -19,6 +19,4 @@ export class UmbDocumentTreeStore extends UmbTreeStoreBase {
}
}
export const UMB_DOCUMENT_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken<UmbDocumentTreeStore>(
'UmbDocumentTreeStore'
);
export const UMB_DOCUMENT_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken<UmbDocumentTreeStore>('UmbDocumentTreeStore');

View File

@@ -1,5 +1,5 @@
import { v4 as uuidv4 } from 'uuid';
import { RepositoryDetailDataSource } from '@umbraco-cms/backoffice/repository';
import { UmbDataSource } from '@umbraco-cms/backoffice/repository';
import {
DocumentResource,
ProblemDetailsModel,
@@ -15,7 +15,7 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
* @class UmbDocumentServerDataSource
* @implements {RepositoryDetailDataSource}
*/
export class UmbDocumentServerDataSource implements RepositoryDetailDataSource<DocumentResponseModel> {
export class UmbDocumentServerDataSource implements UmbDataSource<DocumentResponseModel> {
#host: UmbControllerHostInterface;
/**

View File

@@ -1,4 +1,4 @@
import type { RepositoryTreeDataSource } from '@umbraco-cms/backoffice/repository';
import type { UmbTreeDataSource } from '@umbraco-cms/backoffice/repository';
import { ProblemDetailsModel, DocumentResource } from '@umbraco-cms/backoffice/backend-api';
import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
@@ -9,7 +9,7 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
* @class DocumentTreeServerDataSource
* @implements {DocumentTreeDataSource}
*/
export class DocumentTreeServerDataSource implements RepositoryTreeDataSource {
export class DocumentTreeServerDataSource implements UmbTreeDataSource {
#host: UmbControllerHostInterface;
// TODO: how do we handle trashed items?

View File

@@ -1,7 +1,7 @@
import type { DocumentResponseModel } from '@umbraco-cms/backoffice/backend-api';
import { RepositoryDetailDataSource, DataSourceResponse } from '@umbraco-cms/backoffice/repository';
import { UmbDataSource, DataSourceResponse } from '@umbraco-cms/backoffice/repository';
export interface UmbDocumentDataSource extends RepositoryDetailDataSource<DocumentResponseModel> {
export interface UmbDocumentDataSource extends UmbDataSource<DocumentResponseModel> {
createScaffold(documentTypeKey: string): Promise<DataSourceResponse<DocumentResponseModel>>;
trash(key: string): Promise<DataSourceResponse<DocumentResponseModel>>;
}

View File

@@ -1,5 +1,5 @@
import { UmbDocumentRepository } from '../repository/document.repository';
import type { ManifestTree } from '@umbraco-cms/backoffice/extensions-registry';
import { DOCUMENT_REPOSITORY_ALIAS } from '../repository/manifests';
import type { ManifestTree, ManifestTreeItem } from '@umbraco-cms/backoffice/extensions-registry';
const treeAlias = 'Umb.Tree.Documents';
@@ -8,8 +8,18 @@ const tree: ManifestTree = {
alias: treeAlias,
name: 'Documents Tree',
meta: {
repository: UmbDocumentRepository, // TODO: use alias instead of class
repositoryAlias: DOCUMENT_REPOSITORY_ALIAS,
},
};
export const manifests = [tree];
const treeItem: ManifestTreeItem = {
type: 'treeItem',
alias: 'Umb.TreeItem.Document',
name: 'Document Tree Item',
loader: () => import('./tree-item/document-tree-item.element'),
conditions: {
entityType: 'document',
},
};
export const manifests = [tree, treeItem];

View File

@@ -0,0 +1,10 @@
import { UmbTreeItemContextBase } from '../../../../shared/components/tree/tree-item-base/tree-item-base.context';
import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller';
import { DocumentTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api';
// TODO get unique method from an document repository static method
export class UmbDocumentTreeItemContext extends UmbTreeItemContextBase<DocumentTreeItemResponseModel> {
constructor(host: UmbControllerHostInterface) {
super(host, (x: DocumentTreeItemResponseModel) => x.key);
}
}

View File

@@ -0,0 +1,78 @@
import { css, html, nothing } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement, property } from 'lit/decorators.js';
import { UmbDocumentTreeItemContext } from './document-tree-item.context';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { DocumentTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api';
@customElement('umb-document-tree-item')
export class UmbDocumentTreeItemElement extends UmbLitElement {
static styles = [
UUITextStyles,
css`
#icon-container {
position: relative;
}
#icon {
vertical-align: middle;
}
#status-symbol {
width: 8px;
height: 8px;
background-color: blue;
display: block;
position: absolute;
bottom: 0;
right: 0;
border-radius: 100%;
}
`,
];
private _item?: DocumentTreeItemResponseModel;
@property({ type: Object, attribute: false })
public get item() {
return this._item;
}
public set item(value: DocumentTreeItemResponseModel | undefined) {
this._item = value;
this.#context.setTreeItem(value);
}
#context = new UmbDocumentTreeItemContext(this);
render() {
if (!this.item) return nothing;
return html`
<umb-tree-item-base> ${this.#renderIconWithStatusSymbol()} ${this.#renderLabel()} </umb-tree-item-base>
`;
}
// TODO: implement correct status symbol
#renderIconWithStatusSymbol() {
return html`
<span id="icon-container" slot="icon">
${this.item?.icon
? html`
<uui-icon id="icon" slot="icon" name="${this.item.icon}"></uui-icon> <span id="status-symbol"></span>
`
: nothing}
</span>
`;
}
// TODO: lower opacity if item is not published
#renderLabel() {
return html` <span id="label" slot="label">${this.item?.name}</span> `;
}
}
export default UmbDocumentTreeItemElement;
declare global {
interface HTMLElementTagNameMap {
'umb-document-tree-item': UmbDocumentTreeItemElement;
}
}

View File

@@ -7,14 +7,14 @@ import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-ap
import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller';
import type { MediaTypeDetails } from '@umbraco-cms/backoffice/models';
import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/notification';
import { UmbTreeRepository, RepositoryTreeDataSource } from '@umbraco-cms/backoffice/repository';
import { UmbTreeRepository, UmbTreeDataSource } from '@umbraco-cms/backoffice/repository';
export class UmbMediaTypeRepository implements UmbTreeRepository {
#init!: Promise<unknown>;
#host: UmbControllerHostInterface;
#treeSource: RepositoryTreeDataSource;
#treeSource: UmbTreeDataSource;
#treeStore?: UmbMediaTypeTreeStore;
#detailSource: UmbMediaTypeDetailServerDataSource;

View File

@@ -1,14 +1,14 @@
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UmbTreeStoreBase } from '@umbraco-cms/backoffice/store';
import { UmbEntityTreeStore } from '@umbraco-cms/backoffice/store';
import type { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller';
/**
* @export
* @class UmbMediaTypeTreeStore
* @extends {UmbTreeStoreBase}
* @extends {UmbEntityTreeStore}
* @description - Tree Data Store for Media Types
*/
export class UmbMediaTypeTreeStore extends UmbTreeStoreBase {
export class UmbMediaTypeTreeStore extends UmbEntityTreeStore {
/**
* Creates an instance of UmbMediaTypeTreeStore.
* @param {UmbControllerHostInterface} host

View File

@@ -1,6 +1,6 @@
import { MediaTypeResource, ProblemDetailsModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller';
import { RepositoryTreeDataSource } from '@umbraco-cms/backoffice/repository';
import { UmbTreeDataSource } from '@umbraco-cms/backoffice/repository';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
/**
@@ -9,7 +9,7 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
* @class MediaTypeTreeServerDataSource
* @implements {MediaTypeTreeDataSource}
*/
export class MediaTypeTreeServerDataSource implements RepositoryTreeDataSource {
export class MediaTypeTreeServerDataSource implements UmbTreeDataSource {
#host: UmbControllerHostInterface;
/**

View File

@@ -1,13 +1,23 @@
import { UmbMediaTypeRepository } from '../repository/media-type.repository';
import type { ManifestTree } from '@umbraco-cms/backoffice/extensions-registry';
import { MEDIA_TYPE_REPOSITORY_ALIAS } from '../repository/manifests';
import type { ManifestTree, ManifestTreeItem } from '@umbraco-cms/backoffice/extensions-registry';
const tree: ManifestTree = {
type: 'tree',
alias: 'Umb.Tree.MediaTypes',
name: 'Media Types Tree',
meta: {
repository: UmbMediaTypeRepository,
repositoryAlias: MEDIA_TYPE_REPOSITORY_ALIAS,
},
};
export const manifests = [tree];
const treeItem: ManifestTreeItem = {
type: 'treeItem',
kind: 'entity',
alias: 'Umb.TreeItem.MediaType',
name: 'Media Type Tree Item',
conditions: {
entityType: 'media-type',
},
};
export const manifests = [tree, treeItem];

View File

@@ -3,21 +3,20 @@ import { MediaTreeServerDataSource } from './sources/media.tree.server.data';
import { UmbMediaTreeStore, UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN } from './media.tree.store';
import { UmbMediaStore, UMB_MEDIA_STORE_CONTEXT_TOKEN } from './media.store';
import { UmbMediaDetailServerDataSource } from './sources/media.detail.server.data';
import type { RepositoryTreeDataSource } from '@umbraco-cms/backoffice/repository';
import type { UmbTreeDataSource } from '@umbraco-cms/backoffice/repository';
import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller';
import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api';
import { ProblemDetailsModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbDetailRepository, UmbTreeRepository } from '@umbraco-cms/backoffice/repository';
import { UmbDetailRepository } from '@umbraco-cms/backoffice/repository';
import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/notification';
import type { UmbTreeRepository } from 'libs/repository/tree-repository.interface';
type ItemDetailType = MediaDetails;
export class UmbMediaRepository implements UmbTreeRepository, UmbDetailRepository<ItemDetailType> {
#init!: Promise<unknown>;
#host: UmbControllerHostInterface;
#treeSource: RepositoryTreeDataSource;
#treeSource: UmbTreeDataSource;
#treeStore?: UmbMediaTreeStore;
#detailDataSource: UmbMediaDetailServerDataSource;
@@ -25,6 +24,9 @@ export class UmbMediaRepository implements UmbTreeRepository, UmbDetailRepositor
#notificationContext?: UmbNotificationContext;
#initResolver?: () => void;
#initialized = false;
constructor(host: UmbControllerHostInterface) {
this.#host = host;
@@ -32,19 +34,32 @@ export class UmbMediaRepository implements UmbTreeRepository, UmbDetailRepositor
this.#treeSource = new MediaTreeServerDataSource(this.#host);
this.#detailDataSource = new UmbMediaDetailServerDataSource(this.#host);
this.#init = Promise.all([
new UmbContextConsumerController(this.#host, UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN, (instance) => {
this.#treeStore = instance;
}),
new UmbContextConsumerController(this.#host, UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN, (instance) => {
this.#treeStore = instance;
this.#checkIfInitialized();
});
new UmbContextConsumerController(this.#host, UMB_MEDIA_STORE_CONTEXT_TOKEN, (instance) => {
this.#store = instance;
}),
new UmbContextConsumerController(this.#host, UMB_MEDIA_STORE_CONTEXT_TOKEN, (instance) => {
this.#store = instance;
this.#checkIfInitialized();
});
new UmbContextConsumerController(this.#host, UMB_NOTIFICATION_CONTEXT_TOKEN, (instance) => {
this.#notificationContext = instance;
}),
]);
new UmbContextConsumerController(this.#host, UMB_NOTIFICATION_CONTEXT_TOKEN, (instance) => {
this.#notificationContext = instance;
this.#checkIfInitialized();
});
}
// TODO: make a generic way to wait for initialization
#init = new Promise<void>((resolve) => {
this.#initialized ? resolve() : (this.#initResolver = resolve);
});
#checkIfInitialized() {
if (this.#treeStore && this.#store && this.#notificationContext) {
this.#initialized = true;
this.#initResolver?.();
}
}
async requestRootTreeItems() {

View File

@@ -1,7 +1,7 @@
import { EntityTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { ArrayState } from '@umbraco-cms/backoffice/observable-api';
import { UmbTreeStoreBase } from '@umbraco-cms/backoffice/store';
import { UmbEntityTreeStore } from '@umbraco-cms/backoffice/store';
import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller';
export const UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken<UmbMediaTreeStore>('UmbMediaTreeStore');
@@ -9,10 +9,10 @@ export const UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken<UmbMediaTr
/**
* @export
* @class UmbMediaTreeStore
* @extends {UmbTreeStoreBase}
* @extends {UmbEntityTreeStore}
* @description - Tree Data Store for Media
*/
export class UmbMediaTreeStore extends UmbTreeStoreBase {
export class UmbMediaTreeStore extends UmbEntityTreeStore {
#data = new ArrayState<EntityTreeItemResponseModel>([], (x) => x.key);
/**

View File

@@ -1,5 +1,5 @@
import type { MediaDetails } from '../../';
import { RepositoryDetailDataSource } from '@umbraco-cms/backoffice/repository';
import { UmbDataSource } from '@umbraco-cms/backoffice/repository';
import { ProblemDetailsModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
@@ -10,7 +10,7 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
* @class UmbTemplateDetailServerDataSource
* @implements {TemplateDetailDataSource}
*/
export class UmbMediaDetailServerDataSource implements RepositoryDetailDataSource<MediaDetails> {
export class UmbMediaDetailServerDataSource implements UmbDataSource<MediaDetails> {
#host: UmbControllerHostInterface;
/**

View File

@@ -1,4 +1,4 @@
import type { RepositoryTreeDataSource } from '@umbraco-cms/backoffice/repository';
import type { UmbTreeDataSource } from '@umbraco-cms/backoffice/repository';
import { ProblemDetailsModel, MediaResource } from '@umbraco-cms/backoffice/backend-api';
import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
@@ -9,7 +9,7 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
* @class MediaTreeServerDataSource
* @implements {MediaTreeDataSource}
*/
export class MediaTreeServerDataSource implements RepositoryTreeDataSource {
export class MediaTreeServerDataSource implements UmbTreeDataSource {
#host: UmbControllerHostInterface;
// TODO: how do we handle trashed items?

View File

@@ -1,5 +1,5 @@
import { UmbMediaRepository } from '../repository/media.repository';
import type { ManifestTree } from '@umbraco-cms/backoffice/extensions-registry';
import { MEDIA_REPOSITORY_ALIAS } from '../repository/manifests';
import type { ManifestTree, ManifestTreeItem } from '@umbraco-cms/backoffice/extensions-registry';
const treeAlias = 'Umb.Tree.Media';
@@ -8,8 +8,18 @@ const tree: ManifestTree = {
alias: treeAlias,
name: 'Media Tree',
meta: {
repository: UmbMediaRepository, // TODO: use alias instead of class
repositoryAlias: MEDIA_REPOSITORY_ALIAS,
},
};
export const manifests = [tree];
const treeItem: ManifestTreeItem = {
type: 'treeItem',
kind: 'entity',
alias: 'Umb.TreeItem.Media',
name: 'Media Tree Item',
conditions: {
entityType: 'media',
},
};
export const manifests = [tree, treeItem];

View File

@@ -7,11 +7,7 @@ import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco
import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api';
import type { MemberGroupDetails } from '@umbraco-cms/backoffice/models';
import { ProblemDetailsModel } from '@umbraco-cms/backoffice/backend-api';
import type {
RepositoryTreeDataSource,
UmbDetailRepository,
UmbTreeRepository,
} from '@umbraco-cms/backoffice/repository';
import type { UmbTreeDataSource, UmbDetailRepository, UmbTreeRepository } from '@umbraco-cms/backoffice/repository';
// TODO => Update type when backend updated
export class UmbMemberGroupRepository implements UmbTreeRepository, UmbDetailRepository<any> {
@@ -19,7 +15,7 @@ export class UmbMemberGroupRepository implements UmbTreeRepository, UmbDetailRep
#host: UmbControllerHostInterface;
#treeSource: RepositoryTreeDataSource;
#treeSource: UmbTreeDataSource;
#treeStore?: UmbMemberGroupTreeStore;
#detailSource: UmbMemberGroupDetailServerDataSource;

View File

@@ -1,14 +1,14 @@
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UmbTreeStoreBase } from '@umbraco-cms/backoffice/store';
import { UmbEntityTreeStore } from '@umbraco-cms/backoffice/store';
import type { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller';
/**
* @export
* @class UmbMemberGroupTreeStore
* @extends {UmbTreeStoreBase}
* @extends {UmbEntityTreeStore}
* @description - Tree Data Store for Member Groups
*/
export class UmbMemberGroupTreeStore extends UmbTreeStoreBase {
export class UmbMemberGroupTreeStore extends UmbEntityTreeStore {
/**
* Creates an instance of UmbMemberGroupTreeStore.
* @param {UmbControllerHostInterface} host

View File

@@ -2,7 +2,7 @@ import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
import { ProblemDetailsModel } from '@umbraco-cms/backoffice/backend-api';
import type { MemberGroupDetails } from '@umbraco-cms/backoffice/models';
import { RepositoryDetailDataSource } from '@umbraco-cms/backoffice/repository';
import { UmbDataSource } from '@umbraco-cms/backoffice/repository';
/**
* @description - A data source for the MemberGroup detail that fetches data from the server
@@ -11,7 +11,7 @@ import { RepositoryDetailDataSource } from '@umbraco-cms/backoffice/repository';
* @implements {MemberGroupDetailDataSource}
*/
// TODO => Provide type when it is available
export class UmbMemberGroupDetailServerDataSource implements RepositoryDetailDataSource<any> {
export class UmbMemberGroupDetailServerDataSource implements UmbDataSource<any> {
#host: UmbControllerHostInterface;
constructor(host: UmbControllerHostInterface) {

View File

@@ -1,6 +1,6 @@
import { MemberGroupResource, ProblemDetailsModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller';
import { RepositoryTreeDataSource } from '@umbraco-cms/backoffice/repository';
import { UmbTreeDataSource } from '@umbraco-cms/backoffice/repository';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
/**
@@ -9,7 +9,7 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
* @class MemberGroupTreeServerDataSource
* @implements {MemberGroupTreeDataSource}
*/
export class MemberGroupTreeServerDataSource implements RepositoryTreeDataSource {
export class MemberGroupTreeServerDataSource implements UmbTreeDataSource {
#host: UmbControllerHostInterface;
/**
@@ -22,42 +22,42 @@ export class MemberGroupTreeServerDataSource implements RepositoryTreeDataSource
}
/**
* Fetches the root items for the tree from the server
* @return {*}
* @memberof MemberGroupTreeServerDataSource
*/
async getRootItems() {
return tryExecuteAndNotify(this.#host, MemberGroupResource.getTreeMemberGroupRoot({}));
}
* Fetches the root items for the tree from the server
* @return {*}
* @memberof MemberGroupTreeServerDataSource
*/
async getRootItems() {
return tryExecuteAndNotify(this.#host, MemberGroupResource.getTreeMemberGroupRoot({}));
}
/**
* Fetches the children of a given parent key from the server
* @param {(string | null)} parentKey
* @return {*}
* @memberof MemberGroupTreeServerDataSource
*/
async getChildrenOf(parentKey: string | null) {
// Not implemented for this tree
return {};
}
/**
* Fetches the children of a given parent key from the server
* @param {(string | null)} parentKey
* @return {*}
* @memberof MemberGroupTreeServerDataSource
*/
async getChildrenOf(parentKey: string | null) {
// Not implemented for this tree
return {};
}
/**
* Fetches the items for the given keys from the server
* @param {Array<string>} keys
* @return {*}
* @memberof MemberGroupTreeServerDataSource
*/
async getItems(keys: Array<string>) {
if (!keys || keys.length === 0) {
const error: ProblemDetailsModel = { title: 'Keys are missing' };
return { error };
}
/**
* Fetches the items for the given keys from the server
* @param {Array<string>} keys
* @return {*}
* @memberof MemberGroupTreeServerDataSource
*/
async getItems(keys: Array<string>) {
if (!keys || keys.length === 0) {
const error: ProblemDetailsModel = { title: 'Keys are missing' };
return { error };
}
return tryExecuteAndNotify(
this.#host,
MemberGroupResource.getTreeMemberGroupItem({
key: keys,
})
);
}
return tryExecuteAndNotify(
this.#host,
MemberGroupResource.getTreeMemberGroupItem({
key: keys,
})
);
}
}

View File

@@ -1,5 +1,5 @@
import { UmbMemberGroupRepository } from '../repository/member-group.repository';
import type { ManifestTree } from '@umbraco-cms/backoffice/extensions-registry';
import { MEMBER_GROUP_REPOSITORY_ALIAS } from '../repository/manifests';
import type { ManifestTree, ManifestTreeItem } from '@umbraco-cms/backoffice/extensions-registry';
const treeAlias = 'Umb.Tree.MemberGroups';
@@ -9,8 +9,18 @@ const tree: ManifestTree = {
name: 'Member Groups Tree',
weight: 100,
meta: {
repository: UmbMemberGroupRepository,
repositoryAlias: MEMBER_GROUP_REPOSITORY_ALIAS,
},
};
export const manifests = [tree];
const treeItem: ManifestTreeItem = {
type: 'treeItem',
kind: 'entity',
alias: 'Umb.TreeItem.MemberGroup',
name: 'Member Group Tree Item',
conditions: {
entityType: 'member-group',
},
};
export const manifests = [tree, treeItem];

View File

@@ -4,20 +4,21 @@ import { UmbMemberTypeStore, UMB_MEMBER_TYPE_STORE_CONTEXT_TOKEN } from './membe
import { UmbMemberTypeDetailServerDataSource } from './sources/member-type.detail.server.data';
import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller';
import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api';
import { RepositoryTreeDataSource, UmbDetailRepository, UmbTreeRepository } from '@umbraco-cms/backoffice/repository';
import { UmbTreeDataSource, UmbDetailRepository, UmbTreeRepository } from '@umbraco-cms/backoffice/repository';
import { ProblemDetailsModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/notification';
import type { MemberTypeDetails } from '@umbraco-cms/backoffice/models';
// TODO => use correct type when available
type ItemType = any;
type TreeItemType = any;
export class UmbMemberTypeRepository implements UmbTreeRepository, UmbDetailRepository<ItemType> {
export class UmbMemberTypeRepository implements UmbTreeRepository<TreeItemType>, UmbDetailRepository<ItemType> {
#init!: Promise<unknown>;
#host: UmbControllerHostInterface;
#treeSource: RepositoryTreeDataSource;
#treeSource: UmbTreeDataSource;
#treeStore?: UmbMemberTypeTreeStore;
#detailSource: UmbMemberTypeDetailServerDataSource;

View File

@@ -1,5 +1,5 @@
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UmbTreeStoreBase } from '@umbraco-cms/backoffice/store';
import { UmbEntityTreeStore } from '@umbraco-cms/backoffice/store';
import type { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller';
/**
@@ -8,7 +8,7 @@ import type { UmbControllerHostInterface } from '@umbraco-cms/backoffice/control
* @extends {UmbStoreBase}
* @description - Tree Data Store for Member Types
*/
export class UmbMemberTypeTreeStore extends UmbTreeStoreBase {
export class UmbMemberTypeTreeStore extends UmbEntityTreeStore {
constructor(host: UmbControllerHostInterface) {
super(host, UMB_MEMBER_TYPE_TREE_STORE_CONTEXT_TOKEN.toString());
}

View File

@@ -1,6 +1,6 @@
import { MemberTypeResource, ProblemDetailsModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller';
import { RepositoryTreeDataSource } from '@umbraco-cms/backoffice/repository';
import { UmbTreeDataSource } from '@umbraco-cms/backoffice/repository';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
/**
@@ -9,7 +9,7 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
* @class MemberTypeTreeServerDataSource
* @implements {MemberTypeTreeDataSource}
*/
export class MemberTypeTreeServerDataSource implements RepositoryTreeDataSource {
export class MemberTypeTreeServerDataSource implements UmbTreeDataSource {
#host: UmbControllerHostInterface;
/**

View File

@@ -1,5 +1,5 @@
import { UmbMemberTypeRepository } from '../repository/member-type.repository';
import type { ManifestTree } from '@umbraco-cms/backoffice/extensions-registry';
import { MEMBER_TYPES_REPOSITORY_ALIAS } from '../repository/manifests';
import type { ManifestTree, ManifestTreeItem } from '@umbraco-cms/backoffice/extensions-registry';
const treeAlias = 'Umb.Tree.MemberTypes';
@@ -8,8 +8,18 @@ const tree: ManifestTree = {
alias: treeAlias,
name: 'Member Types Tree',
meta: {
repository: UmbMemberTypeRepository,
repositoryAlias: MEMBER_TYPES_REPOSITORY_ALIAS,
},
};
export const manifests = [tree];
const treeItem: ManifestTreeItem = {
type: 'treeItem',
kind: 'entity',
alias: 'Umb.TreeItem.MemberType',
name: 'Member Type Tree Item',
conditions: {
entityType: 'member-type',
},
};
export const manifests = [tree, treeItem];

View File

@@ -1,5 +1,5 @@
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UmbTreeStoreBase } from '@umbraco-cms/backoffice/store';
import { UmbEntityTreeStore } from '@umbraco-cms/backoffice/store';
import type { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller';
export const UMB_MEMBER_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken<UmbMemberTreeStore>('UmbMemberTreeStore');
@@ -7,10 +7,10 @@ export const UMB_MEMBER_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken<UmbMember
/**
* @export
* @class UmbMemberTreeStore
* @extends {UmbTreeStoreBase}
* @extends {UmbEntityTreeStore}
* @description - Tree Data Store for Members
*/
export class UmbMemberTreeStore extends UmbTreeStoreBase {
export class UmbMemberTreeStore extends UmbEntityTreeStore {
/**
* Creates an instance of UmbTemplateTreeStore.
* @param {UmbControllerHostInterface} host

View File

@@ -1,5 +1,5 @@
import { UmbMemberRepository } from '../repository/member.repository';
import type { ManifestTree } from '@umbraco-cms/backoffice/extensions-registry';
import { MEMBER_REPOSITORY_ALIAS } from '../repository/manifests';
import type { ManifestTree, ManifestTreeItem } from '@umbraco-cms/backoffice/extensions-registry';
const tree: ManifestTree = {
type: 'tree',
@@ -7,8 +7,18 @@ const tree: ManifestTree = {
name: 'Members Tree',
weight: 10,
meta: {
repository: UmbMemberRepository,
repositoryAlias: MEMBER_REPOSITORY_ALIAS,
},
};
export const manifests = [tree];
const treeItem: ManifestTreeItem = {
type: 'treeItem',
kind: 'entity',
alias: 'Umb.TreeItem.Member',
name: 'Member Tree Item',
conditions: {
entityType: 'member',
},
};
export const manifests = [tree, treeItem];

View File

@@ -2,28 +2,25 @@ import { UmbDataTypeTreeStore, UMB_DATA_TYPE_TREE_STORE_CONTEXT_TOKEN } from './
import { UmbDataTypeServerDataSource } from './sources/data-type.server.data';
import { UmbDataTypeStore, UMB_DATA_TYPE_STORE_CONTEXT_TOKEN } from './data-type.store';
import { DataTypeTreeServerDataSource } from './sources/data-type.tree.server.data';
import type {
RepositoryTreeDataSource,
UmbTreeRepository,
UmbDetailRepository,
} from '@umbraco-cms/backoffice/repository';
import type { UmbTreeDataSource, UmbTreeRepository, UmbDetailRepository } from '@umbraco-cms/backoffice/repository';
import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller';
import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api';
import { ProblemDetailsModel, DataTypeResponseModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/notification';
type ItemType = DataTypeResponseModel;
type TreeItemType = any;
// Move to documentation / JSdoc
/* We need to create a new instance of the repository from within the element context. We want the notifications to be displayed in the right context. */
// element -> context -> repository -> (store) -> data source
// All methods should be async and return a promise. Some methods might return an observable as part of the promise response.
export class UmbDataTypeRepository implements UmbTreeRepository, UmbDetailRepository<ItemType> {
export class UmbDataTypeRepository implements UmbTreeRepository<TreeItemType>, UmbDetailRepository<ItemType> {
#init!: Promise<unknown>;
#host: UmbControllerHostInterface;
#treeSource: RepositoryTreeDataSource;
#treeSource: UmbTreeDataSource;
#treeStore?: UmbDataTypeTreeStore;
#detailDataSource: UmbDataTypeServerDataSource;

View File

@@ -1,6 +1,6 @@
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller';
import { UmbTreeStoreBase } from '@umbraco-cms/backoffice/store';
import { UmbEntityTreeStore } from '@umbraco-cms/backoffice/store';
/**
* @export
@@ -9,7 +9,7 @@ import { UmbTreeStoreBase } from '@umbraco-cms/backoffice/store';
* @description - Tree Data Store for Data-Types
*/
// TODO: consider if tree store could be turned into a general EntityTreeStore class?
export class UmbDataTypeTreeStore extends UmbTreeStoreBase {
export class UmbDataTypeTreeStore extends UmbEntityTreeStore {
/**
* Creates an instance of UmbDataTypeTreeStore.
* @param {UmbControllerHostInterface} host
@@ -20,6 +20,4 @@ export class UmbDataTypeTreeStore extends UmbTreeStoreBase {
}
}
export const UMB_DATA_TYPE_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken<UmbDataTypeTreeStore>(
'UmbDataTypeTreeStore'
);
export const UMB_DATA_TYPE_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken<UmbDataTypeTreeStore>('UmbDataTypeTreeStore');

View File

@@ -1,5 +1,5 @@
import { v4 as uuidv4 } from 'uuid';
import { RepositoryDetailDataSource } from '@umbraco-cms/backoffice/repository';
import { UmbDataSource } from '@umbraco-cms/backoffice/repository';
import {
ProblemDetailsModel,
DataTypeResource,
@@ -15,7 +15,7 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
* @class UmbDataTypeServerDataSource
* @implements {RepositoryDetailDataSource}
*/
export class UmbDataTypeServerDataSource implements RepositoryDetailDataSource<DataTypeResponseModel> {
export class UmbDataTypeServerDataSource implements UmbDataSource<DataTypeResponseModel> {
#host: UmbControllerHostInterface;
/**

View File

@@ -1,4 +1,4 @@
import type { RepositoryTreeDataSource } from '@umbraco-cms/backoffice/repository';
import type { UmbTreeDataSource } from '@umbraco-cms/backoffice/repository';
import { ProblemDetailsModel, DataTypeResource } from '@umbraco-cms/backoffice/backend-api';
import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
@@ -9,7 +9,7 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
* @class DocumentTreeServerDataSource
* @implements {DocumentTreeDataSource}
*/
export class DataTypeTreeServerDataSource implements RepositoryTreeDataSource {
export class DataTypeTreeServerDataSource implements UmbTreeDataSource {
#host: UmbControllerHostInterface;
// TODO: how do we handle trashed items?

View File

@@ -1,13 +1,23 @@
import { UmbDataTypeRepository } from '../repository/data-type.repository';
import type { ManifestTree } from '@umbraco-cms/backoffice/extensions-registry';
import { DATA_TYPE_REPOSITORY_ALIAS } from '../repository/manifests';
import type { ManifestTree, ManifestTreeItem } from '@umbraco-cms/backoffice/extensions-registry';
const tree: ManifestTree = {
type: 'tree',
alias: 'Umb.Tree.DataTypes',
name: 'Data Types Tree',
meta: {
repository: UmbDataTypeRepository,
repositoryAlias: DATA_TYPE_REPOSITORY_ALIAS,
},
};
export const manifests = [tree];
const treeItem: ManifestTreeItem = {
type: 'treeItem',
kind: 'entity',
alias: 'Umb.TreeItem.DataType',
name: 'Data Type Tree Item',
conditions: {
entityType: 'data-type',
},
};
export const manifests = [tree, treeItem];

View File

@@ -1,5 +1,5 @@
import { LanguageResponseModel, PagedLanguageResponseModel } from '@umbraco-cms/backoffice/backend-api';
import { RepositoryDetailDataSource, DataSourceResponse } from '@umbraco-cms/backoffice/repository';
import { UmbDataSource, DataSourceResponse } from '@umbraco-cms/backoffice/repository';
// TODO: This is a temporary solution until we have a proper paging interface
type paging = {
@@ -7,7 +7,7 @@ type paging = {
take: number;
};
export interface UmbLanguageDataSource extends RepositoryDetailDataSource<LanguageResponseModel> {
export interface UmbLanguageDataSource extends UmbDataSource<LanguageResponseModel> {
createScaffold(): Promise<DataSourceResponse<LanguageResponseModel>>;
get(isoCode: string): Promise<DataSourceResponse<LanguageResponseModel>>;
delete(isoCode: string): Promise<DataSourceResponse<LanguageResponseModel>>;

View File

@@ -1,15 +1,15 @@
import type { ManifestMenuItem } from '@umbraco-cms/backoffice/extensions-registry';
import type { ManifestTypes } from '@umbraco-cms/backoffice/extensions-registry';
const menuItem: ManifestMenuItem = {
const menuItem: ManifestTypes = {
type: 'menuItem',
kind: 'tree',
alias: 'Umb.MenuItem.RelationTypes',
name: 'Relation Types Menu Item',
weight: 40,
loader: () => import('./relation-types-menu-item.element'),
meta: {
treeAlias: 'Umb.Tree.RelationTypes',
label: 'Relation Types',
icon: 'umb:folder',
entityType: 'relation-type',
},
conditions: {
menus: ['Umb.Menu.Settings'],

View File

@@ -1,40 +0,0 @@
import { html, nothing } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
@customElement('umb-relation-types-menu-item')
export class UmbRelationTypesMenuItemElement extends UmbLitElement {
@state()
private _renderTree = false;
private _onShowChildren() {
this._renderTree = true;
}
private _onHideChildren() {
this._renderTree = false;
}
// TODO: check if root has children before settings the has-children attribute
// TODO: how do we want to cache the tree? (do we want to rerender every time the user opens the tree)?
// TODO: can we make this reusable?
render() {
return html`<umb-tree-item
label="Relation Types"
icon="umb:folder"
entity-type="relation-type"
@show-children=${this._onShowChildren}
@hide-children=${this._onHideChildren}
has-children>
${this._renderTree ? html`<umb-tree alias="Umb.Tree.RelationTypes"></umb-tree>` : nothing}
</umb-tree-item> `;
}
}
export default UmbRelationTypesMenuItemElement;
declare global {
interface HTMLElementTagNameMap {
'umb-relation-types-menu-item': UmbRelationTypesMenuItemElement;
}
}

View File

@@ -10,12 +10,13 @@ import { UmbDetailRepository, UmbTreeRepository } from '@umbraco-cms/backoffice/
import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/notification';
type ItemType = RelationTypeResponseModel;
type TreeItemType = any;
// Move to documentation / JSdoc
/* We need to create a new instance of the repository from within the element context. We want the notifications to be displayed in the right context. */
// element -> context -> repository -> (store) -> data source
// All methods should be async and return a promise. Some methods might return an observable as part of the promise response.
export class UmbRelationTypeRepository implements UmbTreeRepository, UmbDetailRepository<ItemType> {
export class UmbRelationTypeRepository implements UmbTreeRepository<TreeItemType>, UmbDetailRepository<ItemType> {
#init!: Promise<unknown>;
#host: UmbControllerHostInterface;

View File

@@ -1,6 +1,6 @@
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller';
import { UmbTreeStoreBase } from '@umbraco-cms/backoffice/store';
import { UmbEntityTreeStore } from '@umbraco-cms/backoffice/store';
/**
* @export
@@ -9,7 +9,7 @@ import { UmbTreeStoreBase } from '@umbraco-cms/backoffice/store';
* @description - Tree Data Store for relation-types
*/
// TODO: consider if tree store could be turned into a general EntityTreeStore class?
export class UmbRelationTypeTreeStore extends UmbTreeStoreBase {
export class UmbRelationTypeTreeStore extends UmbEntityTreeStore {
/**
* Creates an instance of UmbRelationTypeTreeStore.
* @param {UmbControllerHostInterface} host

View File

@@ -1,4 +1,4 @@
import { RepositoryDetailDataSource } from '@umbraco-cms/backoffice/repository';
import { UmbDataSource } from '@umbraco-cms/backoffice/repository';
import {
ProblemDetailsModel,
RelationTypeResource,
@@ -15,7 +15,7 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
* @class UmbRelationTypeServerDataSource
* @implements {RepositoryDetailDataSource}
*/
export class UmbRelationTypeServerDataSource implements RepositoryDetailDataSource<RelationTypeResponseModel> {
export class UmbRelationTypeServerDataSource implements UmbDataSource<RelationTypeResponseModel> {
#host: UmbControllerHostInterface;
/**

View File

@@ -1,13 +1,23 @@
import { UmbRelationTypeRepository } from '../repository/relation-type.repository';
import type { ManifestTree } from '@umbraco-cms/backoffice/extensions-registry';
import { RELATION_TYPE_REPOSITORY_ALIAS } from '../repository/manifests';
import type { ManifestTree, ManifestTreeItem } from '@umbraco-cms/backoffice/extensions-registry';
const tree: ManifestTree = {
type: 'tree',
alias: 'Umb.Tree.RelationTypes',
name: 'Relation Types Tree',
meta: {
repository: UmbRelationTypeRepository,
repositoryAlias: RELATION_TYPE_REPOSITORY_ALIAS,
},
};
export const manifests = [tree];
const treeItem: ManifestTreeItem = {
type: 'treeItem',
kind: 'entity',
alias: 'Umb.TreeItem.RelationType',
name: 'Relation Type Tree Item',
conditions: {
entityType: 'relation-type',
},
};
export const manifests = [tree, treeItem];

View File

@@ -1,6 +1,5 @@
import { Observable } from 'rxjs';
import type { EntityTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api';
import type { UmbTreeStore } from '@umbraco-cms/backoffice/store';
import type { UmbControllerHostInterface } from '@umbraco-cms/backoffice/controller';
import { UmbContextToken, UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api';
import { ArrayState, UmbObserverController } from '@umbraco-cms/backoffice/observable-api';
@@ -8,17 +7,14 @@ import { umbExtensionsRegistry, createExtensionClass } from '@umbraco-cms/backof
import { UmbTreeRepository } from '@umbraco-cms/backoffice/repository';
// TODO: Clean up the need for store as Media has switched to use Repositories(repository).
export class UmbCollectionContext<
DataType extends EntityTreeItemResponseModel = EntityTreeItemResponseModel,
StoreType extends UmbTreeStore<DataType> = UmbTreeStore<DataType>
> {
export class UmbCollectionContext<DataType extends EntityTreeItemResponseModel = EntityTreeItemResponseModel> {
private _host: UmbControllerHostInterface;
private _entityType: string | null;
private _entityKey: string | null;
#repository?: UmbTreeRepository;
#repository?: UmbTreeRepository<any>;
private _store?: StoreType;
private _store?: any;
protected _dataObserver?: UmbObserverController<DataType[]>;
#data = new ArrayState(<Array<DataType>>[]);
@@ -45,7 +41,7 @@ export class UmbCollectionContext<
this._entityKey = entityKey;
if (storeAlias) {
new UmbContextConsumerController(this._host, storeAlias, (_instance: StoreType) => {
new UmbContextConsumerController(this._host, storeAlias, (_instance) => {
this._store = _instance;
if (!this._store) {
// TODO: if we keep the type assumption of _store existing, then we should here make sure to break the application in a good way.
@@ -174,4 +170,4 @@ export class UmbCollectionContext<
}
}
export const UMB_COLLECTION_CONTEXT_TOKEN = new UmbContextToken<UmbCollectionContext<any, any>>('UmbCollectionContext');
export const UMB_COLLECTION_CONTEXT_TOKEN = new UmbContextToken<UmbCollectionContext<any>>('UmbCollectionContext');

View File

@@ -25,7 +25,7 @@ export class UmbDashboardCollectionElement extends UmbLitElement {
];
// TODO: Use the right type here:
private _collectionContext?: UmbCollectionContext<FolderTreeItemResponseModel, any>;
private _collectionContext?: UmbCollectionContext<FolderTreeItemResponseModel>;
public manifest!: ManifestDashboardCollection;

View File

@@ -1,9 +1,10 @@
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, nothing, TemplateResult } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { UmbContextDebugRequest } from '@umbraco-cms/backoffice/context-api';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN, UMB_CONTEXT_DEBUGGER_MODAL } from '@umbraco-cms/backoffice/modal';
import { UmbModalContext, UMB_CONTEXT_DEBUGGER_MODAL, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/modal';
@customElement('umb-debug')
export class UmbDebug extends UmbLitElement {

View File

@@ -4,7 +4,11 @@ import type { TemplateResult } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { map } from 'rxjs';
import { repeat } from 'lit/directives/repeat.js';
import { createExtensionElement, isManifestElementableType, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extensions-api';
import {
createExtensionElement,
isManifestElementableType,
umbExtensionsRegistry,
} from '@umbraco-cms/backoffice/extensions-api';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
export type InitializedExtension = { alias: string; weight: number; component: HTMLElement | null };
@@ -34,6 +38,16 @@ export class UmbExtensionSlotElement extends UmbLitElement {
@property({ type: Object, attribute: false })
public filter: (manifest: any) => boolean = () => true;
private _props?: Record<string, any> = {};
@property({ type: Object, attribute: false })
get props() {
return this._props;
}
set props(newVal) {
this._props = newVal;
this.#assignPropsToAllComponents();
}
@property({ type: String, attribute: 'default-element' })
public defaultElement = '';
@@ -77,6 +91,7 @@ export class UmbExtensionSlotElement extends UmbLitElement {
// TODO: Lets make an console.error in this case?
}
if (component) {
this.#assignProps(component);
(component as any).manifest = extension;
extensionObject.component = component;
@@ -95,6 +110,18 @@ export class UmbExtensionSlotElement extends UmbLitElement {
);
}
#assignPropsToAllComponents() {
this._extensions.forEach((ext) => this.#assignProps(ext.component));
}
#assignProps = (component: HTMLElement | null) => {
if (!component || !this._props) return;
Object.keys(this._props).forEach((key) => {
(component as any)[key] = this._props?.[key];
});
};
render() {
// TODO: check if we can use repeat directly.
return repeat(

View File

@@ -21,20 +21,26 @@ import './input-checkbox-list/input-checkbox-list.element';
import './input-color-picker/input-color-picker.element';
import './input-culture-select/input-culture-select.element';
import './input-document-picker/input-document-picker.element';
import './input-document-type-picker/input-document-type-picker.element';
import './input-eye-dropper/input-eye-dropper.element';
import './input-language-picker/input-language-picker.element';
import './input-media-picker/input-media-picker.element';
import './input-multi-url-picker/input-multi-url-picker.element';
import './input-slider/input-slider.element';
import './input-toggle/input-toggle.element';
import './input-template-picker/input-template-picker.element';
import './property-type-based-property/property-type-based-property.element';
import './ref-property-editor-ui/ref-property-editor-ui.element';
import './section/section-main/section-main.element';
import './section/section-sidebar/section-sidebar.element';
import './section/section.element';
import './table/table.element';
import './tree/tree.element';
import './tree/entity-tree-item/entity-tree-item.element';
import './tree/tree-menu-item/tree-menu-item.element';
import './variantable-property/variantable-property.element';
import './workspace/workspace-action-menu/workspace-action-menu.element';
@@ -45,8 +51,9 @@ import './history/history-item.element';
import './workspace/workspace-action/workspace-action.element';
import './workspace/workspace-layout/workspace-layout.element';
import './code-editor';
import './workspace/workspace-footer-layout/workspace-footer-layout.element';
import './template-card/template-card.element';
import './code-editor';
export const manifests = [...debugManifests];

View File

@@ -0,0 +1,154 @@
import { css, html, nothing } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement, property, state } from 'lit/decorators.js';
import { ifDefined } from 'lit-html/directives/if-defined.js';
import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins';
import {
UmbDocumentTypeTreeStore,
UMB_DOCUMENT_TYPE_TREE_STORE_CONTEXT_TOKEN,
} from '../../../documents/document-types/repository/document-type.tree.store';
import { UMB_CONFIRM_MODAL_TOKEN } from '../../modals/confirm';
import { UMB_DOCUMENT_TYPE_PICKER_MODAL_TOKEN } from '../../../documents/documents/modals/document-type-picker';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { DocumentTypeResponseModel, EntityTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/modal';
import { UmbObserverController } from '@umbraco-cms/backoffice/observable-api';
@customElement('umb-input-document-type-picker')
export class UmbInputDocumentTypePickerElement extends FormControlMixin(UmbLitElement) {
static styles = [
UUITextStyles,
css`
#add-button {
width: 100%;
}
#current-node {
background-color: var(--uui-color-surface-alt);
}
#wrapper-nodes {
margin-left: var(--uui-size-space-6);
}
`,
];
// TODO: do we need both selectedKeys and value? If we just use value we follow the same pattern as native form controls.
private _selectedKeys: Array<string> = [];
public get selectedKeys(): Array<string> {
return this._selectedKeys;
}
public set selectedKeys(keys: Array<string>) {
this._selectedKeys = keys;
super.value = keys.join(',');
this._observePickedDocuments();
}
@property()
public set value(keysString: string) {
if (keysString !== this._value) {
this.selectedKeys = keysString.split(/[ ,]+/);
}
}
@property()
currentDocumentType?: DocumentTypeResponseModel;
@state()
private _items?: Array<DocumentTypeResponseModel>;
private _modalContext?: UmbModalContext;
private _documentTypeStore?: UmbDocumentTypeTreeStore;
private _pickedItemsObserver?: UmbObserverController<EntityTreeItemResponseModel[]>;
constructor() {
super();
this.consumeContext(UMB_DOCUMENT_TYPE_TREE_STORE_CONTEXT_TOKEN, (instance) => {
this._documentTypeStore = instance;
this._observePickedDocuments();
});
this.consumeContext(UMB_MODAL_CONTEXT_TOKEN, (instance) => {
this._modalContext = instance;
});
}
protected getFormElement() {
return undefined;
}
private _observePickedDocuments() {
this._pickedItemsObserver?.destroy();
if (!this._documentTypeStore) return;
// TODO: consider changing this to the list data endpoint when it is available
this._pickedItemsObserver = this.observe(this._documentTypeStore.items(this._selectedKeys), (items) => {
this._items = items;
});
}
private _openPicker() {
// We send a shallow copy(good enough as its just an array of keys) of our this._selectedKeys, as we don't want the modal to manipulate our data:
const modalHandler = this._modalContext?.open(UMB_DOCUMENT_TYPE_PICKER_MODAL_TOKEN, {
multiple: true,
selection: [...this._selectedKeys],
});
modalHandler?.onSubmit().then(({ selection }: any) => {
this._setSelection(selection);
});
}
private async _removeItem(item: DocumentTypeResponseModel) {
const modalHandler = this._modalContext?.open(UMB_CONFIRM_MODAL_TOKEN, {
color: 'danger',
headline: `Remove ${item.name}?`,
content: 'Are you sure you want to remove this item',
confirmLabel: 'Remove',
});
await modalHandler?.onSubmit();
const newSelection = this._selectedKeys.filter((value) => value !== item.key);
this._setSelection(newSelection);
}
private _setSelection(newSelection: Array<string>) {
this.selectedKeys = newSelection;
this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true }));
}
render() {
return html`
<uui-ref-node id="current-node" .name="${this.currentDocumentType?.name ?? ''} (current)">
<uui-icon slot="icon" .name="${this.currentDocumentType?.icon ?? 'umb:document'}"></uui-icon>
</uui-ref-node>
<div id="wrapper-nodes">
<uui-ref-list> ${this._items?.map((item) => this._renderItem(item))} </uui-ref-list>
<uui-button id="add-button" look="placeholder" @click=${this._openPicker} label="open">Add</uui-button>
</div>
`;
}
private _renderItem(item: DocumentTypeResponseModel) {
// TODO: remove when we have a way to handle trashed items
const tempItem = item as DocumentTypeResponseModel & { isTrashed: boolean };
return html`
<uui-ref-node name=${ifDefined(item.name === null ? undefined : item.name)} detail=${ifDefined(item.key)}>
<uui-icon slot="icon" name="${ifDefined(item.icon)}"></uui-icon>
${tempItem.isTrashed ? html` <uui-tag size="s" slot="tag" color="danger">Trashed</uui-tag> ` : nothing}
<uui-action-bar slot="actions">
<uui-button @click=${() => this._removeItem(item)} label="Remove document ${item.name}">Remove</uui-button>
</uui-action-bar>
</uui-ref-node>
`;
}
}
export default UmbInputDocumentTypePickerElement;
declare global {
interface HTMLElementTagNameMap {
'umb-input-document-type-picker': UmbInputDocumentTypePickerElement;
}
}

View File

@@ -0,0 +1,194 @@
import { css, html } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement, property, state } from 'lit/decorators.js';
import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins';
import { UmbTemplateCardElement } from '../template-card/template-card.element';
import { UMB_TEMPLATE_PICKER_MODAL_TOKEN } from '../../modals/template-picker';
import { UMB_TEMPLATE_MODAL_TOKEN } from '../../modals/template';
import { UmbTemplateRepository } from '../../../templating/templates/repository/template.repository';
import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/modal';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { TemplateResponseModel } from '@umbraco-cms/backoffice/backend-api';
@customElement('umb-input-template-picker')
export class UmbInputTemplatePickerElement extends FormControlMixin(UmbLitElement) {
/**
* This is a minimum amount of selected items in this input.
* @type {number}
* @attr
* @default undefined
*/
@property({ type: Number })
min?: number;
/**
* Min validation message.
* @type {boolean}
* @attr
* @default
*/
@property({ type: String, attribute: 'min-message' })
minMessage = 'This field need more items';
/**
* This is a maximum amount of selected items in this input.
* @type {number}
* @attr
* @default undefined
*/
@property({ type: Number })
max?: number;
/**
* Max validation message.
* @type {boolean}
* @attr
* @default
*/
@property({ type: String, attribute: 'min-message' })
maxMessage = 'This field exceeds the allowed amount of items';
_allowedKeys: Array<string> = [];
@property({ type: Array<string> })
public get allowedKeys() {
return this._allowedKeys;
}
public set allowedKeys(newKeys: Array<string>) {
this._allowedKeys = newKeys;
this.#observePickedTemplates();
}
_defaultKey = '';
@property({ type: String })
public get defaultKey(): string {
return this._defaultKey;
}
public set defaultKey(newKey: string) {
this._defaultKey = newKey;
super.value = newKey;
}
private _modalContext?: UmbModalContext;
private _templateRepository: UmbTemplateRepository = new UmbTemplateRepository(this);
@state()
_pickedTemplates: TemplateResponseModel[] = [];
constructor() {
super();
this.consumeContext(UMB_MODAL_CONTEXT_TOKEN, (instance) => {
this._modalContext = instance;
});
}
async #observePickedTemplates() {
this.observe(
await this._templateRepository.treeItems(this._allowedKeys),
(data) => {
this._pickedTemplates = data;
},
'_templateRepositoryTreeItems'
);
}
protected getFormElement() {
return this;
}
#changeDefault(e: CustomEvent) {
e.stopPropagation();
const newKey = (e.target as UmbTemplateCardElement).value as string;
this.defaultKey = newKey;
this.dispatchEvent(new CustomEvent('change-default'));
}
#openPicker() {
const modalHandler = this._modalContext?.open(UMB_TEMPLATE_PICKER_MODAL_TOKEN, {
multiple: true,
selection: [...this.allowedKeys],
});
modalHandler?.onSubmit().then((data) => {
if (!data.selection) return;
this.allowedKeys = data.selection;
this.dispatchEvent(new CustomEvent('change-allowed'));
});
}
#removeTemplate(key: string) {
/*
TODO: We need to follow up on this experience.
Could we test if this document type is in use, if so we should have a dialog notifying the user(Dialog, are you sure...) about that we might will break something?
If thats the case, Im not why if a template will be removed from an actual document.
If if its just the option that will go away.
(Comment by Niels)
In current backoffice we just prevent deleting a default when there are other templates. But if its the only one its okay. This is a weird experience, so we should make something that makes more sense.
BTW. its weird cause the damage of removing the default template is equally bad when there is one or more templates.
*/
this.allowedKeys = this.allowedKeys.filter((x) => x !== key);
}
#openTemplate(e: CustomEvent) {
const key = (e.target as UmbTemplateCardElement).value;
this._modalContext?.open(UMB_TEMPLATE_MODAL_TOKEN, {
key: key as string,
language: 'razor',
});
}
render() {
return html`
${this._pickedTemplates.map(
(template) => html`
<umb-template-card
class="template-card"
.name="${template.name ?? ''}"
.key="${template.key ?? ''}"
@change-default="${this.#changeDefault}"
@open="${this.#openTemplate}"
?default="${template.key === this.defaultKey}">
<uui-button
slot="actions"
label="Remove document ${template.name}"
@click="${() => this.#removeTemplate(template.key ?? '')}"
compact>
<uui-icon name="umb:trash"> </uui-icon>
</uui-button>
</umb-template-card>
`
)}
<uui-button id="add-button" look="placeholder" label="open" @click="${this.#openPicker}">Add</uui-button>
`;
}
static styles = [
UUITextStyles,
css`
#add-button {
width: 100%;
}
:host {
box-sizing: border-box;
display: flex;
gap: var(--uui-size-space-4);
flex-wrap: wrap;
}
:host > * {
max-width: 180px;
min-width: 180px;
min-height: 150px;
}
`,
];
}
export default UmbInputTemplatePickerElement;
declare global {
interface HTMLElementTagNameMap {
'umb-input-template-picker': UmbInputTemplatePickerElement;
}
}

View File

@@ -13,16 +13,6 @@ export class UmbSectionContext {
public readonly pathname = this.#manifestPathname.asObservable();
public readonly label = this.#manifestLabel.asObservable();
/*
This was not used anywhere
private _activeTree = new BehaviorSubject<ManifestTree | undefined>(undefined);
public readonly activeTree = this._activeTree.asObservable();
*/
// TODO: what is the best context to put this in?
#activeTreeItem = new ObjectState<ActiveTreeItemType | undefined>(undefined);
public readonly activeTreeItem = this.#activeTreeItem.asObservable();
constructor(manifest: ManifestSection) {
this.setManifest(manifest);
}
@@ -32,17 +22,6 @@ export class UmbSectionContext {
this.#manifestPathname.next(manifest?.meta?.pathname);
this.#manifestLabel.next(manifest ? manifest.meta?.label || manifest.name : undefined);
}
/*
This was not used anywhere
public setActiveTree(tree: ManifestTree) {
this._activeTree.next(tree);
}
*/
public setActiveTreeItem(item?: ActiveTreeItemType) {
this.#activeTreeItem.next(item);
}
}
export const UMB_SECTION_CONTEXT_TOKEN = new UmbContextToken<UmbSectionContext>('UmbSectionContext');

View File

@@ -0,0 +1,170 @@
import { css, html } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement, property } from 'lit/decorators.js';
import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
/**
* @element umb-template-card
* @slot actions
* @fires open
* @fires selected
*
*
*/
@customElement('umb-template-card')
export class UmbTemplateCardElement extends FormControlMixin(UmbLitElement) {
static styles = [
UUITextStyles,
css`
:host {
box-sizing: border-box;
display: contents;
position: relative;
height: 100%;
border: 1px solid red;
margin: auto;
}
#card {
box-sizing: border-box;
width: 100%;
max-width: 180px;
//width: 200px;
position: relative;
display: flex;
flex-direction: column;
align-items: stretch;
border-radius: var(--uui-border-radius);
border: 1px solid var(--uui-color-divider-emphasis);
background-color: var(--uui-color-background);
padding: var(--uui-size-4);
}
:host([default]) #card {
border: 1px solid var(--uui-color-selected);
outline: 1px solid var(--uui-color-selected);
}
#card:has(uui-button:hover) {
border: 1px solid var(--uui-color-selected);
}
#bottom {
margin-top: auto;
}
slot[name='actions'] {
position: absolute;
top: var(--uui-size-4);
right: var(--uui-size-4);
display: flex;
justify-content: right;
opacity: 0;
transition: opacity 120ms;
}
:host(:focus) slot[name='actions'],
:host(:focus-within) slot[name='actions'],
:host(:hover) slot[name='actions'] {
opacity: 1;
}
#open-part {
border: none;
outline: none;
background: none;
text-align: center;
display: flex;
flex-direction: column;
font-weight: 700;
align-items: center;
cursor: pointer;
flex-grow: 1;
}
#open-part,
#card {
gap: var(--uui-size-space-2);
}
#open-part strong {
flex-grow: 1;
display: flex;
align-items: center;
}
:host([disabled]) #open-part {
pointer-events: none;
}
#open-part:focus-visible,
#open-part:focus-visible uui-icon,
#open-part:hover,
#open-part:hover uui-icon {
text-decoration: underline;
color: var(--uui-color-interactive-emphasis);
}
#open-part uui-icon {
font-size: var(--uui-size-20);
color: var(--uui-color-divider-emphasis);
}
`,
];
@property({ type: String })
name = '';
@property({ type: Boolean, reflect: true })
default = false;
_key = '';
@property({ type: String })
public set key(newKey: string) {
this._key = newKey;
super.value = newKey;
}
public get key() {
return this._key;
}
protected getFormElement() {
return undefined;
}
#setSelection(e: KeyboardEvent) {
e.preventDefault();
e.stopPropagation();
//this.selected = true;
this.dispatchEvent(new CustomEvent('change-default', { bubbles: true, composed: true }));
}
#openTemplate(e: KeyboardEvent) {
e.preventDefault();
e.stopPropagation();
this.dispatchEvent(new CustomEvent('open', { bubbles: true, composed: true }));
}
render() {
return html`<div id="card">
<button id="open-part" aria-label="Open ${this.name}" @click="${this.#openTemplate}">
<uui-icon class="logo" name="umb:layout"></uui-icon>
<strong>${this.name.length ? this.name : 'Untitled template'}</strong>
</button>
<uui-button id="bottom" label="Default template" ?disabled="${this.default}" @click="${this.#setSelection}">
${this.default ? '(Default template)' : 'Set default'}
</uui-button>
<slot name="actions"></slot>
</div>`;
}
}
export default UmbTemplateCardElement;
declare global {
interface HTMLElementTagNameMap {
'umb-template-card': UmbTemplateCardElement;
}
}

Some files were not shown because too many files have changed in this diff Show More