diff --git a/src/Umbraco.Web.UI.Client/examples/modal-routed/dashboard.element.ts b/src/Umbraco.Web.UI.Client/examples/modal-routed/dashboard.element.ts index 61c0f6862f36..289dc8b2d939 100644 --- a/src/Umbraco.Web.UI.Client/examples/modal-routed/dashboard.element.ts +++ b/src/Umbraco.Web.UI.Client/examples/modal-routed/dashboard.element.ts @@ -3,8 +3,8 @@ import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api'; import type { UmbRoute } from '@umbraco-cms/backoffice/router'; -@customElement('umb-dashboard') -export class UmbDashboardElement extends UmbElementMixin(LitElement) { +@customElement('umb-example-dashboard') +export class UmbExampleDashboardElement extends UmbElementMixin(LitElement) { @state() private _routes: UmbRoute[] = [ { @@ -39,10 +39,10 @@ export class UmbDashboardElement extends UmbElementMixin(LitElement) { static override styles = [UmbTextStyles, css``]; } -export default UmbDashboardElement; +export default UmbExampleDashboardElement; declare global { interface HTMLElementTagNameMap { - 'umb-dashboard': UmbDashboardElement; + 'umb-example-dashboard': UmbExampleDashboardElement; } } diff --git a/src/Umbraco.Web.UI.Client/examples/modal-routed/dashboard2.element.ts b/src/Umbraco.Web.UI.Client/examples/modal-routed/dashboard2.element.ts index 2a3f02e7bfc8..c2a8ec090713 100644 --- a/src/Umbraco.Web.UI.Client/examples/modal-routed/dashboard2.element.ts +++ b/src/Umbraco.Web.UI.Client/examples/modal-routed/dashboard2.element.ts @@ -2,8 +2,8 @@ import { css, html, LitElement, customElement } from '@umbraco-cms/backoffice/ex import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api'; -@customElement('umb-dashboard2') -export class UmbDashboard2Element extends UmbElementMixin(LitElement) { +@customElement('umb-example-dashboard2') +export class UmbExampleDashboard2Element extends UmbElementMixin(LitElement) { constructor() { super(); } @@ -25,10 +25,10 @@ export class UmbDashboard2Element extends UmbElementMixin(LitElement) { static override styles = [UmbTextStyles, css``]; } -export default UmbDashboard2Element; +export default UmbExampleDashboard2Element; declare global { interface HTMLElementTagNameMap { - 'umb-dashboard2': UmbDashboard2Element; + 'umb-example-dashboard2': UmbExampleDashboard2Element; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/_temp-poc-location/README.md b/src/Umbraco.Web.UI.Client/src/packages/core/_temp-poc-location/README.md new file mode 100644 index 000000000000..3f925c0071c0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/_temp-poc-location/README.md @@ -0,0 +1 @@ +The files in this folder is positioned here temporarily. They will be moved to the correct location later. diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/_temp-poc-location/certification/certification-intro-dashboard-app.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/_temp-poc-location/certification/certification-intro-dashboard-app.element.ts new file mode 100644 index 000000000000..166fd37f50b5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/_temp-poc-location/certification/certification-intro-dashboard-app.element.ts @@ -0,0 +1,50 @@ +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { html, customElement, css, property } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { + ManifestDashboardApp, + UmbDashboardAppElement, + UmbDashboardAppSize, +} from '@umbraco-cms/backoffice/dashboard'; + +@customElement('umb-certification-intro-dashboard-app') +export class UmbCertificationIntroDashboardAppElement extends UmbLitElement implements UmbDashboardAppElement { + @property({ type: Object }) + manifest?: ManifestDashboardApp; + + @property({ type: String }) + size?: UmbDashboardAppSize; + + override render() { + return html` + +

+ +

+ +
+ `; + } + + static override styles = [ + UmbTextStyles, + css` + p { + margin-top: 0; + } + `, + ]; +} + +export { UmbCertificationIntroDashboardAppElement as element }; + +declare global { + interface HTMLElementTagNameMap { + ['umb-certification-intro-dashboard-app']: UmbCertificationIntroDashboardAppElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/_temp-poc-location/certification/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/_temp-poc-location/certification/manifests.ts new file mode 100644 index 000000000000..5bae81ef8173 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/_temp-poc-location/certification/manifests.ts @@ -0,0 +1,15 @@ +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array = [ + { + type: 'dashboardApp', + alias: 'Umb.DashboardApp.CertificationIntro', + name: 'Certification Intro Dashboard App', + weight: 100, + element: () => import('./certification-intro-dashboard-app.element.js'), + meta: { + headline: '#settingsDashboard_trainingHeader', + size: 'small', + }, + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/_temp-poc-location/community/community-intro-dashboard-app.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/_temp-poc-location/community/community-intro-dashboard-app.element.ts new file mode 100644 index 000000000000..9f65d8c0d7e7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/_temp-poc-location/community/community-intro-dashboard-app.element.ts @@ -0,0 +1,57 @@ +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { html, customElement, css, property } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { + ManifestDashboardApp, + UmbDashboardAppElement, + UmbDashboardAppSize, +} from '@umbraco-cms/backoffice/dashboard'; + +const elementName = 'umb-community-intro-dashboard-app'; +@customElement(elementName) +export class UmbCommunityIntroDashboardAppElement extends UmbLitElement implements UmbDashboardAppElement { + @property({ type: Object }) + manifest?: ManifestDashboardApp; + + @property({ type: String }) + size?: UmbDashboardAppSize; + + override render() { + return html` + +

+ +

+ + +
+ `; + } + + static override styles = [ + UmbTextStyles, + css` + p { + margin-top: 0; + } + `, + ]; +} + +export { UmbCommunityIntroDashboardAppElement as element }; + +declare global { + interface HTMLElementTagNameMap { + [elementName]: UmbCommunityIntroDashboardAppElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/_temp-poc-location/community/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/_temp-poc-location/community/manifests.ts new file mode 100644 index 000000000000..481da86d7051 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/_temp-poc-location/community/manifests.ts @@ -0,0 +1,15 @@ +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array = [ + { + type: 'dashboardApp', + alias: 'Umb.DashboardApp.CommunityIntro', + name: 'Community Intro Dashboard App', + weight: 200, + element: () => import('./community-intro-dashboard-app.element.js'), + meta: { + headline: '#settingsDashboard_communityHeader', + size: 'small', + }, + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/_temp-poc-location/documentation/documentation-intro-dashboard-app.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/_temp-poc-location/documentation/documentation-intro-dashboard-app.element.ts new file mode 100644 index 000000000000..5b340e0885f1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/_temp-poc-location/documentation/documentation-intro-dashboard-app.element.ts @@ -0,0 +1,51 @@ +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { html, customElement, css, property } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { + ManifestDashboardApp, + UmbDashboardAppElement, + UmbDashboardAppSize, +} from '@umbraco-cms/backoffice/dashboard'; + +const elementName = 'umb-documentation-intro-dashboard-app'; +@customElement(elementName) +export class UmbDocumentationIntroDashboardAppElement extends UmbLitElement implements UmbDashboardAppElement { + @property({ type: Object }) + manifest?: ManifestDashboardApp; + + @property({ type: String }) + size?: UmbDashboardAppSize; + + override render() { + return html` + +

+ +

+ +
+ `; + } + + static override styles = [ + UmbTextStyles, + css` + p { + margin-top: 0; + } + `, + ]; +} + +export { UmbDocumentationIntroDashboardAppElement as element }; + +declare global { + interface HTMLElementTagNameMap { + [elementName]: UmbDocumentationIntroDashboardAppElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/_temp-poc-location/documentation/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/_temp-poc-location/documentation/manifests.ts new file mode 100644 index 000000000000..7af451cd4ed0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/_temp-poc-location/documentation/manifests.ts @@ -0,0 +1,26 @@ +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array = [ + { + type: 'dashboardApp', + alias: 'Umb.DashboardApp.DocumentationIntro', + name: 'Documentation Intro Dashboard App', + weight: 300, + element: () => import('./documentation-intro-dashboard-app.element.js'), + meta: { + headline: '#settingsDashboard_documentationHeader', + size: 'small', + }, + }, + { + type: 'dashboardApp', + alias: 'Umb.DashboardApp.VideosIntro', + name: 'Videos Intro Dashboard App', + weight: 400, + element: () => import('./videos-intro-dashboard-app.element.js'), + meta: { + headline: '#settingsDashboard_videosHeader', + size: 'large', + }, + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/_temp-poc-location/documentation/videos-intro-dashboard-app.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/_temp-poc-location/documentation/videos-intro-dashboard-app.element.ts new file mode 100644 index 000000000000..5df76497b395 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/_temp-poc-location/documentation/videos-intro-dashboard-app.element.ts @@ -0,0 +1,51 @@ +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { html, customElement, css, property } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { + ManifestDashboardApp, + UmbDashboardAppElement, + UmbDashboardAppSize, +} from '@umbraco-cms/backoffice/dashboard'; + +const elementName = 'umb-videos-intro-dashboard-app'; +@customElement(elementName) +export class UmbVideosIntroDashboardAppElement extends UmbLitElement implements UmbDashboardAppElement { + @property({ type: Object }) + manifest?: ManifestDashboardApp; + + @property({ type: String }) + size?: UmbDashboardAppSize; + + override render() { + return html` + +

+ +

+ +
+ `; + } + + static override styles = [ + UmbTextStyles, + css` + p { + margin-top: 0; + } + `, + ]; +} + +export { UmbVideosIntroDashboardAppElement as element }; + +declare global { + interface HTMLElementTagNameMap { + [elementName]: UmbVideosIntroDashboardAppElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/_temp-poc-location/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/_temp-poc-location/manifests.ts new file mode 100644 index 000000000000..38441287fc6d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/_temp-poc-location/manifests.ts @@ -0,0 +1,12 @@ +import { manifests as communityManifests } from './community/manifests.js'; +import { manifests as documentationManifests } from './documentation/manifests.js'; +import { manifests as supportManifests } from './support/manifests.js'; +import { manifests as trainingManifests } from './certification/manifests.js'; +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array = [ + ...communityManifests, + ...documentationManifests, + ...supportManifests, + ...trainingManifests, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/_temp-poc-location/support/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/_temp-poc-location/support/manifests.ts new file mode 100644 index 000000000000..cbd6821714b4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/_temp-poc-location/support/manifests.ts @@ -0,0 +1,15 @@ +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array = [ + { + type: 'dashboardApp', + alias: 'Umb.DashboardApp.SupportIntro', + name: 'Support Intro Dashboard App', + weight: 500, + element: () => import('./support-intro-dashboard-app.element.js'), + meta: { + headline: '#settingsDashboard_supportHeader', + size: 'small', + }, + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/_temp-poc-location/support/support-intro-dashboard-app.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/_temp-poc-location/support/support-intro-dashboard-app.element.ts new file mode 100644 index 000000000000..c2870a4cdd92 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/_temp-poc-location/support/support-intro-dashboard-app.element.ts @@ -0,0 +1,50 @@ +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { html, customElement, css, property } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { + ManifestDashboardApp, + UmbDashboardAppElement, + UmbDashboardAppSize, +} from '@umbraco-cms/backoffice/dashboard'; + +@customElement('umb-support-intro-dashboard-app') +export class UmbSupportIntroDashboardAppElement extends UmbLitElement implements UmbDashboardAppElement { + @property({ type: Object }) + manifest?: ManifestDashboardApp; + + @property({ type: String }) + size?: UmbDashboardAppSize; + + override render() { + return html` + +

+ +

+ +
+ `; + } + + static override styles = [ + UmbTextStyles, + css` + p { + margin-top: 0; + } + `, + ]; +} + +export { UmbSupportIntroDashboardAppElement as element }; + +declare global { + interface HTMLElementTagNameMap { + ['umb-support-intro-dashboard-app']: UmbSupportIntroDashboardAppElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/collection.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/collection.repository.ts new file mode 100644 index 000000000000..ea0281b518e6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/collection.repository.ts @@ -0,0 +1,38 @@ +import type { UmbDashboardAppDetailModel } from './types.js'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import type { UmbCollectionFilterModel, UmbCollectionRepository } from '@umbraco-cms/backoffice/collection'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface UmbDashboardAppCollectionFilterModel extends UmbCollectionFilterModel {} + +export class UmbDashboardAppCollectionRepository extends UmbControllerBase implements UmbCollectionRepository { + async requestCollection(args: UmbDashboardAppCollectionFilterModel) { + let manifests: Array = umbExtensionsRegistry + .getByType('dashboardApp') + .map((manifest) => { + return { + unique: manifest.alias, + entityType: manifest.type, + name: manifest.meta.headline, + }; + }); + + const skip = args.skip || 0; + const take = args.take || 100; + + if (args.filter) { + const text = args.filter.toLowerCase(); + manifests = manifests.filter((x) => x.name.toLowerCase().includes(text)); + } + + manifests.sort((a, b) => a.name.localeCompare(b.name)); + + const total = manifests.length; + const items = manifests.slice(skip, skip + take); + const data = { items, total }; + return { data, error: undefined }; + } +} + +export { UmbDashboardAppCollectionRepository as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/constants.ts new file mode 100644 index 000000000000..15ee5ebd3f39 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/constants.ts @@ -0,0 +1 @@ +export * from './picker/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/dashboard-app.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/dashboard-app.extension.ts new file mode 100644 index 000000000000..aa470edc1642 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/dashboard-app.extension.ts @@ -0,0 +1,27 @@ +import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; +import type { ManifestElement, ManifestWithDynamicConditions } from '@umbraco-cms/backoffice/extension-api'; + +export type UmbDashboardAppSize = 'small' | 'medium' | 'large'; + +export interface UmbDashboardAppElement extends UmbControllerHostElement { + manifest?: ManifestDashboardApp; + size?: UmbDashboardAppSize; +} + +export interface ManifestDashboardApp + extends ManifestElement, + ManifestWithDynamicConditions { + type: 'dashboardApp'; + meta: MetaDashboardApp; +} + +export interface MetaDashboardApp { + headline: string; + size: UmbDashboardAppSize; +} + +declare global { + interface UmbExtensionManifestMap { + umbDashboardApp: ManifestDashboardApp; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/global-components/dashboard-app-layout.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/global-components/dashboard-app-layout.element.ts new file mode 100644 index 000000000000..cd1506b09759 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/global-components/dashboard-app-layout.element.ts @@ -0,0 +1,36 @@ +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { css, html, customElement, property } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; + +@customElement('umb-dashboard-app-layout') +export class UmbDashboardAppLayoutElement extends UmbLitElement { + @property({ type: String }) + headline: string | null = null; + + override render() { + return html`${this.#renderActions()}`; + } + + #renderActions() { + return html`
+ + + +
`; + } + + static override styles = [ + UmbTextStyles, + css` + uui-box { + height: 100%; + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + ['umb-dashboard-app-layout']: UmbDashboardAppLayoutElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/global-components/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/global-components/index.ts new file mode 100644 index 000000000000..1c01668b45a2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/global-components/index.ts @@ -0,0 +1,3 @@ +import './dashboard-app-layout.element.js'; + +export { UmbDashboardAppLayoutElement } from './dashboard-app-layout.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/index.ts new file mode 100644 index 000000000000..61a8532b6b70 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/index.ts @@ -0,0 +1 @@ +export * from './global-components/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/manifests.ts new file mode 100644 index 000000000000..5113dd54728f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/manifests.ts @@ -0,0 +1,4 @@ +import { manifests as pickerManifests } from './picker/manifests.js'; +import { manifests as removeManifests } from './remove/manifests.js'; + +export const manifests: Array = [...pickerManifests, ...removeManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/picker/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/picker/constants.ts new file mode 100644 index 000000000000..76b91d4d0610 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/picker/constants.ts @@ -0,0 +1,2 @@ +export * from './picker-modal.token.js'; +export * from './picker.context.token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/picker/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/picker/manifests.ts new file mode 100644 index 000000000000..90d95f10ce8e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/picker/manifests.ts @@ -0,0 +1,8 @@ +export const manifests: Array = [ + { + type: 'modal', + alias: 'Umb.Modal.DashboardAppPicker', + name: 'Umb Dashboard App Picker Modal', + js: () => import('./picker-modal.element.js'), + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/picker/picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/picker/picker-modal.element.ts new file mode 100644 index 000000000000..c644085c6bc7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/picker/picker-modal.element.ts @@ -0,0 +1,95 @@ +import type { UmbDashboardAppDetailModel } from '../types.js'; +import type { UmbDashboardAppPickerModalValue, UmbDashboardAppPickerModalData } from './picker-modal.token.js'; +import { UmbDashboardAppPickerContext } from './picker.context.js'; +import { html, customElement, state, repeat, nothing, type PropertyValues } from '@umbraco-cms/backoffice/external/lit'; +import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; + +@customElement('umb-dashboard-app-picker-modal') +export class UmbDashboardAppPickerModalElement extends UmbModalBaseElement< + UmbDashboardAppPickerModalData, + UmbDashboardAppPickerModalValue +> { + @state() + private _items: Array = []; + + @state() + private _searchQuery: string | undefined; + + #pickerContext = new UmbDashboardAppPickerContext(this); + + constructor() { + super(); + this.#observeSelection(); + } + + override connectedCallback(): void { + super.connectedCallback(); + this.#pickerContext.selection.setSelectable(true); + this.#pickerContext.selection.setMultiple(this.data?.multiple ?? false); + this.#pickerContext.selection.setSelection(this.value?.selection ?? []); + + this.observe(this.#pickerContext.items, (options) => { + this._items = options; + }); + } + + protected override firstUpdated(_changedProperties: PropertyValues): void { + super.firstUpdated(_changedProperties); + this.#pickerContext.loadData(); + } + + #observeSelection() { + this.observe( + this.#pickerContext.selection.selection, + (selection) => { + this.updateValue({ selection }); + }, + 'uopObserveSelection', + ); + } + override render() { + return html` + ${this.#renderMenu()} + ${this.#renderActions()} + `; + } + + #renderMenu() { + if (this._searchQuery) { + return nothing; + } + + return html` + ${repeat( + this._items, + (item) => item.unique, + (item) => html` + this.#pickerContext.selection.select(item.unique)} + @deselected=${() => this.#pickerContext.selection.deselect(item.unique)} + ?selected=${this.value.selection.includes(item.unique)}> + + `, + )} + `; + } + + #renderActions() { + return html` +
+ + +
+ `; + } +} + +export { UmbDashboardAppPickerModalElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'uop-data-configuration-type-picker-modal': UmbDashboardAppPickerModalElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/picker/picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/picker/picker-modal.token.ts new file mode 100644 index 000000000000..70f4117e026c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/picker/picker-modal.token.ts @@ -0,0 +1,19 @@ +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; + +export interface UmbDashboardAppPickerModalData { + multiple?: boolean; +} + +export interface UmbDashboardAppPickerModalValue { + selection: Array; +} + +export const UMB_DASHBOARD_APP_PICKER_MODAL = new UmbModalToken< + UmbDashboardAppPickerModalData, + UmbDashboardAppPickerModalValue +>('Umb.Modal.DashboardAppPicker', { + modal: { + type: 'sidebar', + size: 'small', + }, +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/picker/picker.context.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/picker/picker.context.token.ts new file mode 100644 index 000000000000..965a28d3aedd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/picker/picker.context.token.ts @@ -0,0 +1,6 @@ +import type { UmbDashboardAppPickerContext } from './picker.context.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export const UMB_DASHBOARD_APP_PICKER_CONTEXT = new UmbContextToken( + 'Umb.Modal.DashboardAppPicker', +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/picker/picker.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/picker/picker.context.ts new file mode 100644 index 000000000000..2794dca9c3d0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/picker/picker.context.ts @@ -0,0 +1,28 @@ +import type { UmbDashboardAppDetailModel } from '../types.js'; +import { UmbDashboardAppCollectionRepository } from '../collection.repository.js'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbCollectionItemPickerContext } from '@umbraco-cms/backoffice/collection'; + +export class UmbDashboardAppPickerContext extends UmbCollectionItemPickerContext { + public getUnique = (item: UmbDashboardAppDetailModel) => item.unique; + + #items = new UmbArrayState([], (x) => x.unique); + public items = this.#items.asObservable(); + + #take = 100; + #collectionRepository = new UmbDashboardAppCollectionRepository(this); + + async loadData() { + const { data, error } = await this.#collectionRepository.requestCollection({ + take: this.#take, + }); + + if (error) { + this.#items.setValue([]); + } + + if (data) { + this.#items.setValue(data.items); + } + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/remove/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/remove/manifests.ts new file mode 100644 index 000000000000..d087f11637bd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/remove/manifests.ts @@ -0,0 +1,15 @@ +export const manifests = [ + { + type: 'entityAction', + kind: 'default', + alias: 'Umb.EntityAction.DashboardApp.Remove', + name: 'Remove Dashboard App', + forEntityTypes: ['dashboardApp'], + api: () => import('./remove-dashboard-app.entity-action.js'), + meta: { + label: '#actions_remove', + icon: 'icon-trash', + weight: 100, + }, + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/remove/remove-dashboard-app.entity-action.ts b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/remove/remove-dashboard-app.entity-action.ts new file mode 100644 index 000000000000..71bf178c88f5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/remove/remove-dashboard-app.entity-action.ts @@ -0,0 +1,9 @@ +import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action'; + +export class UmbRemoveDashboardApp extends UmbEntityActionBase { + override async execute() { + alert('Delete Dashboard App'); + } +} + +export { UmbRemoveDashboardApp as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/types.ts new file mode 100644 index 000000000000..f8b145bc32ff --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/app/types.ts @@ -0,0 +1,7 @@ +import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; + +export interface UmbDashboardAppDetailModel extends UmbEntityModel { + name: string; +} + +export type * from './dashboard-app.extension.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/conditions/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/conditions/constants.ts new file mode 100644 index 000000000000..0b6575ac8d6b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/conditions/constants.ts @@ -0,0 +1 @@ +export const UMB_DASHBOARD_ALIAS_CONDITION_ALIAS = 'Umb.Condition.DashboardAlias'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/conditions/dashboard-alias.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/conditions/dashboard-alias.condition.ts new file mode 100644 index 000000000000..248cf9124dca --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/conditions/dashboard-alias.condition.ts @@ -0,0 +1,36 @@ +import { UMB_DASHBOARD_CONTEXT } from '../default/index.js'; +import type { UmbDashboardAliasConditionConfig } from './types.js'; +import { UmbConditionBase } from '@umbraco-cms/backoffice/extension-registry'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { UmbConditionControllerArguments, UmbExtensionCondition } from '@umbraco-cms/backoffice/extension-api'; + +export class UmbDashboardAliasCondition + extends UmbConditionBase + implements UmbExtensionCondition +{ + constructor(host: UmbControllerHost, args: UmbConditionControllerArguments) { + super(host, args); + + let permissionCheck: ((dashboardAlias: string) => boolean) | undefined = undefined; + + if (this.config.match) { + permissionCheck = (dashboardAlias: string) => dashboardAlias === this.config.match; + } else if (this.config.oneOf) { + permissionCheck = (dashboardAlias: string) => this.config.oneOf!.indexOf(dashboardAlias) !== -1; + } + + if (permissionCheck !== undefined) { + this.consumeContext(UMB_DASHBOARD_CONTEXT, (context) => { + this.observe( + context?.alias, + (dashboardAlias) => { + this.permitted = dashboardAlias ? permissionCheck!(dashboardAlias) : false; + }, + 'observeDashboardAlias', + ); + }); + } + } +} + +export { UmbDashboardAliasCondition as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/conditions/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/conditions/index.ts new file mode 100644 index 000000000000..6c63dc4dfe30 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/conditions/index.ts @@ -0,0 +1,2 @@ +export * from './dashboard-alias.condition.js'; +export * from './constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/conditions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/conditions/manifests.ts new file mode 100644 index 000000000000..99b162e2bef1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/conditions/manifests.ts @@ -0,0 +1,8 @@ +export const manifests: Array = [ + { + type: 'condition', + name: 'Dashboard Alias Condition', + alias: 'Umb.Condition.DashboardAlias', + api: () => import('./dashboard-alias.condition.js'), + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/conditions/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/conditions/types.ts new file mode 100644 index 000000000000..4c9eda39e27d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/conditions/types.ts @@ -0,0 +1,21 @@ +import type { UmbConditionConfigBase } from '@umbraco-cms/backoffice/extension-api'; + +export type UmbDashboardAliasConditionConfig = UmbConditionConfigBase<'Umb.Condition.DashboardAlias'> & { + /** + * Define the dashboard that this extension should be available in + * @example "Umb.Dashboard.Content" + */ + match: string; + /** + * Define one or more dashboards that this extension should be available in + * @example + * ["Umb.Dashboard.Content", "Umb.Dashboard.Media"] + */ + oneOf?: Array; +}; + +declare global { + interface UmbExtensionConditionConfigMap { + umbDashboardAliasConditionConfig: UmbDashboardAliasConditionConfig; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/dashboard-element.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/dashboard-element.interface.ts index 1fe6b91a606f..bb6562b3eb11 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/dashboard-element.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/dashboard-element.interface.ts @@ -1,5 +1,6 @@ import type { ManifestDashboard } from './dashboard.extension.js'; +import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; -export interface UmbDashboardElement extends HTMLElement { +export interface UmbDashboardElement extends UmbControllerHostElement { manifest?: ManifestDashboard; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/dashboard.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/dashboard.extension.ts index cf683f1cb56a..ec3ae6700f90 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/dashboard.extension.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/dashboard.extension.ts @@ -1,8 +1,9 @@ import type { UmbDashboardElement } from './dashboard-element.interface.js'; -import type { ManifestElement, ManifestWithDynamicConditions } from '@umbraco-cms/backoffice/extension-api'; +import type { ManifestElementAndApi, ManifestWithDynamicConditions } from '@umbraco-cms/backoffice/extension-api'; +import type { UmbDashboardContext } from './default/dashboard.context.js'; export interface ManifestDashboard - extends ManifestElement, + extends ManifestElementAndApi, ManifestWithDynamicConditions { type: 'dashboard'; meta: MetaDashboard; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/default/dashboard.context.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/default/dashboard.context.token.ts new file mode 100644 index 000000000000..d3d1c28f7a50 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/default/dashboard.context.token.ts @@ -0,0 +1,4 @@ +import type { UmbDashboardContext } from './dashboard.context.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export const UMB_DASHBOARD_CONTEXT = new UmbContextToken('UmbDashboardContext'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/default/dashboard.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/default/dashboard.context.ts new file mode 100644 index 000000000000..28a999c0600c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/default/dashboard.context.ts @@ -0,0 +1,27 @@ +import type { ManifestDashboard } from '../dashboard.extension.js'; +import { UMB_DASHBOARD_CONTEXT } from './dashboard.context.token.js'; +import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; +import { UmbStringState } from '@umbraco-cms/backoffice/observable-api'; + +export class UmbDashboardContext extends UmbContextBase implements UmbApi { + #manifestAlias = new UmbStringState(undefined); + #manifestPathname = new UmbStringState(undefined); + #manifestLabel = new UmbStringState(undefined); + public readonly alias = this.#manifestAlias.asObservable(); + public readonly pathname = this.#manifestPathname.asObservable(); + public readonly label = this.#manifestLabel.asObservable(); + + constructor(host: UmbControllerHost) { + super(host, UMB_DASHBOARD_CONTEXT); + } + + public setManifest(manifest?: ManifestDashboard) { + this.#manifestAlias.setValue(manifest?.alias); + this.#manifestPathname.setValue(manifest?.meta?.pathname); + this.#manifestLabel.setValue(manifest ? manifest.meta?.label || manifest.name : undefined); + } +} + +export { UmbDashboardContext as api }; 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 new file mode 100644 index 000000000000..895016da6b3a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/default/dashboard.element.ts @@ -0,0 +1,425 @@ +import { UMB_DASHBOARD_APP_PICKER_MODAL } from '../app/constants.js'; +import type { ManifestDashboardApp } from '../app/types.js'; +import type { DashboardAppInstance, UserDashboardAppConfiguration } from './types.js'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { css, html, customElement, state, repeat, styleMap, when } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +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 } from '@umbraco-cms/backoffice/external/uui'; +import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; +import { UmbId } from '@umbraco-cms/backoffice/id'; +import { UserDataService } from '@umbraco-cms/backoffice/external/backend-api'; +import { tryExecute } from '@umbraco-cms/backoffice/resources'; +import { UmbEntityContext } from '@umbraco-cms/backoffice/entity'; + +@customElement('umb-dashboard') +export class UmbDashboardElement extends UmbLitElement { + #defaultSize = 'small'; + + #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() + _editMode = false; + + @state() + _apps: Array = []; + + @state() + _appUniques: Array = []; + + @state() + _availableApps: PermittedControllerType< + UmbExtensionElementInitializer + >[] = []; + + _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.unique, + 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, + 'dashboardApp', + ManifestDashboardApp + >( + this, + umbExtensionsRegistry, + 'dashboardApp', + undefined, + (extensionControllers) => { + this._availableApps = extensionControllers; + + this.#load(); + }, + undefined, // We can leave the alias to undefined, as we destroy this our selfs. + ); + } + + async #drawDashboardApps(config: UserDashboardAppConfiguration) { + const apps = [...this._apps]; + + if (config.apps.length == 0) { + //TODO: Draw all? + } else { + for (const configuredApp of config.apps) { + const 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; + + const gridSize = this.#gridSizeMap.get(size)!; + + const elementPropsValue = controller.manifest.element ?? controller.manifest.js; + const appElementCtor = await loadManifestElement(elementPropsValue!)!; //TODO: Validate + + if (!appElementCtor) return; + + apps.push({ + unique: 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: [], + }, + }).catch(() => undefined); + + if (value) { + const apps = [...this._apps]; + + for (const alias of value.selection) { + const 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; + + const gridSize = this.#gridSizeMap.get(size)!; + + const elementPropsValue = controller.manifest.element ?? controller.manifest.js; + const appElementCtor = await loadManifestElement(elementPropsValue!)!; //TODO: Validate + + if (!appElementCtor) return; + + apps.push({ + unique: UmbId.new(), + alias: controller.alias, + columns: gridSize.columns, + rows: gridSize?.rows, + headline: headline, + component: new appElementCtor(), + }); + } + + this._apps = apps; + + this.#save(); + } + } + + async #save() { + const config = { + apps: this._apps.map((x) => { + return { alias: x.alias }; + }), + } as UserDashboardAppConfiguration; + + if (this._userDataKey) { + await tryExecute( + this, + UserDataService.putUserData({ + body: { + key: this._userDataKey, + group: 'test', + identifier: '', + value: JSON.stringify(config), + }, + }), + ); + } else { + await tryExecute( + this, + UserDataService.postUserData({ + body: { + group: 'test', + identifier: '', + value: JSON.stringify(config), + }, + }), + ); + } + } + + async #load() { + const res = await tryExecute( + this, + 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.unique != elementKey); + } + + override render() { + return html` +
+
+ ${when( + this._editMode, + () => + html`Done `, + () => + html` + + + + + + + + + + + + + + `, + )} +
+
+ ${repeat( + this._apps, + (element) => element.unique, + (element) => + html`
+ ${when( + this._editMode, + () => + html` this.#remove(element.unique)} + >`, + )} + ${this.#renderComponent(element)} +
`, + )} +
+
+ `; + } + + #renderComponent(element: DashboardAppInstance) { + // TODO: Hacky rendering of component and entity context + const component = element.component; + if (!component) throw new Error('Dashboard app component is not defined'); + const entityContext = new UmbEntityContext(component); + entityContext.setEntityType('dashboardApp'); + entityContext.setUnique(element.unique); + return html`${element.component}`; + } + + 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; + } + + 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; + 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%; + + &::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; + } + + & > uui-button { + position: absolute; + top: 0; + right: 0; + z-index: 1; + } + + &[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; + } + } + } + `, + ]; +} + +export { UmbDashboardElement as element }; + +declare global { + interface HTMLElementTagNameMap { + ['umb-dashboard']: UmbDashboardElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/default/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/default/index.ts new file mode 100644 index 000000000000..75b83ef72d33 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/default/index.ts @@ -0,0 +1 @@ +export * from './dashboard.context.token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/default/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/default/manifests.ts new file mode 100644 index 000000000000..0d274813ba21 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/default/manifests.ts @@ -0,0 +1,15 @@ +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array = [ + { + type: 'kind', + alias: 'Umb.Kind.Dashboard.Default', + matchKind: 'default', + matchType: 'dashboard', + manifest: { + type: 'dashboard', + element: () => import('./dashboard.element.js'), + api: () => import('./dashboard.context.js'), + }, + }, +]; 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 new file mode 100644 index 000000000000..e16d3474eba8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/default/types.ts @@ -0,0 +1,39 @@ +import type { ManifestDashboard, MetaDashboard } from '../dashboard.extension.js'; +import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; + +export interface ManifestDashboardDefaultKind extends ManifestDashboard { + type: 'dashboard'; + kind: 'default'; +} + +export interface MetaDashboardDefaultKind extends MetaDashboard { + headline: string; +} + +export interface DashboardAppInstance { + unique: string; + /** Dashboard App Alias */ + alias: string; + rows?: number; + columns?: number; + headline?: string; + component?: UmbControllerHostElement; +} + +/** + * 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/core/dashboard/global-components.ts b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/global-components.ts new file mode 100644 index 000000000000..8ec547ca72de --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/global-components.ts @@ -0,0 +1 @@ +import './app/global-components/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/index.ts index 0a1fc5bbf79b..14a2f7c8448f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/index.ts @@ -1,3 +1,7 @@ -export type * from './dashboard.extension.js'; -export type * from './dashboard-element.interface.js'; +export * from './app/index.js'; +export * from './conditions/index.js'; export * from './paths.js'; + +export type * from './app/types.js'; +export type * from './dashboard-element.interface.js'; +export type * from './dashboard.extension.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/manifests.ts new file mode 100644 index 000000000000..9979eee3b865 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/dashboard/manifests.ts @@ -0,0 +1,10 @@ +import type { UmbExtensionManifestKind } from '../extension-registry/registry.js'; +import { manifests as appManifests } from './app/manifests.js'; +import { manifests as conditionManifests } from './conditions/manifests.js'; +import { manifests as defaultManifests } from './default/manifests.js'; + +export const manifests: Array = [ + ...appManifests, + ...conditionManifests, + ...defaultManifests, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entry-point.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entry-point.ts index 0a23247e5ac7..ad376aa2c4b5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entry-point.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entry-point.ts @@ -10,6 +10,7 @@ import './property-action/components/index.js'; import './menu/components/index.js'; import './extension-registry/components/index.js'; import './entity-item/global-components.js'; +import './dashboard/global-components.js'; export const onInit: UmbEntryPointOnInit = (host, extensionRegistry) => { new UmbExtensionsApiInitializer(host, extensionRegistry, 'globalContext', [host]); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/manifests.ts index 1e046ec8184b..2cd709eddf3c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/manifests.ts @@ -1,6 +1,7 @@ import { manifests as authManifests } from './auth/manifests.js'; import { manifests as collectionManifests } from './collection/manifests.js'; import { manifests as cultureManifests } from './culture/manifests.js'; +import { manifests as dashboardManifests } from './dashboard/manifests.js'; import { manifests as debugManifests } from './debug/manifests.js'; import { manifests as entityActionManifests } from './entity-action/manifests.js'; import { manifests as entityBulkActionManifests } from './entity-bulk-action/manifests.js'; @@ -21,6 +22,7 @@ import { manifests as temporaryFileManifests } from './temporary-file/manifests. import { manifests as themeManifests } from './themes/manifests.js'; import { manifests as treeManifests } from './tree/manifests.js'; import { manifests as workspaceManifests } from './workspace/manifests.js'; +import { manifests as tempLocationManifests } from './_temp-poc-location/manifests.js'; import type { UmbExtensionManifestKind } from './extension-registry/index.js'; @@ -28,6 +30,7 @@ export const manifests: Array = ...authManifests, ...collectionManifests, ...cultureManifests, + ...dashboardManifests, ...debugManifests, ...entityActionManifests, ...entityBulkActionManifests, @@ -48,4 +51,5 @@ export const manifests: Array = ...themeManifests, ...treeManifests, ...workspaceManifests, + ...tempLocationManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/section/section-main-views/section-main-views.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/section/section-main-views/section-main-views.element.ts index 685fff6ed0d6..ef29a62852e1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/section/section-main-views/section-main-views.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/section/section-main-views/section-main-views.element.ts @@ -63,7 +63,7 @@ export class UmbSectionMainViewElement extends UmbLitElement { setup: (component: UmbDashboardElement) => { component.manifest = manifest; }, - } as UmbRoute; + } as unknown as UmbRoute; }); const viewRoutes = this._views?.map((manifest) => { diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/dashboard/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/members/dashboard/constants.ts new file mode 100644 index 000000000000..ac760e8a25a0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/dashboard/constants.ts @@ -0,0 +1 @@ +export const UMB_MEMBER_MANAGEMENT_DASHBOARD_ALIAS = 'Umb.Dashboard.MemberManagement.Overview'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/dashboard/index.ts b/src/Umbraco.Web.UI.Client/src/packages/members/dashboard/index.ts new file mode 100644 index 000000000000..4f07201dcf0a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/dashboard/index.ts @@ -0,0 +1 @@ +export * from './constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/dashboard/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/members/dashboard/manifests.ts new file mode 100644 index 000000000000..9b4df5a1342e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/dashboard/manifests.ts @@ -0,0 +1,22 @@ +import { UMB_MEMBER_MANAGEMENT_SECTION_ALIAS } from '../section/index.js'; +import { UMB_MEMBER_MANAGEMENT_DASHBOARD_ALIAS } from './constants.js'; + +export const manifests: Array = [ + { + type: 'dashboard', + kind: 'default', + alias: UMB_MEMBER_MANAGEMENT_DASHBOARD_ALIAS, + name: 'Member Management Overview Dashboard', + weight: 1000, + meta: { + label: 'Overview', + pathname: 'overview', + }, + conditions: [ + { + alias: 'Umb.Condition.SectionAlias', + match: UMB_MEMBER_MANAGEMENT_SECTION_ALIAS, + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/members/manifests.ts index 54398e8e590b..6586019f1682 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/manifests.ts @@ -3,6 +3,7 @@ import { manifests as memberManifests } from './member/manifests.js'; import { manifests as memberPublicAccessManifests } from './member-public-access/manifests.js'; import { manifests as memberTypeManifests } from './member-type/manifests.js'; import { manifests as sectionManifests } from './section/manifests.js'; +import { manifests as dashboardManifests } from './dashboard/manifests.js'; import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; @@ -15,4 +16,5 @@ export const manifests: Array = ...memberPublicAccessManifests, ...memberTypeManifests, ...sectionManifests, + ...dashboardManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/dashboard/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/user/dashboard/constants.ts new file mode 100644 index 000000000000..6f8a7964de4d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/dashboard/constants.ts @@ -0,0 +1 @@ +export const UMB_USER_MANAGEMENT_DASHBOARD_ALIAS = 'Umb.Dashboard.UserManagement.Overview'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/dashboard/index.ts b/src/Umbraco.Web.UI.Client/src/packages/user/dashboard/index.ts new file mode 100644 index 000000000000..4f07201dcf0a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/dashboard/index.ts @@ -0,0 +1 @@ +export * from './constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/dashboard/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/dashboard/manifests.ts new file mode 100644 index 000000000000..7eac25f42192 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/dashboard/manifests.ts @@ -0,0 +1,21 @@ +import { UMB_USER_MANAGEMENT_SECTION_ALIAS } from '../section/index.js'; + +export const manifests: Array = [ + { + type: 'dashboard', + kind: 'default', + alias: 'Umb.Dashboard.UserManagement.Overview', + name: 'User Management Overview Dashboard', + weight: 1000, + meta: { + label: 'Overview', + pathname: 'overview', + }, + conditions: [ + { + alias: 'Umb.Condition.SectionAlias', + match: UMB_USER_MANAGEMENT_SECTION_ALIAS, + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/manifests.ts index d649d8f06ff4..b232cb11d6e3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/manifests.ts @@ -1,9 +1,10 @@ +import { manifests as changePasswordManifests } from './change-password/manifests.js'; +import { manifests as currentUserManifests } from './current-user/manifests.js'; +import { manifests as dashboardManifests } from './dashboard/manifests.js'; import { manifests as userGroupManifests } from './user-group/manifests.js'; import { manifests as userManifests } from './user/manifests.js'; -import { manifests as userSectionManifests } from './section/manifests.js'; -import { manifests as currentUserManifests } from './current-user/manifests.js'; import { manifests as userPermissionManifests } from './user-permission/manifests.js'; -import { manifests as changePasswordManifests } from './change-password/manifests.js'; +import { manifests as userSectionManifests } from './section/manifests.js'; // We need to load any components that are not loaded by the user management bundle to register them in the browser. import './user-group/components/index.js'; @@ -11,10 +12,11 @@ import './user-permission/components/index.js'; import './user/components/index.js'; export const manifests = [ + ...changePasswordManifests, + ...currentUserManifests, + ...dashboardManifests, ...userGroupManifests, ...userManifests, - ...userSectionManifests, - ...currentUserManifests, ...userPermissionManifests, - ...changePasswordManifests, + ...userSectionManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/invite/dashboard-app/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/invite/dashboard-app/manifests.ts new file mode 100644 index 000000000000..b775ccf88e86 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/invite/dashboard-app/manifests.ts @@ -0,0 +1,24 @@ +//import { UMB_USER_MANAGEMENT_DASHBOARD_ALIAS } from '../../../dashboard/index.js'; +//import { UMB_DASHBOARD_ALIAS_CONDITION_ALIAS } from '@umbraco-cms/backoffice/dashboard'; + +export const manifests: Array = [ + { + type: 'dashboardApp', + alias: 'Umb.DashboardApp.User.PendingInvites', + name: 'Pending User Invites Dashboard App', + weight: 300, + element: () => import('./pending-user-invites-dashboard-app.element.js'), + meta: { + headline: 'Pending Invites', + size: 'medium', + }, + /* + conditions: [ + { + alias: UMB_DASHBOARD_ALIAS_CONDITION_ALIAS, + match: UMB_USER_MANAGEMENT_DASHBOARD_ALIAS, + }, + ], + */ + }, +]; 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 new file mode 100644 index 000000000000..ce311f5fef08 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/invite/dashboard-app/pending-user-invites-dashboard-app.element.ts @@ -0,0 +1,70 @@ +import { UmbUserStateFilter } from '../../collection/utils/index.js'; +import { UmbUserCollectionRepository } from '../../collection/index.js'; +import type { UmbUserDetailModel } from '../../types.js'; +import { UMB_USER_WORKSPACE_PATH } from '../../paths.js'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { css, html, customElement, state, property } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { + ManifestDashboardApp, + UmbDashboardAppElement, + UmbDashboardAppSize, +} from '@umbraco-cms/backoffice/dashboard'; + +@customElement('umb-pending-user-invites-dashboard-app') +export class UmbPendingUserInvitesDashboardAppElement extends UmbLitElement implements UmbDashboardAppElement { + @property({ type: Object }) + manifest?: ManifestDashboardApp; + + @property({ type: String }) + size?: UmbDashboardAppSize; + + #userCollectionRepository = new UmbUserCollectionRepository(this); + + @state() + private _pendingUserInvites: UmbUserDetailModel[] = []; + + protected override firstUpdated(): void { + this.#loadInvitedUsers(); + } + + async #loadInvitedUsers() { + const { data } = await this.#userCollectionRepository.requestCollection({ + take: 5, + userStates: [UmbUserStateFilter.INVITED], + }); + this._pendingUserInvites = data?.items ?? []; + } + + override render() { + return html` + + ${this._pendingUserInvites.map( + (user) => html` + + + + + + + `, + )} + + `; + } + + static override styles = [UmbTextStyles, css``]; +} + +export { UmbPendingUserInvitesDashboardAppElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'umb-pending-user-invites-dashboard-app': UmbPendingUserInvitesDashboardAppElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/invite/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/invite/manifests.ts index f92c72811660..67c74a67f1aa 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/invite/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/invite/manifests.ts @@ -1,12 +1,14 @@ import { manifests as collectionActionManifests } from './collection-action/manifests.js'; +import { manifests as dashboardAppManifests } from './dashboard-app/manifests.js'; +import { manifests as entityActionManifests } from './entity-action/manifests.js'; import { manifests as modalManifests } from './modal/manifests.js'; import { manifests as repositoryManifests } from './repository/manifests.js'; -import { manifests as entityActionManifests } from './entity-action/manifests.js'; import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; export const manifests: Array = [ ...collectionActionManifests, + ...dashboardAppManifests, + ...entityActionManifests, ...modalManifests, ...repositoryManifests, - ...entityActionManifests, ];