Merge branch 'feature/entity-actions' of https://github.com/umbraco/Umbraco.CMS.Backoffice into feature/entity-actions

This commit is contained in:
Mads Rasmussen
2023-02-03 15:03:12 +01:00
19 changed files with 352 additions and 78 deletions

View File

@@ -0,0 +1,27 @@
import type { ClassConstructor, ManifestClass } from '../models';
import { hasDefaultExport } from './has-default-export.function';
import { isManifestClassConstructorType } from './is-manifest-class-instance-type.function';
import { loadExtension } from './load-extension.function';
//TODO: Write tests for this method:
export async function createExtensionClass<T = unknown>(manifest: ManifestClass, constructorArguments: unknown[]): Promise<T | undefined> {
const js = await loadExtension(manifest);
if (isManifestClassConstructorType(manifest)) {
return new manifest.class(...constructorArguments) as T;
}
if (js) {
if (hasDefaultExport<ClassConstructor<T>>(js)) {
return new js.default(...constructorArguments);
}
console.error('-- Extension did not succeed creating an class instance, missing a default export of the served JavaScript file', manifest);
return undefined;
}
console.error('-- Extension did not succeed creating an class instance, missing a default export or `class` in the manifest.', manifest);
return undefined;
}

View File

@@ -1,10 +1,10 @@
import type { ManifestElement } from '../models';
import type { HTMLElementConstructor, ManifestElement } from '../models';
import { hasDefaultExport } from './has-default-export.function';
import { isManifestElementNameType } from './is-manifest-element-name-type.function';
import { loadExtension } from './load-extension.function';
export async function createExtensionElement(manifest: ManifestElement): Promise<HTMLElement | undefined> {
//TODO: Write tests for these extension options:
const js = await loadExtension(manifest);
@@ -15,7 +15,7 @@ export async function createExtensionElement(manifest: ManifestElement): Promise
// TODO: Do we need this except for the default() loader?
if (js) {
if (hasDefaultExport(js)) {
if (hasDefaultExport<HTMLElementConstructor>(js)) {
// created by default class
return new js.default();
}

View File

@@ -1,5 +1,3 @@
import type { HTMLElementConstructor } from '../models';
export function hasDefaultExport(object: unknown): object is { default: HTMLElementConstructor } {
export function hasDefaultExport<ConstructorType>(object: unknown): object is { default: ConstructorType } {
return typeof object === 'object' && object !== null && 'default' in object;
}

View File

@@ -0,0 +1,7 @@
import type { ManifestClass, ManifestClassWithClassConstructor } from '../models';
export function isManifestClassConstructorType(manifest: unknown): manifest is ManifestClassWithClassConstructor {
return (
typeof manifest === 'object' && manifest !== null && (manifest as ManifestClass).class !== undefined
);
}

View File

@@ -0,0 +1,8 @@
import { isManifestJSType } from './is-manifest-js-type.function';
import { isManifestLoaderType } from './is-manifest-loader-type.function';
import { isManifestClassConstructorType } from './is-manifest-class-instance-type.function';
import type { ManifestBase, ManifestClass } from '@umbraco-cms/extensions-registry';
export function isManifestClassableType(manifest: ManifestBase): manifest is ManifestClass {
return isManifestClassConstructorType(manifest) || isManifestLoaderType(manifest) || isManifestJSType(manifest);
}

View File

@@ -1,75 +1,78 @@
import type { ManifestCollectionBulkAction } from './collection-bulk-action.models';
import type { ManifestCollectionView } from './collection-view.models';
import type { ManifestDashboard } from './dashboard.models';
import type { ManifestDashboardCollection } from './dashboard-collection.models';
import type { ManifestEntityAction } from './entity-action.models';
import type { ManifestExternalLoginProvider } from './external-login-provider.models';
import type { ManifestHeaderApp } from './header-app.models';
import type { ManifestHealthCheck } from './health-check.models';
import type { ManifestPackageView } from './package-view.models';
import type { ManifestPropertyAction } from './property-action.models';
import type { ManifestPropertyEditorUI, ManifestPropertyEditorModel } from './property-editor.models';
import type { ManifestSection } from './section.models';
import type { ManifestSectionView } from './section-view.models';
import type { ManifestSidebarMenuItem } from './sidebar-menu-item.models';
import type { ManifestTheme } from './theme.models';
import type { ManifestTree } from './tree.models';
import type { ManifestTreeItemAction } from './tree-item-action.models';
import type { ManifestUserDashboard } from './user-dashboard.models';
import type { ManifestWorkspace } from './workspace.models';
import type { ManifestWorkspaceAction } from './workspace-action.models';
import type { ManifestWorkspaceView } from './workspace-view.models';
import type { ManifestWorkspaceViewCollection } from './workspace-view-collection.models';
import type { ManifestPropertyEditorUI, ManifestPropertyEditorModel } from './property-editor.models';
import type { ManifestDashboard } from './dashboard.models';
import type { ManifestDashboardCollection } from './dashboard-collection.models';
import type { ManifestUserDashboard } from './user-dashboard.models';
import type { ManifestPropertyAction } from './property-action.models';
import type { ManifestPackageView } from './package-view.models';
import type { ManifestExternalLoginProvider } from './external-login-provider.models';
import type { ManifestCollectionBulkAction } from './collection-bulk-action.models';
import type { ManifestCollectionView } from './collection-view.models';
import type { ManifestHealthCheck } from './health-check.models';
import type { ManifestSidebarMenuItem } from './sidebar-menu-item.models';
import type { ManifestTheme } from './theme.models';
import type { ManifestEntityAction } from './entity-action.models';
import { ManifestRepository } from './repository.models';
import type { ClassConstructor } from '@umbraco-cms/models';
export * from './header-app.models';
export * from './section.models';
export * from './section-view.models';
export * from './tree.models';
export * from './tree-item-action.models';
export * from './workspace.models';
export * from './workspace-action.models';
export * from './workspace-view.models';
export * from './workspace-view-collection.models';
export * from './property-editor.models';
export * from './dashboard.models';
export * from './dashboard-collection.models';
export * from './user-dashboard.models';
export * from './property-action.models';
export * from './package-view.models';
export * from './external-login-provider.models';
export * from './collection-bulk-action.models';
export * from './collection-view.models';
export * from './dashboard-collection.models';
export * from './dashboard.models';
export * from './entity-action.models';
export * from './external-login-provider.models';
export * from './header-app.models';
export * from './health-check.models';
export * from './package-view.models';
export * from './property-action.models';
export * from './property-editor.models';
export * from './section-view.models';
export * from './section.models';
export * from './sidebar-menu-item.models';
export * from './theme.models';
export * from './entity-action.models';
export * from './tree-item-action.models';
export * from './tree.models';
export * from './user-dashboard.models';
export * from './workspace-action.models';
export * from './workspace-view-collection.models';
export * from './workspace-view.models';
export * from './workspace.models';
export type ManifestTypes =
| ManifestCollectionBulkAction
| ManifestCollectionView
| ManifestCustom
| ManifestDashboard
| ManifestDashboardCollection
| ManifestEntityAction
| ManifestEntrypoint
| ManifestExternalLoginProvider
| ManifestHeaderApp
| ManifestHealthCheck
| ManifestPackageView
| ManifestPropertyAction
| ManifestPropertyEditorModel
| ManifestPropertyEditorUI
| ManifestRepository
| ManifestSection
| ManifestSectionView
| ManifestSidebarMenuItem
| ManifestTheme
| ManifestTree
| ManifestTreeItemAction
| ManifestUserDashboard
| ManifestWorkspace
| ManifestWorkspaceAction
| ManifestWorkspaceView
| ManifestWorkspaceViewCollection
| ManifestTreeItemAction
| ManifestPropertyEditorUI
| ManifestPropertyEditorModel
| ManifestDashboard
| ManifestDashboardCollection
| ManifestUserDashboard
| ManifestPropertyAction
| ManifestPackageView
| ManifestExternalLoginProvider
| ManifestEntrypoint
| ManifestCollectionBulkAction
| ManifestCollectionView
| ManifestHealthCheck
| ManifestSidebarMenuItem
| ManifestTheme
| ManifestEntityAction;
| ManifestWorkspaceViewCollection;
export type ManifestStandardTypes = ManifestTypes['type'];
@@ -88,6 +91,18 @@ export interface ManifestWithLoader<LoaderReturnType> extends ManifestBase {
loader?: () => Promise<LoaderReturnType>;
}
export interface ManifestClass extends ManifestWithLoader<object> {
type: ManifestStandardTypes;
js?: string;
className?: string;
class?: ClassConstructor<unknown>;
//loader?: () => Promise<object | HTMLElement>;
}
export interface ManifestClassWithClassConstructor extends ManifestClass {
class: ClassConstructor<unknown>;
}
export interface ManifestElement extends ManifestWithLoader<object | HTMLElement> {
type: ManifestStandardTypes;
js?: string;

View File

@@ -0,0 +1,5 @@
import type { ManifestClass } from './models';
export interface ManifestRepository extends ManifestClass {
type: 'repository';
}

View File

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

View File

@@ -5,11 +5,13 @@ export interface ManifestWorkspaceViewCollection extends ManifestBase {
meta: MetaEditorViewCollection;
}
// TODO: Get rid of store alias, when we are done migrating to repositories(remember to enforce repositoryAlias):
export interface MetaEditorViewCollection {
workspaces: string[];
pathname: string;
label: string;
icon: string;
entityType: string;
storeAlias: string;
storeAlias?: string;
repositoryAlias?: string;
}

View File

@@ -4,12 +4,8 @@ import {
DocumentTypeTreeItem,
EntityTreeItem,
FolderTreeItem,
PagedEntityTreeItem,
ProblemDetails,
} from '@umbraco-cms/backend-api';
import { UmbControllerHostInterface } from '@umbraco-cms/controller';
import { UmbTreeRepository } from 'libs/repository/tree-repository.interface';
import { Observable } from 'rxjs';
// Extension Manifests
export * from '@umbraco-cms/extensions-registry';
@@ -17,6 +13,8 @@ export * from '@umbraco-cms/extensions-registry';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type HTMLElementConstructor<T = HTMLElement> = new (...args: any[]) => T;
export type ClassConstructor<T> = new (...args: any[]) => T;
// Users
// TODO: would the right name be Node? as entity is just something with a Key. But node is something in a content structure, aka. with hasChildren and parentKey.
export interface Entity {
@@ -159,6 +157,3 @@ export interface DataSourceResponse<T = undefined> {
data?: T;
error?: ProblemDetails;
}
export interface UmbRepositoryFactory<T> {
new (host: UmbControllerHostInterface): T;
}

View File

@@ -1,4 +1,5 @@
import { manifests as sidebarMenuItemManifests } from './sidebar-menu-item/manifests';
import { manifests as repositoryManifests } from './repository/manifests';
import { manifests as treeManifests } from './tree/manifests';
import { manifests as workspaceManifests } from './workspace/manifests';
import { manifests as entityActionManifests } from './entity-actions/manifests';
@@ -6,6 +7,7 @@ import { manifests as entityActionManifests } from './entity-actions/manifests';
export const manifests = [
...sidebarMenuItemManifests,
...treeManifests,
...repositoryManifests,
...workspaceManifests,
...entityActionManifests,
];

View File

@@ -0,0 +1,13 @@
import { UmbDocumentRepository } from '../repository/document.repository';
import { ManifestRepository } from 'libs/extensions-registry/repository.models';
export const DOCUMENT_REPOSITORY_ALIAS = 'Umb.Repository.Documents';
const repository: ManifestRepository = {
type: 'repository',
alias: DOCUMENT_REPOSITORY_ALIAS,
name: 'Documents Repository',
class: UmbDocumentRepository,
};
export const manifests = [repository];

View File

@@ -1,6 +1,6 @@
import { UmbWorkspaceContext } from '../../../shared/components/workspace/workspace-context/workspace-context';
import { UmbDocumentRepository } from '../repository/document.repository';
import { UmbWorkspaceContextInterface } from '../../../shared/components/workspace/workspace-context/workspace-context.interface';
import type { UmbWorkspaceEntityContextInterface } from '../../../shared/components/workspace/workspace-context/workspace-entity-context.interface';
import type { DocumentDetails } from '@umbraco-cms/models';
import { appendToFrozenArray, ObjectState } from '@umbraco-cms/observable-api';
import { UmbControllerHostInterface } from '@umbraco-cms/controller';
@@ -8,7 +8,7 @@ import { UmbControllerHostInterface } from '@umbraco-cms/controller';
// TODO: should this contex be called DocumentDraft instead of workspace? or should the draft be part of this?
type EntityType = DocumentDetails;
export class UmbDocumentWorkspaceContext extends UmbWorkspaceContext implements UmbWorkspaceContextInterface<EntityType | undefined> {
export class UmbDocumentWorkspaceContext extends UmbWorkspaceContext implements UmbWorkspaceEntityContextInterface<EntityType | undefined> {
#host: UmbControllerHostInterface;
#templateDetailRepo: UmbDocumentRepository;
@@ -33,6 +33,10 @@ export class UmbDocumentWorkspaceContext extends UmbWorkspaceContext implements
}
*/
getEntityKey() {
return this.getData()?.key || '';
}
getEntityType() {
return 'document';
}

View File

@@ -1,6 +1,6 @@
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, nothing } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { customElement, state } from 'lit/decorators.js';
import type { UmbWorkspaceEntityElement } from '../../../shared/components/workspace/workspace-entity-element.interface';
import { UmbDocumentWorkspaceContext } from './document-workspace.context';
import { UmbLitElement } from '@umbraco-cms/element';

View File

@@ -1,4 +1,5 @@
import type { ManifestWorkspace, ManifestWorkspaceAction, ManifestWorkspaceView } from '@umbraco-cms/models';
import { DOCUMENT_REPOSITORY_ALIAS } from '../repository/manifests';
import type { ManifestWorkspace, ManifestWorkspaceAction, ManifestWorkspaceView, ManifestWorkspaceViewCollection } from '@umbraco-cms/models';
const workspace: ManifestWorkspace = {
type: 'workspace',
@@ -41,6 +42,23 @@ const workspaceViews: Array<ManifestWorkspaceView> = [
},
];
const workspaceViewCollections: Array<ManifestWorkspaceViewCollection> = [
{
type: 'workspaceViewCollection',
alias: 'Umb.WorkspaceView.Document.Collection',
name: 'Document Workspace Collection View',
weight: 300,
meta: {
workspaces: ['Umb.Workspace.Document'],
label: 'Documents',
pathname: 'collection',
icon: 'umb:grid',
entityType: 'document',
repositoryAlias: DOCUMENT_REPOSITORY_ALIAS
},
},
];
const workspaceActions: Array<ManifestWorkspaceAction> = [
{
type: 'workspaceAction',
@@ -77,4 +95,4 @@ const workspaceActions: Array<ManifestWorkspaceAction> = [
},
];
export const manifests = [workspace, ...workspaceViews, ...workspaceActions];
export const manifests = [workspace, ...workspaceViews, ...workspaceViewCollections, ...workspaceActions];

View File

@@ -3,6 +3,9 @@ import { UmbTreeStore } from '@umbraco-cms/store';
import { UmbControllerHostInterface } from '@umbraco-cms/controller';
import { UmbContextToken, UmbContextConsumerController } from '@umbraco-cms/context-api';
import { ArrayState, UmbObserverController } from '@umbraco-cms/observable-api';
import { umbExtensionsRegistry } from '@umbraco-cms/extensions-api';
import { createExtensionClass } from 'libs/extensions-api/create-extension-class.function';
import { UmbTreeRepository } from '@umbraco-cms/repository';
export class UmbCollectionContext<
DataType extends ContentTreeItem,
StoreType extends UmbTreeStore<DataType> = UmbTreeStore<DataType>
@@ -11,6 +14,8 @@ export class UmbCollectionContext<
private _host: UmbControllerHostInterface;
private _entityKey: string | null;
#repository?: UmbTreeRepository;
private _store?: StoreType;
protected _dataObserver?: UmbObserverController<DataType[]>;
@@ -26,18 +31,35 @@ export class UmbCollectionContext<
public readonly search = this._search.asObservable();
*/
constructor(host: UmbControllerHostInterface, entityKey: string | null, storeAlias: string) {
constructor(host: UmbControllerHostInterface, entityKey: string | null, storeAlias?: string, repositoryAlias?: string) {
this._host = host;
this._entityKey = entityKey;
new UmbContextConsumerController(this._host, storeAlias, (_instance: StoreType) => {
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.
return;
}
this._onStoreSubscription();
});
if(storeAlias) {
new UmbContextConsumerController(this._host, storeAlias, (_instance: StoreType) => {
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.
return;
}
this._onStoreSubscription();
});
} else if (repositoryAlias) {
console.log("has repo alias:", repositoryAlias);
new UmbObserverController(this._host,
umbExtensionsRegistry.getByTypeAndAlias('repository', repositoryAlias),
async (repositoryManifest) => {
// Do something..
if(repositoryManifest) {
// TODO: use the right interface here, we might need a collection repository interface.
const result = await createExtensionClass<UmbTreeRepository>(repositoryManifest, [this._host]);
this.#repository = result;
console.log("this.#repository", this.#repository)
}
}
);
}
}
/*

View File

@@ -0,0 +1,139 @@
import { UUITextStyles } from '@umbraco-ui/uui-css';
import { css, html } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { UmbCollectionContext, UMB_COLLECTION_CONTEXT_TOKEN } from '../collection.context';
import {
UmbTableColumn,
UmbTableConfig,
UmbTableDeselectedEvent,
UmbTableElement,
UmbTableItem,
UmbTableOrderedEvent,
UmbTableSelectedEvent,
} from '../../components/table';
import type { DocumentDetails } from '@umbraco-cms/models';
import { UmbLitElement } from '@umbraco-cms/element';
type EntityType = DocumentDetails;
@customElement('umb-collection-view-document-table')
export class UmbCollectionViewDocumentTableElement extends UmbLitElement {
static styles = [
UUITextStyles,
css`
:host {
display: block;
box-sizing: border-box;
height: 100%;
width: 100%;
padding: var(--uui-size-space-3) var(--uui-size-space-6);
}
/* TODO: Should we have embedded padding in the table component? */
umb-table {
padding: 0; /* To fix the embedded padding in the table component. */
}
`,
];
@state()
private _items?: Array<EntityType>;
@state()
private _tableConfig: UmbTableConfig = {
allowSelection: true,
};
@state()
private _tableColumns: Array<UmbTableColumn> = [
{
name: 'Name',
alias: 'entityName',
},
];
@state()
private _tableItems: Array<UmbTableItem> = [];
@state()
private _selection: Array<string> = [];
private _collectionContext?: UmbCollectionContext<EntityType>;
constructor() {
super();
this.consumeContext(UMB_COLLECTION_CONTEXT_TOKEN, (instance) => {
this._collectionContext = instance;
this._observeCollectionContext();
});
}
private _observeCollectionContext() {
if (!this._collectionContext) return;
this.observe(this._collectionContext.data, (items) => {
this._items = items;
this._createTableItems(this._items);
});
this.observe(this._collectionContext.selection, (selection) => {
this._selection = selection;
});
}
private _createTableItems(items: Array<any>) {
this._tableItems = items.map((item) => {
return {
key: item.key,
icon: item.icon,
data: [
{
columnAlias: 'entityName',
value: item.name || 'Untitled',
},
],
};
});
}
private _handleSelect(event: UmbTableSelectedEvent) {
event.stopPropagation();
const table = event.target as UmbTableElement;
const selection = table.selection;
this._collectionContext?.setSelection(selection);
}
private _handleDeselect(event: UmbTableDeselectedEvent) {
event.stopPropagation();
const table = event.target as UmbTableElement;
const selection = table.selection;
this._collectionContext?.setSelection(selection);
}
private _handleOrdering(event: UmbTableOrderedEvent) {
const table = event.target as UmbTableElement;
const orderingColumn = table.orderingColumn;
const orderingDesc = table.orderingDesc;
console.log(`fetch media items, order column: ${orderingColumn}, desc: ${orderingDesc}`);
}
render() {
return html`
<umb-table
.config=${this._tableConfig}
.columns=${this._tableColumns}
.items=${this._tableItems}
.selection=${this._selection}
@selected="${this._handleSelect}"
@deselected="${this._handleDeselect}"
@ordered="${this._handleOrdering}"></umb-table>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'umb-collection-view-document-table': UmbCollectionViewDocumentTableElement;
}
}

View File

@@ -1,6 +1,24 @@
import type { ManifestCollectionView } from '@umbraco-cms/models';
export const manifests: Array<ManifestCollectionView> = [
{
type: 'collectionView',
alias: 'Umb.CollectionView.Table',
name: 'Table',
elementName: 'umb-collection-view-document-table',
loader: () => import('./collection-view-document-table.element'),
weight: 200,
meta: {
label: 'Table',
icon: 'umb:box',
entityType: 'document',
pathName: 'table',
},
},
{
type: 'collectionView',
alias: 'Umb.CollectionView.Grid',

View File

@@ -52,7 +52,8 @@ export class UmbWorkspaceViewCollectionElement extends UmbLitElement {
this._collectionContext = new UmbCollectionContext(
this,
entityKey,
manifestMeta.storeAlias
manifestMeta.storeAlias,
manifestMeta.repositoryAlias
);
this.provideContext(UMB_COLLECTION_CONTEXT_TOKEN, this._collectionContext);
}