diff --git a/src/Umbraco.Web.UI.Client/examples/collection/README.md b/src/Umbraco.Web.UI.Client/examples/collection/README.md new file mode 100644 index 0000000000..d845cb46aa --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/collection/README.md @@ -0,0 +1,20 @@ +# Collection Example + +This example demonstrates how to register a collection with collection views. + +The example includes: + +- Collection Registration +- Collection Repository +- Collection Pagination +- Table Collection View +- Card Collection View +- Collection as a Dashboard +- Collection as a Workspace View + +TODO: This example is not complete, it is missing the following features: + +- Collection Action +- Collection Filtering +- Entity Actions +- Selection + Bulk Actions diff --git a/src/Umbraco.Web.UI.Client/examples/collection/collection/card-view/collection-view.element.ts b/src/Umbraco.Web.UI.Client/examples/collection/collection/card-view/collection-view.element.ts new file mode 100644 index 0000000000..64a49f2073 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/collection/collection/card-view/collection-view.element.ts @@ -0,0 +1,82 @@ +import type { ExampleCollectionItemModel } from '../repository/types.js'; +import type { UmbDefaultCollectionContext } from '@umbraco-cms/backoffice/collection'; +import { UMB_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection'; +import { css, html, customElement, state, repeat } from '@umbraco-cms/backoffice/external/lit'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; + +@customElement('example-card-collection-view') +export class ExampleCardCollectionViewElement extends UmbLitElement { + @state() + private _items: Array = []; + + #collectionContext?: UmbDefaultCollectionContext; + + constructor() { + super(); + + this.consumeContext(UMB_COLLECTION_CONTEXT, (instance) => { + this.#collectionContext = instance; + this.#observeCollectionItems(); + }); + } + + #observeCollectionItems() { + this.observe(this.#collectionContext?.items, (items) => (this._items = items || []), 'umbCollectionItemsObserver'); + } + + override render() { + return html` +
+ ${repeat( + this._items, + (item) => item.unique, + (item) => + html` + +
${item.name}
+
`, + )} +
+ `; + } + + static override styles = [ + UmbTextStyles, + css` + :host { + display: flex; + flex-direction: column; + } + + #card-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + grid-auto-rows: 200px; + gap: var(--uui-size-space-5); + } + + uui-card { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + height: 100%; + + uui-icon { + font-size: 2em; + margin-bottom: var(--uui-size-space-4); + } + } + `, + ]; +} + +export { ExampleCardCollectionViewElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'example-card-collection-view': ExampleCardCollectionViewElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/examples/collection/collection/card-view/index.ts b/src/Umbraco.Web.UI.Client/examples/collection/collection/card-view/index.ts new file mode 100644 index 0000000000..e9d6c50fc5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/collection/collection/card-view/index.ts @@ -0,0 +1 @@ +export { UMB_LANGUAGE_TABLE_COLLECTION_VIEW_ALIAS } from './manifests.js'; diff --git a/src/Umbraco.Web.UI.Client/examples/collection/collection/card-view/manifests.ts b/src/Umbraco.Web.UI.Client/examples/collection/collection/card-view/manifests.ts new file mode 100644 index 0000000000..bf7299c530 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/collection/collection/card-view/manifests.ts @@ -0,0 +1,23 @@ +import { EXAMPLE_COLLECTION_ALIAS } from '../constants.js'; +import { UMB_COLLECTION_ALIAS_CONDITION } from '@umbraco-cms/backoffice/collection'; + +export const manifests: Array = [ + { + type: 'collectionView', + alias: 'Example.CollectionView.Card', + name: 'Example Card Collection View', + js: () => import('./collection-view.element.js'), + weight: 50, + meta: { + label: 'Card', + icon: 'icon-grid', + pathName: 'card', + }, + conditions: [ + { + alias: UMB_COLLECTION_ALIAS_CONDITION, + match: EXAMPLE_COLLECTION_ALIAS, + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/examples/collection/collection/constants.ts b/src/Umbraco.Web.UI.Client/examples/collection/collection/constants.ts new file mode 100644 index 0000000000..7afa1073f7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/collection/collection/constants.ts @@ -0,0 +1 @@ +export const EXAMPLE_COLLECTION_ALIAS = 'Example.Collection'; diff --git a/src/Umbraco.Web.UI.Client/examples/collection/collection/manifests.ts b/src/Umbraco.Web.UI.Client/examples/collection/collection/manifests.ts new file mode 100644 index 0000000000..eb8b44d061 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/collection/collection/manifests.ts @@ -0,0 +1,20 @@ +import { EXAMPLE_COLLECTION_ALIAS } from './constants.js'; +import { EXAMPLE_COLLECTION_REPOSITORY_ALIAS } from './repository/constants.js'; +import { manifests as cardViewManifests } from './card-view/manifests.js'; +import { manifests as repositoryManifests } from './repository/manifests.js'; +import { manifests as tableViewManifests } from './table-view/manifests.js'; + +export const manifests: Array = [ + { + type: 'collection', + kind: 'default', + alias: EXAMPLE_COLLECTION_ALIAS, + name: 'Example Collection', + meta: { + repositoryAlias: EXAMPLE_COLLECTION_REPOSITORY_ALIAS, + }, + }, + ...cardViewManifests, + ...repositoryManifests, + ...tableViewManifests, +]; diff --git a/src/Umbraco.Web.UI.Client/examples/collection/collection/repository/collection.repository.ts b/src/Umbraco.Web.UI.Client/examples/collection/collection/repository/collection.repository.ts new file mode 100644 index 0000000000..ecd8a1495c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/collection/collection/repository/collection.repository.ts @@ -0,0 +1,64 @@ +import type { ExampleCollectionFilterModel, ExampleCollectionItemModel } from './types.js'; +import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository'; +import type { UmbCollectionRepository } from '@umbraco-cms/backoffice/collection'; + +export class ExampleCollectionRepository + extends UmbRepositoryBase + implements UmbCollectionRepository +{ + async requestCollection(args: ExampleCollectionFilterModel) { + const skip = args.skip || 0; + const take = args.take || 10; + + // Simulating a data fetch. This would in most cases be replaced with an API call. + let items = [ + { + unique: '3e31e9c5-7d66-4c99-a9e5-d9f2b1e2b22f', + entityType: 'example', + name: 'Example Item 1', + }, + { + unique: 'bc9b6e24-4b11-4dd6-8d4e-7c4f70e59f3c', + entityType: 'example', + name: 'Example Item 2', + }, + { + unique: '5a2f4e3a-ef7e-470e-8c3c-3d859c02ae0d', + entityType: 'example', + name: 'Example Item 3', + }, + { + unique: 'f4c3d8b8-6d79-4c87-9aa9-56b1d8fda702', + entityType: 'example', + name: 'Example Item 4', + }, + { + unique: 'c9f0a8a3-1b49-4724-bde3-70e31592eb6e', + entityType: 'example', + name: 'Example Item 5', + }, + ]; + + // Simulating filtering based on the args + if (args.filter) { + items = items.filter((item) => item.name.toLowerCase().includes(args.filter!.toLowerCase())); + } + + // Get the total number of items before pagination + const totalItems = items.length; + + // Simulating pagination + const start = skip; + const end = start + take; + items = items.slice(start, end); + + const data = { + items, + total: totalItems, + }; + + return { data }; + } +} + +export { ExampleCollectionRepository as api }; diff --git a/src/Umbraco.Web.UI.Client/examples/collection/collection/repository/constants.ts b/src/Umbraco.Web.UI.Client/examples/collection/collection/repository/constants.ts new file mode 100644 index 0000000000..edfe07d179 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/collection/collection/repository/constants.ts @@ -0,0 +1 @@ +export const EXAMPLE_COLLECTION_REPOSITORY_ALIAS = 'Example.Repository.Collection'; diff --git a/src/Umbraco.Web.UI.Client/examples/collection/collection/repository/manifests.ts b/src/Umbraco.Web.UI.Client/examples/collection/collection/repository/manifests.ts new file mode 100644 index 0000000000..c699409c4e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/collection/collection/repository/manifests.ts @@ -0,0 +1,10 @@ +import { EXAMPLE_COLLECTION_REPOSITORY_ALIAS } from './constants.js'; + +export const manifests: Array = [ + { + type: 'repository', + alias: EXAMPLE_COLLECTION_REPOSITORY_ALIAS, + name: 'Example Collection Repository', + api: () => import('./collection.repository.js'), + }, +]; diff --git a/src/Umbraco.Web.UI.Client/examples/collection/collection/repository/types.ts b/src/Umbraco.Web.UI.Client/examples/collection/collection/repository/types.ts new file mode 100644 index 0000000000..4430638c16 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/collection/collection/repository/types.ts @@ -0,0 +1,9 @@ +import type { UmbCollectionFilterModel } from '@umbraco-cms/backoffice/collection'; + +export interface ExampleCollectionItemModel { + unique: string; + entityType: string; + name: string; +} + +export interface ExampleCollectionFilterModel extends UmbCollectionFilterModel {} diff --git a/src/Umbraco.Web.UI.Client/examples/collection/collection/table-view/collection-view.element.ts b/src/Umbraco.Web.UI.Client/examples/collection/collection/table-view/collection-view.element.ts new file mode 100644 index 0000000000..530ee846dd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/collection/collection/table-view/collection-view.element.ts @@ -0,0 +1,89 @@ +import type { ExampleCollectionItemModel } from '../repository/types.js'; +import type { UmbDefaultCollectionContext } from '@umbraco-cms/backoffice/collection'; +import { UMB_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection'; +import type { UmbTableColumn, UmbTableConfig, UmbTableItem } from '@umbraco-cms/backoffice/components'; +import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; + +@customElement('example-table-collection-view') +export class ExampleTableCollectionViewElement extends UmbLitElement { + @state() + private _tableConfig: UmbTableConfig = { + allowSelection: false, + }; + + @state() + private _tableColumns: Array = [ + { + name: 'Name', + alias: 'name', + }, + ]; + + @state() + private _tableItems: Array = []; + + #collectionContext?: UmbDefaultCollectionContext; + + constructor() { + super(); + + this.consumeContext(UMB_COLLECTION_CONTEXT, (instance) => { + this.#collectionContext = instance; + this.#observeCollectionItems(); + }); + } + + #observeCollectionItems() { + this.observe( + this.#collectionContext?.items, + (items) => this.#createTableItems(items), + 'umbCollectionItemsObserver', + ); + } + + #createTableItems(items: Array | undefined) { + if (!items) { + this._tableItems = []; + return; + } + + this._tableItems = items.map((item) => { + return { + id: item.unique, + icon: 'icon-newspaper', + data: [ + { + columnAlias: 'name', + value: item.name, + }, + ], + }; + }); + } + + override render() { + return html` + + `; + } + + static override styles = [ + UmbTextStyles, + css` + :host { + display: flex; + flex-direction: column; + } + `, + ]; +} + +export { ExampleTableCollectionViewElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'example-table-collection-view': ExampleTableCollectionViewElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/examples/collection/collection/table-view/index.ts b/src/Umbraco.Web.UI.Client/examples/collection/collection/table-view/index.ts new file mode 100644 index 0000000000..e9d6c50fc5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/collection/collection/table-view/index.ts @@ -0,0 +1 @@ +export { UMB_LANGUAGE_TABLE_COLLECTION_VIEW_ALIAS } from './manifests.js'; diff --git a/src/Umbraco.Web.UI.Client/examples/collection/collection/table-view/manifests.ts b/src/Umbraco.Web.UI.Client/examples/collection/collection/table-view/manifests.ts new file mode 100644 index 0000000000..495a2bcbe9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/collection/collection/table-view/manifests.ts @@ -0,0 +1,23 @@ +import { EXAMPLE_COLLECTION_ALIAS } from '../constants.js'; +import { UMB_COLLECTION_ALIAS_CONDITION } from '@umbraco-cms/backoffice/collection'; + +export const manifests: Array = [ + { + type: 'collectionView', + alias: 'Example.CollectionView.Table', + name: 'Example Table Collection View', + js: () => import('./collection-view.element.js'), + weight: 100, + meta: { + label: 'Table', + icon: 'icon-list', + pathName: 'table', + }, + conditions: [ + { + alias: UMB_COLLECTION_ALIAS_CONDITION, + match: EXAMPLE_COLLECTION_ALIAS, + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/examples/collection/dashboard-with-collection/dashboard-with-collection.element.ts b/src/Umbraco.Web.UI.Client/examples/collection/dashboard-with-collection/dashboard-with-collection.element.ts new file mode 100644 index 0000000000..c11c88ef71 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/collection/dashboard-with-collection/dashboard-with-collection.element.ts @@ -0,0 +1,23 @@ +import { EXAMPLE_COLLECTION_ALIAS } from '../collection/constants.js'; +import { html, customElement, LitElement } from '@umbraco-cms/backoffice/external/lit'; +import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api'; +import type { UmbCollectionConfiguration } from '@umbraco-cms/backoffice/collection'; + +@customElement('example-dashboard-with-collection') +export class ExampleDashboardWithCollection extends UmbElementMixin(LitElement) { + #config: UmbCollectionConfiguration = { + pageSize: 3, + }; + + override render() { + return html``; + } +} + +export { ExampleDashboardWithCollection as element }; + +declare global { + interface HTMLElementTagNameMap { + 'example-dashboard-with-collection': ExampleDashboardWithCollection; + } +} diff --git a/src/Umbraco.Web.UI.Client/examples/collection/dashboard-with-collection/manifests.ts b/src/Umbraco.Web.UI.Client/examples/collection/dashboard-with-collection/manifests.ts new file mode 100644 index 0000000000..3f8c7a6715 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/collection/dashboard-with-collection/manifests.ts @@ -0,0 +1,14 @@ +export const manifests: Array = [ + { + type: 'dashboard', + kind: 'default', + name: 'Example Dashboard With Collection', + alias: 'Example.Dashboard.WithCollection', + element: () => import('./dashboard-with-collection.element.js'), + weight: 3000, + meta: { + label: 'Collection Example', + pathname: 'collection-example', + }, + }, +]; diff --git a/src/Umbraco.Web.UI.Client/examples/collection/index.ts b/src/Umbraco.Web.UI.Client/examples/collection/index.ts new file mode 100644 index 0000000000..4d7691f330 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/collection/index.ts @@ -0,0 +1,9 @@ +import { manifests as collectionManifests } from './collection/manifests.js'; +import { manifests as dashboardManifests } from './dashboard-with-collection/manifests.js'; +import { manifests as workspaceViewManifests } from './workspace-view-with-collection/manifests.js'; + +export const manifests: Array = [ + ...collectionManifests, + ...dashboardManifests, + ...workspaceViewManifests, +]; diff --git a/src/Umbraco.Web.UI.Client/examples/collection/workspace-view-with-collection/manifests.ts b/src/Umbraco.Web.UI.Client/examples/collection/workspace-view-with-collection/manifests.ts new file mode 100644 index 0000000000..d4650f9f65 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/collection/workspace-view-with-collection/manifests.ts @@ -0,0 +1,25 @@ +import { EXAMPLE_COLLECTION_ALIAS } from '../collection/constants.js'; +import { UMB_WORKSPACE_CONDITION_ALIAS } from '@umbraco-cms/backoffice/workspace'; +import { UMB_DOCUMENT_WORKSPACE_ALIAS } from '@umbraco-cms/backoffice/document'; + +export const manifests: Array = [ + { + type: 'workspaceView', + kind: 'collection', + name: 'Example Workspace View With Collection', + alias: 'Example.WorkspaceView.WithCollection', + weight: 3000, + meta: { + label: 'Collection Example', + pathname: 'collection-example', + icon: 'icon-layers', + collectionAlias: EXAMPLE_COLLECTION_ALIAS, + }, + conditions: [ + { + alias: UMB_WORKSPACE_CONDITION_ALIAS, + match: UMB_DOCUMENT_WORKSPACE_ALIAS, + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/server-extension-registrator.controller.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/server-extension-registrator.controller.ts index bd366ddb06..786e9bcf8b 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/server-extension-registrator.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/server-extension-registrator.controller.ts @@ -67,7 +67,7 @@ export class UmbServerExtensionRegistrator extends UmbControllerBase { const apiBaseUrl = serverContext?.getServerUrl(); - packages.forEach((p) => { + packages?.forEach((p) => { p.extensions?.forEach((e) => { // Crudely validate that the extension at least follows a basic manifest structure // Idea: Use `Zod` to validate the manifest