Merge remote-tracking branch 'origin/feature/header-apps' into feature/user-dialog

# Conflicts:
#	src/backoffice/components/backoffice-header-tools.element.ts
#	src/core/extensions-registry/models.ts
This commit is contained in:
Niels Lyngsø
2022-12-14 12:31:28 +01:00
19 changed files with 307 additions and 99 deletions

View File

@@ -29,7 +29,7 @@ import { umbExtensionsRegistry } from '../src/core/extensions-registry';
import '../src/core/context-api/provide/context-provider.element';
import '../src/core/css/custom-properties.css';
import '../src/backoffice/components/backoffice-modal-container.element';
import '../src/backoffice/components/backoffice-frame/backoffice-modal-container.element';
import '../src/backoffice/components/shared/code-block.element';
class UmbStoryBookElement extends LitElement {

View File

@@ -1,13 +1,14 @@
//TODO: we need to figure out what components should be available for extensions and load them upfront
import './editors/shared/editor-entity-layout/editor-entity-layout.element';
import './components/ref-property-editor-ui/ref-property-editor-ui.element';
import './components/backoffice-header.element';
import './components/backoffice-main.element';
import './components/backoffice-modal-container.element';
import './components/backoffice-notification-container.element';
import './components/backoffice-frame/backoffice-header.element';
import './components/backoffice-frame/backoffice-main.element';
import './components/backoffice-frame/backoffice-modal-container.element';
import './components/backoffice-frame/backoffice-notification-container.element';
import './components/node-property/node-property.element';
import './components/table/table.element';
import './components/shared/code-block.element';
import './components/extension-slot/extension-slot.element';
import './sections/shared/section-main/section-main.element';
import './sections/shared/section-sidebar/section-sidebar.element';
import './sections/shared/section.element';

View File

@@ -0,0 +1,84 @@
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, CSSResultGroup, html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import { ManifestWithLoader, umbExtensionsRegistry } from '@umbraco-cms/extensions-registry';
import { ManifestHeaderApp } from 'src/core/extensions-registry/header-app.models';
@customElement('umb-backoffice-header-apps')
export class UmbBackofficeHeaderApps extends LitElement {
static styles: CSSResultGroup = [
UUITextStyles,
css`
#apps {
display: flex;
align-items: center;
gap: var(--uui-size-space-2);
}
`,
];
constructor() {
super();
this._registerHeaderApps();
}
private _registerHeaderApps() {
const headerApps: Array<ManifestWithLoader<ManifestHeaderApp>> = [
{
type: 'headerApp',
alias: 'Umb.HeaderApp.Search',
name: 'Header App Search',
loader: () => import('../../header-apps/header-app-button.element'),
weight: 10,
meta: {
label: 'Search',
icon: 'search',
pathname: 'search',
},
},
{
type: 'headerApp',
alias: 'Umb.HeaderApp.Favorites',
name: 'Header App Favorites',
loader: () => import('../../header-apps/header-app-button.element'),
weight: 100,
meta: {
label: 'Favorites',
icon: 'favorite',
pathname: 'favorites',
},
},
{
type: 'headerApp',
alias: 'Umb.HeaderApp.CurrentUser',
name: 'Current User',
loader: () => import('../../header-apps/header-app-current-user.element'),
weight: 1000,
meta: {
label: 'TODO: how should we enable this to not be set.',
icon: 'TODO: how should we enable this to not be set.',
pathname: 'user',
},
},
];
// TODO: Can we make this functionality reuseable...
headerApps.forEach((headerApp) => {
if (umbExtensionsRegistry.isRegistered(headerApp.alias)) return;
umbExtensionsRegistry.register(headerApp);
});
}
render() {
return html`
<umb-extension-slot id="apps" type="headerApp"></umb-extension-slot>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'umb-backoffice-header-apps': UmbBackofficeHeaderApps;
}
}

View File

@@ -2,7 +2,7 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, CSSResultGroup, html, LitElement } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { when } from 'lit/directives/when.js';
import { UmbSectionStore } from '../../core/stores/section.store';
import { UmbSectionStore } from '../../../core/stores/section.store';
import { UmbObserverMixin } from '@umbraco-cms/observable-api';
import { UmbContextConsumerMixin, UmbContextProviderMixin } from '@umbraco-cms/context-api';
import type { ManifestSection } from '@umbraco-cms/models';

View File

@@ -3,7 +3,7 @@ import { css, CSSResultGroup, html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import './backoffice-header-sections.element';
import './backoffice-header-tools.element';
import './backoffice-header-apps.element';
@customElement('umb-backoffice-header')
export class UmbBackofficeHeader extends LitElement {
@@ -47,7 +47,7 @@ export class UmbBackofficeHeader extends LitElement {
</uui-button>
<umb-backoffice-header-sections id="sections"></umb-backoffice-header-sections>
<umb-backoffice-header-tools></umb-backoffice-header-tools>
<umb-backoffice-header-apps></umb-backoffice-header-apps>
</div>
`;
}

View File

@@ -3,8 +3,8 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { state } from 'lit/decorators.js';
import { IRoutingInfo } from 'router-slot';
import { UmbSectionStore } from '../../core/stores/section.store';
import { UmbSectionContext } from '../sections/section.context';
import { UmbSectionStore } from '../../../core/stores/section.store';
import { UmbSectionContext } from '../../sections/section.context';
import { UmbObserverMixin } from '@umbraco-cms/observable-api';
import { createExtensionElement } from '@umbraco-cms/extensions-api';
import { UmbContextConsumerMixin, UmbContextProviderMixin } from '@umbraco-cms/context-api';

View File

@@ -2,7 +2,7 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, CSSResultGroup, html, LitElement } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import { UmbModalHandler, UmbModalService } from '../../core/services/modal';
import { UmbModalHandler, UmbModalService } from '@umbraco-cms/services';
import { UmbObserverMixin } from '@umbraco-cms/observable-api';
import { UmbContextConsumerMixin } from '@umbraco-cms/context-api';

View File

@@ -2,7 +2,8 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, CSSResultGroup, html, LitElement } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import type { UmbNotificationService, UmbNotificationHandler } from '../../core/services/notification';
import type { UmbNotificationHandler } from '../../../core/services/notification';
import type { UmbNotificationService } from '@umbraco-cms/services';
import { UmbObserverMixin } from '@umbraco-cms/observable-api';
import { UmbContextConsumerMixin } from '@umbraco-cms/context-api';

View File

@@ -1,71 +0,0 @@
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, CSSResultGroup, html, LitElement } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { UmbContextConsumerMixin } from '@umbraco-cms/context-api';
import type { UserDetails } from '@umbraco-cms/models';
import { UmbObserverMixin } from '@umbraco-cms/observable-api';
import { UmbModalService } from '@umbraco-cms/services';
import { umbCurrentUserService } from 'src/core/services/current-user';
@customElement('umb-backoffice-header-tools')
export class UmbBackofficeHeaderTools extends UmbContextConsumerMixin(UmbObserverMixin(LitElement)) {
static styles: CSSResultGroup = [
UUITextStyles,
css`
#tools {
display: flex;
align-items: center;
gap: var(--uui-size-space-2);
}
.tool {
font-size: 18px;
}
`,
];
@state()
private _currentUser?: UserDetails;
private _modalService?: UmbModalService;
constructor() {
super();
this.consumeAllContexts(['umbUserStore', 'umbModalService'], (instances) => {
this._modalService = instances['umbModalService'];
this._observeCurrentUser();
});
}
private async _observeCurrentUser() {
this.observe<UserDetails>(umbCurrentUserService.currentUser, (currentUser) => {
this._currentUser = currentUser;
});
}
private _handleUserClick() {
this._modalService?.userSettings();
}
render() {
return html`
<div id="tools">
<uui-button class="tool" look="primary" label="Search" compact>
<uui-icon name="search"></uui-icon>
</uui-button>
<uui-button class="tool" look="primary" label="Help" compact>
<uui-icon name="favorite"></uui-icon>
</uui-button>
<uui-button @click=${this._handleUserClick} look="primary" style="font-size: 14px;" label="User" compact>
<uui-avatar name="${this._currentUser?.name || ''}"></uui-avatar>
</uui-button>
</div>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'umb-backoffice-header-tools': UmbBackofficeHeaderTools;
}
}

View File

@@ -0,0 +1,97 @@
import { html, LitElement, nothing } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { map } from 'rxjs';
import { repeat } from 'lit/directives/repeat.js';
import { ManifestTypes, umbExtensionsRegistry } from '@umbraco-cms/extensions-registry';
import { UmbObserverMixin } from '@umbraco-cms/observable-api';
import { createExtensionElement } from '@umbraco-cms/extensions-api';
type InitializedExtensionItem = {alias: string, weight: number, component: HTMLElement|null}
/**
* @element umb-extension-slot
* @description
* @slot default - slot for inserting additional things into this slot.
* @export
* @class UmbExtensionSlot
* @extends {UmbObserverMixin(LitElement)}
*/
@customElement('umb-extension-slot')
export class UmbExtensionSlotElement extends UmbObserverMixin(LitElement) {
@state()
private _extensions:InitializedExtensionItem[] = [];
@property({ type: String })
public type= "";
@property({ type: Object, attribute: false })
public filter: (manifest:ManifestTypes) => boolean = () => true;
constructor() {
super();
/*
this.extensionManager = new ExtensionManager(this, (x) => {x.meta.entityType === this.entityType}, (extensionManifests) => {
this._createElement(extensionManifests[0]);
});
*/
}
connectedCallback(): void {
super.connectedCallback();
this._observeExtensions();
}
private _observeExtensions() {
this.observe(
umbExtensionsRegistry
?.extensionsOfType(this.type)
.pipe(map((extensions) => extensions.filter(this.filter))),
async (extensions: ManifestTypes[]) => {
const oldLength = this._extensions.length;
this._extensions = this._extensions.filter(current => extensions.find(incoming => incoming.alias === current.alias));
if(this._extensions.length !== oldLength) {
this.requestUpdate('_extensions');
}
extensions.forEach(async (extension: ManifestTypes) => {
const hasExt = this._extensions.find(x => x.alias === extension.alias);
if(!hasExt) {
const extensionObject:InitializedExtensionItem = {alias: extension.alias, weight: (extension as any).weight || 0, component: null};
this._extensions.push(extensionObject);
const component = await createExtensionElement(extension);
if(component) {
(component as any).manifest = extension;
extensionObject.component = component;
// sort:
// TODO: Make sure its right to have highest last?
this._extensions.sort((a, b) => a.weight - b.weight);
} else {
// Remove cause we could not get the component, so we will get rid of this.
//this._extensions.splice(this._extensions.indexOf(extensionObject), 1);
// Actually not, because if, then the same extension would come around again in next update.
}
this.requestUpdate('_extensions');
}
});
}
);
}
render() {
// TODO: check if we can use repeat directly.
return repeat(this._extensions, (ext) => ext.alias, (ext, index) => ext.component || nothing);
}
}
declare global {
interface HTMLElementTagNameMap {
'umb-extension-slot': UmbExtensionSlotElement;
}
}

View File

@@ -32,6 +32,7 @@ export class UmbEditorExtensionsElement extends UmbContextConsumerMixin(UmbObser
<uui-table-head-cell>Type</uui-table-head-cell>
<uui-table-head-cell>Name</uui-table-head-cell>
<uui-table-head-cell>Alias</uui-table-head-cell>
<uui-table-head-cell>Actions</uui-table-head-cell>
</uui-table-head>
${this._extensions.map(
@@ -42,6 +43,7 @@ export class UmbEditorExtensionsElement extends UmbContextConsumerMixin(UmbObser
${isManifestElementType(extension) ? extension.name : 'Custom extension'}
</uui-table-cell>
<uui-table-cell>${extension.alias}</uui-table-cell>
<uui-table-cell><uui-button label="unload" @click=${() => umbExtensionsRegistry.unregister(extension.alias)}></uui-button></uui-table-cell>
</uui-table-row>
`
)}

View File

@@ -0,0 +1,36 @@
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, CSSResultGroup, html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { ManifestHeaderApp } from '@umbraco-cms/extensions-registry';
@customElement('umb-header-app-button')
export class UmbHeaderAppButton extends LitElement {
static styles: CSSResultGroup = [
UUITextStyles,
css`
uui-button {
font-size: 18px;
}
`,
];
public manifest?: ManifestHeaderApp;
render() {
return html`
<uui-button look="primary" label="${ifDefined(this.manifest?.meta.label)}" compact>
<uui-icon name="${ifDefined(this.manifest?.meta.icon)}"></uui-icon>
</uui-button>
`;
}
}
export default UmbHeaderAppButton;
declare global {
interface HTMLElementTagNameMap {
'umb-header-app-button': UmbHeaderAppButton;
}
}

View File

@@ -0,0 +1,33 @@
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, CSSResultGroup, html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
@customElement('umb-header-app-current-user')
export class UmbHeaderAppCurrentUser extends LitElement {
static styles: CSSResultGroup = [
UUITextStyles,
css`
uui-button {
font-size: 14px;
}
`,
];
// TODO: Get current user information.
render() {
return html`
<uui-button look="primary" label="My User Name" compact>
<uui-avatar name="Extended Rabbit"></uui-avatar>
</uui-button>
`;
}
}
export default UmbHeaderAppCurrentUser;
declare global {
interface HTMLElementTagNameMap {
'umb-header-app-current-user': UmbHeaderAppCurrentUser;
}
}

View File

@@ -1,10 +1,10 @@
import { Meta, Story } from '@storybook/web-components';
import { html } from 'lit-html';
import { UmbModalService } from '../../../core/services/modal';
import type { UmbPropertyEditorUIContentPickerElement } from './property-editor-ui-content-picker.element';
import './property-editor-ui-content-picker.element';
import { UmbModalService } from '../../../core/services/modal';
import '../../components/backoffice-modal-container.element';
import '../../components/backoffice-frame/backoffice-modal-container.element';
export default {
title: 'Property Editor UIs/Content Picker',

View File

@@ -19,6 +19,10 @@ export async function createExtensionElement(manifest: ManifestTypes): Promise<H
return new js.default();
}
if(!Object.getOwnPropertyDescriptor(manifest, 'element')) {
console.error('-- Extension did not succeed creating an element, missing the manifest `element` or default export', manifest);
}
// If some JS was loaded and it did not at least contain a default export, then we are safe to assume that it executed as a module and does not need to be returned
return undefined;
}

View File

@@ -18,6 +18,7 @@ import type {
ManifestExternalLoginProvider,
} from '../../models';
import { createExtensionElement } from '../create-extension-element.function';
import { ManifestHeaderApp } from 'src/core/extensions-registry/header-app.models';
export class UmbExtensionRegistry {
private _extensions = new BehaviorSubject<Array<ManifestTypes>>([]);
@@ -40,6 +41,19 @@ export class UmbExtensionRegistry {
}
}
unregister(alias:string): void {
const oldExtensionsValues = this._extensions.getValue();
const newExtensionsValues = oldExtensionsValues.filter((extension) => extension.alias !== alias);
// TODO: Maybe its not needed to fire an console.error. as you might want to call this method without needing to check the existence first.
if (oldExtensionsValues.length === newExtensionsValues.length) {
console.error(`Unable to unregister extension with alias ${alias}`);
return;
}
this._extensions.next(newExtensionsValues);
}
isRegistered(alias: string): boolean {
const values = this._extensions.getValue();
return values.some((ext) => ext.alias === alias);
@@ -54,6 +68,7 @@ export class UmbExtensionRegistry {
// TODO: implement unregister of extension
// Typings concept, need to put all core types to get a good array return type for the provided type...
extensionsOfType(type: 'headerApp'): Observable<Array<ManifestHeaderApp>>;
extensionsOfType(type: 'section'): Observable<Array<ManifestSection>>;
extensionsOfType(type: 'sectionView'): Observable<Array<ManifestSectionView>>;
extensionsOfType(type: 'tree'): Observable<Array<ManifestTree>>;
@@ -75,9 +90,7 @@ export class UmbExtensionRegistry {
map((exts) =>
exts
.filter((ext) => ext.type === type)
.sort((a, b) => {
return (b.weight || 0) - (a.weight || 0);
})
.sort((a, b) => (b.weight || 0) - (a.weight || 0))
)
);
}

View File

@@ -0,0 +1,12 @@
import type { ManifestElement } from './models';
export interface ManifestHeaderApp extends ManifestElement {
type: 'headerApp';
meta: MetaHeaderApp;
}
export interface MetaHeaderApp {
pathname: string;
label: string;
icon: string;
}

View File

@@ -9,9 +9,9 @@ import type { ManifestPropertyEditorUI, ManifestPropertyEditorModel } from './pr
import type { ManifestDashboard } from './dashboard.models';
import type { ManifestPropertyAction } from './property-action.models';
import type { ManifestPackageView } from './package-view.models';
import type { ManifestExternalLoginProvider } from './external-login-provider.models';
import { ManifestUserDashboard } from './user-dashboard.models';
import type { ManifestHeaderApp } from './header-app.models';
export * from './header-app.models';
export * from './section.models';
export * from './section-view.models';
export * from './tree.models';
@@ -23,10 +23,9 @@ export * from './property-editor.models';
export * from './dashboard.models';
export * from './property-action.models';
export * from './package-view.models';
export * from './external-login-provider.models';
export * from './user-dashboard.models';
export type ManifestTypes =
| ManifestHeaderApp
| ManifestSection
| ManifestSectionView
| ManifestTree
@@ -40,11 +39,10 @@ export type ManifestTypes =
| ManifestPropertyAction
| ManifestPackageView
| ManifestEntrypoint
| ManifestCustom
| ManifestExternalLoginProvider
| ManifestUserDashboard;
| ManifestCustom;
export type ManifestStandardTypes =
| 'headerApp'
| 'section'
| 'sectionView'
| 'tree'
@@ -57,9 +55,7 @@ export type ManifestStandardTypes =
| 'dashboard'
| 'propertyAction'
| 'packageView'
| 'entrypoint'
| 'externalLoginProvider'
| 'userDashboard';
| 'entrypoint';
export type ManifestElementType =
| ManifestSection

View File

@@ -1,4 +1,4 @@
import '../../../backoffice/components/backoffice-notification-container.element';
import '../../../backoffice/components/backoffice-frame/backoffice-notification-container.element';
import '../../context-api/provide/context-provider.element';
import './layouts/default';