Merge branch 'main' into feature/global-search

This commit is contained in:
JesmoDev
2024-04-19 14:33:56 +02:00
45 changed files with 625 additions and 295 deletions

View File

@@ -13,8 +13,11 @@ import type { Guard, UmbRoute } from '@umbraco-cms/backoffice/router';
import { pathWithoutBasePath } from '@umbraco-cms/backoffice/router';
import { OpenAPI, RuntimeLevelModel } from '@umbraco-cms/backoffice/external/backend-api';
import { UmbContextDebugController } from '@umbraco-cms/backoffice/debug';
import { UmbServerExtensionRegistrator } from '@umbraco-cms/backoffice/extension-api';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { UmbBundleExtensionInitializer, UmbServerExtensionRegistrator } from '@umbraco-cms/backoffice/extension-api';
import {
UmbAppEntryPointExtensionInitializer,
umbExtensionsRegistry,
} from '@umbraco-cms/backoffice/extension-registry';
@customElement('umb-app')
export class UmbAppElement extends UmbLitElement {
@@ -79,6 +82,9 @@ export class UmbAppElement extends UmbLitElement {
OpenAPI.BASE = window.location.origin;
new UmbBundleExtensionInitializer(this, umbExtensionsRegistry);
new UmbAppEntryPointExtensionInitializer(this, umbExtensionsRegistry);
new UmbIconRegistry().attach(this);
new UUIIconRegistryEssential().attach(this);
@@ -99,7 +105,7 @@ export class UmbAppElement extends UmbLitElement {
// Register Core extensions (this is specifically done here because we need these extensions to be registered before the application is initialized)
onInit(this, umbExtensionsRegistry);
// Register public extensions
// Register public extensions (login extensions)
await new UmbServerExtensionRegistrator(this, umbExtensionsRegistry).registerPublicExtensions();
// Try to initialise the auth flow and get the runtime status

View File

@@ -1,11 +1,11 @@
import { UmbBackofficeContext } from './backoffice.context.js';
import { css, html, customElement } from '@umbraco-cms/backoffice/external/lit';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import {
UmbBundleExtensionInitializer,
UmbBackofficeEntryPointExtensionInitializer,
UmbEntryPointExtensionInitializer,
UmbServerExtensionRegistrator,
} from '@umbraco-cms/backoffice/extension-api';
umbExtensionsRegistry,
} from '@umbraco-cms/backoffice/extension-registry';
import { UmbServerExtensionRegistrator } from '@umbraco-cms/backoffice/extension-api';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import './components/index.js';
@@ -52,7 +52,7 @@ export class UmbBackofficeElement extends UmbLitElement {
new UmbBackofficeContext(this);
new UmbBundleExtensionInitializer(this, umbExtensionsRegistry);
new UmbBackofficeEntryPointExtensionInitializer(this, umbExtensionsRegistry);
new UmbEntryPointExtensionInitializer(this, umbExtensionsRegistry);
new UmbServerExtensionRegistrator(this, umbExtensionsRegistry).registerPrivateExtensions();

View File

@@ -1948,8 +1948,8 @@ export default {
stateInactive: 'Inactive',
sortNameAscending: 'Name (A-Z)',
sortNameDescending: 'Name (Z-A)',
sortCreateDateAscending: 'Newest',
sortCreateDateDescending: 'Oldest',
sortCreateDateDescending: 'Newest',
sortCreateDateAscending: 'Oldest',
sortLastLoginDateDescending: 'Last login',
noUserGroupsAdded: 'No user groups have been added',
'2faDisableText':

View File

@@ -0,0 +1,8 @@
import type { UmbEntryPointModule } from '../models/entry-point.interface.js';
/**
* Validate if an ESModule has exported a function called `onUnload`
*/
export function hasOnUnloadExport(obj: unknown): obj is Pick<UmbEntryPointModule, 'onUnload'> {
return obj !== null && typeof obj === 'object' && 'onUnload' in obj;
}

View File

@@ -2,6 +2,7 @@ export * from './create-extension-api.function.js';
export * from './create-extension-element-with-api.function.js';
export * from './create-extension-element.function.js';
export * from './has-init-export.function.js';
export * from './has-on-unload-export.function.js';
export * from './load-manifest-api.function.js';
export * from './load-manifest-element.function.js';
export * from './load-manifest-plain-css.function.js';

View File

@@ -2,7 +2,7 @@ export * from './condition/index.js';
export * from './controller/index.js';
export * from './functions/index.js';
export * from './initializers/index.js';
export type * from './models/index.js';
export * from './registry/extension.registry.js';
export * from './type-guards/index.js';
export type * from './models/index.js';
export type * from './types/index.js';

View File

@@ -1,35 +1,18 @@
import type { ManifestBase, ManifestBundle } from '../types/index.js';
import type { UmbExtensionRegistry } from '../registry/extension.registry.js';
import { loadManifestPlainJs } from '../functions/load-manifest-plain-js.function.js';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbExtensionInitializerBase } from './extension-initializer-base.js';
import type { UmbElement } from '@umbraco-cms/backoffice/element-api';
export class UmbBundleExtensionInitializer extends UmbControllerBase {
#extensionRegistry;
#bundleMap = new Map();
constructor(host: UmbControllerHost, extensionRegistry: UmbExtensionRegistry<ManifestBundle>) {
super(host);
this.#extensionRegistry = extensionRegistry;
this.observe(extensionRegistry.byType('bundle'), (bundles) => {
// Unregister removed bundles:
this.#bundleMap.forEach((existingBundle) => {
if (!bundles.find((b) => b.alias === existingBundle.alias)) {
this.unregisterBundle(existingBundle);
this.#bundleMap.delete(existingBundle.alias);
}
});
// Register new bundles:
bundles.forEach((bundle) => {
if (this.#bundleMap.has(bundle.alias)) return;
this.#bundleMap.set(bundle.alias, bundle);
this.instantiateBundle(bundle);
});
});
/**
* Extension initializer for the `bundle` extension type
*/
export class UmbBundleExtensionInitializer extends UmbExtensionInitializerBase<'bundle', ManifestBundle> {
constructor(host: UmbElement, extensionRegistry: UmbExtensionRegistry<ManifestBundle>) {
super(host, extensionRegistry, 'bundle');
}
async instantiateBundle(manifest: ManifestBundle) {
async instantiateExtension(manifest: ManifestBundle): Promise<void> {
if (manifest.js) {
const js = await loadManifestPlainJs(manifest.js);
@@ -38,16 +21,16 @@ export class UmbBundleExtensionInitializer extends UmbControllerBase {
const value = js[key];
if (Array.isArray(value)) {
this.#extensionRegistry.registerMany(value);
this.extensionRegistry.registerMany(value);
} else if (typeof value === 'object') {
this.#extensionRegistry.register(value);
this.extensionRegistry.register(value);
}
});
}
}
}
async unregisterBundle(manifest: ManifestBundle) {
async unloadExtension(manifest: ManifestBundle): Promise<void> {
if (manifest.js) {
const js = await loadManifestPlainJs(manifest.js);
@@ -56,9 +39,9 @@ export class UmbBundleExtensionInitializer extends UmbControllerBase {
const value = js[key];
if (Array.isArray(value)) {
this.#extensionRegistry.unregisterMany(value.map((v) => v.alias));
this.extensionRegistry.unregisterMany(value.map((v) => v.alias));
} else if (typeof value === 'object') {
this.#extensionRegistry.unregister((value as ManifestBase).alias);
this.extensionRegistry.unregister((value as ManifestBase).alias);
}
});
}

View File

@@ -1,35 +0,0 @@
import type { ManifestEntryPoint } from '../types/index.js';
import type { UmbExtensionRegistry } from '../registry/extension.registry.js';
import { hasInitExport, loadManifestPlainJs } from '../functions/index.js';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbElement } from '@umbraco-cms/backoffice/element-api';
export class UmbEntryPointExtensionInitializer extends UmbControllerBase {
#host;
#extensionRegistry;
#entryPointMap = new Map();
constructor(host: UmbElement, extensionRegistry: UmbExtensionRegistry<ManifestEntryPoint>) {
super(host);
this.#host = host;
this.#extensionRegistry = extensionRegistry;
this.observe(extensionRegistry.byType('entryPoint'), (entryPoints) => {
entryPoints.forEach((entryPoint) => {
if (this.#entryPointMap.has(entryPoint.alias)) return;
this.#entryPointMap.set(entryPoint.alias, entryPoint);
// TODO: Should we unInit a entry point if is removed?
this.instantiateEntryPoint(entryPoint);
});
});
}
async instantiateEntryPoint(manifest: ManifestEntryPoint) {
if (manifest.js) {
const js = await loadManifestPlainJs(manifest.js);
// If the extension has an onInit export, be sure to run that or else let the module handle itself
if (hasInitExport(js)) {
js.onInit(this.#host, this.#extensionRegistry);
}
}
}
}

View File

@@ -0,0 +1,48 @@
import type { ManifestBase } from '../types/index.js';
import type { UmbExtensionRegistry } from '../registry/extension.registry.js';
import type { SpecificManifestTypeOrManifestBase } from '../types/map.types.js';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbElement } from '@umbraco-cms/backoffice/element-api';
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
/**
* Base class for extension initializers, which are responsible for loading and unloading extensions.
*/
export abstract class UmbExtensionInitializerBase<
Key extends string,
T extends ManifestBase = SpecificManifestTypeOrManifestBase<ManifestTypes, Key>,
> extends UmbControllerBase {
protected host;
protected extensionRegistry;
#extensionMap = new Map();
constructor(host: UmbElement, extensionRegistry: UmbExtensionRegistry<T>, manifestType: Key) {
super(host);
this.host = host;
this.extensionRegistry = extensionRegistry;
this.observe(extensionRegistry.byType<Key, T>(manifestType), (extensions) => {
this.#extensionMap.forEach((existingExt) => {
if (!extensions.find((b) => b.alias === existingExt.alias)) {
this.unloadExtension(existingExt);
this.#extensionMap.delete(existingExt.alias);
}
});
extensions.forEach((extension) => {
if (this.#extensionMap.has(extension.alias)) return;
this.#extensionMap.set(extension.alias, extension);
this.instantiateExtension(extension);
});
});
}
/**
* Perform any logic required to instantiate the extension.
*/
abstract instantiateExtension(manifest: T): Promise<void> | void;
/**
* Perform any logic required to unload the extension.
*/
abstract unloadExtension(manifest: T): Promise<void> | void;
}

View File

@@ -1,2 +1,2 @@
export * from './bundle-extension-initializer.js';
export * from './entry-point-extension-initializer.js';
export * from './extension-initializer-base.js';

View File

@@ -4,9 +4,22 @@ import type { UmbElement } from '@umbraco-cms/backoffice/element-api';
export type UmbEntryPointOnInit = (host: UmbElement, extensionRegistry: UmbExtensionRegistry<ManifestBase>) => void;
export type UmbEntryPointOnUnload = (host: UmbElement, extensionRegistry: UmbExtensionRegistry<ManifestBase>) => void;
/**
* Interface containing supported life-cycle functions for ESModule entry points
*/
export interface UmbEntryPointModule {
/**
* Function that will be called when the host element is initialized and/or the extension is loaded for the first time.
* @optional
*/
onInit: UmbEntryPointOnInit;
/**
* Function that will be called when the extension is unregistered.
* @remark This does not mean the host element is destroyed, only that the extension is no longer available. You should listen to the host element's `destroy` event if you need to clean up after the host element.
* @optional
*/
onUnload: UmbEntryPointOnUnload;
}

View File

@@ -3,6 +3,5 @@ export * from './condition.types.js';
export * from './manifest-base.interface.js';
export * from './manifest-bundle.interface.js';
export * from './manifest-condition.interface.js';
export * from './manifest-entrypoint.interface.js';
export * from './manifest-kind.interface.js';
export * from './utils.js';

View File

@@ -1,10 +0,0 @@
import type { UmbEntryPointModule } from '../models/index.js';
import type { ManifestPlainJs } from './base.types.js';
/**
* This type of extension gives full control and will simply load the specified JS file
* You could have custom logic to decide which extensions to load/register by using extensionRegistry
*/
export interface ManifestEntryPoint extends ManifestPlainJs<UmbEntryPointModule> {
type: 'entryPoint';
}

View File

@@ -56,7 +56,7 @@ export const manifestDevelopmentHandlers = [
name: 'Package with an entry point',
extensions: [
{
type: 'entryPoint',
type: 'backofficeEntryPoint',
name: 'My Custom Entry Point',
alias: 'My.Entrypoint.Custom',
js: '/App_Plugins/custom-entrypoint.js',

View File

@@ -110,11 +110,6 @@ export class UmbCollectionViewBundleElement extends UmbLitElement {
#onClick(view: UmbCollectionViewLayout) {
this.#collectionContext?.setLastSelectedView(this._entityUnique, view.alias);
// TODO: This ignorer is just neede for JSON SCHEMA TO WORK, As its not updated with latest TS jet.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this._popover?.hidePopover();
}
render() {
@@ -123,7 +118,7 @@ export class UmbCollectionViewBundleElement extends UmbLitElement {
return html`
<uui-button compact popovertarget="collection-view-bundle-popover" label="status">
${this.#renderItemDisplay(this._currentView)}
<umb-icon name=${this._currentView.icon}></umb-icon>
</uui-button>
<uui-popover-container id="collection-view-bundle-popover" placement="bottom-end">
<umb-popover-layout>
@@ -141,31 +136,27 @@ export class UmbCollectionViewBundleElement extends UmbLitElement {
#renderItem(view: UmbCollectionViewLayout) {
return html`
<uui-button compact href="${this._collectionRootPathName}/${view.pathName}" @click=${() => this.#onClick(view)}>
${this.#renderItemDisplay(view)}
<span class="label">${view.label}</span>
</uui-button>
<uui-menu-item
label=${view.label}
href="${this._collectionRootPathName}/${view.pathName}"
@click-label=${() => this.#onClick(view)}
?active=${view.alias === this._currentView?.alias}>
<umb-icon slot="icon" name=${view.icon}></umb-icon>
</uui-menu-item>
`;
}
#renderItemDisplay(view: UmbCollectionViewLayout) {
return html`<umb-icon name=${view.icon}></umb-icon>`;
}
static styles = [
UmbTextStyles,
css`
:host {
--uui-button-content-align: left;
}
.label {
margin-left: var(--uui-size-space-1);
}
.filter-dropdown {
display: flex;
gap: var(--uui-size-space-1);
flex-direction: column;
padding: var(--uui-size-space-3);
}
umb-icon {
display: inline-block;
}

View File

@@ -51,6 +51,7 @@ export class UmbDefaultCollectionContext<
public readonly view = new UmbCollectionViewManager(this);
#defaultViewAlias: string;
#defaultFilter: Partial<FilterModelType>;
#initResolver?: () => void;
#initialized = false;
@@ -59,10 +60,11 @@ export class UmbDefaultCollectionContext<
this.#initialized ? resolve() : (this.#initResolver = resolve);
});
constructor(host: UmbControllerHost, defaultViewAlias: string) {
constructor(host: UmbControllerHost, defaultViewAlias: string, defaultFilter: Partial<FilterModelType> = {}) {
super(host, UMB_DEFAULT_COLLECTION_CONTEXT);
this.#defaultViewAlias = defaultViewAlias;
this.#defaultFilter = defaultFilter;
this.pagination.addEventListener(UmbChangeEvent.TYPE, this.#onPageChange);
}
@@ -79,6 +81,7 @@ export class UmbDefaultCollectionContext<
}
this.#filter.setValue({
...this.#defaultFilter,
...this.#config,
...this.#filter.getValue(),
skip: 0,

View File

@@ -1,7 +1,8 @@
export * from './conditions/index.js';
export type * from './interfaces/index.js';
export type * from './models/index.js';
export * from './initializers/index.js';
export * from './registry.js';
export * from './utils/index.js';
export type * from './interfaces/index.js';
export type * from './models/index.js';
export { UmbExtensionElementAndApiSlotElementBase } from './extension-element-and-api-slot-element-base.js';

View File

@@ -0,0 +1,51 @@
import type { ManifestAppEntryPoint } from '../models/app-entry-point.model.js';
import type { UmbElement } from '@umbraco-cms/backoffice/element-api';
import {
type UmbEntryPointModule,
UmbExtensionInitializerBase,
type UmbExtensionRegistry,
loadManifestPlainJs,
hasInitExport,
hasOnUnloadExport,
} from '@umbraco-cms/backoffice/extension-api';
/**
* Extension initializer for the `appEntryPoint` extension type
*/
export class UmbAppEntryPointExtensionInitializer extends UmbExtensionInitializerBase<
'appEntryPoint',
ManifestAppEntryPoint
> {
#instanceMap = new Map<string, UmbEntryPointModule>();
constructor(host: UmbElement, extensionRegistry: UmbExtensionRegistry<ManifestAppEntryPoint>) {
super(host, extensionRegistry, 'appEntryPoint' satisfies ManifestAppEntryPoint['type']);
}
async instantiateExtension(manifest: ManifestAppEntryPoint) {
if (manifest.js) {
const moduleInstance = await loadManifestPlainJs(manifest.js);
if (!moduleInstance) return;
this.#instanceMap.set(manifest.alias, moduleInstance);
// If the extension has known exports, be sure to run those
if (hasInitExport(moduleInstance)) {
moduleInstance.onInit(this.host, this.extensionRegistry);
}
}
}
async unloadExtension(manifest: ManifestAppEntryPoint): Promise<void> {
const moduleInstance = this.#instanceMap.get(manifest.alias);
if (!moduleInstance) return;
if (hasOnUnloadExport(moduleInstance)) {
moduleInstance.onUnload(this.host, this.extensionRegistry);
}
this.#instanceMap.delete(manifest.alias);
}
}

View File

@@ -0,0 +1,51 @@
import type { ManifestBackofficeEntryPoint } from '../models/backoffice-entry-point.model.js';
import type { UmbElement } from '@umbraco-cms/backoffice/element-api';
import {
type UmbEntryPointModule,
UmbExtensionInitializerBase,
type UmbExtensionRegistry,
loadManifestPlainJs,
hasInitExport,
hasOnUnloadExport,
} from '@umbraco-cms/backoffice/extension-api';
/**
* Extension initializer for the `backofficeEntryPoint` extension type
*/
export class UmbBackofficeEntryPointExtensionInitializer extends UmbExtensionInitializerBase<
'backofficeEntryPoint',
ManifestBackofficeEntryPoint
> {
#instanceMap = new Map<string, UmbEntryPointModule>();
constructor(host: UmbElement, extensionRegistry: UmbExtensionRegistry<ManifestBackofficeEntryPoint>) {
super(host, extensionRegistry, 'backofficeEntryPoint' satisfies ManifestBackofficeEntryPoint['type']);
}
async instantiateExtension(manifest: ManifestBackofficeEntryPoint) {
if (manifest.js) {
const moduleInstance = await loadManifestPlainJs(manifest.js);
if (!moduleInstance) return;
this.#instanceMap.set(manifest.alias, moduleInstance);
// If the extension has known exports, be sure to run those
if (hasInitExport(moduleInstance)) {
moduleInstance.onInit(this.host, this.extensionRegistry);
}
}
}
async unloadExtension(manifest: ManifestBackofficeEntryPoint): Promise<void> {
const moduleInstance = this.#instanceMap.get(manifest.alias);
if (!moduleInstance) return;
if (hasOnUnloadExport(moduleInstance)) {
moduleInstance.onUnload(this.host, this.extensionRegistry);
}
this.#instanceMap.delete(manifest.alias);
}
}

View File

@@ -0,0 +1,48 @@
import type { ManifestEntryPoint } from '../models/entry-point.model.js';
import type { UmbElement } from '@umbraco-cms/backoffice/element-api';
import {
type UmbEntryPointModule,
UmbExtensionInitializerBase,
type UmbExtensionRegistry,
loadManifestPlainJs,
hasInitExport,
hasOnUnloadExport,
} from '@umbraco-cms/backoffice/extension-api';
/**
* Extension initializer for the `entryPoint` extension type
*/
export class UmbEntryPointExtensionInitializer extends UmbExtensionInitializerBase<'entryPoint', ManifestEntryPoint> {
#instanceMap = new Map<string, UmbEntryPointModule>();
constructor(host: UmbElement, extensionRegistry: UmbExtensionRegistry<ManifestEntryPoint>) {
super(host, extensionRegistry, 'entryPoint' satisfies ManifestEntryPoint['type']);
}
async instantiateExtension(manifest: ManifestEntryPoint) {
if (manifest.js) {
const moduleInstance = await loadManifestPlainJs(manifest.js);
if (!moduleInstance) return;
this.#instanceMap.set(manifest.alias, moduleInstance);
// If the extension has known exports, be sure to run those
if (hasInitExport(moduleInstance)) {
moduleInstance.onInit(this.host, this.extensionRegistry);
}
}
}
async unloadExtension(manifest: ManifestEntryPoint): Promise<void> {
const moduleInstance = this.#instanceMap.get(manifest.alias);
if (!moduleInstance) return;
if (hasOnUnloadExport(moduleInstance)) {
moduleInstance.onUnload(this.host, this.extensionRegistry);
}
this.#instanceMap.delete(manifest.alias);
}
}

View File

@@ -0,0 +1,3 @@
export * from './app-entry-point-extension-initializer.js';
export * from './backoffice-entry-point-extension-initializer.js';
export * from './entry-point-extension-initializer.js';

View File

@@ -0,0 +1,12 @@
import type { ManifestPlainJs, UmbEntryPointModule } from '@umbraco-cms/backoffice/extension-api';
/**
* Manifest for an `appEntryPoint`, which is loaded up front when the app starts.
*
* This type of extension gives full control and will simply load the specified JS file.
* You could have custom logic to decide which extensions to load/register by using extensionRegistry.
* This is useful for extensions that need to be loaded up front, like an `authProvider`.
*/
export interface ManifestAppEntryPoint extends ManifestPlainJs<UmbEntryPointModule> {
type: 'appEntryPoint';
}

View File

@@ -0,0 +1,11 @@
import type { ManifestPlainJs, UmbEntryPointModule } from '@umbraco-cms/backoffice/extension-api';
/**
* Manifest for an `backofficeEntryPoint`, which is loaded after the Backoffice has been loaded and authentication has been done.
*
* This type of extension gives full control and will simply load the specified JS file.
* You could have custom logic to decide which extensions to load/register by using extensionRegistry.
*/
export interface ManifestBackofficeEntryPoint extends ManifestPlainJs<UmbEntryPointModule> {
type: 'backofficeEntryPoint';
}

View File

@@ -0,0 +1,13 @@
import type { ManifestPlainJs, UmbEntryPointModule } from '@umbraco-cms/backoffice/extension-api';
/**
* Manifest for an `entryPoint`, which is loaded after the Backoffice has been loaded and authentication has been done.
*
* This type of extension gives full control and will simply load the specified JS file.
* You could have custom logic to decide which extensions to load/register by using extensionRegistry.
*
* @deprecated Use `ManifestBackofficeEntryPoint` instead.
*/
export interface ManifestEntryPoint extends ManifestPlainJs<UmbEntryPointModule> {
type: 'entryPoint';
}

View File

@@ -63,14 +63,13 @@ 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 {
ManifestBase,
ManifestBundle,
ManifestCondition,
ManifestEntryPoint,
} from '@umbraco-cms/backoffice/extension-api';
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';
import type { ManifestBase, ManifestBundle, ManifestCondition } from '@umbraco-cms/backoffice/extension-api';
export type * from './auth-provider.model.js';
export type * from './backoffice-entry-point.model.js';
export type * from './block-editor-custom-view.model.js';
export type * from './collection-action.model.js';
export type * from './collection-view.model.js';
@@ -82,6 +81,7 @@ export type * from './dynamic-root.model.js';
export type * from './entity-action.model.js';
export type * from './entity-bulk-action.model.js';
export type * from './entity-user-permission.model.js';
export type * from './entry-point.model.js';
export type * from './external-login-provider.model.js';
export type * from './global-context.model.js';
export type * from './header-app.model.js';
@@ -113,6 +113,7 @@ export type * from './workspace-context.model.js';
export type * from './workspace-footer-app.model.js';
export type * from './workspace-view.model.js';
export type * from './workspace.model.js';
export type * from './app-entry-point.model.js';
export type ManifestEntityActions =
| ManifestEntityAction
@@ -143,7 +144,9 @@ export type ManifestWorkspaces = ManifestWorkspace | ManifestWorkspaceRoutableKi
export type ManifestWorkspaceViews = ManifestWorkspaceView | ManifestWorkspaceViewContentTypeDesignEditorKind;
export type ManifestTypes =
| ManifestAppEntryPoint
| ManifestAuthProvider
| ManifestBackofficeEntryPoint
| ManifestBlockEditorCustomView
| ManifestBundle<ManifestTypes>
| ManifestCollection

View File

@@ -17,7 +17,7 @@ export const manifest: ManifestPropertyEditorSchema = {
{
alias: 'max',
label: 'Maximum',
description: 'Enter the minimum amount of number to be entered',
description: 'Enter the maximum amount of number to be entered',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.Decimal',
},
{

View File

@@ -17,7 +17,7 @@ export const manifest: ManifestPropertyEditorSchema = {
{
alias: 'max',
label: 'Maximum',
description: 'Enter the minimum amount of number to be entered',
description: 'Enter the maximum amount of number to be entered',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.Number',
},
{

View File

@@ -1,66 +1,46 @@
import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit';
import { html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor';
import type { NumberRangeValueType } from '@umbraco-cms/backoffice/models';
import type { UmbInputMemberGroupElement } from '@umbraco-cms/backoffice/member-group';
import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry';
/**
* @element umb-property-editor-ui-member-group-picker
*/
@customElement('umb-property-editor-ui-member-group-picker')
export class UmbPropertyEditorUIMemberGroupPickerElement extends UmbLitElement implements UmbPropertyEditorUiElement {
// private _value: Array<string> = [];
// @property({ type: Array })
// public set value(value: Array<string>) {
// this._value = Array.isArray(value) ? value : value ? [value] : [];
// }
// public get value(): Array<string> {
// return this._value;
// }
@property({ type: String })
public value: string = '';
@property()
public value?: string;
public set config(config: UmbPropertyEditorConfigCollection | undefined) {
const validationLimit = config?.find((x) => x.alias === 'validationLimit');
if (!config) return;
this._limitMin = (validationLimit?.value as any)?.min;
this._limitMax = (validationLimit?.value as any)?.max;
const minMax = config?.getValueByAlias<NumberRangeValueType>('validationLimit');
this.min = minMax?.min ?? 0;
this.max = minMax?.max ?? Infinity;
}
@state()
_items: Array<string> = [];
min = 0;
@state()
private _limitMin?: number;
@state()
private _limitMax?: number;
max = Infinity;
protected updated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
super.updated(_changedProperties);
if (_changedProperties.has('value')) {
this._items = this.value ? this.value.split(',') : [];
}
}
private _onChange(event: CustomEvent) {
//TODO: This is a hack, something changed so now we need to convert the array to a comma separated string to make it work with the server.
const toCommaSeparatedString = (event.target as UmbInputMemberGroupElement).selection.join(',');
// this.value = (event.target as UmbInputMemberGroupElement).selection;
this.value = toCommaSeparatedString;
this.dispatchEvent(new CustomEvent('property-value-change'));
#onChange(event: CustomEvent & { target: UmbInputMemberGroupElement }) {
this.value = event.target.value;
this.dispatchEvent(new UmbPropertyValueChangeEvent());
}
render() {
return html`
<umb-input-member-group
@change=${this._onChange}
.selection=${this._items}
.min=${this._limitMin ?? 0}
.max=${this._limitMax ?? Infinity}>
</umb-input-member-group>
.min=${this.min}
.max=${this.max}
.value=${this.value ?? ''}
?showOpenButton=${true}
@change=${this.#onChange}></umb-input-member-group>
`;
}
}

View File

@@ -0,0 +1,6 @@
export type UmbDirectionType = 'Ascending' | 'Descending';
export const UmbDirection = Object.freeze({
ASCENDING: 'Ascending',
DESCENDING: 'Descending',
});

View File

@@ -1,3 +1,6 @@
export * from './debounce/debounce.function.js';
export * from './direction/index.js';
export * from './download/blob-download.function.js';
export * from './get-processed-image-url.function.js';
export * from './math/math.js';
export * from './pagination-manager/pagination.manager.js';
@@ -7,11 +10,9 @@ export * from './path/path-encode.function.js';
export * from './path/path-folder-name.function.js';
export * from './path/umbraco-path.function.js';
export * from './selection-manager/selection.manager.js';
export * from './string/from-camel-case.function.js';
export * from './string/generate-umbraco-alias.function.js';
export * from './string/increment-string.function.js';
export * from './string/split-string-to-array.js';
export * from './type/diff.type.js';
export * from './string/to-camel-case/to-camel-case.function.js';
export * from './string/from-camel-case.function.js';
export * from './debounce/debounce.function.js';
export * from './download/blob-download.function.js';
export * from './type/diff.type.js';

View File

@@ -1,3 +1,4 @@
import { UmbApi } from '@umbraco-cms/backoffice/extension-api';
import type { UmbDataTypeDetailModel } from '../../types.js';
import { UmbDataTypeServerDataSource } from './data-type-detail.server.data-source.js';
import type { UmbDataTypeDetailStore } from './data-type-detail.store.js';
@@ -24,3 +25,5 @@ export class UmbDataTypeDetailRepository extends UmbDetailRepositoryBase<UmbData
return this.#detailStore!.withPropertyEditorUiAlias(propertyEditorUiAlias);
}
}
export { UmbDataTypeDetailRepository as api };

View File

@@ -25,3 +25,5 @@ export class UmbDataTypeDetailStore extends UmbDetailStoreBase<UmbDataTypeDetail
}
export const UMB_DATA_TYPE_DETAIL_STORE_CONTEXT = new UmbContextToken<UmbDataTypeDetailStore>('UmbDataTypeDetailStore');
export { UmbDataTypeDetailStore as api };

View File

@@ -1,5 +1,3 @@
import { UmbDataTypeDetailRepository } from './data-type-detail.repository.js';
import { UmbDataTypeDetailStore } from './data-type-detail.store.js';
import type { ManifestRepository, ManifestStore } from '@umbraco-cms/backoffice/extension-registry';
export const UMB_DATA_TYPE_DETAIL_REPOSITORY_ALIAS = 'Umb.Repository.DataType.Detail';
@@ -8,7 +6,7 @@ const repository: ManifestRepository = {
type: 'repository',
alias: UMB_DATA_TYPE_DETAIL_REPOSITORY_ALIAS,
name: 'Data Type Detail Repository',
api: UmbDataTypeDetailRepository,
api: () => import('./data-type-detail.repository.js'),
};
export const UMB_DATA_TYPE_DETAIL_STORE_ALIAS = 'Umb.Store.DataType.Detail';
@@ -17,7 +15,7 @@ const store: ManifestStore = {
type: 'store',
alias: UMB_DATA_TYPE_DETAIL_STORE_ALIAS,
name: 'Data Type Detail Store',
api: UmbDataTypeDetailStore,
api: () => import('./data-type-detail.store.js'),
};
export const manifests = [repository, store];

View File

@@ -9,3 +9,5 @@ export class UmbDataTypeItemRepository extends UmbItemRepositoryBase<UmbDataType
super(host, UmbDataTypeItemServerDataSource, UMB_DATA_TYPE_ITEM_STORE_CONTEXT);
}
}
export { UmbDataTypeItemRepository as api };

View File

@@ -22,3 +22,5 @@ export class UmbDataTypeItemStore extends UmbItemStoreBase<UmbDataTypeItemModel>
}
export const UMB_DATA_TYPE_ITEM_STORE_CONTEXT = new UmbContextToken<UmbDataTypeItemStore>('UmbDataTypeItemStore');
export { UmbDataTypeItemStore as api };

View File

@@ -1,5 +1,3 @@
import { UmbDataTypeItemStore } from './data-type-item.store.js';
import { UmbDataTypeItemRepository } from './data-type-item.repository.js';
import type { ManifestRepository, ManifestItemStore } from '@umbraco-cms/backoffice/extension-registry';
export const UMB_DATA_TYPE_ITEM_REPOSITORY_ALIAS = 'Umb.Repository.DataType.Item';
@@ -9,14 +7,14 @@ const itemRepository: ManifestRepository = {
type: 'repository',
alias: UMB_DATA_TYPE_ITEM_REPOSITORY_ALIAS,
name: 'Data Type Item Repository',
api: UmbDataTypeItemRepository,
api: () => import('./data-type-item.repository.js'),
};
const itemStore: ManifestItemStore = {
type: 'itemStore',
alias: UMB_DATA_TYPE_STORE_ALIAS,
name: 'Data Type Item Store',
api: UmbDataTypeItemStore,
api: () => import('./data-type-item.store.js'),
};
export const manifests = [itemRepository, itemStore];

View File

@@ -29,3 +29,5 @@ export class UmbDataTypeTreeRepository
return { data };
}
}
export { UmbDataTypeTreeRepository as api };

View File

@@ -20,3 +20,5 @@ export class UmbDataTypeTreeStore extends UmbUniqueTreeStore {
}
export const UMB_DATA_TYPE_TREE_STORE_CONTEXT = new UmbContextToken<UmbDataTypeTreeStore>('UmbDataTypeTreeStore');
export { UmbDataTypeTreeStore as api };

View File

@@ -1,7 +1,5 @@
import { manifests as folderManifests } from './folder/manifests.js';
import { manifests as reloadManifests } from './reload-tree-item-children/manifests.js';
import { UmbDataTypeTreeRepository } from './data-type-tree.repository.js';
import { UmbDataTypeTreeStore } from './data-type-tree.store.js';
import {
UMB_DATA_TYPE_TREE_ALIAS,
UMB_DATA_TYPE_TREE_REPOSITORY_ALIAS,
@@ -18,14 +16,14 @@ const treeRepository: ManifestRepository = {
type: 'repository',
alias: UMB_DATA_TYPE_TREE_REPOSITORY_ALIAS,
name: 'Data Type Tree Repository',
api: UmbDataTypeTreeRepository,
api: () => import('./data-type-tree.repository.js'),
};
const treeStore: ManifestTreeStore = {
type: 'treeStore',
alias: UMB_DATA_TYPE_TREE_STORE_ALIAS,
name: 'Data Type Tree Store',
api: UmbDataTypeTreeStore,
api: () => import('./data-type-tree.store.js'),
};
const tree: ManifestTree = {

View File

@@ -1,31 +1,28 @@
import type { UmbMemberGroupItemModel } from '../../repository/index.js';
import { UmbMemberPickerContext } from './input-member-group.context.js';
import { css, html, customElement, property, state, ifDefined, repeat } from '@umbraco-cms/backoffice/external/lit';
import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { MemberItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
import { splitStringToArray } from '@umbraco-cms/backoffice/utils';
import { UMB_WORKSPACE_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal';
import { type UmbSorterConfig, UmbSorterController } from '@umbraco-cms/backoffice/sorter';
const SORTER_CONFIG: UmbSorterConfig<string> = {
getUniqueOfElement: (element) => {
return element.getAttribute('detail');
},
getUniqueOfModel: (modelEntry) => {
return modelEntry;
},
identifier: 'Umb.SorterIdentifier.InputMemberGroup',
itemSelector: 'uui-ref-node',
containerSelector: 'uui-ref-list',
};
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
import { UmbModalRouteRegistrationController, UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/modal';
import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui';
@customElement('umb-input-member-group')
export class UmbInputMemberGroupElement extends UUIFormControlMixin(UmbLitElement, '') {
#sorter = new UmbSorterController(this, {
...SORTER_CONFIG,
#sorter = new UmbSorterController<string>(this, {
getUniqueOfElement: (element) => {
return element.id;
},
getUniqueOfModel: (modelEntry) => {
return modelEntry;
},
identifier: 'Umb.SorterIdentifier.InputMemberGroup',
itemSelector: 'uui-ref-node',
containerSelector: 'uui-ref-list',
onChange: ({ model }) => {
this.selection = model;
this.dispatchEvent(new UmbChangeEvent());
},
});
@@ -91,7 +88,6 @@ export class UmbInputMemberGroupElement extends UUIFormControlMixin(UmbLitElemen
@property()
public set value(idsString: string) {
// Its with full purpose we don't call super.value, as thats being handled by the observation of the context selection.
this.selection = splitStringToArray(idsString);
}
public get value(): string {
@@ -112,7 +108,6 @@ export class UmbInputMemberGroupElement extends UUIFormControlMixin(UmbLitElemen
constructor() {
super();
// TODO: This would have to be more specific if used in a property editor context... [NL]
new UmbModalRouteRegistrationController(this, UMB_WORKSPACE_MODAL)
.addAdditionalPath('member-group')
.onSetup(() => {
@@ -122,10 +117,8 @@ export class UmbInputMemberGroupElement extends UUIFormControlMixin(UmbLitElemen
this._editMemberGroupPath = routeBuilder({});
});
this.observe(this.#pickerContext.selection, (selection) => (this.value = selection.join(',')));
this.observe(this.#pickerContext.selectedItems, (selectedItems) => {
this._items = selectedItems;
});
this.observe(this.#pickerContext.selection, (selection) => (this.value = selection.join(',')), '_observeSelection');
this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems), '_observeItems');
}
connectedCallback(): void {
@@ -144,16 +137,6 @@ export class UmbInputMemberGroupElement extends UUIFormControlMixin(UmbLitElemen
);
}
protected _openPicker() {
this.#pickerContext.openPicker({
hideTreeRoot: true,
});
}
protected _requestRemoveItem(item: UmbMemberGroupItemModel) {
this.#pickerContext.requestRemoveItem(item.unique!);
}
protected getFormElement() {
return undefined;
}
@@ -164,29 +147,31 @@ export class UmbInputMemberGroupElement extends UUIFormControlMixin(UmbLitElemen
});
}
#requestRemoveItem(item: MemberItemResponseModel) {
this.#pickerContext.requestRemoveItem(item.id!);
#removeItem(item: UmbMemberGroupItemModel) {
this.#pickerContext.requestRemoveItem(item.unique);
}
render() {
return html` ${this.#renderItems()} ${this.#renderAddButton()} `;
return html`${this.#renderItems()} ${this.#renderAddButton()}`;
}
#renderItems() {
if (!this._items) return;
return html`<uui-ref-list>
${repeat(
this._items,
(item) => item.unique,
(item) => this.#renderItem(item),
)}
</uui-ref-list>`;
return html`
<uui-ref-list>
${repeat(
this._items,
(item) => item.unique,
(item) => this.#renderItem(item),
)}
</uui-ref-list>
`;
}
#renderAddButton() {
if (this.max === 1 && this.selection.length >= this.max) return;
return html`<uui-button
id="add-button"
id="btn-add"
look="placeholder"
@click=${this.#openPicker}
label=${this.localize.term('general_choose')}></uui-button>`;
@@ -194,17 +179,11 @@ export class UmbInputMemberGroupElement extends UUIFormControlMixin(UmbLitElemen
#renderItem(item: UmbMemberGroupItemModel) {
if (!item.unique) return;
// TODO: get the correct variant name
const name = item.name;
return html`
<uui-ref-node name=${ifDefined(item.name)} detail=${ifDefined(item.unique)}>
<uui-ref-node name=${item.name} id=${item.unique}>
<uui-action-bar slot="actions">
${this.#renderOpenButton(item)}
<uui-button
@click=${() => this._requestRemoveItem(item)}
label="${this.localize.term('general_remove')} ${name}">
${this.localize.term('general_remove')}
</uui-button>
<uui-button @click=${() => this.#removeItem(item)} label=${this.localize.term('general_remove')}></uui-button>
</uui-action-bar>
</uui-ref-node>
`;
@@ -212,21 +191,18 @@ export class UmbInputMemberGroupElement extends UUIFormControlMixin(UmbLitElemen
#renderOpenButton(item: UmbMemberGroupItemModel) {
if (!this.showOpenButton) return;
// TODO: get the correct variant name
const name = item.name;
return html`
<uui-button
compact
href="${this._editMemberGroupPath}edit/${item.unique}"
label=${this.localize.term('general_edit') + ` ${name}`}>
<uui-icon name="icon-edit"></uui-icon>
label="${this.localize.term('general_open')} ${item.name}">
${this.localize.term('general_open')}
</uui-button>
`;
}
static styles = [
css`
#add-button {
#btn-add {
width: 100%;
}

View File

@@ -2,7 +2,12 @@ import type { UmbUserDetailModel } from '../../types.js';
import { UMB_USER_ENTITY_TYPE } from '../../entity.js';
import type { UmbUserCollectionFilterModel } from '../types.js';
import type { UmbCollectionDataSource } from '@umbraco-cms/backoffice/collection';
import type { UserResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
import type {
DirectionModel,
UserOrderModel,
UserResponseModel,
UserStateModel,
} from '@umbraco-cms/backoffice/external/backend-api';
import { UserService } from '@umbraco-cms/backoffice/external/backend-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
@@ -32,7 +37,18 @@ export class UmbUserCollectionServerDataSource implements UmbCollectionDataSourc
* @memberof UmbUserCollectionServerDataSource
*/
async getCollection(filter: UmbUserCollectionFilterModel) {
const { data, error } = await tryExecuteAndNotify(this.#host, UserService.getFilterUser(filter));
const { data, error } = await tryExecuteAndNotify(
this.#host,
UserService.getFilterUser({
filter: filter.filter,
orderBy: filter.orderBy as unknown as UserOrderModel, // TODO: This is a temporary workaround to avoid a type error.
orderDirection: filter.orderDirection as unknown as DirectionModel, // TODO: This is a temporary workaround to avoid a type error.
skip: filter.skip,
take: filter.take,
userGroupIds: filter.userGroupIds,
userStates: filter.userStates as unknown as Array<UserStateModel>, // TODO: This is a temporary workaround to avoid a type error.
}),
);
if (data) {
const { items, total } = data;

View File

@@ -1,11 +1,21 @@
import type { DirectionModel, UserOrderModel, UserStateModel } from '@umbraco-cms/backoffice/external/backend-api';
import type { UmbUserOrderByType, UmbUserStateFilterType } from './utils/index.js';
import type { UmbDirectionType } from '@umbraco-cms/backoffice/utils';
export interface UmbUserCollectionFilterModel {
skip?: number;
take?: number;
orderBy?: UserOrderModel;
orderDirection?: DirectionModel;
orderBy?: UmbUserOrderByType;
orderDirection?: UmbDirectionType;
userGroupIds?: string[];
userStates?: UserStateModel[];
userStates?: UmbUserStateFilterType[];
filter?: string;
}
export interface UmbUserOrderByOption {
unique: string;
label: string;
config: {
orderBy: UmbUserOrderByType;
orderDirection: UmbDirectionType;
};
}

View File

@@ -1,29 +1,22 @@
import type { UmbUserCollectionContext } from './user-collection.context.js';
import type {
UUIBooleanInputEvent,
UUICheckboxElement,
UUIRadioGroupElement,
UUIRadioGroupEvent,
} from '@umbraco-cms/backoffice/external/uui';
import type { UmbUserOrderByOption } from './types.js';
import type { UmbUserStateFilterType } from './utils/index.js';
import { UmbUserStateFilter } from './utils/index.js';
import type { UUIBooleanInputEvent, UUICheckboxElement } from '@umbraco-cms/backoffice/external/uui';
import { css, html, customElement, state, repeat, ifDefined } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UMB_DEFAULT_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection';
import type { UmbModalManagerContext } from '@umbraco-cms/backoffice/modal';
import type { UserOrderModel } from '@umbraco-cms/backoffice/external/backend-api';
import { UserStateModel } from '@umbraco-cms/backoffice/external/backend-api';
import type { UmbUserGroupDetailModel } from '@umbraco-cms/backoffice/user-group';
import { UmbUserGroupCollectionRepository } from '@umbraco-cms/backoffice/user-group';
import { observeMultiple } from '@umbraco-cms/backoffice/observable-api';
@customElement('umb-user-collection-header')
export class UmbUserCollectionHeaderElement extends UmbLitElement {
@state()
private _stateFilterOptions: Array<UserStateModel> = Object.values(UserStateModel);
private _stateFilterOptions: Array<UmbUserStateFilterType> = Object.values(UmbUserStateFilter);
@state()
private _stateFilterSelection: Array<UserStateModel> = [];
@state()
private _orderBy?: UserOrderModel;
private _stateFilterSelection: Array<UmbUserStateFilterType> = [];
@state()
private _userGroups: Array<UmbUserGroupDetailModel> = [];
@@ -31,7 +24,12 @@ export class UmbUserCollectionHeaderElement extends UmbLitElement {
@state()
private _userGroupFilterSelection: Array<UmbUserGroupDetailModel> = [];
#modalContext?: UmbModalManagerContext;
@state()
private _orderByOptions: Array<UmbUserOrderByOption> = [];
@state()
_activeOrderByOption?: UmbUserOrderByOption;
#collectionContext?: UmbUserCollectionContext;
#inputTimer?: NodeJS.Timeout;
#inputTimerAmount = 500;
@@ -43,9 +41,28 @@ export class UmbUserCollectionHeaderElement extends UmbLitElement {
this.consumeContext(UMB_DEFAULT_COLLECTION_CONTEXT, (instance) => {
this.#collectionContext = instance as UmbUserCollectionContext;
this.#observeOrderByOptions();
});
}
#observeOrderByOptions() {
if (!this.#collectionContext) return;
this.observe(
observeMultiple([this.#collectionContext.orderByOptions, this.#collectionContext.activeOrderByOption]),
([options, activeOption]) => {
// the options are hardcoded in the context, so we can just compare the length
if (this._orderByOptions.length !== options.length) {
this._orderByOptions = options;
}
if (activeOption && activeOption !== this._activeOrderByOption?.unique) {
this._activeOrderByOption = this._orderByOptions.find((option) => option.unique === activeOption);
}
},
'_umbObserveUserOrderByOptions',
);
}
protected firstUpdated() {
this.#requestUserGroups();
}
@@ -68,7 +85,7 @@ export class UmbUserCollectionHeaderElement extends UmbLitElement {
#onStateFilterChange(event: UUIBooleanInputEvent) {
event.stopPropagation();
const target = event.currentTarget as UUICheckboxElement;
const value = target.value as UserStateModel;
const value = target.value as UmbUserStateFilterType;
const isChecked = target.checked;
this._stateFilterSelection = isChecked
@@ -78,34 +95,6 @@ export class UmbUserCollectionHeaderElement extends UmbLitElement {
this.#collectionContext?.setStateFilter(this._stateFilterSelection);
}
#onOrderByChange(event: UUIRadioGroupEvent) {
event.stopPropagation();
const target = event.currentTarget as UUIRadioGroupElement | null;
if (target) {
this._orderBy = target.value as UserOrderModel;
this.#collectionContext?.setOrderByFilter(this._orderBy);
}
}
render() {
return html`
<umb-collection-action-bundle></umb-collection-action-bundle>
${this.#renderSearch()}
<div>${this.#renderFilters()} ${this.#renderCollectionViews()}</div>
`;
}
#renderSearch() {
return html`
<uui-input
@input=${this._updateSearch}
label=${this.localize.term('visuallyHiddenTexts_userSearchLabel')}
placeholder=${this.localize.term('visuallyHiddenTexts_userSearchLabel')}
id="input-search"></uui-input>
`;
}
#onUserGroupFilterChange(event: UUIBooleanInputEvent) {
const target = event.currentTarget as UUICheckboxElement;
const item = this._userGroups.find((group) => group.unique === target.value);
@@ -122,6 +111,10 @@ export class UmbUserCollectionHeaderElement extends UmbLitElement {
this.#collectionContext?.setUserGroupFilter(uniques);
}
#onOrderByChange(option: UmbUserOrderByOption) {
this.#collectionContext?.setActiveOrderByOption(option.unique);
}
#getUserGroupFilterLabel() {
const length = this._userGroupFilterSelection.length;
const max = 2;
@@ -146,8 +139,26 @@ export class UmbUserCollectionHeaderElement extends UmbLitElement {
.join(', ') + (length > max ? ' + ' + (length - max) : '');
}
render() {
return html`
<umb-collection-action-bundle></umb-collection-action-bundle>
${this.#renderSearch()}
<div>${this.#renderFilters()} ${this.#renderCollectionViews()}</div>
`;
}
#renderSearch() {
return html`
<uui-input
@input=${this._updateSearch}
label=${this.localize.term('visuallyHiddenTexts_userSearchLabel')}
placeholder=${this.localize.term('visuallyHiddenTexts_userSearchLabel')}
id="input-search"></uui-input>
`;
}
#renderFilters() {
return html` ${this.#renderStatusFilter()} ${this.#renderUserGroupFilter()} `;
return html` ${this.#renderStatusFilter()} ${this.#renderUserGroupFilter()} ${this.#renderOrderBy()} `;
}
#renderStatusFilter() {
@@ -196,6 +207,29 @@ export class UmbUserCollectionHeaderElement extends UmbLitElement {
`;
}
#renderOrderBy() {
return html`
<uui-button popovertarget="popover-order-by-filter" label="order by">
<umb-localize key="general_orderBy"></umb-localize>:
<b> ${this._activeOrderByOption ? this.localize.string(this._activeOrderByOption.label) : ''}</b>
</uui-button>
<uui-popover-container id="popover-order-by-filter" placement="bottom">
<umb-popover-layout>
<div class="filter-dropdown">
${this._orderByOptions.map(
(option) => html`
<uui-menu-item
label=${this.localize.string(option.label)}
@click-label=${() => this.#onOrderByChange(option)}
?active=${this._activeOrderByOption?.unique === option.unique}></uui-menu-item>
`,
)}
</div>
</umb-popover-layout>
</uui-popover-container>
`;
}
#renderCollectionViews() {
return html` <umb-collection-view-bundle></umb-collection-view-bundle> `;
}
@@ -224,6 +258,7 @@ export class UmbUserCollectionHeaderElement extends UmbLitElement {
display: flex;
gap: var(--uui-size-space-3);
flex-direction: column;
padding: var(--uui-size-space-3);
}
`,
];

View File

@@ -1,33 +1,105 @@
import type { UmbUserDetailModel } from '../types.js';
import { UMB_COLLECTION_VIEW_USER_GRID } from './views/index.js';
import type { UmbUserCollectionFilterModel } from './types.js';
import type { UmbUserCollectionFilterModel, UmbUserOrderByOption } from './types.js';
import type { UmbUserOrderByType, UmbUserStateFilterType } from './utils/index.js';
import { UmbUserOrderBy } from './utils/index.js';
import { UmbDefaultCollectionContext } from '@umbraco-cms/backoffice/collection';
import type { UserOrderModel, UserStateModel } from '@umbraco-cms/backoffice/external/backend-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbArrayState, UmbStringState } from '@umbraco-cms/backoffice/observable-api';
import type { UmbDirectionType } from '@umbraco-cms/backoffice/utils';
import { UmbDirection } from '@umbraco-cms/backoffice/utils';
const orderByOptions: Array<UmbUserOrderByOption> = [
{
unique: 'nameAscending',
label: '#user_sortNameAscending',
config: {
orderBy: UmbUserOrderBy.NAME,
orderDirection: UmbDirection.ASCENDING,
},
},
{
unique: 'nameDescending',
label: '#user_sortNameDescending',
config: {
orderBy: UmbUserOrderBy.NAME,
orderDirection: UmbDirection.DESCENDING,
},
},
{
unique: 'createDateDescending',
label: '#user_sortCreateDateDescending',
config: {
orderBy: UmbUserOrderBy.CREATE_DATE,
orderDirection: UmbDirection.DESCENDING,
},
},
{
unique: 'createDateAscending',
label: '#user_sortCreateDateAscending',
config: {
orderBy: UmbUserOrderBy.CREATE_DATE,
orderDirection: UmbDirection.ASCENDING,
},
},
{
unique: 'lastLoginDateDescending',
label: '#user_sortLastLoginDateDescending',
config: {
orderBy: UmbUserOrderBy.LAST_LOGIN_DATE,
orderDirection: UmbDirection.DESCENDING,
},
},
];
export class UmbUserCollectionContext extends UmbDefaultCollectionContext<
UmbUserDetailModel,
UmbUserCollectionFilterModel
> {
#orderByOptions = new UmbArrayState<UmbUserOrderByOption>([], (x) => x.label);
orderByOptions = this.#orderByOptions.asObservable();
#activeOrderByOption = new UmbStringState<string | undefined>(undefined);
activeOrderByOption = this.#activeOrderByOption.asObservable();
constructor(host: UmbControllerHost) {
super(host, UMB_COLLECTION_VIEW_USER_GRID);
const firstOption: UmbUserOrderByOption = orderByOptions[0];
super(host, UMB_COLLECTION_VIEW_USER_GRID, {
orderBy: firstOption.config.orderBy,
orderDirection: firstOption.config.orderDirection,
});
this.#orderByOptions.setValue(orderByOptions);
this.#activeOrderByOption.setValue(firstOption.unique);
}
/**
* Sets the active order by option for the collection and refreshes the collection.
* @param {string} unique
* @memberof UmbUserCollectionContext
*/
setActiveOrderByOption(unique: string) {
const option = this.#orderByOptions.getValue().find((x) => x.unique === unique);
this.#activeOrderByOption.setValue(unique);
this.setFilter({ orderBy: option?.config.orderBy, orderDirection: option?.config.orderDirection });
}
/**
* Sets the state filter for the collection and refreshes the collection.
* @param {Array<UserStateModel>} selection
* @param {Array<UmbUserStateFilterModel>} selection
* @memberof UmbUserCollectionContext
*/
setStateFilter(selection: Array<UserStateModel>) {
setStateFilter(selection: Array<UmbUserStateFilterType>) {
this.setFilter({ userStates: selection });
}
/**
* Sets the order by filter for the collection and refreshes the collection.
* @param {UserOrderModel} orderBy
* @param {UmbUserOrderByModel} orderBy
* @memberof UmbUserCollectionContext
*/
setOrderByFilter(orderBy: UserOrderModel) {
setOrderByFilter(orderBy: UmbUserOrderByType) {
this.setFilter({ orderBy });
}
@@ -39,6 +111,15 @@ export class UmbUserCollectionContext extends UmbDefaultCollectionContext<
setUserGroupFilter(selection: Array<string>) {
this.setFilter({ userGroupIds: selection });
}
/**
* Sets the order direction filter for the collection and refreshes the collection.
* @param {any} orderDirection
* @memberof UmbUserCollectionContext
*/
setOrderDirectionFilter(orderDirection: UmbDirectionType) {
this.setFilter({ orderDirection });
}
}
export default UmbUserCollectionContext;

View File

@@ -0,0 +1,18 @@
export type UmbUserOrderByType = 'Name' | 'CreateDate' | 'LastLoginDate';
export const UmbUserOrderBy = Object.freeze({
NAME: 'Name',
CREATE_DATE: 'CreateDate',
LAST_LOGIN_DATE: 'LastLoginDate',
});
export type UmbUserStateFilterType = 'Active' | 'Disabled' | 'LockedOut' | 'Invited' | 'Inactive' | 'All';
export const UmbUserStateFilter = Object.freeze({
ACTIVE: 'Active',
DISABLED: 'Disabled',
LOCKED_OUT: 'LockedOut',
INVITED: 'Invited',
INACTIVE: 'Inactive',
ALL: 'All',
});