Merge branch 'main' into bugfix/silo-based-audit-log

This commit is contained in:
Mads Rasmussen
2024-04-30 21:27:12 +02:00
84 changed files with 1851 additions and 303 deletions

View File

@@ -68,6 +68,7 @@
"./repository": "./dist-cms/packages/core/repository/index.js",
"./resources": "./dist-cms/packages/core/resources/index.js",
"./router": "./dist-cms/packages/core/router/index.js",
"./search": "./dist-cms/packages/search/index.js",
"./section": "./dist-cms/packages/core/section/index.js",
"./settings": "./dist-cms/packages/settings/index.js",
"./server-file-system": "./dist-cms/packages/core/server-file-system/index.js",

View File

@@ -804,7 +804,7 @@ export const data: Array<UmbMockDataTypeModel> = [
value: [
{ alias: 'sortOrder', header: 'Sort order', isSystem: true, nameTemplate: '' },
{ alias: 'updateDate', header: 'Last edited', isSystem: true },
{ alias: 'owner', header: 'Created by', isSystem: true },
{ alias: 'creator', header: 'Created by', isSystem: true },
],
},
{ alias: 'orderBy', value: 'updateDate' },
@@ -849,7 +849,7 @@ export const data: Array<UmbMockDataTypeModel> = [
value: [
{ alias: 'sortOrder', header: 'Sort order', isSystem: true, nameTemplate: '' },
{ alias: 'updateDate', header: 'Last edited', isSystem: true },
{ alias: 'owner', header: 'Created by', isSystem: true },
{ alias: 'creator', header: 'Created by', isSystem: true },
],
},
{ alias: 'orderBy', value: 'updateDate' },

View File

@@ -75,7 +75,11 @@ export class UmbEntityActionsBundleElement extends UmbLitElement {
#openContextMenu() {
if (!this.entityType) throw new Error('Entity type is not defined');
if (this.unique === undefined) throw new Error('Unique is not defined');
this.#sectionSidebarContext?.toggleContextMenu(this.entityType, this.unique, this.label);
this.#sectionSidebarContext?.toggleContextMenu(this, {
entityType: this.entityType,
unique: this.unique,
headline: this.label,
});
}
async #onFirstActionClick(event: PointerEvent) {

View File

@@ -62,6 +62,8 @@ import type { ManifestEntityUserPermission } from './entity-user-permission.mode
import type { ManifestGranularUserPermission } from './user-granular-permission.model.js';
import type { ManifestCollectionAction } from './collection-action.model.js';
import type { ManifestMfaLoginProvider } from './mfa-login-provider.model.js';
import type { ManifestSearchProvider } from './search-provider.model.js';
import type { ManifestSearchResultItem } from './search-result-item.model.js';
import type { ManifestAppEntryPoint } from './app-entry-point.model.js';
import type { ManifestBackofficeEntryPoint } from './backoffice-entry-point.model.js';
import type { ManifestEntryPoint } from './entry-point.model.js';
@@ -96,6 +98,8 @@ export type * from './package-view.model.js';
export type * from './property-action.model.js';
export type * from './property-editor.model.js';
export type * from './repository.model.js';
export type * from './search-provider.model.js';
export type * from './search-result-item.model.js';
export type * from './section-sidebar-app.model.js';
export type * from './section-view.model.js';
export type * from './section.model.js';
@@ -145,28 +149,31 @@ export type ManifestTypes =
| ManifestAppEntryPoint
| ManifestAuthProvider
| ManifestBackofficeEntryPoint
| ManifestBundle<ManifestTypes>
| ManifestBlockEditorCustomView
| ManifestBundle<ManifestTypes>
| ManifestCollection
| ManifestCollectionView
| ManifestCollectionAction
| ManifestCollectionView
| ManifestCondition
| ManifestCurrentUserAction
| ManifestCurrentUserActionDefaultKind
| ManifestCondition
| ManifestDashboard
| ManifestDashboardCollection
| ManifestDynamicRootOrigin
| ManifestDynamicRootQueryStep
| ManifestEntityActions
| ManifestEntityBulkAction
| ManifestEntityUserPermission
| ManifestEntryPoint
| ManifestExternalLoginProvider
| ManifestGlobalContext
| ManifestGranularUserPermission
| ManifestHeaderApp
| ManifestHeaderAppButtonKind
| ManifestHealthCheck
| ManifestIcons
| ManifestItemStore
| ManifestLocalization
| ManifestMenu
| ManifestMenuItem
| ManifestMenuItemTreeKind
@@ -177,6 +184,8 @@ export type ManifestTypes =
| ManifestPropertyEditorSchema
| ManifestPropertyEditorUi
| ManifestRepository
| ManifestSearchProvider
| ManifestSearchResultItem
| ManifestSection
| ManifestSectionSidebarApp
| ManifestSectionSidebarAppMenuKind
@@ -184,7 +193,6 @@ export type ManifestTypes =
| ManifestStore
| ManifestTheme
| ManifestTinyMcePlugin
| ManifestLocalization
| ManifestTree
| ManifestTreeItem
| ManifestTreeStore
@@ -195,6 +203,4 @@ export type ManifestTypes =
| ManifestWorkspaceFooterApps
| ManifestWorkspaces
| ManifestWorkspaceViews
| ManifestEntityUserPermission
| ManifestGranularUserPermission
| ManifestBase;

View File

@@ -0,0 +1,18 @@
import type { UmbSearchProvider, UmbSearchResultItemModel } from '@umbraco-cms/backoffice/search';
import type { ManifestApi } from '@umbraco-cms/backoffice/extension-api';
/**
* Represents an search provider that can be used to search.
*/
export interface ManifestSearchProvider extends ManifestApi<UmbSearchProvider<UmbSearchResultItemModel>> {
type: 'searchProvider';
meta?: MetaSearchProvider;
}
export interface MetaSearchProvider {
/**
* The label of the provider that is shown to the user.
*/
label?: string;
}

View File

@@ -0,0 +1,9 @@
import type { ManifestElement } from '@umbraco-cms/backoffice/extension-api';
/**
* Represents a search result element.
*/
export interface ManifestSearchResultItem extends ManifestElement {
type: 'searchResultItem';
forEntityTypes: Array<string>;
}

View File

@@ -4,6 +4,8 @@ import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, nothing, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { observeMultiple } from '@umbraco-cms/backoffice/observable-api';
import type { UmbContextRequestEvent } from '@umbraco-cms/backoffice/context-api';
import { UMB_CONTENT_REQUEST_EVENT_TYPE } from '@umbraco-cms/backoffice/context-api';
@customElement('umb-section-sidebar-context-menu')
export class UmbSectionSidebarContextMenuElement extends UmbLitElement {
@@ -66,6 +68,17 @@ export class UmbSectionSidebarContextMenuElement extends UmbLitElement {
this.#closeContextMenu();
}
#proxyContextRequests(event: UmbContextRequestEvent) {
if (!this.#sectionSidebarContext) return;
// Note for this hack (The if-sentence): [NL]
// We do not currently have a good enough control to ensure that the proxy is last, meaning if another context is provided at this element, it might respond after the proxy event has been dispatched.
// To avoid such this hack just prevents proxying the event if its a request for its own context.
if (event.contextAlias !== UMB_SECTION_SIDEBAR_CONTEXT.contextAlias) {
event.stopImmediatePropagation();
this.#sectionSidebarContext.getContextElement()?.dispatchEvent(event.clone());
}
}
render() {
return html`
${this.#renderBackdrop()}
@@ -84,7 +97,7 @@ export class UmbSectionSidebarContextMenuElement extends UmbLitElement {
#renderModal() {
return this._isOpen && this._unique !== undefined && this._entityType
? html`<div id="action-modal">
? html`<div id="action-modal" @umb:context-request=${this.#proxyContextRequests}>
${this._headline ? html`<h3>${this.localize.string(this._headline)}</h3>` : nothing}
<umb-entity-action-list
@action-executed=${this.#onActionExecuted}

View File

@@ -1,3 +1,4 @@
import type { UmbOpenContextMenuArgs } from './types.js';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
@@ -16,21 +17,24 @@ export class UmbSectionSidebarContext extends UmbContextBase<UmbSectionSidebarCo
#headline = new UmbStringState<undefined>(undefined);
headline = this.#headline.asObservable();
#contextElement: Element | undefined = undefined;
constructor(host: UmbControllerHost) {
super(host, UMB_SECTION_SIDEBAR_CONTEXT);
}
toggleContextMenu(entityType: string, unique: string | null | undefined, headline: string | undefined) {
this.openContextMenu(entityType, unique, headline);
toggleContextMenu(host: Element, args: UmbOpenContextMenuArgs) {
this.openContextMenu(host, args);
}
// TODO: we wont get notified about tree item name changes because we don't have a subscription
// we need to figure out how we best can handle this when we only know the entity and unique id
openContextMenu(entityType: string, unique: string | null | undefined, headline: string | undefined) {
this.#entityType.setValue(entityType);
this.#unique.setValue(unique);
this.#headline.setValue(headline);
openContextMenu(host: Element, args: UmbOpenContextMenuArgs) {
this.#entityType.setValue(args.entityType);
this.#unique.setValue(args.unique);
this.#headline.setValue(args.headline);
this.#contextMenuIsOpen.setValue(true);
this.#contextElement = host;
}
closeContextMenu() {
@@ -38,6 +42,11 @@ export class UmbSectionSidebarContext extends UmbContextBase<UmbSectionSidebarCo
this.#entityType.setValue(undefined);
this.#unique.setValue(undefined);
this.#headline.setValue(undefined);
this.#contextElement = undefined;
}
getContextElement() {
return this.#contextElement;
}
}

View File

@@ -0,0 +1,5 @@
export interface UmbOpenContextMenuArgs {
entityType: string;
unique: string | null | undefined;
headline: string | undefined;
}

View File

@@ -180,7 +180,11 @@ export abstract class UmbTreeItemContextBase<TreeItemType extends UmbTreeItemMod
throw new Error('Could not request children, tree item is not set');
}
this.#sectionSidebarContext?.toggleContextMenu(this.entityType, this.unique, this.getTreeItem()?.name || '');
this.#sectionSidebarContext?.toggleContextMenu(this.getHostElement(), {
entityType: this.entityType,
unique: this.unique,
headline: this.getTreeItem()?.name || '',
});
}
public select() {

View File

@@ -1,5 +1,6 @@
import type { UmbDataTypeCollectionFilterModel } from '../types.js';
import type { UmbDataTypeItemModel } from '../../repository/index.js';
import { UMB_DATA_TYPE_ENTITY_TYPE } from '../../entity.js';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
import { DataTypeService } from '@umbraco-cms/backoffice/external/backend-api';
import type { UmbCollectionDataSource } from '@umbraco-cms/backoffice/collection';
@@ -53,6 +54,7 @@ export class UmbDataTypeCollectionServerDataSource implements UmbCollectionDataS
const mappedItems: Array<UmbDataTypeItemModel> = items.map((item: DataTypeItemResponseModel) => {
const dataTypeDetail: UmbDataTypeItemModel = {
entityType: UMB_DATA_TYPE_ENTITY_TYPE,
unique: item.id,
name: item.name,
propertyEditorUiAlias: item.editorUiAlias!,

View File

@@ -1,18 +1,20 @@
import { manifests as collectionManifests } from './collection/manifests.js';
import { manifests as entityActions } from './entity-actions/manifests.js';
import { manifests as repositoryManifests } from './repository/manifests.js';
import { manifests as menuManifests } from './menu/manifests.js';
import { manifests as modalManifests } from './modals/manifests.js';
import { manifests as repositoryManifests } from './repository/manifests.js';
import { manifests as searchProviderManifests } from './search/manifests.js';
import { manifests as treeManifests } from './tree/manifests.js';
import { manifests as workspaceManifests } from './workspace/manifests.js';
import { manifests as modalManifests } from './modals/manifests.js';
import { manifests as collectionManifests } from './collection/manifests.js';
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
export const manifests: Array<ManifestTypes> = [
...collectionManifests,
...entityActions,
...repositoryManifests,
...menuManifests,
...modalManifests,
...repositoryManifests,
...searchProviderManifests,
...treeManifests,
...workspaceManifests,
...modalManifests,
...collectionManifests,
];

View File

@@ -30,14 +30,13 @@ export class UmbPropertyEditorUIPickerModalElement extends UmbModalBaseElement<
connectedCallback(): void {
super.connectedCallback();
this._submitLabel = this.data?.submitLabel ?? this._submitLabel;
// TODO: We never parse on a submit label, so this seem weird as we don't enable this of other places.
//this._submitLabel = this.data?.submitLabel ?? this._submitLabel;
this.#usePropertyEditorUIs();
}
#usePropertyEditorUIs() {
if (!this.data) return;
this.observe(umbExtensionsRegistry.byType('propertyEditorUi'), (propertyEditorUIs) => {
// Only include Property Editor UIs which has Property Editor Schema Alias
this._propertyEditorUIs = propertyEditorUIs.filter(

View File

@@ -1,3 +1,4 @@
import { UMB_DATA_TYPE_ENTITY_TYPE } from '../../entity.js';
import type { UmbDataTypeItemModel } from './types.js';
import { UmbItemServerDataSourceBase } from '@umbraco-cms/backoffice/repository';
import type { DataTypeItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
@@ -43,6 +44,7 @@ const getItems = (uniques: Array<string>) => DataTypeService.getItemDataType({ i
const mapper = (item: DataTypeItemResponseModel): UmbDataTypeItemModel => {
return {
entityType: UMB_DATA_TYPE_ENTITY_TYPE,
unique: item.id,
name: item.name,
propertyEditorUiAlias: item.editorUiAlias || '', // TODO: why can this be undefined or null on the server?

View File

@@ -1,4 +1,7 @@
import type { UmbDataTypeEntityType } from '../../entity.js';
export interface UmbDataTypeItemModel {
entityType: UmbDataTypeEntityType;
unique: string;
name: string;
propertyEditorUiAlias: string;

View File

@@ -0,0 +1,23 @@
import { UmbDataTypeSearchServerDataSource } from './data-type-search.server.data-source.js';
import type { UmbDataTypeSearchItemModel } from './data-type.search-provider.js';
import type { UmbSearchRepository, UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
export class UmbDataTypeSearchRepository
extends UmbControllerBase
implements UmbSearchRepository<UmbDataTypeSearchItemModel>, UmbApi
{
#dataSource: UmbDataTypeSearchServerDataSource;
constructor(host: UmbControllerHost) {
super(host);
this.#dataSource = new UmbDataTypeSearchServerDataSource(this);
}
search(args: UmbSearchRequestArgs) {
return this.#dataSource.search(args);
}
}

View File

@@ -0,0 +1,55 @@
import { UMB_DATA_TYPE_ENTITY_TYPE } from '../entity.js';
import type { UmbDataTypeSearchItemModel } from './data-type.search-provider.js';
import type { UmbSearchDataSource, UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { DataTypeService } from '@umbraco-cms/backoffice/external/backend-api';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
/**
* A data source for the Rollback that fetches data from the server
* @export
* @class UmbDataTypeSearchServerDataSource
* @implements {RepositoryDetailDataSource}
*/
export class UmbDataTypeSearchServerDataSource implements UmbSearchDataSource<UmbDataTypeSearchItemModel> {
#host: UmbControllerHost;
/**
* Creates an instance of UmbDataTypeSearchServerDataSource.
* @param {UmbControllerHost} host
* @memberof UmbDataTypeSearchServerDataSource
*/
constructor(host: UmbControllerHost) {
this.#host = host;
}
/**
* Get a list of versions for a data
* @return {*}
* @memberof UmbDataTypeSearchServerDataSource
*/
async search(args: UmbSearchRequestArgs) {
const { data, error } = await tryExecuteAndNotify(
this.#host,
DataTypeService.getItemDataTypeSearch({
query: args.query,
}),
);
if (data) {
const mappedItems: Array<UmbDataTypeSearchItemModel> = data.items.map((item) => {
return {
href: '/section/settings/workspace/data-type/edit/' + item.id,
entityType: UMB_DATA_TYPE_ENTITY_TYPE,
unique: item.id,
name: item.name,
propertyEditorUiAlias: item.editorUiAlias || '',
};
});
return { data: { items: mappedItems, total: data.total } };
}
return { error };
}
}

View File

@@ -0,0 +1,25 @@
import type { UmbDataTypeItemModel } from '../index.js';
import { UmbDataTypeSearchRepository } from './data-type-search.repository.js';
import type { UmbSearchProvider, UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
export interface UmbDataTypeSearchItemModel extends UmbDataTypeItemModel {
href: string;
}
export class UmbDataTypeSearchProvider
extends UmbControllerBase
implements UmbSearchProvider<UmbDataTypeSearchItemModel>
{
#repository = new UmbDataTypeSearchRepository(this);
async search(args: UmbSearchRequestArgs) {
return this.#repository.search(args);
}
destroy(): void {
this.#repository.destroy();
}
}
export { UmbDataTypeSearchProvider as api };

View File

@@ -0,0 +1,21 @@
import { UMB_DATA_TYPE_ENTITY_TYPE } from '../entity.js';
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
export const manifests: Array<ManifestTypes> = [
{
name: 'Data Type Search Provider',
alias: 'Umb.SearchProvider.DataType',
type: 'searchProvider',
api: () => import('./data-type.search-provider.js'),
weight: 400,
meta: {
label: 'Data Types',
},
},
{
name: 'Data Type Search Result Item ',
alias: 'Umb.SearchResultItem.DataType',
type: 'searchResultItem',
forEntityTypes: [UMB_DATA_TYPE_ENTITY_TYPE],
},
];

View File

@@ -2,6 +2,7 @@ import { manifests as entityActionsManifests } from './entity-actions/manifests.
import { manifests as menuManifests } from './menu/manifests.js';
import { manifests as propertyEditorManifests } from './property-editors/manifests.js';
import { manifests as repositoryManifests } from './repository/manifests.js';
import { manifests as searchManifests } from './search/manifests.js';
import { manifests as treeManifests } from './tree/manifests.js';
import { manifests as workspaceManifests } from './workspace/manifests.js';
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
@@ -11,6 +12,7 @@ export const manifests: Array<ManifestTypes> = [
...menuManifests,
...propertyEditorManifests,
...repositoryManifests,
...searchManifests,
...treeManifests,
...workspaceManifests,
];

View File

@@ -1,3 +1,4 @@
import { UMB_DOCUMENT_TYPE_ENTITY_TYPE } from '../../entity.js';
import type { UmbDocumentTypeItemModel } from './types.js';
import { UmbItemServerDataSourceBase } from '@umbraco-cms/backoffice/repository';
import type { DocumentTypeItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
@@ -29,6 +30,7 @@ const getItems = (uniques: Array<string>) => DocumentTypeService.getItemDocument
const mapper = (item: DocumentTypeItemResponseModel): UmbDocumentTypeItemModel => {
return {
entityType: UMB_DOCUMENT_TYPE_ENTITY_TYPE,
isElement: item.isElement,
icon: item.icon,
unique: item.id,

View File

@@ -1,4 +1,7 @@
import type { UmbDocumentTypeEntityType } from '../../entity.js';
export type UmbDocumentTypeItemModel = {
entityType: UmbDocumentTypeEntityType;
unique: string;
name: string;
isElement: boolean;

View File

@@ -0,0 +1,23 @@
import { UmbDocumentTypeSearchServerDataSource } from './document-type-search.server.data-source.js';
import type { UmbDocumentTypeSearchItemModel } from './document-type.search-provider.js';
import type { UmbSearchRepository, UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
export class UmbDocumentTypeSearchRepository
extends UmbControllerBase
implements UmbSearchRepository<UmbDocumentTypeSearchItemModel>, UmbApi
{
#dataSource: UmbDocumentTypeSearchServerDataSource;
constructor(host: UmbControllerHost) {
super(host);
this.#dataSource = new UmbDocumentTypeSearchServerDataSource(this);
}
search(args: UmbSearchRequestArgs) {
return this.#dataSource.search(args);
}
}

View File

@@ -0,0 +1,56 @@
import { UMB_DOCUMENT_TYPE_ENTITY_TYPE } from '../entity.js';
import type { UmbDocumentTypeSearchItemModel } from './document-type.search-provider.js';
import type { UmbSearchDataSource, UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { DocumentTypeService } from '@umbraco-cms/backoffice/external/backend-api';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
/**
* A data source for the Rollback that fetches data from the server
* @export
* @class UmbDocumentTypeSearchServerDataSource
* @implements {RepositoryDetailDataSource}
*/
export class UmbDocumentTypeSearchServerDataSource implements UmbSearchDataSource<UmbDocumentTypeSearchItemModel> {
#host: UmbControllerHost;
/**
* Creates an instance of UmbDocumentTypeSearchServerDataSource.
* @param {UmbControllerHost} host
* @memberof UmbDocumentTypeSearchServerDataSource
*/
constructor(host: UmbControllerHost) {
this.#host = host;
}
/**
* Get a list of versions for a data
* @return {*}
* @memberof UmbDocumentTypeSearchServerDataSource
*/
async search(args: UmbSearchRequestArgs) {
const { data, error } = await tryExecuteAndNotify(
this.#host,
DocumentTypeService.getItemDocumentTypeSearch({
query: args.query,
}),
);
if (data) {
const mappedItems: Array<UmbDocumentTypeSearchItemModel> = data.items.map((item) => {
return {
href: '/section/settings/workspace/document-type/edit/' + item.id,
entityType: UMB_DOCUMENT_TYPE_ENTITY_TYPE,
isElement: item.isElement,
icon: item.icon,
unique: item.id,
name: item.name,
};
});
return { data: { items: mappedItems, total: data.total } };
}
return { error };
}
}

View File

@@ -0,0 +1,25 @@
import type { UmbDocumentTypeItemModel } from '../index.js';
import { UmbDocumentTypeSearchRepository } from './document-type-search.repository.js';
import type { UmbSearchProvider, UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
export interface UmbDocumentTypeSearchItemModel extends UmbDocumentTypeItemModel {
href: string;
}
export class UmbDocumentTypeSearchProvider
extends UmbControllerBase
implements UmbSearchProvider<UmbDocumentTypeSearchItemModel>
{
#repository = new UmbDocumentTypeSearchRepository(this);
async search(args: UmbSearchRequestArgs) {
return this.#repository.search(args);
}
destroy(): void {
this.#repository.destroy();
}
}
export { UmbDocumentTypeSearchProvider as api };

View File

@@ -0,0 +1,21 @@
import { UMB_DOCUMENT_TYPE_ENTITY_TYPE } from '../entity.js';
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
export const manifests: Array<ManifestTypes> = [
{
name: 'Document Type Search Provider',
alias: 'Umb.SearchProvider.DocumentType',
type: 'searchProvider',
api: () => import('./document-type.search-provider.js'),
weight: 600,
meta: {
label: 'Document Types',
},
},
{
name: 'Document Type Search Result Item ',
alias: 'Umb.SearchResultItem.DocumentType',
type: 'searchResultItem',
forEntityTypes: [UMB_DOCUMENT_TYPE_ENTITY_TYPE],
},
];

View File

@@ -24,8 +24,8 @@ export class UmbDocumentCollectionServerDataSource implements UmbCollectionDataS
orderCulture: query.orderCulture ?? 'en-US',
orderDirection: query.orderDirection === 'asc' ? DirectionModel.ASCENDING : DirectionModel.DESCENDING,
filter: query.filter,
skip: query.skip ?? 0,
take: query.take ?? 100,
skip: query.skip || 0,
take: query.take || 100,
};
const { data, error } = await tryExecuteAndNotify(this.#host, DocumentService.getCollectionDocumentById(params));
@@ -43,6 +43,7 @@ export class UmbDocumentCollectionServerDataSource implements UmbCollectionDataS
creator: item.creator,
icon: item.documentType.icon,
name: variant.name,
sortOrder: item.sortOrder,
state: variant.state,
updateDate: new Date(variant.updateDate),
updater: item.updater,

View File

@@ -17,6 +17,7 @@ export interface UmbDocumentCollectionItemModel {
creator?: string | null;
icon: string;
name: string;
sortOrder: number;
state: string;
updateDate: Date;
updater?: string | null;

View File

@@ -96,14 +96,13 @@ export class UmbDocumentGridCollectionViewElement extends UmbLitElement {
${repeat(
this._items,
(item) => item.unique,
(item, index) => this.#renderCard(index, item),
(item) => this.#renderCard(item),
)}
</div>
`;
}
#renderCard(index: number, item: UmbDocumentCollectionItemModel) {
const sortOrder = this._skip + index;
#renderCard(item: UmbDocumentCollectionItemModel) {
return html`
<uui-card-content-node
.name=${item.name ?? 'Unnamed Document'}
@@ -114,7 +113,7 @@ export class UmbDocumentGridCollectionViewElement extends UmbLitElement {
@selected=${() => this.#onSelect(item)}
@deselected=${() => this.#onDeselect(item)}>
<umb-icon slot="icon" name=${item.icon}></umb-icon>
${this.#renderState(item)} ${this.#renderProperties(sortOrder, item)}
${this.#renderState(item)} ${this.#renderProperties(item)}
</uui-card-content-node>
`;
}
@@ -142,15 +141,14 @@ export class UmbDocumentGridCollectionViewElement extends UmbLitElement {
}
}
#renderProperties(sortOrder: number, item: UmbDocumentCollectionItemModel) {
#renderProperties(item: UmbDocumentCollectionItemModel) {
if (!this._userDefinedProperties) return;
return html`
<ul>
${repeat(
this._userDefinedProperties,
(column) => column.alias,
(column) =>
html`<li><span>${column.header}:</span> ${getPropertyValueByAlias(sortOrder, item, column.alias)}</li>`,
(column) => html`<li><span>${column.header}:</span> ${getPropertyValueByAlias(item, column.alias)}</li>`,
)}
</ul>
`;

View File

@@ -1,25 +1,25 @@
import type { UmbDocumentCollectionItemModel } from '../types.js';
import { fromCamelCase } from '@umbraco-cms/backoffice/utils';
export { UMB_DOCUMENT_GRID_COLLECTION_VIEW_ALIAS, UMB_DOCUMENT_TABLE_COLLECTION_VIEW_ALIAS } from './manifests.js';
export function getPropertyValueByAlias(sortOrder: number, item: UmbDocumentCollectionItemModel, alias: string) {
export function getPropertyValueByAlias(item: UmbDocumentCollectionItemModel, alias: string) {
switch (alias) {
case 'contentTypeAlias':
return item.contentTypeAlias;
case 'createDate':
return item.createDate.toLocaleString();
case 'creator':
return item.creator;
case 'entityName':
return item.name;
case 'entityState':
return item.state.replace(/([A-Z])/g, ' $1');
case 'owner':
return item.creator;
case 'name':
return item.name;
case 'state':
return fromCamelCase(item.state);
case 'published':
return item.state !== 'Draft' ? 'True' : 'False';
case 'sortOrder':
return sortOrder;
return item.sortOrder;
case 'updateDate':
return item.updateDate.toLocaleString();
case 'updater':

View File

@@ -3,6 +3,7 @@ import { css, customElement, html, property, state } from '@umbraco-cms/backoffi
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UMB_WORKSPACE_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal';
import type { UmbTableColumn, UmbTableColumnLayoutElement, UmbTableItem } from '@umbraco-cms/backoffice/components';
import type { UUIButtonElement } from '@umbraco-cms/backoffice/external/uui';
@customElement('umb-document-table-column-name')
export class UmbDocumentTableColumnNameElement extends UmbLitElement implements UmbTableColumnLayoutElement {
@@ -31,20 +32,20 @@ export class UmbDocumentTableColumnNameElement extends UmbLitElement implements
});
}
#onClick(event: Event) {
// TODO: [LK] Review the `stopPropagation` usage, as it causes a page reload.
// But we still need a say to prevent the `umb-table` from triggering a selection event.
#onClick(event: Event & { target: UUIButtonElement }) {
event.preventDefault();
event.stopPropagation();
window.history.pushState({}, '', event.target.href);
}
render() {
return html`<uui-button
look="default"
color="default"
compact
href="${this._editDocumentPath}edit/${this.value.unique}"
label="${this.value.name}"
@click=${this.#onClick}></uui-button>`;
return html`
<uui-button
compact
href="${this._editDocumentPath}edit/${this.value.unique}"
label=${this.value.name}
@click=${this.#onClick}></uui-button>
`;
}
static styles = [

View File

@@ -41,15 +41,15 @@ export class UmbDocumentTableCollectionViewElement extends UmbLitElement {
#systemColumns: Array<UmbTableColumn> = [
{
name: this.localize.term('general_name'),
alias: 'entityName',
alias: 'name',
elementName: 'umb-document-table-column-name',
allowSorting: true,
},
{
name: this.localize.term('content_publishStatus'),
alias: 'entityState',
alias: 'state',
elementName: 'umb-document-table-column-state',
allowSorting: true,
allowSorting: false,
},
];
@@ -126,16 +126,14 @@ export class UmbDocumentTableCollectionViewElement extends UmbLitElement {
}
#createTableItems(items: Array<UmbDocumentCollectionItemModel>) {
this._tableItems = items.map((item, rowIndex) => {
this._tableItems = items.map((item) => {
if (!item.unique) throw new Error('Item id is missing.');
const sortOrder = this._skip + rowIndex;
const data =
this._tableColumns?.map((column) => {
return {
columnAlias: column.alias,
value: column.elementName ? item : getPropertyValueByAlias(sortOrder, item, column.alias),
value: column.elementName ? item : getPropertyValueByAlias(item, column.alias),
};
}) ?? [];

View File

@@ -1,30 +1,32 @@
import { manifests as collectionManifests } from './collection/manifests.js';
import { manifests as entityActionManifests } from './entity-actions/manifests.js';
import { manifests as entityBulkActionManifests } from './entity-bulk-actions/manifests.js';
import { manifests as modalManifests } from './modals/manifests.js';
import { manifests as globalContextManifests } from './global-contexts/manifests.js';
import { manifests as menuManifests } from './menu/manifests.js';
import { manifests as modalManifests } from './modals/manifests.js';
import { manifests as propertyEditorManifests } from './property-editors/manifests.js';
import { manifests as recycleBinManifests } from './recycle-bin/manifests.js';
import { manifests as repositoryManifests } from './repository/manifests.js';
import { manifests as searchProviderManifests } from './search/manifests.js';
import { manifests as trackedReferenceManifests } from './reference/manifests.js';
import { manifests as treeManifests } from './tree/manifests.js';
import { manifests as userPermissionManifests } from './user-permissions/manifests.js';
import { manifests as workspaceManifests } from './workspace/manifests.js';
import { manifests as globalContextManifests } from './global-contexts/manifests.js';
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
export const manifests: Array<ManifestTypes> = [
...collectionManifests,
...entityActionManifests,
...entityBulkActionManifests,
...modalManifests,
...globalContextManifests,
...menuManifests,
...modalManifests,
...propertyEditorManifests,
...recycleBinManifests,
...repositoryManifests,
...searchProviderManifests,
...trackedReferenceManifests,
...treeManifests,
...userPermissionManifests,
...workspaceManifests,
...globalContextManifests,
];

View File

@@ -0,0 +1,128 @@
import type { UmbDocumentItemModel, UmbDocumentItemVariantModel } from '../repository/item/types.js';
import type { UmbSearchResultItemModel } from '@umbraco-cms/backoffice/search';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { css, customElement, html, nothing, property, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { UmbAppLanguageContext } from '@umbraco-cms/backoffice/language';
import { UMB_APP_LANGUAGE_CONTEXT } from '@umbraco-cms/backoffice/language';
const elementName = 'umb-document-search-result-item';
@customElement(elementName)
export class UmbSearchResultItemElement extends UmbLitElement {
@property({ type: Object })
item?: UmbSearchResultItemModel & UmbDocumentItemModel;
@state()
_currentCulture?: string;
@state()
_defaultCulture?: string;
@state()
_variant?: UmbDocumentItemVariantModel;
constructor() {
super();
this.consumeContext(UMB_APP_LANGUAGE_CONTEXT, (instance) => {
this.#observeAppCulture(instance);
this.#observeDefaultCulture(instance);
});
}
#observeAppCulture(context: UmbAppLanguageContext) {
this.observe(context.appLanguageCulture, (value) => {
this._currentCulture = value;
this._variant = this.#getVariant(value);
});
}
#observeDefaultCulture(context: UmbAppLanguageContext) {
this.observe(context.appDefaultLanguage, (value) => {
this._defaultCulture = value?.unique;
});
}
#getVariant(culture: string | undefined) {
return this.item?.variants.find((x) => x.culture === culture);
}
#isInvariant() {
const firstVariant = this.item?.variants[0];
return firstVariant?.culture === null;
}
#getLabel() {
if (this.#isInvariant()) {
return this.item?.name ?? 'Unknown';
}
const fallbackName = this.#getVariant(this._defaultCulture)?.name ?? this.item?.name ?? 'Unknown';
return this._variant?.name ?? `(${fallbackName})`;
}
render() {
if (!this.item) return nothing;
return html`
<span class="item-icon">
${this.item.icon ? html`<umb-icon name="${this.item.icon}"></umb-icon>` : this.#renderHashTag()}
</span>
<span class="item-name"> ${this.#getLabel()} </span>
`;
}
#renderHashTag() {
return html`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path fill="none" d="M0 0h24v24H0z" />
<path
fill="currentColor"
d="M7.784 14l.42-4H4V8h4.415l.525-5h2.011l-.525 5h3.989l.525-5h2.011l-.525 5H20v2h-3.784l-.42 4H20v2h-4.415l-.525 5h-2.011l.525-5H9.585l-.525 5H7.049l.525-5H4v-2h3.784zm2.011 0h3.99l.42-4h-3.99l-.42 4z" />
</svg>
`;
}
static styles = [
UmbTextStyles,
css`
:host {
padding: var(--uui-size-space-3) var(--uui-size-space-5);
border-radius: var(--uui-border-radius);
display: grid;
grid-template-columns: var(--uui-size-space-6) 1fr var(--uui-size-space-5);
align-items: center;
width: 100%;
outline-offset: -3px;
}
.item-icon {
margin-bottom: auto;
margin-top: 5px;
}
.item-icon {
opacity: 0.4;
}
.item-name {
display: flex;
flex-direction: column;
}
.item-icon > * {
height: 1rem;
display: flex;
width: min-content;
}
a {
text-decoration: none;
color: inherit;
}
`,
];
}
export { UmbSearchResultItemElement as element };
declare global {
interface HTMLElementTagNameMap {
[elementName]: UmbSearchResultItemElement;
}
}

View File

@@ -0,0 +1,23 @@
import { UmbDocumentSearchServerDataSource } from './document-search.server.data-source.js';
import type { UmbDocumentSearchItemModel } from './document.search-provider.js';
import type { UmbSearchRepository, UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
export class UmbDocumentSearchRepository
extends UmbControllerBase
implements UmbSearchRepository<UmbDocumentSearchItemModel>, UmbApi
{
#dataSource: UmbDocumentSearchServerDataSource;
constructor(host: UmbControllerHost) {
super(host);
this.#dataSource = new UmbDocumentSearchServerDataSource(this);
}
search(args: UmbSearchRequestArgs) {
return this.#dataSource.search(args);
}
}

View File

@@ -0,0 +1,68 @@
import { UMB_DOCUMENT_ENTITY_TYPE } from '../entity.js';
import type { UmbDocumentSearchItemModel } from './document.search-provider.js';
import type { UmbSearchDataSource, UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { DocumentService } from '@umbraco-cms/backoffice/external/backend-api';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
/**
* A data source for the Rollback that fetches data from the server
* @export
* @class UmbDocumentSearchServerDataSource
* @implements {RepositoryDetailDataSource}
*/
export class UmbDocumentSearchServerDataSource implements UmbSearchDataSource<UmbDocumentSearchItemModel> {
#host: UmbControllerHost;
/**
* Creates an instance of UmbDocumentSearchServerDataSource.
* @param {UmbControllerHost} host
* @memberof UmbDocumentSearchServerDataSource
*/
constructor(host: UmbControllerHost) {
this.#host = host;
}
/**
* Get a list of versions for a document
* @return {*}
* @memberof UmbDocumentSearchServerDataSource
*/
async search(args: UmbSearchRequestArgs) {
const { data, error } = await tryExecuteAndNotify(
this.#host,
DocumentService.getItemDocumentSearch({
query: args.query,
}),
);
if (data) {
const mappedItems: Array<UmbDocumentSearchItemModel> = data.items.map((item) => {
return {
href: '/section/content/workspace/document/edit/' + item.id,
entityType: UMB_DOCUMENT_ENTITY_TYPE,
unique: item.id,
isTrashed: item.isTrashed,
isProtected: item.isProtected,
documentType: {
unique: item.documentType.id,
icon: item.documentType.icon,
collection: item.documentType.collection ? { unique: item.documentType.collection.id } : null,
},
variants: item.variants.map((variant) => {
return {
culture: variant.culture || null,
name: variant.name,
state: variant.state,
};
}),
name: item.variants[0]?.name, // TODO: this is not correct. We need to get it from the variants. This is a temp solution.
};
});
return { data: { items: mappedItems, total: data.total } };
}
return { error };
}
}

View File

@@ -0,0 +1,24 @@
import type { UmbDocumentItemModel } from '../index.js';
import { UmbDocumentSearchRepository } from './document-search.repository.js';
import type { UmbSearchProvider, UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
export interface UmbDocumentSearchItemModel extends UmbDocumentItemModel {
href: string;
}
export class UmbDocumentSearchProvider
extends UmbControllerBase
implements UmbSearchProvider<UmbDocumentSearchItemModel>
{
#repository = new UmbDocumentSearchRepository(this);
search(args: UmbSearchRequestArgs) {
return this.#repository.search(args);
}
destroy(): void {
this.#repository.destroy();
}
}
export { UmbDocumentSearchProvider as api };

View File

@@ -0,0 +1,22 @@
import { UMB_DOCUMENT_ENTITY_TYPE } from '../entity.js';
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
export const manifests: Array<ManifestTypes> = [
{
name: 'Document Search Provider',
alias: 'Umb.SearchProvider.Document',
type: 'searchProvider',
api: () => import('./document.search-provider.js'),
weight: 800,
meta: {
label: 'Documents',
},
},
{
name: 'Document Search Result Item ',
alias: 'Umb.SearchResultItem.Document',
type: 'searchResultItem',
js: () => import('./document-search-result-item.element.js'),
forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE],
},
];

View File

@@ -4,6 +4,7 @@ import { manifests as repositoryManifests } from './repository/manifests.js';
import { manifests as treeManifests } from './tree/manifests.js';
import { manifests as workspaceManifests } from './workspace/manifests.js';
import { manifests as propertyEditorUiManifests } from './property-editors/manifests.js';
import { manifests as searchManifests } from './search/manifests.js';
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
export const manifests: Array<ManifestTypes> = [
@@ -13,4 +14,5 @@ export const manifests: Array<ManifestTypes> = [
...treeManifests,
...workspaceManifests,
...propertyEditorUiManifests,
...searchManifests,
];

View File

@@ -1,3 +1,4 @@
import { UMB_MEDIA_TYPE_ENTITY_TYPE } from '../../entity.js';
import type { UmbMediaTypeItemModel } from './types.js';
import { UmbItemServerDataSourceBase } from '@umbraco-cms/backoffice/repository';
import type { MediaTypeItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
@@ -32,6 +33,7 @@ const getItems = (uniques: Array<string>) => MediaTypeService.getItemMediaType({
const mapper = (item: MediaTypeItemResponseModel): UmbMediaTypeItemModel => {
return {
entityType: UMB_MEDIA_TYPE_ENTITY_TYPE,
icon: item.icon || null,
name: item.name,
unique: item.id,

View File

@@ -1,4 +1,7 @@
import type { UmbMediaTypeEntityType } from '../../entity.js';
export interface UmbMediaTypeItemModel {
entityType: UmbMediaTypeEntityType;
icon: string | null;
name: string;
unique: string;

View File

@@ -0,0 +1,21 @@
import { UMB_MEDIA_TYPE_ENTITY_TYPE } from '../entity.js';
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
export const manifests: Array<ManifestTypes> = [
{
name: 'Media Type Search Provider',
alias: 'Umb.SearchProvider.MediaType',
type: 'searchProvider',
api: () => import('./media-type.search-provider.js'),
weight: 500,
meta: {
label: 'Media Types',
},
},
{
name: 'Media Type Search Result Item ',
alias: 'Umb.SearchResultItem.MediaType',
type: 'searchResultItem',
forEntityTypes: [UMB_MEDIA_TYPE_ENTITY_TYPE],
},
];

View File

@@ -0,0 +1,23 @@
import { UmbMediaTypeSearchServerDataSource } from './media-type-search.server.data-source.js';
import type { UmbMediaTypeSearchItemModel } from './media-type.search-provider.js';
import type { UmbSearchRepository, UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
export class UmbMediaTypeSearchRepository
extends UmbControllerBase
implements UmbSearchRepository<UmbMediaTypeSearchItemModel>, UmbApi
{
#dataSource: UmbMediaTypeSearchServerDataSource;
constructor(host: UmbControllerHost) {
super(host);
this.#dataSource = new UmbMediaTypeSearchServerDataSource(this);
}
search(args: UmbSearchRequestArgs) {
return this.#dataSource.search(args);
}
}

View File

@@ -0,0 +1,55 @@
import { UMB_MEDIA_TYPE_ENTITY_TYPE } from '../entity.js';
import type { UmbMediaTypeSearchItemModel } from './media-type.search-provider.js';
import type { UmbSearchDataSource, UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { MediaTypeService } from '@umbraco-cms/backoffice/external/backend-api';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
/**
* A data source for the Rollback that fetches data from the server
* @export
* @class UmbMediaTypeSearchServerDataSource
* @implements {RepositoryDetailDataSource}
*/
export class UmbMediaTypeSearchServerDataSource implements UmbSearchDataSource<UmbMediaTypeSearchItemModel> {
#host: UmbControllerHost;
/**
* Creates an instance of UmbMediaTypeSearchServerDataSource.
* @param {UmbControllerHost} host
* @memberof UmbMediaTypeSearchServerDataSource
*/
constructor(host: UmbControllerHost) {
this.#host = host;
}
/**
* Get a list of versions for a data
* @return {*}
* @memberof UmbMediaTypeSearchServerDataSource
*/
async search(args: UmbSearchRequestArgs) {
const { data, error } = await tryExecuteAndNotify(
this.#host,
MediaTypeService.getItemMediaTypeSearch({
query: args.query,
}),
);
if (data) {
const mappedItems: Array<UmbMediaTypeSearchItemModel> = data.items.map((item) => {
return {
href: '/section/settings/workspace/media-type/edit/' + item.id,
entityType: UMB_MEDIA_TYPE_ENTITY_TYPE,
unique: item.id,
name: item.name,
icon: item.icon || null,
};
});
return { data: { items: mappedItems, total: data.total } };
}
return { error };
}
}

View File

@@ -0,0 +1,25 @@
import type { UmbMediaTypeItemModel } from '../index.js';
import { UmbMediaTypeSearchRepository } from './media-type-search.repository.js';
import type { UmbSearchProvider, UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
export interface UmbMediaTypeSearchItemModel extends UmbMediaTypeItemModel {
href: string;
}
export class UmbMediaTypeSearchProvider
extends UmbControllerBase
implements UmbSearchProvider<UmbMediaTypeSearchItemModel>
{
#repository = new UmbMediaTypeSearchRepository(this);
async search(args: UmbSearchRequestArgs) {
return this.#repository.search(args);
}
destroy(): void {
this.#repository.destroy();
}
}
export { UmbMediaTypeSearchProvider as api };

View File

@@ -36,6 +36,7 @@ export class UmbMediaCollectionServerDataSource implements UmbCollectionDataSour
creator: item.creator,
icon: item.mediaType.icon,
name: variant.name,
sortOrder: item.sortOrder,
updateDate: new Date(variant.updateDate),
values: item.values.map((item) => {
return { alias: item.alias, value: item.value as string };

View File

@@ -14,6 +14,7 @@ export interface UmbMediaCollectionItemModel {
creator?: string | null;
icon: string;
name: string;
sortOrder: number;
updateDate: Date;
values: Array<{ alias: string; value: string }>;
}

View File

@@ -39,7 +39,7 @@ export class UmbMediaTableCollectionViewElement extends UmbLitElement {
#systemColumns: Array<UmbTableColumn> = [
{
name: this.localize.term('general_name'),
alias: 'entityName',
alias: 'name',
elementName: 'umb-media-table-column-name',
allowSorting: true,
},
@@ -124,16 +124,14 @@ export class UmbMediaTableCollectionViewElement extends UmbLitElement {
this.#createTableHeadings();
}
this._tableItems = items.map((item, rowIndex) => {
this._tableItems = items.map((item) => {
if (!item.unique) throw new Error('Item id is missing.');
const sortOrder = this._skip + rowIndex;
const data =
this._tableColumns?.map((column) => {
return {
columnAlias: column.alias,
value: column.elementName ? item : this.#getPropertyValueByAlias(sortOrder, item, column.alias),
value: column.elementName ? item : this.#getPropertyValueByAlias(item, column.alias),
};
}) ?? [];
@@ -145,16 +143,17 @@ export class UmbMediaTableCollectionViewElement extends UmbLitElement {
});
}
#getPropertyValueByAlias(sortOrder: number, item: UmbMediaCollectionItemModel, alias: string) {
#getPropertyValueByAlias(item: UmbMediaCollectionItemModel, alias: string) {
switch (alias) {
case 'createDate':
return item.createDate.toLocaleString();
case 'entityName':
case 'name':
return item.name;
case 'creator':
case 'owner':
return item.creator;
case 'sortOrder':
return sortOrder;
return item.sortOrder;
case 'updateDate':
return item.updateDate.toLocaleString();
default:

View File

@@ -5,6 +5,7 @@ import { manifests as menuManifests } from './menu/manifests.js';
import { manifests as propertyEditorsManifests } from './property-editors/manifests.js';
import { manifests as recycleBinManifests } from './recycle-bin/manifests.js';
import { manifests as repositoryManifests } from './repository/manifests.js';
import { manifests as searchManifests } from './search/manifests.js';
import { manifests as sectionViewManifests } from './section-view/manifests.js';
import { manifests as treeManifests } from './tree/manifests.js';
import { manifests as workspaceManifests } from './workspace/manifests.js';
@@ -18,6 +19,7 @@ export const manifests: Array<ManifestTypes> = [
...propertyEditorsManifests,
...recycleBinManifests,
...repositoryManifests,
...searchManifests,
...sectionViewManifests,
...treeManifests,
...workspaceManifests,

View File

@@ -1,3 +1,4 @@
import { UMB_MEDIA_ENTITY_TYPE, UmbMediaEntityType } from '../../entity.js';
import type { UmbMediaItemModel } from './types.js';
import type { MediaItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
import { MediaService } from '@umbraco-cms/backoffice/external/backend-api';
@@ -32,6 +33,7 @@ const getItems = (uniques: Array<string>) => MediaService.getItemMedia({ id: uni
const mapper = (item: MediaItemResponseModel): UmbMediaItemModel => {
return {
entityType: UMB_MEDIA_ENTITY_TYPE,
unique: item.id,
isTrashed: item.isTrashed,
mediaType: {

View File

@@ -1,6 +1,8 @@
import type { UmbMediaEntityType } from '../../entity.js';
import type { UmbReferenceByUnique } from '@umbraco-cms/backoffice/models';
export interface UmbMediaItemModel {
entityType: UmbMediaEntityType;
unique: string;
isTrashed: boolean;
mediaType: {

View File

@@ -0,0 +1,21 @@
import { UMB_MEDIA_ENTITY_TYPE } from '../entity.js';
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
export const manifests: Array<ManifestTypes> = [
{
name: 'Media Search Provider',
alias: 'Umb.SearchProvider.Media',
type: 'searchProvider',
api: () => import('./media.search-provider.js'),
weight: 700,
meta: {
label: 'Media',
},
},
{
name: 'Media Search Result Item ',
alias: 'Umb.SearchResultItem.Media',
type: 'searchResultItem',
forEntityTypes: [UMB_MEDIA_ENTITY_TYPE],
},
];

View File

@@ -0,0 +1,23 @@
import { UmbMediaSearchServerDataSource } from './media-search.server.data-source.js';
import type { UmbMediaSearchItemModel } from './media.search-provider.js';
import type { UmbSearchRepository, UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
export class UmbMediaSearchRepository
extends UmbControllerBase
implements UmbSearchRepository<UmbMediaSearchItemModel>, UmbApi
{
#dataSource: UmbMediaSearchServerDataSource;
constructor(host: UmbControllerHost) {
super(host);
this.#dataSource = new UmbMediaSearchServerDataSource(this);
}
search(args: UmbSearchRequestArgs) {
return this.#dataSource.search(args);
}
}

View File

@@ -0,0 +1,66 @@
import { UMB_MEDIA_ENTITY_TYPE } from '../entity.js';
import type { UmbMediaSearchItemModel } from './media.search-provider.js';
import type { UmbSearchDataSource, UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { MediaService } from '@umbraco-cms/backoffice/external/backend-api';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
/**
* A data source for the Rollback that fetches data from the server
* @export
* @class UmbMediaSearchServerDataSource
* @implements {RepositoryDetailDataSource}
*/
export class UmbMediaSearchServerDataSource implements UmbSearchDataSource<UmbMediaSearchItemModel> {
#host: UmbControllerHost;
/**
* Creates an instance of UmbMediaSearchServerDataSource.
* @param {UmbControllerHost} host
* @memberof UmbMediaSearchServerDataSource
*/
constructor(host: UmbControllerHost) {
this.#host = host;
}
/**
* Get a list of versions for a data
* @return {*}
* @memberof UmbMediaSearchServerDataSource
*/
async search(args: UmbSearchRequestArgs) {
const { data, error } = await tryExecuteAndNotify(
this.#host,
MediaService.getItemMediaSearch({
query: args.query,
}),
);
if (data) {
const mappedItems: Array<UmbMediaSearchItemModel> = data.items.map((item) => {
return {
href: '/section/media/workspace/media/edit/' + item.id,
entityType: UMB_MEDIA_ENTITY_TYPE,
unique: item.id,
isTrashed: item.isTrashed,
mediaType: {
unique: item.mediaType.id,
icon: item.mediaType.icon,
collection: item.mediaType.collection ? { unique: item.mediaType.collection.id } : null,
},
variants: item.variants.map((variant) => {
return {
culture: variant.culture || null,
name: variant.name,
};
}),
name: item.variants[0]?.name, // TODO: get correct variant name
};
});
return { data: { items: mappedItems, total: data.total } };
}
return { error };
}
}

View File

@@ -0,0 +1,22 @@
import type { UmbMediaItemModel } from '../index.js';
import { UmbMediaSearchRepository } from './media-search.repository.js';
import type { UmbSearchProvider, UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
export interface UmbMediaSearchItemModel extends UmbMediaItemModel {
href: string;
}
export class UmbMediaSearchProvider extends UmbControllerBase implements UmbSearchProvider<UmbMediaSearchItemModel> {
#repository = new UmbMediaSearchRepository(this);
async search(args: UmbSearchRequestArgs) {
return this.#repository.search(args);
}
destroy(): void {
this.#repository.destroy();
}
}
export { UmbMediaSearchProvider as api };

View File

@@ -1,6 +1,7 @@
import { manifests as entityActionsManifests } from './entity-actions/manifests.js';
import { manifests as menuManifests } from './menu/manifests.js';
import { manifests as repositoryManifests } from './repository/manifests.js';
import { manifests as searchManifests } from './search/manifests.js';
import { manifests as treeManifests } from './tree/manifests.js';
import { manifests as workspaceManifests } from './workspace/manifests.js';
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
@@ -11,6 +12,7 @@ export const manifests: Array<ManifestTypes> = [
...entityActionsManifests,
...menuManifests,
...repositoryManifests,
...searchManifests,
...treeManifests,
...workspaceManifests,
];

View File

@@ -0,0 +1,21 @@
import { UMB_MEMBER_TYPE_ENTITY_TYPE } from '../entity.js';
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
export const manifests: Array<ManifestTypes> = [
{
name: 'Member Type Search Provider',
alias: 'Umb.SearchProvider.MemberType',
type: 'searchProvider',
api: () => import('./member-type.search-provider.js'),
weight: 200,
meta: {
label: 'Member Types',
},
},
{
name: 'Member Type Search Result Item ',
alias: 'Umb.SearchResultItem.MemberType',
type: 'searchResultItem',
forEntityTypes: [UMB_MEMBER_TYPE_ENTITY_TYPE],
},
];

View File

@@ -0,0 +1,23 @@
import { UmbMemberTypeSearchServerDataSource } from './member-type-search.server.data-source.js';
import type { UmbMemberTypeSearchItemModel } from './member-type.search-provider.js';
import type { UmbSearchRepository, UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
export class UmbMemberTypeSearchRepository
extends UmbControllerBase
implements UmbSearchRepository<UmbMemberTypeSearchItemModel>, UmbApi
{
#dataSource: UmbMemberTypeSearchServerDataSource;
constructor(host: UmbControllerHost) {
super(host);
this.#dataSource = new UmbMemberTypeSearchServerDataSource(this);
}
search(args: UmbSearchRequestArgs) {
return this.#dataSource.search(args);
}
}

View File

@@ -0,0 +1,55 @@
import { UMB_MEMBER_TYPE_ENTITY_TYPE } from '../entity.js';
import type { UmbMemberTypeSearchItemModel } from './member-type.search-provider.js';
import type { UmbSearchDataSource, UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { MemberTypeService } from '@umbraco-cms/backoffice/external/backend-api';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
/**
* A data source for the Rollback that fetches data from the server
* @export
* @class UmbMemberTypeSearchServerDataSource
* @implements {RepositoryDetailDataSource}
*/
export class UmbMemberTypeSearchServerDataSource implements UmbSearchDataSource<UmbMemberTypeSearchItemModel> {
#host: UmbControllerHost;
/**
* Creates an instance of UmbMemberTypeSearchServerDataSource.
* @param {UmbControllerHost} host
* @memberof UmbMemberTypeSearchServerDataSource
*/
constructor(host: UmbControllerHost) {
this.#host = host;
}
/**
* Get a list of versions for a data
* @return {*}
* @memberof UmbMemberTypeSearchServerDataSource
*/
async search(args: UmbSearchRequestArgs) {
const { data, error } = await tryExecuteAndNotify(
this.#host,
MemberTypeService.getItemMemberTypeSearch({
query: args.query,
}),
);
if (data) {
const mappedItems: Array<UmbMemberTypeSearchItemModel> = data.items.map((item) => {
return {
href: '/section/settings/workspace/member-type/edit/' + item.id,
entityType: UMB_MEMBER_TYPE_ENTITY_TYPE,
unique: item.id,
name: item.name,
icon: item.icon || '',
};
});
return { data: { items: mappedItems, total: data.total } };
}
return { error };
}
}

View File

@@ -0,0 +1,25 @@
import type { UmbMemberTypeItemModel } from '../repository/item/types.js';
import { UmbMemberTypeSearchRepository } from './member-type-search.repository.js';
import type { UmbSearchProvider, UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
export interface UmbMemberTypeSearchItemModel extends UmbMemberTypeItemModel {
href: string;
}
export class UmbMemberTypeSearchProvider
extends UmbControllerBase
implements UmbSearchProvider<UmbMemberTypeSearchItemModel>
{
#repository = new UmbMemberTypeSearchRepository(this);
async search(args: UmbSearchRequestArgs) {
return this.#repository.search(args);
}
destroy(): void {
this.#repository.destroy();
}
}
export { UmbMemberTypeSearchProvider as api };

View File

@@ -3,6 +3,7 @@ import { manifests as entityActionManifests } from './entity-actions/manifests.j
import { manifests as memberPickerModalManifests } from './components/member-picker-modal/manifests.js';
import { manifests as propertyEditorManifests } from './property-editor/manifests.js';
import { manifests as repositoryManifests } from './repository/manifests.js';
import { manifests as searchManifests } from './search/manifests.js';
import { manifests as sectionViewManifests } from './section-view/manifests.js';
import { manifests as workspaceManifests } from './workspace/manifests.js';
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
@@ -13,6 +14,7 @@ export const manifests: Array<ManifestTypes> = [
...memberPickerModalManifests,
...propertyEditorManifests,
...repositoryManifests,
...searchManifests,
...sectionViewManifests,
...workspaceManifests,
];

View File

@@ -1,3 +1,4 @@
import { UMB_MEMBER_ENTITY_TYPE } from '../../entity.js';
import type { UmbMemberItemModel } from './types.js';
import { UmbItemServerDataSourceBase } from '@umbraco-cms/backoffice/repository';
import type { MemberItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
@@ -32,6 +33,7 @@ const getItems = (uniques: Array<string>) => MemberService.getItemMember({ id: u
const mapper = (item: MemberItemResponseModel): UmbMemberItemModel => {
return {
entityType: UMB_MEMBER_ENTITY_TYPE,
unique: item.id,
name: item.variants[0].name || '',
memberType: {

View File

@@ -1,6 +1,8 @@
import type { UmbMemberEntityType } from '../../entity.js';
import type { UmbReferenceByUnique } from '@umbraco-cms/backoffice/models';
export interface UmbMemberItemModel {
entityType: UmbMemberEntityType;
unique: string;
name: string; // TODO: this is not correct. We need to get it from the variants. This is a temp solution.
memberType: {

View File

@@ -0,0 +1,21 @@
import { UMB_MEMBER_ENTITY_TYPE } from '../entity.js';
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
export const manifests: Array<ManifestTypes> = [
{
name: 'Member Search Provider',
alias: 'Umb.SearchProvider.Member',
type: 'searchProvider',
api: () => import('./member.search-provider.js'),
weight: 300,
meta: {
label: 'Members',
},
},
{
name: 'Member Search Result Item ',
alias: 'Umb.SearchResultItem.Member',
type: 'searchResultItem',
forEntityTypes: [UMB_MEMBER_ENTITY_TYPE],
},
];

View File

@@ -0,0 +1,23 @@
import { UmbMemberSearchServerDataSource } from './member-search.server.data-source.js';
import type { UmbMemberSearchItemModel } from './member.search-provider.js';
import type { UmbSearchRepository, UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
export class UmbMemberSearchRepository
extends UmbControllerBase
implements UmbSearchRepository<UmbMemberSearchItemModel>, UmbApi
{
#dataSource: UmbMemberSearchServerDataSource;
constructor(host: UmbControllerHost) {
super(host);
this.#dataSource = new UmbMemberSearchServerDataSource(this);
}
search(args: UmbSearchRequestArgs) {
return this.#dataSource.search(args);
}
}

View File

@@ -0,0 +1,65 @@
import { UMB_MEMBER_ENTITY_TYPE } from '../entity.js';
import type { UmbMemberSearchItemModel } from './member.search-provider.js';
import type { UmbSearchDataSource, UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { MemberService } from '@umbraco-cms/backoffice/external/backend-api';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
/**
* A data source for the Rollback that fetches data from the server
* @export
* @class UmbMemberSearchServerDataSource
* @implements {RepositoryDetailDataSource}
*/
export class UmbMemberSearchServerDataSource implements UmbSearchDataSource<UmbMemberSearchItemModel> {
#host: UmbControllerHost;
/**
* Creates an instance of UmbMemberSearchServerDataSource.
* @param {UmbControllerHost} host
* @memberof UmbMemberSearchServerDataSource
*/
constructor(host: UmbControllerHost) {
this.#host = host;
}
/**
* Get a list of versions for a data
* @return {*}
* @memberof UmbMemberSearchServerDataSource
*/
async search(args: UmbSearchRequestArgs) {
const { data, error } = await tryExecuteAndNotify(
this.#host,
MemberService.getItemMemberSearch({
query: args.query,
}),
);
if (data) {
const mappedItems: Array<UmbMemberSearchItemModel> = data.items.map((item) => {
return {
href: '/section/member-management/workspace/member/edit/' + item.id,
entityType: UMB_MEMBER_ENTITY_TYPE,
unique: item.id,
name: item.variants[0].name || '',
memberType: {
unique: item.memberType.id,
icon: item.memberType.icon,
collection: item.memberType.collection ? { unique: item.memberType.collection.id } : null,
},
variants: item.variants.map((variant) => {
return {
name: variant.name,
culture: variant.culture || null,
};
}),
};
});
return { data: { items: mappedItems, total: data.total } };
}
return { error };
}
}

View File

@@ -0,0 +1,22 @@
import type { UmbMemberItemModel } from '../repository/item/types.js';
import { UmbMemberSearchRepository } from './member-search.repository.js';
import type { UmbSearchProvider, UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
export interface UmbMemberSearchItemModel extends UmbMemberItemModel {
href: string;
}
export class UmbMemberSearchProvider extends UmbControllerBase implements UmbSearchProvider<UmbMemberSearchItemModel> {
#repository = new UmbMemberSearchRepository(this);
async search(args: UmbSearchRequestArgs) {
return this.#repository.search(args);
}
destroy(): void {
this.#repository.destroy();
}
}
export { UmbMemberSearchProvider as api };

View File

@@ -57,7 +57,12 @@ export class UmbInputCollectionContentTypePropertyElement extends UUIFormControl
value: 'createDate',
icon: 'icon-settings',
},
{ label: this.localize.term('content_createBy'), description: 'owner', value: 'owner', icon: 'icon-settings' },
{
label: this.localize.term('content_createBy'),
description: 'creator',
value: 'creator',
icon: 'icon-settings',
},
{
label: this.localize.term('content_isPublished'),
description: 'published',
@@ -124,7 +129,12 @@ export class UmbInputCollectionContentTypePropertyElement extends UUIFormControl
value: 'createDate',
icon: 'icon-settings',
},
{ label: this.localize.term('content_createBy'), description: 'owner', value: 'owner', icon: 'icon-settings' },
{
label: this.localize.term('content_createBy'),
description: 'creator',
value: 'creator',
icon: 'icon-settings',
},
{
label: this.localize.term('general_sort'),
description: 'sortOrder',

View File

@@ -89,7 +89,7 @@ const propertyEditorUiManifest: ManifestPropertyEditorUi = {
],
},
{ alias: 'pageSize', value: 10 },
{ alias: 'orderBy', value: 'updateDate' },
{ alias: 'orderBy', value: 'sortOrder' },
{ alias: 'orderDirection', value: 'desc' },
{
alias: 'bulkActionPermissions',

View File

@@ -0,0 +1,3 @@
export type { UmbSearchResultItemModel, UmbSearchRequestArgs, UmbSearchProvider } from './types.js';
export type { UmbSearchDataSource } from './search-data-source.interface.js';
export type { UmbSearchRepository } from './search-repository.interface.js';

View File

@@ -0,0 +1,11 @@
import type { UmbSearchRequestArgs, UmbSearchResultItemModel } from './types.js';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type { UmbDataSourceResponse, UmbPagedModel } from '@umbraco-cms/backoffice/repository';
export interface UmbSearchDataSourceConstructor<SearchResultItemType extends UmbSearchResultItemModel> {
new (host: UmbControllerHost): UmbSearchDataSource<SearchResultItemType>;
}
export interface UmbSearchDataSource<SearchResultItemType extends UmbSearchResultItemModel> {
search(args: UmbSearchRequestArgs): Promise<UmbDataSourceResponse<UmbPagedModel<SearchResultItemType>>>;
}

View File

@@ -1,305 +1,444 @@
import type { UmbSearchProvider, UmbSearchResultItemModel } from '../types.js';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import {
css,
html,
LitElement,
nothing,
repeat,
customElement,
query,
state,
property,
} from '@umbraco-cms/backoffice/external/lit';
import type { ManifestSearchResultItem } from '@umbraco-cms/backoffice/extension-registry';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbExtensionsManifestInitializer, createExtensionApi } from '@umbraco-cms/backoffice/extension-api';
export type SearchItem = {
import '../search-result/search-result-item.element.js';
import type { UmbModalContext } from '@umbraco-cms/backoffice/modal';
type SearchProvider = {
name: string;
icon?: string;
href: string;
parent: string;
url?: string;
};
export type SearchGroupItem = {
name: string;
items: Array<SearchItem>;
api: UmbSearchProvider<UmbSearchResultItemModel>;
alias: string;
};
@customElement('umb-search-modal')
export class UmbSearchModalElement extends LitElement {
export class UmbSearchModalElement extends UmbLitElement {
@query('#input-wrapper-fake-cursor')
private _inputFakeCursor!: HTMLElement;
@query('input')
private _input!: HTMLInputElement;
@property({ attribute: false })
modalContext?: UmbModalContext;
@state()
private _search = '';
@state()
private _groups: Array<SearchGroupItem> = [];
private _searchResults: Array<UmbSearchResultItemModel> = [];
@state()
private _searchProviders: Array<SearchProvider> = [];
@state()
_currentProvider?: SearchProvider;
@state()
_loading: boolean = false;
#searchItemNavIndex = 0;
#inputTimer?: NodeJS.Timeout;
#inputTimerAmount = 300;
constructor() {
super();
this.#observeProviders();
}
connectedCallback() {
super.connectedCallback();
this.addEventListener('keydown', this.#onKeydown);
requestAnimationFrame(() => {
this._input.focus();
this.#focusInput();
});
}
#observeProviders() {
new UmbExtensionsManifestInitializer(this, umbExtensionsRegistry, 'searchProvider', null, async (providers) => {
const searchProviders: Array<SearchProvider> = [];
for (const provider of providers) {
const api = await createExtensionApi<UmbSearchProvider<UmbSearchResultItemModel>>(this, provider.manifest);
if (api) {
searchProviders.push({
name: provider.manifest.meta?.label || provider.manifest.name,
api,
alias: provider.alias,
});
}
}
this._searchProviders = searchProviders;
if (this._searchProviders.length > 0) {
this._currentProvider = this._searchProviders[0];
}
});
}
async #setSearchItemNavIndex(index: number) {
const prevElement = this.shadowRoot?.querySelector(
`a[data-item-index="${this.#searchItemNavIndex}"]`,
) as HTMLElement | null;
prevElement?.classList.remove('active');
this.#searchItemNavIndex = index;
const element = this.shadowRoot?.querySelector(`a[data-item-index="${index}"]`) as HTMLElement | null;
element?.classList.add('active');
if (!element) return;
if (!this._searchResults.length) return;
element.focus();
}
#focusInput() {
this._input.focus();
}
async #setShowFakeCursor(show: boolean) {
if (show) {
await new Promise((resolve) => requestAnimationFrame(resolve));
const getTextBeforeCursor = this._search.substring(0, this._input.selectionStart ?? 0);
this._inputFakeCursor.textContent = getTextBeforeCursor;
this._inputFakeCursor.style.display = 'block';
} else {
this._inputFakeCursor.style.display = 'none';
}
}
#setCurrentProvider(searchProvider: SearchProvider) {
if (this._currentProvider === searchProvider) return;
this._currentProvider = searchProvider;
this.#focusInput();
this._loading = true;
this._searchResults = [];
this.#updateSearchResults();
}
async #updateSearchResults() {
if (this._search && this._currentProvider?.api) {
const { data } = await this._currentProvider.api.search({ query: this._search });
if (!data) return;
this._searchResults = data.items;
} else {
this._searchResults = [];
}
this._loading = false;
this.#searchItemNavIndex = -1;
}
#closeModal(event: MouseEvent | KeyboardEvent) {
if (event instanceof KeyboardEvent && event.key !== 'Enter') return;
requestAnimationFrame(() => {
// In the case where the browser has not triggered focus-visible and we keyboard navigate and press enter.
// It is necessary to wait one frame.
this.modalContext?.reject();
});
}
#onSearchChange(event: InputEvent) {
const target = event.target as HTMLInputElement;
this._search = target.value;
this._search = target.value.trim();
this.#updateGroups();
clearTimeout(this.#inputTimer);
if (!this._search) {
this._loading = false;
this._searchResults = [];
return;
}
this._loading = true;
this.#inputTimer = setTimeout(() => this.#updateSearchResults(), this.#inputTimerAmount);
}
#onClearSearch() {
this._search = '';
this._input.value = '';
this._input.focus();
this.#updateGroups();
}
#onKeydown(event: KeyboardEvent) {
const root = this.shadowRoot;
if (!root) return;
#updateGroups() {
const filtered = this.#mockData.filter((item) => {
return item.name.toLowerCase().includes(this._search.toLowerCase());
});
if (event.key === 'Tab') {
const isFirstProvider = (element: Element) => element === root.querySelector('.search-provider:first-child');
const isLastProvider = (element: Element) => element === root.querySelector('.search-provider:last-child');
const setFocus = (element?: Element | null) => (element as HTMLElement)?.focus();
const providerHasFocus = () => {
const providerElements = root.querySelectorAll('.search-provider') || [];
return Array.from(providerElements).some((element) => element === root.activeElement);
};
const isFocusingLastProvider = () => {
const providerElements = root.querySelectorAll('.search-provider') || [];
return providerElements[providerElements.length - 1] === root.activeElement;
};
const isFocusingFirstProvider = () => {
const providerElements = root.querySelectorAll('.search-provider') || [];
return providerElements[0] === root.activeElement;
};
const grouped: Array<SearchGroupItem> = filtered.reduce((acc, item) => {
const group = acc.find((group) => group.name === item.parent);
if (group) {
group.items.push(item);
} else {
acc.push({
name: item.parent,
items: [item],
});
const activeProvider = root.querySelector('.search-provider.active') as HTMLElement | null;
if (!activeProvider) return;
// When moving backwards in search providers
if (event.shiftKey) {
// If the FOCUS is on a provider, and it is the first in the list, we need to wrap around and focus the LAST one
if (providerHasFocus()) {
if (isFocusingFirstProvider()) {
setFocus(root.querySelector('.search-provider:last-child'));
event.preventDefault();
}
return;
}
// If the currently ACTIVE provider is the first in the list, we need to wrap around and focus the LAST one
if (isFirstProvider(activeProvider)) {
setFocus(root.querySelector('.search-provider:last-child'));
event.preventDefault();
return;
}
// We set the focus to current provider, and because we don't prevent the default tab behavior, the previous provider will be focused
setFocus(activeProvider);
}
return acc;
}, [] as Array<SearchGroupItem>);
// When moving forwards in search providers
else {
// If the FOCUS is on a provider, and it is the last in the list, we need to wrap around and focus the FIRST one
if (providerHasFocus()) {
if (isFocusingLastProvider()) {
setFocus(root.querySelector('.search-provider:first-child'));
event.preventDefault();
}
return;
}
this._groups = grouped;
// If the currently ACTIVE provider is the last in the list, we need to wrap around and focus the FIRST one
if (isLastProvider(activeProvider)) {
setFocus(root.querySelector('.search-provider:first-child'));
event.preventDefault();
return;
}
// We set the focus to current provider, and because we don't prevent the default tab behavior, the next provider will be focused
setFocus(activeProvider);
}
}
switch (event.key) {
case 'Tab':
case 'Shift':
case 'Escape':
case 'Enter':
break;
case 'ArrowDown':
event.preventDefault();
this.#setSearchItemNavIndex(Math.min(this.#searchItemNavIndex + 1, this._searchResults.length - 1));
break;
case 'ArrowUp':
event.preventDefault();
this.#setSearchItemNavIndex(Math.max(this.#searchItemNavIndex - 1, 0));
break;
default:
if (this._input === root.activeElement) return;
this.#focusInput();
break;
}
}
render() {
return html`
<div id="top">
<div id="search-icon">
<uui-icon name="search"></uui-icon>
</div>
<input
value=${this._search}
@input=${this.#onSearchChange}
type="text"
placeholder="Search..."
autocomplete="off" />
<div id="close-icon">
<button @click=${this.#onClearSearch}>clear</button>
${this.#renderSearchIcon()}
<div id="input-wrapper">
<div id="input-wrapper-fake-cursor" aria-hidden="true"></div>
<input
value=${this._search}
@input=${this.#onSearchChange}
@blur=${() => this.#setShowFakeCursor(true)}
@focus=${() => this.#setShowFakeCursor(false)}
type="text"
placeholder="Search..."
autocomplete="off" />
</div>
</div>
${this.#renderSearchTags()}
${this._search
? html`<div id="main">
${this._groups.length > 0
? repeat(
this._groups,
(group) => group.name,
(group) => this.#renderGroup(group.name, group.items),
)
: html`<div id="no-results">Only mock data for now <strong>Search for blog</strong></div>`}
</div>`
? html`<div id="main">${this._searchResults.length > 0 ? this.#renderResults() : this.#renderNoResults()}</div>`
: nothing}
`;
}
#renderGroup(name: string, items: Array<SearchItem>) {
return html`
<div class="group">
<div class="group-name">${name}</div>
<div class="group-items">${repeat(items, (item) => item.name, this.#renderItem.bind(this))}</div>
</div>
`;
#renderSearchIcon() {
return html` <div id="search-icon">
${this._loading ? html`<uui-loader-circle></uui-loader-circle>` : html`<uui-icon name="search"></uui-icon>`}
</div>`;
}
#renderItem(item: SearchItem) {
#renderSearchTags() {
return html`<div id="search-providers">
${repeat(
this._searchProviders,
(searchProvider) => searchProvider,
(searchProvider) =>
html`<button
data-provider-alias=${searchProvider.alias}
@click=${() => this.#setCurrentProvider(searchProvider)}
@keydown=${() => ''}
class="search-provider ${this._currentProvider?.alias === searchProvider.alias ? 'active' : ''}">
${searchProvider.name}
</button>`,
)}
</div> `;
}
#renderResults() {
return repeat(
this._searchResults,
(item) => item.unique,
(item, index) => this.#renderResultItem(item, index),
);
}
#renderResultItem(item: UmbSearchResultItemModel, index: number) {
return html`
<a href="${item.href}" class="item">
<span class="item-icon">
${item.icon ? html`<umb-icon name="${item.icon}"></umb-icon>` : this.#renderHashTag()}
</span>
<span class="item-name">
${item.name} ${item.url ? html`<span class="item-url">${item.url}</span>` : nothing}
</span>
<span class="item-symbol">></span>
<a
href=${item.href}
data-item-index=${index}
class="search-item"
@click=${this.#closeModal}
@keydown=${this.#closeModal}>
<umb-extension-slot
type="searchResultItem"
.props=${{ item }}
.filter=${(manifest: ManifestSearchResultItem) => manifest.forEntityTypes.includes(item.entityType)}
default-element="umb-search-result-item"></umb-extension-slot>
</a>
`;
}
#renderHashTag() {
return html`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path fill="none" d="M0 0h24v24H0z" />
<path
fill="currentColor"
d="M7.784 14l.42-4H4V8h4.415l.525-5h2.011l-.525 5h3.989l.525-5h2.011l-.525 5H20v2h-3.784l-.42 4H20v2h-4.415l-.525 5h-2.011l.525-5H9.585l-.525 5H7.049l.525-5H4v-2h3.784zm2.011 0h3.99l.42-4h-3.99l-.42 4z" />
</svg>
`;
#renderNoResults() {
return this._loading ? nothing : html`<div id="no-results">${this.localize.term('general_searchNoResult')}</div>`;
}
#mockData: Array<SearchItem> = [
{
name: 'Blog',
href: '#',
icon: 'icon-thumbnail-list',
parent: 'Content',
url: '/blog/',
},
{
name: 'Popular blogs',
href: '#',
icon: 'icon-article',
parent: 'Content',
url: '/blog/popular-blogs/',
},
{
name: 'How to write a blog',
href: '#',
icon: 'icon-article',
parent: 'Content',
url: '/blog/how-to-write-a-blog/',
},
{
name: 'Blog hero',
href: '#',
icon: 'icon-picture',
parent: 'Media',
},
{
name: 'Contact form for blog',
href: '#',
parent: 'Document Types',
},
{
name: 'Blog',
href: '#',
parent: 'Document Types',
},
{
name: 'Blog link item',
href: '#',
parent: 'Document Types',
},
];
static styles = [
UmbTextStyles,
css`
:host {
display: flex;
flex-direction: column;
width: min(500px, 100vw);
height: 100%;
background-color: var(--uui-color-background);
width: min(610px, 100vw);
height: max(600px, 80dvh);
max-height: 100dvh;
background-color: var(--uui-color-surface);
box-sizing: border-box;
color: var(--uui-color-text);
font-size: 1rem;
padding-bottom: var(--uui-size-space-2);
}
#top {
background-color: var(--uui-color-surface);
display: flex;
height: 48px;
flex-shrink: 0;
}
#main {
display: flex;
flex-direction: column;
height: 100%;
overflow: auto;
}
#search-providers {
display: flex;
flex-wrap: wrap;
gap: var(--uui-size-space-2);
padding: 0 var(--uui-size-space-5);
padding-bottom: var(--uui-size-space-2);
}
.search-provider {
padding: var(--uui-size-space-3) var(--uui-size-space-4);
background: var(--uui-color-surface-alt);
line-height: 1;
white-space: nowrap;
border-radius: var(--uui-border-radius);
color: var(--uui-color-interactive);
cursor: pointer;
border: 2px solid transparent;
}
.search-provider:hover {
background: var(--uui-color-surface-emphasis);
color: var(--uui-color-interactive-emphasis);
}
.search-provider.active {
background: var(--uui-color-focus);
color: var(--uui-color-selected-contrast);
border-color: transparent;
}
.search-provider.active:focus {
outline-offset: -4px;
outline-color: var(--uui-color-focus);
}
input {
all: unset;
height: 100%;
width: 100%;
}
#search-icon,
#close-icon {
#input-wrapper {
width: 100%;
position: relative;
}
#input-wrapper-fake-cursor {
position: absolute;
left: 0;
border-right: 1px solid var(--uui-color-text);
height: 1.2rem;
color: transparent;
user-select: none;
pointer-events: none;
bottom: 14px;
animation: blink-animation 1s infinite;
}
@keyframes blink-animation {
0%,
50% {
border-color: var(--uui-color-text);
}
51%,
100% {
border-color: transparent;
}
}
button {
font-family: unset;
font-size: unset;
cursor: pointer;
}
#search-icon {
display: flex;
align-items: center;
justify-content: center;
aspect-ratio: 1;
height: 100%;
}
#close-icon {
padding: 0 var(--uui-size-space-4);
}
#close-icon > button {
background: var(--uui-color-surface-alt);
border: 1px solid var(--uui-color-border);
padding: 3px 6px 4px 6px;
line-height: 1;
border-radius: 3px;
color: var(--uui-color-text-alt);
font-weight: 800;
font-size: 12px;
cursor: pointer;
}
#close-icon > button:hover {
border-color: var(--uui-color-focus);
color: var(--uui-color-focus);
}
#top {
background-color: var(--uui-color-surface);
display: flex;
height: 48px;
}
#main {
display: flex;
flex-direction: column;
padding: 0px var(--uui-size-space-6) var(--uui-size-space-5) var(--uui-size-space-6);
height: 100%;
border-top: 1px solid var(--uui-color-border);
}
.group {
margin-top: var(--uui-size-space-4);
}
.group-name {
font-weight: 600;
margin-bottom: var(--uui-size-space-1);
}
.group-items {
display: flex;
flex-direction: column;
gap: var(--uui-size-space-3);
}
.item {
background: var(--uui-color-surface);
border: 1px solid var(--uui-color-border);
padding: var(--uui-size-space-3) var(--uui-size-space-4);
border-radius: var(--uui-border-radius);
color: var(--uui-color-interactive);
display: grid;
grid-template-columns: var(--uui-size-space-6) 1fr var(--uui-size-space-5);
height: min-content;
align-items: center;
}
.item:hover {
background-color: var(--uui-color-surface-emphasis);
color: var(--uui-color-interactive-emphasis);
}
.item:hover .item-symbol {
font-weight: unset;
opacity: 1;
}
.item-icon {
margin-bottom: auto;
margin-top: 5px;
}
.item-icon,
.item-symbol {
opacity: 0.4;
}
.item-url {
font-size: 0.8rem;
line-height: 1.2;
font-weight: 100;
}
.item-name {
display: flex;
flex-direction: column;
}
.item-icon > * {
height: 1rem;
display: flex;
width: min-content;
}
.item-symbol {
font-weight: 100;
}
a {
text-decoration: none;
color: inherit;
}
#no-results {
display: flex;
flex-direction: column;
@@ -309,6 +448,27 @@ export class UmbSearchModalElement extends LitElement {
width: 100%;
margin-top: var(--uui-size-space-5);
color: var(--uui-color-text-alt);
margin: var(--uui-size-space-5) 0;
}
.search-item {
color: var(--uui-color-text);
text-decoration: none;
outline-offset: -3px;
display: flex;
}
.search-item:hover {
background: var(--uui-color-surface-emphasis);
color: var(--uui-color-interactive-emphasis);
}
.search-item:focus {
outline: 2px solid var(--uui-color-interactive-emphasis);
border-radius: 6px;
outline-offset: -4px;
}
.search-item.active:not(:focus-within) {
outline: 2px solid var(--uui-color-border);
border-radius: 6px;
outline-offset: -4px;
}
`,
];

View File

@@ -0,0 +1,6 @@
import type { UmbSearchRequestArgs, UmbSearchResultItemModel } from './types.js';
import type { UmbRepositoryResponse, UmbPagedModel } from '@umbraco-cms/backoffice/repository';
export interface UmbSearchRepository<SearchResultItemType extends UmbSearchResultItemModel> {
search(args: UmbSearchRequestArgs): Promise<UmbRepositoryResponse<UmbPagedModel<SearchResultItemType>>>;
}

View File

@@ -0,0 +1,76 @@
import type { UmbSearchResultItemModel } from '../types.js';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { css, customElement, html, nothing, property } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
const elementName = 'umb-search-result-item';
@customElement(elementName)
export class UmbSearchResultItemElement extends UmbLitElement {
@property({ type: Object })
item?: UmbSearchResultItemModel;
render() {
if (!this.item) return nothing;
return html`
<span class="item-icon">
${this.item.icon ? html`<umb-icon name="${this.item.icon}"></umb-icon>` : this.#renderHashTag()}
</span>
<span class="item-name"> ${this.item.name} </span>
`;
}
#renderHashTag() {
return html`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path fill="none" d="M0 0h24v24H0z" />
<path
fill="currentColor"
d="M7.784 14l.42-4H4V8h4.415l.525-5h2.011l-.525 5h3.989l.525-5h2.011l-.525 5H20v2h-3.784l-.42 4H20v2h-4.415l-.525 5h-2.011l.525-5H9.585l-.525 5H7.049l.525-5H4v-2h3.784zm2.011 0h3.99l.42-4h-3.99l-.42 4z" />
</svg>
`;
}
static styles = [
UmbTextStyles,
css`
:host {
padding: var(--uui-size-space-3) var(--uui-size-space-5);
border-radius: var(--uui-border-radius);
display: grid;
grid-template-columns: var(--uui-size-space-6) 1fr var(--uui-size-space-5);
align-items: center;
width: 100%;
outline-offset: -3px;
}
.item-icon {
margin-bottom: auto;
margin-top: 5px;
}
.item-icon {
opacity: 0.4;
}
.item-name {
display: flex;
flex-direction: column;
}
.item-icon > * {
height: 1rem;
display: flex;
width: min-content;
}
a {
text-decoration: none;
color: inherit;
}
`,
];
}
export { UmbSearchResultItemElement as element };
declare global {
interface HTMLElementTagNameMap {
[elementName]: UmbSearchResultItemElement;
}
}

View File

@@ -0,0 +1,18 @@
import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
import type { UmbPagedModel, UmbRepositoryResponse } from '@umbraco-cms/backoffice/repository';
export type UmbSearchResultItemModel = {
entityType: string;
icon?: string | null;
name: string;
unique: string;
href: string;
};
export type UmbSearchRequestArgs = {
query: string;
};
export interface UmbSearchProvider<SearchResultItemType extends UmbSearchResultItemModel> extends UmbApi {
search(args: UmbSearchRequestArgs): Promise<UmbRepositoryResponse<UmbPagedModel<SearchResultItemType>>>;
}

View File

@@ -69,7 +69,7 @@ export class UmbTemplateFieldDropdownListElement extends UmbLitElement {
{ alias: 'updateDate', name: this.localize.term('content_updateDate') },
{ alias: 'updater', name: this.localize.term('content_updatedBy') },
{ alias: 'createDate', name: this.localize.term('content_createDate') },
{ alias: 'owner', name: this.localize.term('content_createBy') },
{ alias: 'creator', name: this.localize.term('content_createBy') },
{ alias: 'published', name: this.localize.term('content_isPublished') },
{ alias: 'contentTypeAlias', name: this.localize.term('content_documentType') },
];

View File

@@ -1,16 +1,18 @@
import { manifests as repositoryManifests } from './repository/manifests.js';
import { manifests as menuManifests } from './menu/manifests.js';
import { manifests as treeManifests } from './tree/manifests.js';
import { manifests as entityActionsManifests } from './entity-actions/manifests.js';
import { manifests as workspaceManifests } from './workspace/manifests.js';
import { manifests as menuManifests } from './menu/manifests.js';
import { manifests as modalManifests } from './modals/manifests.js';
import { manifests as repositoryManifests } from './repository/manifests.js';
import { manifests as searchManifests } from './search/manifests.js';
import { manifests as treeManifests } from './tree/manifests.js';
import { manifests as workspaceManifests } from './workspace/manifests.js';
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
export const manifests: Array<ManifestTypes> = [
...entityActionsManifests,
...menuManifests,
...modalManifests,
...repositoryManifests,
...menuManifests,
...searchManifests,
...treeManifests,
...entityActionsManifests,
...workspaceManifests,
];

View File

@@ -1,3 +1,4 @@
import { UMB_TEMPLATE_ENTITY_TYPE } from '../../entity.js';
import type { UmbTemplateItemModel } from './types.js';
import { UmbItemServerDataSourceBase } from '@umbraco-cms/backoffice/repository';
import type { TemplateItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
@@ -32,6 +33,7 @@ const getItems = (uniques: Array<string>) => TemplateService.getItemTemplate({ i
const mapper = (item: TemplateItemResponseModel): UmbTemplateItemModel => {
return {
entityType: UMB_TEMPLATE_ENTITY_TYPE,
unique: item.id,
name: item.name,
alias: item.alias,

View File

@@ -1,4 +1,7 @@
import type { UmbTemplateEntityType } from '../../entity.js';
export interface UmbTemplateItemModel {
entityType: UmbTemplateEntityType;
unique: string;
name: string;
alias: string;

View File

@@ -0,0 +1,21 @@
import { UMB_TEMPLATE_ENTITY_TYPE } from '../entity.js';
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
export const manifests: Array<ManifestTypes> = [
{
name: 'Template Search Provider',
alias: 'Umb.SearchProvider.Template',
type: 'searchProvider',
api: () => import('./template.search-provider.js'),
weight: 100,
meta: {
label: 'Templates',
},
},
{
name: 'Template Search Result Item ',
alias: 'Umb.SearchResultItem.Template',
type: 'searchResultItem',
forEntityTypes: [UMB_TEMPLATE_ENTITY_TYPE],
},
];

View File

@@ -0,0 +1,23 @@
import { UmbTemplateSearchServerDataSource } from './template-search.server.data-source.js';
import type { UmbTemplateSearchItemModel } from './template.search-provider.js';
import type { UmbSearchRepository, UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
export class UmbTemplateSearchRepository
extends UmbControllerBase
implements UmbSearchRepository<UmbTemplateSearchItemModel>, UmbApi
{
#dataSource: UmbTemplateSearchServerDataSource;
constructor(host: UmbControllerHost) {
super(host);
this.#dataSource = new UmbTemplateSearchServerDataSource(this);
}
search(args: UmbSearchRequestArgs) {
return this.#dataSource.search(args);
}
}

View File

@@ -0,0 +1,55 @@
import { UMB_TEMPLATE_ENTITY_TYPE } from '../entity.js';
import type { UmbTemplateSearchItemModel } from './template.search-provider.js';
import type { UmbSearchDataSource, UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { TemplateService } from '@umbraco-cms/backoffice/external/backend-api';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
/**
* A data source for the Rollback that fetches data from the server
* @export
* @class UmbTemplateSearchServerDataSource
* @implements {RepositoryDetailDataSource}
*/
export class UmbTemplateSearchServerDataSource implements UmbSearchDataSource<UmbTemplateSearchItemModel> {
#host: UmbControllerHost;
/**
* Creates an instance of UmbTemplateSearchServerDataSource.
* @param {UmbControllerHost} host
* @memberof UmbTemplateSearchServerDataSource
*/
constructor(host: UmbControllerHost) {
this.#host = host;
}
/**
* Get a list of versions for a data
* @return {*}
* @memberof UmbTemplateSearchServerDataSource
*/
async search(args: UmbSearchRequestArgs) {
const { data, error } = await tryExecuteAndNotify(
this.#host,
TemplateService.getItemTemplateSearch({
query: args.query,
}),
);
if (data) {
const mappedItems: Array<UmbTemplateSearchItemModel> = data.items.map((item) => {
return {
href: '/section/settings/workspace/template/edit/' + item.id,
entityType: UMB_TEMPLATE_ENTITY_TYPE,
unique: item.id,
name: item.name,
alias: item.alias,
};
});
return { data: { items: mappedItems, total: data.total } };
}
return { error };
}
}

View File

@@ -0,0 +1,25 @@
import type { UmbTemplateItemModel } from '../repository/item/types.js';
import { UmbTemplateSearchRepository } from './template-search.repository.js';
import type { UmbSearchProvider, UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
export interface UmbTemplateSearchItemModel extends UmbTemplateItemModel {
href: string;
}
export class UmbTemplateSearchProvider
extends UmbControllerBase
implements UmbSearchProvider<UmbTemplateSearchItemModel>
{
#repository = new UmbTemplateSearchRepository(this);
async search(args: UmbSearchRequestArgs) {
return this.#repository.search(args);
}
destroy(): void {
this.#repository.destroy();
}
}
export { UmbTemplateSearchProvider as api };

View File

@@ -86,6 +86,7 @@
"@umbraco-cms/backoffice/repository": ["./src/packages/core/repository/index.ts"],
"@umbraco-cms/backoffice/resources": ["./src/packages/core/resources/index.ts"],
"@umbraco-cms/backoffice/router": ["./src/packages/core/router/index.ts"],
"@umbraco-cms/backoffice/search": ["./src/packages/search/index.ts"],
"@umbraco-cms/backoffice/section": ["./src/packages/core/section/index.ts"],
"@umbraco-cms/backoffice/settings": ["./src/packages/settings/index.ts"],
"@umbraco-cms/backoffice/server-file-system": ["./src/packages/core/server-file-system/index.ts"],