This commit is contained in:
Mads Rasmussen
2022-05-25 17:07:17 +02:00
14 changed files with 245 additions and 192 deletions

View File

@@ -6,7 +6,7 @@ import './installer/installer.element';
import './auth/login/login.element';
import './auth/auth-layout.element';
import './backoffice/backoffice.element';
import './backoffice/node-editor.element';
import './backoffice/node-editor-layout.element';
import { UmbSectionContext } from './section.context';
@@ -15,7 +15,14 @@ import { customElement } from 'lit/decorators.js';
import { Subscription } from 'rxjs';
import { getInitStatus } from './api/fetcher';
import { isUmbRouterBeforeEnterEvent, UmbRoute, UmbRouteLocation, UmbRouter, UmbRouterBeforeEnterEvent, umbRouterBeforeEnterEventType } from './core/router';
import {
isUmbRouterBeforeEnterEvent,
UmbRoute,
UmbRouteLocation,
UmbRouter,
UmbRouterBeforeEnterEvent,
umbRouterBeforeEnterEventType,
} from './core/router';
import { UmbContextProviderMixin } from './core/context';
const routes: Array<UmbRoute> = [
@@ -70,7 +77,7 @@ export class UmbApp extends UmbContextProviderMixin(LitElement) {
private _onBeforeEnter = (event: Event) => {
if (!isUmbRouterBeforeEnterEvent(event)) return;
this._handleUnauthorizedNavigation(event);
}
};
private _handleUnauthorizedNavigation(event: UmbRouterBeforeEnterEvent) {
if (event.to.route.meta.requiresAuth && !this._isAuthorized()) {
@@ -100,26 +107,24 @@ export class UmbApp extends UmbContextProviderMixin(LitElement) {
this._router.push('/install');
return;
}
if (!this._isAuthorized() || window.location.pathname === '/install') {
this._router.push('/login');
} else {
const next = window.location.pathname === '/' ? '/section/Content' : window.location.pathname;
const next = window.location.pathname === '/' ? '/section/Content' : window.location.pathname;
this._router.push(next);
}
this._useLocation();
} catch (error) {
console.log(error);
}
}
private _useLocation () {
private _useLocation() {
this._locationSubscription?.unsubscribe();
this._locationSubscription = this._router?.location
.subscribe((location: UmbRouteLocation) => {
this._locationSubscription = this._router?.location.subscribe((location: UmbRouteLocation) => {
if (location.route.alias === 'login') {
this._renderView('umb-login');
return;
@@ -134,7 +139,7 @@ export class UmbApp extends UmbContextProviderMixin(LitElement) {
});
}
_renderView (view: string) {
_renderView(view: string) {
if (this._view?.tagName === view.toUpperCase()) return;
this._view = document.createElement(view);
this.requestUpdate();

View File

@@ -33,7 +33,7 @@ export class UmbBackofficeMain extends UmbContextConsumerMixin(LitElement) {
private _sectionContext?: UmbSectionContext;
private _currentSectionSubscription?: Subscription;
constructor () {
constructor() {
super();
this.consumeContext('umbSectionContext', (_instance: UmbSectionContext) => {
@@ -42,11 +42,10 @@ export class UmbBackofficeMain extends UmbContextConsumerMixin(LitElement) {
});
}
private _useCurrentSection () {
private _useCurrentSection() {
this._currentSectionSubscription?.unsubscribe();
this._currentSectionSubscription = this._sectionContext?.getCurrent()
.subscribe(section => {
this._currentSectionSubscription = this._sectionContext?.getCurrent().subscribe((section) => {
this._createSectionElement(section);
});
}
@@ -56,12 +55,12 @@ export class UmbBackofficeMain extends UmbContextConsumerMixin(LitElement) {
this._currentSectionSubscription?.unsubscribe();
}
private async _createSectionElement (section: UmbExtensionManifest<UmbManifestSectionMeta>) {
private async _createSectionElement(section: UmbExtensionManifest<UmbManifestSectionMeta>) {
if (!section) return;
// TODO: How do we handle dynamic imports of our files?
if (section.js) {
await import(/* @vite-ignore */section.js);
await import(/* @vite-ignore */ section.js);
}
if (section.elementName) {
@@ -70,9 +69,7 @@ export class UmbBackofficeMain extends UmbContextConsumerMixin(LitElement) {
}
render() {
return html`
${ this._sectionElement }
`;
return html` ${this._sectionElement} `;
}
}

View File

@@ -0,0 +1,88 @@
import { css, html, LitElement } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement } from 'lit/decorators.js';
import '../properties/node-property.element.ts';
import '../properties/property-editor-text.element.ts';
import '../properties/property-editor-textarea.element.ts';
@customElement('umb-node-editor-layout')
class UmbNodeEditorLayout extends LitElement {
static styles = [
UUITextStyles,
css`
:host {
display: block;
width: 100%;
height: 100%;
}
#node-editor {
background-color: var(--uui-color-background);
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
#header {
background-color: var(--uui-color-surface);
width: 100%;
display: flex;
flex: none;
gap: 16px;
align-items: center;
border-bottom: 1px solid var(--uui-color-border);
}
#content {
padding: var(--uui-size-6);
display: flex;
flex: 1;
flex-direction: column;
gap: 16px;
}
#footer {
display: flex;
flex: none;
justify-content: end;
align-items: center;
height: 70px;
width: 100%;
gap: 16px;
padding-right: 24px;
border-top: 1px solid var(--uui-color-border);
background-color: var(--uui-color-surface);
box-sizing: border-box;
}
`,
];
private _onSaveAndPublish() {
console.log('Save and publish');
}
render() {
return html`
<div id="node-editor">
<div id="header">
<slot name="name"></slot>
<slot name="apps"></slot>
</div>
<uui-scroll-container id="content">
<slot name="content"></slot>
</uui-scroll-container>
<div id="footer">
<slot name="actions"></slot>
</div>
</div>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'umb-node-editor-layout': UmbNodeEditorLayout;
}
}

View File

@@ -1,136 +0,0 @@
import { css, html, LitElement } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement } from 'lit/decorators.js';
import '../properties/node-property.element.ts';
import '../properties/property-editor-text.element.ts';
import '../properties/property-editor-textarea.element.ts';
@customElement('umb-node-editor')
class UmbNodeEditor extends LitElement {
static styles = [
UUITextStyles,
css`
:host {
display: block;
width: 100%;
height: 100%;
}
#node-editor {
background-color: var(--uui-color-background);
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
#node-editor-top {
background-color: var(--uui-color-surface);
width: 100%;
display: flex;
flex: none;
gap: 16px;
align-items: center;
border-bottom: 1px solid var(--uui-color-border);
}
#node-editor-top uui-input {
width: 100%;
margin-left: 16px;
}
#node-editor-top uui-tab-group {
--uui-tab-divider: var(--uui-color-border);
border-left: 1px solid var(--uui-color-border);
flex-wrap: nowrap;
height: 60px;
}
#node-editor-content {
padding: var(--uui-size-6);
display: flex;
flex: 1;
flex-direction: column;
gap: 16px;
}
uui-tab {
font-size: 0.8rem;
}
#node-editor-bottom {
display: flex;
flex: none;
justify-content: end;
align-items: center;
height: 70px;
width: 100%;
gap: 16px;
padding-right: 24px;
border-top: 1px solid var(--uui-color-border);
background-color: var(--uui-color-surface);
box-sizing: border-box;
}
.property {
display: grid;
grid-template-columns: 200px 600px;
gap: 32px;
}
.property > .property-label > p {
color: var(--uui-color-text-alt);
}
.property uui-input,
.property uui-textarea {
width: 100%;
}
uui-box hr {
margin-bottom: var(--uui-size-6);
}
hr {
border: 0;
border-top: 1px solid var(--uui-color-border-alt);
}
`,
];
render() {
return html`
<div id="node-editor">
<div id="node-editor-top">
<uui-input value="Home"></uui-input>
<uui-tab-group>
<uui-tab active>Content</uui-tab>
<uui-tab>Info</uui-tab>
<uui-tab disabled>Actions</uui-tab>
</uui-tab-group>
</div>
<uui-scroll-container id="node-editor-content">
<uui-box>
<umb-node-property label="Text string label" description="This is the a text string property">
<umb-property-editor-text></umb-property-editor-text>
</umb-node-property>
<hr />
<umb-node-property label="Textarea label" description="this is a textarea property">
<umb-property-editor-textarea></umb-property-editor-textarea>
</umb-node-property>
</uui-box>
</uui-scroll-container>
<div id="node-editor-bottom">
<uui-button>Save and preview</uui-button>
<uui-button look="secondary">Save</uui-button>
<uui-button look="primary" color="positive">Save and publish</uui-button>
</div>
</div>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'umb-node-editor': UmbNodeEditor;
}
}

View File

@@ -0,0 +1,95 @@
import { css, html, LitElement } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement } from 'lit/decorators.js';
import '../properties/node-property.element.ts';
import '../properties/property-editor-text.element.ts';
import '../properties/property-editor-textarea.element.ts';
import '../backoffice/node-editor-layout.element.ts';
@customElement('umb-content-editor')
class UmbContentEditor extends LitElement {
static styles = [
UUITextStyles,
css`
:host {
display: block;
width: 100%;
height: 100%;
}
uui-input {
width: 100%;
margin-left: 16px;
}
uui-tab-group {
--uui-tab-divider: var(--uui-color-border);
border-left: 1px solid var(--uui-color-border);
flex-wrap: nowrap;
height: 60px;
}
uui-tab {
font-size: 0.8rem;
}
uui-box hr {
margin-bottom: var(--uui-size-6);
}
hr {
border: 0;
/* TODO: Use correct color property */
border-top: 1px solid #e7e7e7;
}
`,
];
private _onSaveAndPublish() {
console.log('Save and publish');
}
private _onSave() {
console.log('Save');
}
private _onSaveAndPreview() {
console.log('Save and preview');
}
render() {
return html`
<umb-node-editor-layout>
<uui-input slot="name" value="Home"></uui-input>
<uui-tab-group slot="apps">
<uui-tab active>Content</uui-tab>
<uui-tab>Info</uui-tab>
<uui-tab disabled>Actions</uui-tab>
</uui-tab-group>
<uui-box slot="content">
<umb-node-property label="Text string label" description="This is the a text string property">
<umb-property-editor-text></umb-property-editor-text>
</umb-node-property>
<hr />
<umb-node-property label="Textarea label" description="this is a textarea property">
<umb-property-editor-textarea></umb-property-editor-textarea>
</umb-node-property>
</uui-box>
<div slot="actions">
<uui-button @click=${this._onSaveAndPreview}>Save and preview</uui-button>
<uui-button @click=${this._onSave} look="secondary">Save</uui-button>
<uui-button @click=${this._onSaveAndPublish} look="primary" color="positive">Save and publish</uui-button>
</div>
</umb-node-editor-layout>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'umb-content-editor': UmbContentEditor;
}
}

View File

@@ -1,7 +1,7 @@
import { defineElement } from '@umbraco-ui/uui-base/lib/registration';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import './content-editor.element';
@defineElement('umb-content-section')
export class UmbContentSection extends LitElement {
static styles = [
@@ -16,7 +16,7 @@ export class UmbContentSection extends LitElement {
];
render() {
return html`<umb-node-editor></umb-node-editor>`;
return html`<umb-content-editor></umb-content-editor>`;
}
}

View File

@@ -19,16 +19,17 @@ describe('UmbContextConsumer', () => {
describe('Public API', () => {
describe('methods', () => {
it('has a dispatchRequest method', () => {
expect(consumer).to.have.property('dispatchRequest').that.is.a('function');
it('has a request method', () => {
expect(consumer).to.have.property('request').that.is.a('function');
});
});
describe('events', () => {
it('dispatches request context event when constructed', async () => {
it('dispatches context request event when constructed', async () => {
const listener = oneEvent(window, umbContextRequestEventType);
// eslint-disable-next-line @typescript-eslint/no-empty-function
new UmbContextConsumer(document.body, testContextKey, () => {});
consumer.attach();
const event = await listener as unknown as UmbContextRequestEventImplementation;
expect(event).to.exist;
expect(event.type).to.eq(umbContextRequestEventType);
@@ -39,14 +40,16 @@ describe('UmbContextConsumer', () => {
it('works with UmbContextProvider', (done: any) => {
const provider = new UmbContextProvider(document.body, testContextKey, new MyClass());
provider.attach();
const element = document.createElement('div');
document.body.appendChild(element);
new UmbContextConsumer(element, testContextKey, (_instance) => {
const localConsumer = new UmbContextConsumer(element, testContextKey, (_instance) => {
expect(_instance.prop).to.eq('value from provider');
done();
});
})
localConsumer.attach();
provider.detach();
});

View File

@@ -9,13 +9,13 @@ export class UmbContextConsumer {
/**
* Creates an instance of UmbContextConsumer.
* @param {HTMLElement} element
* @param {EventTarget} target
* @param {string} _contextKey
* @param {UmbContextCallback} _callback
* @memberof UmbContextConsumer
*/
constructor (
protected element: HTMLElement,
protected target: EventTarget,
private _contextKey: string,
private _callback: UmbContextCallback
) {
@@ -27,7 +27,7 @@ export class UmbContextConsumer {
*/
public request() {
const event = new UmbContextRequestEventImplementation(this._contextKey, this._callback);
this.element.dispatchEvent(event);
this.target.dispatchEvent(event);
}
public attach() {

View File

@@ -17,7 +17,7 @@ describe('UmbContextProvideEvent', () => {
expect(event.composed).to.be.true;
});
it('is cancelable', () => {
expect(event.composed).to.be.false;
it('is not cancelable', () => {
expect(event.cancelable).to.be.false;
});
});

View File

@@ -1,11 +1,12 @@
import { expect, fixture, html } from '@open-wc/testing';
import { UmbContextProvider } from './context-provider';
import { UmbContextProviderMixin } from './context-provider.mixin';
class MyClass {
prop: string;
constructor(text: string) {
this.prop = text
this.prop = text;
}
}
@@ -20,8 +21,14 @@ class MyTestProviderElement extends UmbContextProviderMixin(HTMLElement) {
customElements.define('my-test-provider-element', MyTestProviderElement);
describe('UmbContextProviderMixin', async () => {
const element: MyTestProviderElement = await fixture(html`<my-test-provider-element></my-test-provider-element>`);
const _providers = (element as any)['_providers'];
let element: MyTestProviderElement;
let _providers: Map<string, UmbContextProvider>;
beforeEach(async () => {
element = await fixture(html`<my-test-provider-element></my-test-provider-element>`);
_providers = (element as any)['_providers'];
});
it('sets all providers to element', () => {
expect(_providers.has('my-test-context-1')).to.be.true;
@@ -32,8 +39,8 @@ describe('UmbContextProviderMixin', async () => {
const provider = _providers.get('my-test-context-1');
expect(provider).to.not.be.undefined;
if (!provider) return;
expect(provider['_instance'].prop).to.eq('context value 1');
expect((provider['_instance'] as MyClass).prop).to.eq('context value 1');
element.provideContext('my-test-context-1', new MyClass('new context value 1'));
expect(provider['_instance'].prop).to.eq('context value 1');
expect((provider['_instance'] as MyClass).prop).to.eq('context value 1');
});
});

View File

@@ -12,6 +12,7 @@ describe('UmbContextProvider', () => {
beforeEach(() => {
provider = new UmbContextProvider(document.body, 'my-test-context', new MyClass());
provider.attach();
});
afterEach(async () => {
@@ -38,7 +39,7 @@ describe('UmbContextProvider', () => {
});
it('handles context request events', (done) => {
const event = new UmbContextRequestEventImplementation('my-test-context', (_instance) => {
const event = new UmbContextRequestEventImplementation('my-test-context', (_instance: MyClass) => {
expect(_instance.prop).to.eq('value from provider');
done();
});
@@ -50,9 +51,11 @@ describe('UmbContextProvider', () => {
const element = document.createElement('div');
document.body.appendChild(element);
new UmbContextConsumer(element, 'my-test-context', (_instance) => {
const localConsumer = new UmbContextConsumer(element, 'my-test-context', (_instance: MyClass) => {
expect(_instance.prop).to.eq('value from provider');
done();
localConsumer.detach();
});
localConsumer.attach();
});
});

View File

@@ -6,18 +6,18 @@ import { UmbContextProvideEventImplementation } from './context-provide.event';
* @class UmbContextProvider
*/
export class UmbContextProvider {
protected host: HTMLElement;
protected host: EventTarget;
private _contextKey: string;
private _instance: any;
private _instance: unknown;
/**
* Creates an instance of UmbContextProvider.
* @param {HTMLElement} host
* @param {EventTarget} host
* @param {string} contextKey
* @param {*} instance
* @memberof UmbContextProvider
*/
constructor (host: HTMLElement, contextKey: string, instance: unknown) {
constructor (host: EventTarget, contextKey: string, instance: unknown) {
this.host = host;
this._contextKey = contextKey;
this._instance = instance;
@@ -36,7 +36,7 @@ export class UmbContextProvider {
*/
public detach () {
this.host.removeEventListener(umbContextRequestEventType, this._handleContextRequest);
// TODO: fire unprovide event.
// TODO: fire unprovided event.
}
/**

View File

@@ -25,6 +25,6 @@ describe('UmbContextRequestEvent', () => {
});
it('is cancelable', () => {
expect(event.composed).to.be.true;
expect(event.cancelable).to.be.true;
});
});

View File

@@ -22,15 +22,6 @@ class UmbNodeProperty extends LitElement {
.property uui-textarea {
width: 100%;
}
uui-box hr {
margin-bottom: var(--uui-size-6);
}
hr {
border: 0;
border-top: 1px solid var(--uui-color-border-alt);
}
`,
];