Squashed commit of the following:

commit d7bdb05df56ebd7f2f3b64d2a9c71fd105ea534f
Author: Niels Lyngsø <niels.lyngso@gmail.com>
Date:   Thu Jun 2 09:36:53 2022 +0200

    rename event

commit 0fa096d3f1a971672e2170f199046365f433c0d0
Author: Niels Lyngsø <niels.lyngso@gmail.com>
Date:   Thu Jun 2 09:34:09 2022 +0200

    rename datatype event

commit ba92bdd11410b4e4beef167306e4512a75e4dc69
Author: Mads Rasmussen <madsr@hey.com>
Date:   Thu Jun 2 08:49:14 2022 +0200

    fix width of router slot

commit 41960b8e6ac74cd616fec5d6871710aa60f31adc
Author: Mads Rasmussen <madsr@hey.com>
Date:   Wed Jun 1 22:01:19 2022 +0200

    clean up

commit 7dcde3153d69b05cf5560519c8cb8ca62911b777
Author: Mads Rasmussen <madsr@hey.com>
Date:   Wed Jun 1 21:44:50 2022 +0200

    format

commit 7645687c6f81a9c83449c6396a1741d55a795b49
Author: Mads Rasmussen <madsr@hey.com>
Date:   Wed Jun 1 21:42:43 2022 +0200

    lazy load routes

commit a439164c53ad687c2ddab83d15a79a3009f6dbfb
Author: Mads Rasmussen <madsr@hey.com>
Date:   Wed Jun 1 19:06:13 2022 +0200

    use createExtensionElement in router

commit e851bca7fe02f14b9d60680b4de536caba924cc2
Merge: e6435d8 6779bd3
Author: Mads Rasmussen <madsr@hey.com>
Date:   Wed Jun 1 16:55:29 2022 +0200

    Merge branch 'main' into feature/try-router-slot

commit e6435d86034ba0daa91b92dddc56c33b26e649b1
Author: Mads Rasmussen <madsr@hey.com>
Date:   Wed Jun 1 16:40:31 2022 +0200

    remove section context

commit e0e63777069a8c4222f1fd3d0868bb68601ce929
Author: Mads Rasmussen <madsr@hey.com>
Date:   Wed Jun 1 16:17:39 2022 +0200

    Remove unused code

commit 70c46bbfe5d26b50f449de3161f87605bd4e64d2
Author: Mads Rasmussen <madsr@hey.com>
Date:   Wed Jun 1 16:14:37 2022 +0200

    add missing import

commit 42bf326e3ca9921d8c22e59d9c7bbda8b7954fc9
Author: Mads Rasmussen <madsr@hey.com>
Date:   Wed Jun 1 16:13:31 2022 +0200

    wire update current state + redirects

commit 3b85abcc97481f3c3a1b9b7def85e4fd7577975e
Author: Mads Rasmussen <madsr@hey.com>
Date:   Wed Jun 1 15:00:44 2022 +0200

    set current section

commit 78a13a217252a45bd625ff9a53071069d9906c0b
Author: Mads Rasmussen <madsr@hey.com>
Date:   Wed Jun 1 14:46:42 2022 +0200

    remove unused router

commit 0d0ca217f5214d08d039c12fbbbb131c99c73101
Author: Mads Rasmussen <madsr@hey.com>
Date:   Wed Jun 1 14:43:15 2022 +0200

    wire up href on tree item with router-slot

commit ea1676e8fcf9897aa632999353dd1ccd4aeca2cf
Author: Mads Rasmussen <madsr@hey.com>
Date:   Wed Jun 1 14:15:50 2022 +0200

    setup root routing logic with router-slot

commit d3751118b0b67c5607d30ca9276b20b09381992a
Author: Mads Rasmussen <madsr@hey.com>
Date:   Wed Jun 1 13:56:19 2022 +0200

    setup up content nodes with router-slot

commit 81875b33544b05c68197772a421d4e6ba75c5aca
Author: Mads Rasmussen <madsr@hey.com>
Date:   Wed Jun 1 13:45:27 2022 +0200

    remove unused

commit c53ee6ee7eab6d5a21b2e5b35afd583891727599
Author: Mads Rasmussen <madsr@hey.com>
Date:   Wed Jun 1 13:38:25 2022 +0200

    render dashboards through router-slot

commit 93fd2d7d05669c9b869c50da5d0e45ff81c4caa7
Author: Mads Rasmussen <madsr@hey.com>
Date:   Wed Jun 1 13:26:42 2022 +0200

    redirect after login

commit 2ea950547b4521cf21f4653c127f77effa9fba9e
Author: Niels Lyngsø <niels.lyngso@gmail.com>
Date:   Wed Jun 1 12:41:06 2022 +0200

    type

commit e78ab72ef8002ca49457c43998a2945c3e9587a7
Author: Niels Lyngsø <niels.lyngso@gmail.com>
Date:   Wed Jun 1 11:59:40 2022 +0200

    router-slot basic impl
This commit is contained in:
Mads Rasmussen
2022-06-02 09:44:34 +02:00
parent 6779bd3784
commit 575465b059
26 changed files with 229 additions and 464 deletions

View File

@@ -7,6 +7,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Umbraco</title>
<script type="module" src="/src/index.ts"></script>
<base href="/">
</head>
<body class="uui-font uui-text" style="margin: 0; padding: 0">

View File

@@ -13,6 +13,7 @@
"element-internals-polyfill": "^1.1.4",
"lit": "^2.2.5",
"openapi-typescript-fetch": "^1.1.3",
"router-slot": "^1.5.5",
"rxjs": "^7.5.5"
},
"devDependencies": {
@@ -6337,6 +6338,11 @@
"fsevents": "~2.3.2"
}
},
"node_modules/router-slot": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/router-slot/-/router-slot-1.5.5.tgz",
"integrity": "sha512-wu+6ZAaU37ARD24Jox9CWFvQ4s6PWL6LiqkqpphLLGNer+J6530HzZ64HXPDQBaCwAx82TAzUhZDvSAtaxJX2A=="
},
"node_modules/run-async": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
@@ -12206,6 +12212,11 @@
"fsevents": "~2.3.2"
}
},
"router-slot": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/router-slot/-/router-slot-1.5.5.tgz",
"integrity": "sha512-wu+6ZAaU37ARD24Jox9CWFvQ4s6PWL6LiqkqpphLLGNer+J6530HzZ64HXPDQBaCwAx82TAzUhZDvSAtaxJX2A=="
},
"run-async": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",

View File

@@ -36,6 +36,7 @@
"element-internals-polyfill": "^1.1.4",
"lit": "^2.2.5",
"openapi-typescript-fetch": "^1.1.3",
"router-slot": "^1.5.5",
"rxjs": "^7.5.5"
},
"devDependencies": {

View File

@@ -1,61 +1,35 @@
import './auth/auth-layout.element';
import './auth/login/login.element';
import './backoffice/backoffice.element';
import './installer/installer.element';
import './node-editor/node-editor-layout.element';
import './node-editor/node-property-data-type.element';
import './node-editor/node-property.element';
import 'router-slot';
import '@umbraco-ui/uui-css/dist/uui-css.css';
import { UUIIconRegistryEssential } from '@umbraco-ui/uui';
import { css, html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import { Subscription } from 'rxjs';
import { getInitStatus } from './core/api/fetcher';
import { UmbContextProviderMixin } from './core/context';
import {
isUmbRouterBeforeEnterEvent,
UmbRoute,
UmbRouteLocation,
UmbRouter,
UmbRouterBeforeEnterEvent,
umbRouterBeforeEnterEventType,
} from './core/router';
import { UmbSectionContext } from './section.context';
import { UmbNodeStore } from './core/stores/node.store';
import { UmbDataTypeStore } from './core/stores/data-type.store';
// TODO: lazy load these
const routes: Array<UmbRoute> = [
// Load these in the correct components
import './node-editor/node-editor-layout.element';
import './node-editor/node-property-data-type.element';
import './node-editor/node-property.element';
const routes = [
{
path: '/login',
alias: 'login',
meta: { requiresAuth: false },
path: 'login',
component: () => import('./auth/login/login.element'),
},
{
path: '/install',
alias: 'install',
meta: { requiresAuth: false },
path: 'install',
component: () => import('./installer/installer.element'),
},
{
path: '/section/:section',
alias: 'app',
meta: { requiresAuth: true },
},
{
path: '/section/:section/dashboard/:dashboard',
alias: 'dashboard',
meta: { requiresAuth: true },
},
{
path: '/section/:section/node/:nodeId',
alias: 'node',
meta: { requiresAuth: true },
path: '**',
component: () => import('./backoffice/backoffice.element'),
},
];
// Import somewhere else?
@customElement('umb-app')
export class UmbApp extends UmbContextProviderMixin(LitElement) {
static styles = css`
@@ -68,106 +42,50 @@ export class UmbApp extends UmbContextProviderMixin(LitElement) {
`;
private _iconRegistry: UUIIconRegistryEssential = new UUIIconRegistryEssential();
private _isInstalled = false;
private _view?: HTMLElement;
private _router?: UmbRouter;
private _locationSubscription?: Subscription;
constructor() {
super();
this.addEventListener(umbRouterBeforeEnterEventType, this._onBeforeEnter);
this._iconRegistry.attach(this);
const { extensionRegistry } = window.Umbraco;
this.provideContext('umbExtensionRegistry', window.Umbraco.extensionRegistry);
this.provideContext('umbSectionContext', new UmbSectionContext(extensionRegistry));
this.provideContext('umbExtensionRegistry', extensionRegistry);
// TODO: consider providing somethings for install/login and some only for 'backoffice'.
this.provideContext('umbNodeStore', new UmbNodeStore());
this.provideContext('umbDataTypeStore', new UmbDataTypeStore());
}
private _onBeforeEnter = (event: Event) => {
if (!isUmbRouterBeforeEnterEvent(event)) return;
this._handleUnauthorizedNavigation(event);
};
private _handleUnauthorizedNavigation(event: UmbRouterBeforeEnterEvent) {
if (event.to.route.meta.requiresAuth && !this._isAuthorized()) {
event.preventDefault();
this._router?.push('/login');
}
}
private _isAuthorized(): boolean {
return sessionStorage.getItem('is-authenticated') === 'true';
}
protected async firstUpdated(): Promise<void> {
this._router = new UmbRouter(this);
this._router.setRoutes(routes);
this.shadowRoot?.querySelector('router-slot')?.render();
// TODO: find a solution for magic strings
this.provideContext('umbRouter', this._router);
this._useLocation(); // TODO: Are we sure we want to do this here? The installer cannot be shown if we don't act on the routes at this point...
// TODO: this is a temporary routing solution for shell elements
try {
const { data } = await getInitStatus({});
this._isInstalled = data.installed;
if (!this._isInstalled) {
this._router.push('/install');
history.pushState(null, '', '/install');
return;
}
if (!this._isAuthorized() || window.location.pathname === '/install') {
this._router.push('/login');
history.pushState(null, '', 'login');
} else {
const next = window.location.pathname === '/' ? '/section/content' : window.location.pathname;
this._router.push(next);
history.pushState(null, '', next);
}
} catch (error) {
console.log(error);
}
}
private _useLocation() {
this._locationSubscription?.unsubscribe();
this._locationSubscription = this._router?.location.subscribe((location: UmbRouteLocation) => {
if (location.route.alias === 'login') {
this._renderView('umb-login');
return;
}
if (location.route.alias === 'install') {
this._renderView('umb-installer');
return;
}
this._renderView('umb-backoffice');
});
}
_renderView(view: string) {
if (this._view?.tagName === view.toUpperCase()) return;
this._view = document.createElement(view);
this.requestUpdate();
}
disconnectedCallback(): void {
super.disconnectedCallback();
this._locationSubscription?.unsubscribe();
}
render() {
return html`${this._view}`;
return html`<router-slot id="outlet" .routes=${routes}></router-slot>`;
}
}

View File

@@ -4,13 +4,11 @@ import { customElement, state } from 'lit/decorators.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { postUserLogin } from '../../core/api/fetcher';
import { UmbContextConsumerMixin } from '../../core/context';
import { UmbRouter } from '../../core/router';
// create custom element with lit-element named 'umb-login'
import '../auth-layout.element';
@customElement('umb-login')
export class UmbLogin extends UmbContextConsumerMixin(LitElement) {
export default class UmbLogin extends LitElement {
static styles: CSSResultGroup = [
UUITextStyles,
css`
@@ -24,18 +22,6 @@ export class UmbLogin extends UmbContextConsumerMixin(LitElement) {
@state()
private _loggingIn = false;
_router?: UmbRouter;
connectedCallback() {
super.connectedCallback();
// TODO: find solution for magic string
// TODO: can we use a property decorator and a callback?
this.consumeContext('umbRouter', (api: unknown) => {
this._router = api as UmbRouter;
});
}
private _handleSubmit = (e: SubmitEvent) => {
e.preventDefault();
@@ -55,14 +41,12 @@ export class UmbLogin extends UmbContextConsumerMixin(LitElement) {
};
private async _login(username: string, password: string, persist: boolean) {
console.log('LOGIN', username, password, persist);
this._loggingIn = true;
try {
await postUserLogin({ username, password, persist });
this._loggingIn = false;
// TODO: how do we know where to go?
this._router?.push('/section/content');
history.pushState(null, '', '/section');
} catch (error) {
console.log(error);
this._loggingIn = false;

View File

@@ -1,14 +1,13 @@
import { Subscription, map } from 'rxjs';
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 { Subscription } from 'rxjs';
import { isPathActive, path } from 'router-slot';
import { getUserSections } from '../core/api/fetcher';
import { UmbExtensionRegistry, UmbExtensionManifest, UmbExtensionManifestSection } from '../core/extension';
import { UmbContextConsumerMixin } from '../core/context';
import { UmbExtensionManifestSection } from '../core/extension';
import { UmbRouteLocation, UmbRouter } from '../core/router';
import { UmbSectionContext } from '../section.context';
@customElement('umb-backoffice-header-sections')
export class UmbBackofficeHeaderSections extends UmbContextConsumerMixin(LitElement) {
@@ -56,24 +55,15 @@ export class UmbBackofficeHeaderSections extends UmbContextConsumerMixin(LitElem
@state()
private _currentSectionAlias = '';
private _router?: UmbRouter;
private _sectionContext?: UmbSectionContext;
private _extensionRegistry?: UmbExtensionRegistry;
private _sectionSubscription?: Subscription;
private _currentSectionSubscription?: Subscription;
private _locationSubscription?: Subscription;
private _location?: UmbRouteLocation;
constructor() {
super();
this.consumeContext('umbRouter', (_instance: UmbRouter) => {
this._router = _instance;
this._useLocation();
});
this.consumeContext('umbSectionContext', (_instance: UmbSectionContext) => {
this._sectionContext = _instance;
this._useCurrentSection();
this.consumeContext('umbExtensionRegistry', (extensionRegistry: UmbExtensionRegistry) => {
this._extensionRegistry = extensionRegistry;
this._useSections();
});
}
@@ -92,69 +82,48 @@ export class UmbBackofficeHeaderSections extends UmbContextConsumerMixin(LitElem
}
// TODO: this could maybe be handled by an anchor tag
this._router?.push(`/section/${section.meta.pathname}`);
this._sectionContext?.setCurrent(section.alias);
history.pushState(null, '', `/section/${section.meta.pathname}`);
}
private _handleLabelClick(e: PointerEvent) {
const label = (e.target as any).label;
// TODO: set current section
//this._sectionContext?.setCurrent(section.alias);
private _handleLabelClick() {
const moreTab = this.shadowRoot?.getElementById('moreTab');
moreTab?.setAttribute('active', 'true');
this._open = false;
}
private _useLocation() {
this._locationSubscription?.unsubscribe();
this._locationSubscription = this._router?.location.subscribe((location: UmbRouteLocation) => {
this._location = location;
});
}
private _useCurrentSection() {
this._currentSectionSubscription?.unsubscribe();
this._currentSectionSubscription = this._sectionContext?.getCurrent().subscribe((section) => {
this._currentSectionAlias = section.alias;
});
}
private async _useSections() {
this._sectionSubscription?.unsubscribe();
const { data } = await getUserSections({});
this._allowedSection = data.sections;
this._sectionSubscription = this._sectionContext?.getSections().subscribe((sectionExtensions) => {
this._sections = sectionExtensions.filter((section) => this._allowedSection.includes(section.alias));
this._visibleSections = this._sections;
const currentSectionAlias = this._sections.find(
(section) => section.meta.pathname === this._location?.params?.section
)?.alias;
if (!currentSectionAlias) return;
this._sectionContext?.setCurrent(currentSectionAlias);
});
this._sectionSubscription = this._extensionRegistry?.extensions
.pipe(
map((extensions: Array<UmbExtensionManifest>) =>
extensions
.filter((extension) => extension.type === 'section')
.sort((a: any, b: any) => b.meta.weight - a.meta.weight)
)
)
.subscribe((sections: Array<any>) => {
this._sections = sections.filter((section: any) => this._allowedSection.includes(section.alias));
this._visibleSections = this._sections;
});
}
disconnectedCallback(): void {
super.disconnectedCallback();
this._locationSubscription?.unsubscribe();
this._sectionSubscription?.unsubscribe();
this._currentSectionSubscription?.unsubscribe();
}
render() {
return html`
<uui-tab-group id="tabs">
${this._visibleSections.map(
(section) => html`
(section: any) => html`
<uui-tab
?active="${this._currentSectionAlias === section.alias}"
?active="${isPathActive(`/section/${section.meta.pathname}`, path())}"
label="${section.name}"
@click="${(e: PointerEvent) => this._handleTabClick(e, section)}"></uui-tab>
`

View File

@@ -4,6 +4,7 @@ import { customElement } from 'lit/decorators.js';
import './backoffice-header-sections.element';
import './backoffice-header-tools.element';
@customElement('umb-backoffice-header')
export class UmbBackofficeHeader extends LitElement {
static styles: CSSResultGroup = [

View File

@@ -2,11 +2,15 @@ import { defineElement } from '@umbraco-ui/uui-base/lib/registration';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { state } from 'lit/decorators.js';
import { Subscription } from 'rxjs';
import { Subscription, map } from 'rxjs';
import { UmbContextConsumerMixin } from '../core/context';
import { UmbSectionContext } from '../section.context';
import { UmbExtensionManifest } from '../core/extension';
import {
UmbExtensionRegistry,
createExtensionElement,
UmbExtensionManifest,
UmbExtensionManifestSection,
} from '../core/extension';
@defineElement('umb-backoffice-main')
export class UmbBackofficeMain extends UmbContextConsumerMixin(LitElement) {
@@ -22,52 +26,59 @@ export class UmbBackofficeMain extends UmbContextConsumerMixin(LitElement) {
];
@state()
private _sectionElement?: HTMLElement;
private _routes: Array<any> = [];
private _sectionContext?: UmbSectionContext;
private _currentSectionSubscription?: Subscription;
@state()
private _sections: Array<UmbExtensionManifestSection> = [];
private _extensionRegistry?: UmbExtensionRegistry;
private _sectionSubscription?: Subscription;
constructor() {
super();
this.consumeContext('umbSectionContext', (_instance: UmbSectionContext) => {
this._sectionContext = _instance;
this._useCurrentSection();
this.consumeContext('umbExtensionRegistry', (_instance: UmbExtensionRegistry) => {
this._extensionRegistry = _instance;
this._useSections();
});
}
private _useCurrentSection() {
this._currentSectionSubscription?.unsubscribe();
private _useSections() {
this._sectionSubscription?.unsubscribe();
this._currentSectionSubscription = this._sectionContext?.getCurrent().subscribe((section) => {
this._createSectionElement(section);
});
this._sectionSubscription = this._extensionRegistry?.extensions
.pipe(
map((extensions: Array<UmbExtensionManifest>) =>
extensions
.filter((extension) => extension.type === 'section')
.sort((a: any, b: any) => b.meta.weight - a.meta.weight)
)
)
.subscribe((sections) => {
this._routes = [];
this._sections = sections as Array<UmbExtensionManifestSection>;
this._routes = this._sections.map((section) => {
return {
path: 'section/' + section.meta.pathname,
component: () => createExtensionElement(section),
};
});
this._routes.push({
path: '**',
redirectTo: 'section/' + this._sections[0].meta.pathname,
});
});
}
disconnectedCallback(): void {
super.disconnectedCallback();
this._currentSectionSubscription?.unsubscribe();
}
private async _createSectionElement(section: UmbExtensionManifest) {
if (!section) return;
// TODO: How do we handle dynamic imports of our files?
if (typeof section.js === 'string') {
await import(/* @vite-ignore */ section.js);
}
if (typeof section.js === 'function') {
await section.js();
}
if (section.elementName) {
this._sectionElement = document.createElement(section.elementName);
}
this._sectionSubscription?.unsubscribe();
}
render() {
return html` ${this._sectionElement} `;
return html`<router-slot .routes=${this._routes}></router-slot>`;
}
}

View File

@@ -1,8 +1,8 @@
import { defineElement } from '@umbraco-ui/uui-base/lib/registration';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
@defineElement('umb-backoffice-sidebar')
@customElement('umb-backoffice-sidebar')
export class UmbBackofficeSidebar extends LitElement {
static styles = [
UUITextStyles,

View File

@@ -7,7 +7,7 @@ import './backoffice-sidebar.element';
import './backoffice-main.element';
@defineElement('umb-backoffice')
export class UmbBackoffice extends LitElement {
export default class UmbBackoffice extends LitElement {
static styles = [
UUITextStyles,
css`

View File

@@ -1,11 +1,16 @@
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { IRoutingInfo } from 'router-slot';
import { map, Subscription } from 'rxjs';
import { UmbContextConsumerMixin } from '../core/context';
import { UmbExtensionManifestDashboard, UmbExtensionRegistry } from '../core/extension';
import { UmbRouteLocation, UmbRouter } from '../core/router';
import {
UmbExtensionManifestDashboard,
UmbExtensionManifest,
UmbExtensionRegistry,
createExtensionElement,
} from '../core/extension';
@customElement('umb-content-dashboards')
export class UmbContentDashboards extends UmbContextConsumerMixin(LitElement) {
@@ -25,11 +30,7 @@ export class UmbContentDashboards extends UmbContextConsumerMixin(LitElement) {
private _current = '';
@state()
private _outlet?: HTMLElement;
private _router?: UmbRouter;
private _locationSubscription?: Subscription;
private _location?: UmbRouteLocation;
private _routes: Array<any> = [];
private _extensionRegistry?: UmbExtensionRegistry;
private _dashboardsSubscription?: Subscription;
@@ -37,80 +38,51 @@ export class UmbContentDashboards extends UmbContextConsumerMixin(LitElement) {
constructor() {
super();
this.consumeContext('umbRouter', (_instance: UmbRouter) => {
this._router = _instance;
this._useLocation();
});
this.consumeContext('umbExtensionRegistry', (_instance: UmbExtensionRegistry) => {
this._extensionRegistry = _instance;
this._useDashboards();
});
}
private _useLocation() {
this._locationSubscription?.unsubscribe();
this._router?.location.subscribe((location: UmbRouteLocation) => {
this._location = location;
});
}
private _useDashboards() {
this._dashboardsSubscription?.unsubscribe();
this._dashboardsSubscription = this._extensionRegistry
?.extensionsOfType('dashboard')
.pipe(map((extensions) => extensions.sort((a, b) => b.meta.weight - a.meta.weight)))
.subscribe((dashboards) => {
// TODO: What do we want to use as path?
this._dashboards = dashboards;
const dashboardLocation = decodeURIComponent(this._location?.params?.dashboard);
const sectionLocation = this._location?.params?.section;
this._dashboardsSubscription = this._extensionRegistry?.extensions
.pipe(
map((extensions: Array<UmbExtensionManifest>) =>
extensions
.filter((extension) => extension.type === 'dashboard')
.sort((a: any, b: any) => b.meta.weight - a.meta.weight)
)
)
.subscribe((dashboards: Array<UmbExtensionManifest>) => {
this._dashboards = dashboards as Array<UmbExtensionManifestDashboard>;
this._routes = [];
// TODO: Temp redirect solution
if (dashboardLocation === 'undefined') {
this._router?.push(`/section/${sectionLocation}/dashboard/${this._dashboards[0].meta.pathname}`);
this._setCurrent(this._dashboards[0]);
return;
}
this._routes = this._dashboards.map((dashboard) => {
return {
path: `${dashboard.meta.pathname}`,
component: () => createExtensionElement(dashboard),
setup: (element: UmbExtensionManifestDashboard, info: IRoutingInfo) => {
this._current = info.match.route.path;
},
};
});
const dashboard = this._dashboards.find((dashboard) => dashboard.meta.pathname === dashboardLocation);
if (!dashboard) {
this._router?.push(`/section/${sectionLocation}/dashboard/${this._dashboards[0].meta.pathname}`);
this._setCurrent(this._dashboards[0]);
return;
}
this._setCurrent(dashboard);
this._routes.push({
path: '**',
redirectTo: this._dashboards[0].meta.pathname,
});
});
}
private _handleTabClick(_e: PointerEvent, dashboard: UmbExtensionManifestDashboard) {
// TODO: this could maybe be handled by an anchor tag
const section = this._location?.params?.section;
this._router?.push(`/section/${section}/dashboard/${dashboard.meta.pathname}`);
this._setCurrent(dashboard);
}
// TODO: Temp outlet solution
private async _setCurrent(dashboard: UmbExtensionManifestDashboard) {
if (typeof dashboard.js === 'function') {
await dashboard.js();
}
if (dashboard.elementName) {
const element = document.createElement(dashboard.elementName);
this._outlet = element;
}
private _handleTabClick(e: PointerEvent, dashboard: UmbExtensionManifestDashboard) {
history.pushState(null, '', `/section/content/dashboard/${dashboard.meta.pathname}`);
this._current = dashboard.name;
}
disconnectedCallback() {
super.disconnectedCallback();
this._locationSubscription?.unsubscribe();
this._dashboardsSubscription?.unsubscribe();
}
@@ -118,19 +90,21 @@ export class UmbContentDashboards extends UmbContextConsumerMixin(LitElement) {
return html`
<uui-tab-group id="tabs">
${this._dashboards.map(
(dashboard) => html`
(dashboard: UmbExtensionManifestDashboard) => html`
<uui-tab
label=${dashboard.name}
?active="${this._current === dashboard.name}"
?active="${dashboard.meta.pathname === this._current}"
@click="${(e: PointerEvent) => this._handleTabClick(e, dashboard)}"></uui-tab>
`
)}
</uui-tab-group>
${this._outlet}
<router-slot .routes="${this._routes}"></router-slot>
`;
}
}
export default UmbContentDashboards;
declare global {
interface HTMLElementTagNameMap {
'umb-content-dashboards': UmbContentDashboards;

View File

@@ -7,7 +7,7 @@ import { Subscription } from 'rxjs';
import { DocumentNode } from '../mocks/data/content.data';
@customElement('umb-content-editor')
class UmbContentEditor extends UmbContextConsumerMixin(LitElement) {
export class UmbContentEditor extends UmbContextConsumerMixin(LitElement) {
static styles = [
UUITextStyles,
css`
@@ -143,6 +143,8 @@ class UmbContentEditor extends UmbContextConsumerMixin(LitElement) {
}
}
export default UmbContentEditor;
declare global {
interface HTMLElementTagNameMap {
'umb-content-editor': UmbContentEditor;

View File

@@ -1,20 +1,17 @@
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import { UmbContextConsumerMixin, UmbContextProviderMixin } from '../core/context';
import { UmbRouteLocation, UmbRouter } from '../core/router';
import { Subscription } from 'rxjs';
import { customElement, state } from 'lit/decorators.js';
import { IRoute, IRoutingInfo } from 'router-slot';
import './content-tree.element';
import './content-dashboards.element';
import './content-editor.element';
@customElement('umb-content-section')
export class UmbContentSection extends UmbContextProviderMixin(UmbContextConsumerMixin(LitElement)) {
export class UmbContentSection extends LitElement {
static styles = [
UUITextStyles,
css`
:host {
:host,
#router-slot {
display: flex;
width: 100%;
height: 100%;
@@ -22,54 +19,36 @@ export class UmbContentSection extends UmbContextProviderMixin(UmbContextConsume
`,
];
private _router?: UmbRouter;
private _locationSubscription?: Subscription;
private _outlet?: HTMLElement;
@state()
private _routes: Array<IRoute> = [
{
path: 'dashboard',
component: () => import('./content-dashboards.element'),
},
{
path: 'node/:nodeId',
component: () => import('./content-editor.element'),
setup: (component: HTMLElement, info: IRoutingInfo) => {
this._currentNodeId = info.match.params.nodeId;
component.id = this._currentNodeId;
},
},
{
path: '**',
redirectTo: 'dashboard',
},
];
constructor() {
super();
this.consumeContext('umbRouter', (_instance: UmbRouter) => {
this._router = _instance;
this._useLocation();
});
}
private _useLocation() {
this._locationSubscription?.unsubscribe();
this._locationSubscription = this._router?.location.subscribe((location: UmbRouteLocation) => {
// TODO: temp outlet solution
const nodeId = location.params.nodeId;
this._outlet?.parentNode?.removeChild(this._outlet);
if (nodeId !== undefined) {
const contentEditor = document.createElement('umb-content-editor');
contentEditor.id = nodeId;
this._outlet = contentEditor;
this.requestUpdate();
return;
}
const dashboards = document.createElement('umb-content-dashboards');
this._outlet = dashboards;
this.requestUpdate();
});
}
disconnectedCallback(): void {
super.disconnectedCallback();
this._locationSubscription?.unsubscribe();
}
@state()
private _currentNodeId!: string;
render() {
return html`
<!-- TODO: Figure out how we name layout components -->
<umb-backoffice-sidebar>
<umb-content-tree></umb-content-tree>
<umb-content-tree .id="${this._currentNodeId}"></umb-content-tree>
</umb-backoffice-sidebar>
${this._outlet}
<router-slot id="router-slot" .routes="${this._routes}"></router-slot>
`;
}
}

View File

@@ -1,14 +1,10 @@
import { css, html, LitElement } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { customElement, state, property } from 'lit/decorators.js';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { UmbContextConsumerMixin } from '../core/context';
import { UmbRouteLocation, UmbRouter } from '../core/router';
import { Subscription } from 'rxjs';
import { data } from '../mocks/data/content.data';
import { UUIMenuItemElement } from '@umbraco-ui/uui';
@customElement('umb-content-tree')
class UmbContentTree extends UmbContextConsumerMixin(LitElement) {
class UmbContentTree extends LitElement {
static styles = [
UUITextStyles,
css`
@@ -18,81 +14,30 @@ class UmbContentTree extends UmbContextConsumerMixin(LitElement) {
`,
];
// simplified tree for testing
@property()
public id!: string;
// simplified tree data for testing
@state()
_tree: Array<any> = [];
_tree: Array<any> = data;
@state()
_section?: string;
@state()
_currentNodeId?: number;
private _router?: UmbRouter;
private _location?: UmbRouteLocation;
private _locationSubscription?: Subscription;
constructor() {
super();
// TODO: implement correct tree data
this._tree = data;
this.consumeContext('umbRouter', (router: UmbRouter) => {
this._router = router;
this._useLocation();
});
}
private _useLocation() {
this._locationSubscription?.unsubscribe();
this._locationSubscription = this._router?.location.subscribe((location) => {
this._location = location;
this._section = location.params.section;
this._currentNodeId = parseInt(location.params.nodeId);
});
}
disconnectedCallback(): void {
super.disconnectedCallback();
this._locationSubscription?.unsubscribe();
}
/* TODO: there are some problems with menu items and click events. They can happen on element inside and outside of the shadow dom
which makes it difficult to find the right href in the router.
It might make sense to make it possible to use your own anchor tag or button inside a label slot instead.
This is a temp solution to get the href from the menu item and overwrite the router hijacking.
*/
private _handleMenuItemClick(e: PointerEvent) {
e.preventDefault();
e.stopPropagation();
const target = e.target as UUIMenuItemElement;
if (!target) return;
const href = target.href;
if (!href) return;
this._router?.push(href);
}
render() {
return html`
<a href="${`/section/${this._section}`}">
<a href="${'/section/content'}">
<h3>Content</h3>
</a>
<div class="nav-list">
<!-- TODO: make menu item events bubble so we don't have to attach event listeners on every item -->
${this._tree.map(
(item) => html`
<uui-menu-item
@click="${this._handleMenuItemClick}"
?active="${item.id === this._currentNodeId}"
?active="${parseInt(this.id) === item.id}"
data-id="${item.id}"
label="${item.name}"
href="/section/${this._section}/node/${item.id}">
href="/section/content/node/${item.id}">
<uui-icon slot="icon" name="${item.icon}"></uui-icon>
</uui-menu-item>
`

View File

@@ -2,16 +2,14 @@ import { UmbExtensionManifest } from './extension.registry';
import { loadExtension } from './load-extension.function';
export function createExtensionElement(manifest: UmbExtensionManifest): Promise<HTMLElement> | Promise<undefined> {
//TODO: Write tests for these extension options:
return loadExtension(manifest).then((js) => {
if (manifest.elementName) {
console.log('-- created by elementName', manifest.elementName);
return document.createElement(manifest.elementName as any);
}
console.log(js)
console.log(js);
if (js) {
if (js instanceof HTMLElement) {
@@ -24,11 +22,11 @@ export function createExtensionElement(manifest: UmbExtensionManifest): Promise<
}
if ((js as any).default) {
console.log('-- created by default class', (js as any).default);
return new ((js as any).default) as HTMLElement;
return new (js as any).default() as HTMLElement;
}
}
console.error('-- Extension did not succeed creating an element');
return Promise.resolve(undefined);
});
}
}

View File

@@ -1 +1,3 @@
export * from './extension.registry';
export * from './create-extension-element.function';
export * from './load-extension.function';

View File

@@ -1,14 +1,13 @@
import { UmbExtensionManifest } from './extension.registry';
export function loadExtension(manifest: UmbExtensionManifest): Promise<object|HTMLElement> | Promise<null> {
export function loadExtension(manifest: UmbExtensionManifest): Promise<object | HTMLElement> | Promise<null> {
if (typeof manifest.js === 'function') {
return manifest.js() as Promise<object|HTMLElement>;
return manifest.js() as Promise<object | HTMLElement>;
}
// TODO: verify if this is acceptable solution.
if (typeof manifest.js === 'string') {
return import(/* @vite-ignore */manifest.js);
return import(/* @vite-ignore */ manifest.js);
/*
return new Promise((resolve, reject) => {
const script = document.createElement('script');
@@ -27,6 +26,6 @@ export function loadExtension(manifest: UmbExtensionManifest): Promise<object|HT
*/
}
console.log('-- Extension does not have any referenced JS')
console.log('-- Extension does not have any referenced JS');
return Promise.resolve(null);
}

View File

@@ -17,15 +17,15 @@ export class UmbDataTypeStore {
id: 1244,
key: 'dt-2',
name: 'Textarea (DataType)',
propertyEditorUIAlias: 'Umb.PropertyEditorUI.Textarea'
propertyEditorUIAlias: 'Umb.PropertyEditorUI.Textarea',
},
{
id: 1246,
key: 'dt-3',
name: 'External Test (DataType)',
propertyEditorUIAlias: 'External.PropertyEditorUI.Test'
}
])
propertyEditorUIAlias: 'External.PropertyEditorUI.Test',
},
]);
}
getById(id: number): Observable<DataTypeEntity | null> {

View File

@@ -1,8 +1,8 @@
import { defineElement } from '@umbraco-ui/uui-base/lib/registration';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
@defineElement('umb-dashboard-redirect-management')
@customElement('umb-dashboard-redirect-management')
export class UmbDashboardRedirectManagement extends LitElement {
static styles = [UUITextStyles, css``];

View File

@@ -1,8 +1,8 @@
import { defineElement } from '@umbraco-ui/uui-base/lib/registration';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
@defineElement('umb-dashboard-welcome')
@customElement('umb-dashboard-welcome')
export class UmbDashboardWelcome extends LitElement {
static styles = [UUITextStyles, css``];

View File

@@ -105,7 +105,7 @@ const registerInternalManifests = async () => {
group: 'common',
},
},
/*
/*
{
type: 'propertyEditorUI',
alias: 'External.PropertyEditorUI.Test',

View File

@@ -11,7 +11,7 @@ import { getInstall, postInstall } from '../core/api/fetcher';
import { PostInstallRequest, UmbracoInstaller, UmbracoPerformInstallRequest } from '../core/models';
@customElement('umb-installer')
export class UmbInstaller extends LitElement {
export default class UmbInstaller extends LitElement {
static styles: CSSResultGroup = [css``];
@state()

View File

@@ -53,7 +53,7 @@ export const data: Array<DocumentNode> = [
label: 'External label 1',
description: 'This is the a external property',
dataTypeKey: 'dt-3',
tempValue: 'Tex lkasdfkljdfsa 1'
tempValue: 'Tex lkasdfkljdfsa 1',
},
],
/*

View File

@@ -34,7 +34,7 @@ class UmbNodePropertyDataType extends UmbContextConsumerMixin(LitElement) {
// TODO: make interface for UMBPropertyEditorElement
@state()
private _element?: {value?:string} & HTMLElement;// TODO: invent interface for propertyEditorUI.
private _element?: { value?: string } & HTMLElement; // TODO: invent interface for propertyEditorUI.
@property()
value?: string;
@@ -93,34 +93,36 @@ class UmbNodePropertyDataType extends UmbContextConsumerMixin(LitElement) {
return;
}
createExtensionElement(_propertyEditorUI)
.then((el) => {
const oldValue = this._element;
this._element = el;
createExtensionElement(_propertyEditorUI).then(el => {
// TODO: Set/Parse Data-Type-UI-configuration
const oldValue = this._element;
this._element = el;
// TODO: Set/Parse Data-Type-UI-configuration
if(oldValue) {
oldValue.removeEventListener('property-editor-change', this._onPropertyEditorChange as any as EventListener);
}
if(this._element) {
this._element.addEventListener('property-editor-change', this._onPropertyEditorChange as any as EventListener);
this._element.value = this.value;// Be aware its duplicated code
}
this.requestUpdate('element', oldValue);
}).catch(() => {
// TODO: loading JS failed so we should do some nice UI. (This does only happen if extension has a js prop, otherwise we concluded that no source was needed resolved the load.)
});
if (oldValue) {
oldValue.removeEventListener('property-editor-change', this._onPropertyEditorChange as any as EventListener);
}
if (this._element) {
this._element.addEventListener(
'property-editor-change',
this._onPropertyEditorChange as any as EventListener
);
this._element.value = this.value; // Be aware its duplicated code
}
this.requestUpdate('element', oldValue);
})
.catch(() => {
// TODO: loading JS failed so we should do some nice UI. (This does only happen if extension has a js prop, otherwise we concluded that no source was needed resolved the load.)
});
}
private _onPropertyEditorChange = (e: CustomEvent) => {
if (e.currentTarget === this._element) {
this.value = this._element.value;
//
this.dispatchEvent(new CustomEvent('property-data-type-change', { bubbles: true, composed: true }));
this.dispatchEvent(new CustomEvent('property-data-type-value-change', { bubbles: true, composed: true }));
}
// make sure no event leave this scope.
e.stopPropagation();

View File

@@ -50,7 +50,7 @@ class UmbNodeProperty extends LitElement {
<umb-node-property-data-type
.dataTypeKey=${this.property.dataTypeKey}
.value=${this.value}
@property-data-type-change=${this._onPropertyDataTypeChange}></umb-node-property-data-type>
@property-data-type-value-change=${this._onPropertyDataTypeChange}></umb-node-property-data-type>
</div>
</div>
`;

View File

@@ -1,32 +0,0 @@
import { firstValueFrom, map, ReplaySubject } from 'rxjs';
import { UmbExtensionManifestSection, UmbExtensionRegistry } from './core/extension';
export class UmbSectionContext {
private _extensionRegistry!: UmbExtensionRegistry;
private _current = new ReplaySubject<UmbExtensionManifestSection>(1);
public readonly current = this._current.asObservable();
constructor(_extensionRegistry: UmbExtensionRegistry) {
this._extensionRegistry = _extensionRegistry;
}
getSections() {
return this._extensionRegistry
.extensionsOfType('section')
.pipe(map((extensions) => extensions.sort((a, b) => b.meta.weight - a.meta.weight)));
}
getCurrent() {
return this.current;
}
async setCurrent(sectionAlias: string) {
const sections = await firstValueFrom(this.getSections());
const matchedSection = sections.find((section) => section.alias === sectionAlias);
if (matchedSection) {
this._current.next(matchedSection);
}
}
}