diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/default/dashboard.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/default/dashboard.element.ts index 0e82c4938b27..afc774848a72 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/default/dashboard.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/default/dashboard.element.ts @@ -1,31 +1,80 @@ -import type { ManifestDashboardApp, UmbDashboardAppSize } from '../app/dashboard-app.extension.js'; +import type { ManifestDashboardApp } from '../dashboard-app.extension.js'; import { UMB_DASHBOARD_APP_PICKER_MODAL } from '../app/picker/picker-modal.token.js'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import { css, html, customElement, state, repeat } from '@umbraco-cms/backoffice/external/lit'; +import { css, html, customElement, nothing, ifDefined, state, repeat, styleMap, when } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { UmbExtensionsElementInitializer } from '@umbraco-cms/backoffice/extension-api'; +import { + loadManifestElement, + UmbExtensionsElementInitializer, + type PermittedControllerType, + type UmbExtensionElementInitializer, +} from '@umbraco-cms/backoffice/extension-api'; import { umbOpenModal } from '@umbraco-cms/backoffice/modal'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { UUIBlinkAnimationValue, UUIBlinkKeyframes } from '@umbraco-cms/backoffice/external/uui'; +import type { DashboardAppInstance, UserDashboardAppConfiguration } from './types.js'; +import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; +import { UmbId } from '@umbraco-cms/backoffice/id'; +import { UserDataService } from '@umbraco-cms/backoffice/external/backend-api'; @customElement('umb-dashboard') export class UmbDashboardElement extends UmbLitElement { - #defaultSize: UmbDashboardAppSize = 'small'; + #defaultSize = 'small'; - #sizeMap = new Map([ + #sizeMap = new Map([ ['small', 'small'], ['medium', 'medium'], ['large', 'large'], ]); + #gridSizeMap = new Map([ + ['small',{columns:1, rows: 1}], + ['medium',{columns:2, rows: 2}], + ['large',{columns:2, rows: 2}], + ]); + + #sorter? : UmbSorterController; + #extensionsController?: UmbExtensionsElementInitializer; @state() - _appElements: Array = []; + _editMode = false; + + @state() + _apps : Array = []; @state() _appUniques: Array = []; + @state() + _availableApps : PermittedControllerType>[] = []; + + _userDataKey : string | undefined; + + constructor() { + super(); + + this.#sorter = new UmbSorterController(this, { + itemSelector: '.dashboard-app', + containerSelector: '.grid-container', + getUniqueOfElement: (element) => element.getAttribute('data-sorter-id'), + getUniqueOfModel: (modelEntry) => modelEntry.key, + onChange: ({ model }) => { + const oldValue = this._apps; + this._apps = model; + this.requestUpdate('_appElements', oldValue); + }, + }); + + this.#sorter.setModel(this._apps); + this.#sorter.disable(); + + this.#observeDashboardApps(); + + } + #observeDashboardApps(): void { + this.#extensionsController?.destroy(); this.#extensionsController = new UmbExtensionsElementInitializer< UmbExtensionManifest, @@ -35,48 +84,228 @@ export class UmbDashboardElement extends UmbLitElement { this, umbExtensionsRegistry, 'dashboardApp', - (manifest) => this._appUniques.includes(manifest.alias), + (manifest) => true, (extensionControllers) => { - this._appElements = extensionControllers.map((controller) => { - if (controller.component && controller.manifest) { - const size = this.#sizeMap.get(controller.manifest.meta?.size) ?? this.#defaultSize; - controller.component.setAttribute('size', `${size}`); - controller.component.manifest = controller.manifest; - return html`${controller.component}`; - } else { - return html`Not Found`; - } - }); + + this._availableApps = extensionControllers; + + this.#load(); + }, undefined, // We can leave the alias to undefined, as we destroy this our selfs. ); } + async #drawDashboardApps(config : UserDashboardAppConfiguration) { + + let apps = [...this._apps]; + + if(config.apps.length == 0) { + //TODO: Draw all? + } + else + { + + for(const configuredApp of config.apps){ + + var controller = this._availableApps.find(x=>x.alias == configuredApp.alias); + if(!controller) + return undefined; + + const size = this.#sizeMap.get(controller.manifest.meta?.size) ?? this.#defaultSize; + const headline = controller.manifest?.meta?.headline + ? this.localize.string(controller.manifest?.meta?.headline) + : undefined; + + let gridSize = this.#gridSizeMap.get(size)!; + + const elementPropsValue = controller.manifest.element ?? controller.manifest.js; + var appElementCtor = await loadManifestElement(elementPropsValue!)!; //TODO: Validate + + if(!appElementCtor) + return; + + apps.push({ + key : UmbId.new(), + alias : controller.alias, + columns : gridSize.columns, + rows : gridSize?.rows, + headline : headline, + component : new appElementCtor(), + }); + + } + } + + this._apps = apps; + + } + async #openAppPicker() { + const value = await umbOpenModal(this, UMB_DASHBOARD_APP_PICKER_MODAL, { data: { multiple: true, }, - value: { - selection: this._appUniques, - }, + value : { + selection : [] + } }).catch(() => undefined); if (value) { - this._appUniques = value.selection.filter((item) => item !== null) as Array; - this.#observeDashboardApps(); + + let apps = [...this._apps]; + + for(const alias of value.selection) { + + var controller = this._availableApps.find(x=>x.alias == alias); + if(!controller) + return undefined; + + const size = this.#sizeMap.get(controller.manifest.meta?.size) ?? this.#defaultSize; + const headline = controller.manifest?.meta?.headline + ? this.localize.string(controller.manifest?.meta?.headline) + : undefined; + + let gridSize = this.#gridSizeMap.get(size)!; + + const elementPropsValue = controller.manifest.element ?? controller.manifest.js; + var appElementCtor = await loadManifestElement(elementPropsValue!)!; //TODO: Validate + + if(!appElementCtor) + return; + + apps.push({ + key : UmbId.new(), + alias : controller.alias, + columns : gridSize.columns, + rows : gridSize?.rows, + headline : headline, + component : new appElementCtor(), + }); + + } + + this._apps = apps; + + this.#save(); + + } + } + + async #save(){ + + let config = { + apps : this._apps.map((x)=> {return {alias : x.alias }}) + } as UserDashboardAppConfiguration; + + if(this._userDataKey){ + var res = await UserDataService.putUserData({ + body : { + key : this._userDataKey, + group : 'test', + identifier : '', + value : JSON.stringify(config), + } + }); + } + else { + var res = await UserDataService.postUserData({ + body : { + group : 'test', + identifier : '', + value : JSON.stringify(config), + } + }); } + } + async #load(){ + + var res = await UserDataService.getUserData({ + query : { + groups : ['test'] + } + }); + + if(res.data.items.length == 0) + return; + + this._userDataKey = res.data.items[0].key; + + const userConfig = JSON.parse(res.data.items[0].value); + + this.#drawDashboardApps(userConfig); + + } + + async #enterEditMode() { + this.#sorter?.setModel(this._apps); + this.#sorter?.enable() + this._editMode = true; + } + + async #leaveEditMode() { + this.#sorter?.disable(); + this._editMode = false; + this.#save(); + } + + async #remove(elementKey : string) { + + this._apps = this._apps.filter(x=>x.key != elementKey); + + } + + #onPopoverToggle = false; + override render() { return html`
- Add -
+
+ + ${when(this._editMode, + ()=>html`Done `, + ()=>html` + + + + + + + + + + + + + + `) + } +
+
${repeat( - this._appElements, - (element) => element, - (element) => element, + this._apps, + (element) => element.key, + (element) => + html` +
+ ${when(this._editMode, + ()=>html` this.#remove(element.key)}>` + )} + ${element.component} + +
`, )}
@@ -86,35 +315,109 @@ export class UmbDashboardElement extends UmbLitElement { static override styles = [ UmbTextStyles, css` + :host { + container-type: inline-size; + --uui-menu-item-flat-structure: 1; + } + #content { padding: var(--uui-size-layout-1); + padding-top: var(--uui-size-space-3); + container-type: inline-size; + } + + .main-actions { + display: flex; + justify-content: flex-end; + } + + uui-box div[slot='header-actions'] uui-button { + font-size:12px; + --uui-button-height: auto; } - #grid-container { + uui-box { + height:100%; + position:relative; + } + + .grid-container { + margin-top:var(--uui-size-space-3); display: grid; grid-template-columns: repeat(4, 1fr); grid-auto-rows: 225px; - margin: calc(var(--uui-size-space-3) * -1); - margin-bottom: 20px; + gap: 20px; + } + + @container (inline-size < 900px) { + .grid-container { + grid-template-columns: repeat(3, 1fr); + } + } + + @container (inline-size < 601px) { + .grid-container { + grid-template-columns: repeat(1, 1fr); + } + .grid-container > * { + grid-column: span 1 !important; + } + } + + .dashboard-app { + position:relative; + display:block; + height:100%; - [size='small'] { - grid-column: span 1; - grid-row: span 1; - margin: var(--uui-size-space-3); + &::after { + content: ''; + position: absolute; + z-index: 1; + pointer-events: none; + inset: 0; + border: 1px solid transparent; + border-radius: var(--uui-border-radius); + transition: border-color 240ms ease-in; } - [size='medium'] { - grid-column: span 2; - grid-row: span 2; - margin: var(--uui-size-space-3); + & > uui-button { + position:absolute; + top:0; + right:0; + z-index:1; } - [size='large'] { - grid-column: span 2; - grid-row: span 3; - margin: var(--uui-size-space-3); + &[drag-placeholder] { + position: relative; + display: block; + --umb-block-grid-entry-actions-opacity: 0; + + &::after { + display: block; + border-width: 2px; + border-color: var(--uui-color-interactive-emphasis); + animation: ${UUIBlinkAnimationValue}; + } + + &::before { + content: ''; + position: absolute; + pointer-events: none; + inset: 0; + border-radius: var(--uui-border-radius); + background-color: var(--uui-color-interactive-emphasis); + opacity: 0.12; + } + + & > * { + transition: opacity 50ms 16ms; + opacity: 0; + } + } + } + `, ]; } @@ -126,3 +429,4 @@ declare global { ['umb-dashboard']: UmbDashboardElement; } } + diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/default/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/default/types.ts index 317fdcd2d0e2..e87e64176c38 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/default/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/default/types.ts @@ -9,6 +9,28 @@ export interface MetaDashboardDefaultKind extends MetaDashboard { headline: string; } +export interface DashboardAppInstance { + key : string; + /** Dashboard App Alias */ + alias : string; + rows? : number; + columns? : number; + headline? : string; + component? : HTMLElement; +} + +/** + * Defines a configured dashboard app added by a user. Used e.g for serialization. + */ +export type ConfiguredDashboardApp = { + /** Dashboard App Alias */ + alias : string; +} + +export interface UserDashboardAppConfiguration { + apps : ConfiguredDashboardApp[]; +} + declare global { interface UmbExtensionManifestMap { umbManifestDashboardDefaultKind: ManifestDashboardDefaultKind; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/invite/dashboard-app/pending-user-invites-dashboard-app.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/invite/dashboard-app/pending-user-invites-dashboard-app.element.ts index b8c15445c594..ce311f5fef08 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/invite/dashboard-app/pending-user-invites-dashboard-app.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/invite/dashboard-app/pending-user-invites-dashboard-app.element.ts @@ -38,21 +38,23 @@ export class UmbPendingUserInvitesDashboardAppElement extends UmbLitElement impl override render() { return html` - ${this._pendingUserInvites.map( - (user) => html` - - - - - - - `, - )} + + ${this._pendingUserInvites.map( + (user) => html` + + + + + + + `, + )} + `; }