Merge remote-tracking branch 'origin/main' into feature/tree-navigator
This commit is contained in:
@@ -43,6 +43,8 @@ jobs:
|
||||
# retention-days: 30
|
||||
- name: Report code coverage
|
||||
uses: zgosalvez/github-actions-report-lcov@v1
|
||||
if: always()
|
||||
continue-on-error: true
|
||||
with:
|
||||
coverage-files: coverage/lcov.info
|
||||
artifact-name: code-coverage-report
|
||||
|
||||
20
src/Umbraco.Web.UI.Client/.github/workflows/dependency-review.yml
vendored
Normal file
20
src/Umbraco.Web.UI.Client/.github/workflows/dependency-review.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
# Dependency Review Action
|
||||
#
|
||||
# This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging.
|
||||
#
|
||||
# Source repository: https://github.com/actions/dependency-review-action
|
||||
# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement
|
||||
name: 'Dependency Review'
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v3
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v2
|
||||
34
src/Umbraco.Web.UI.Client/.github/workflows/devskim.yml
vendored
Normal file
34
src/Umbraco.Web.UI.Client/.github/workflows/devskim.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
name: DevSkim
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
schedule:
|
||||
- cron: '19 14 * * 5'
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: DevSkim
|
||||
runs-on: ubuntu-20.04
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Run DevSkim scanner
|
||||
uses: microsoft/DevSkim-Action@v1
|
||||
|
||||
- name: Upload DevSkim scan results to GitHub Security tab
|
||||
uses: github/codeql-action/upload-sarif@v2
|
||||
with:
|
||||
sarif_file: devskim-results.sarif
|
||||
@@ -1,26 +1,91 @@
|
||||
import { rest } from 'msw';
|
||||
|
||||
import umbracoPath from '../src/core/helpers/umbraco-path';
|
||||
import { StatusResponse } from '../src/core/models';
|
||||
import { ProblemDetails, StatusResponse } from '../src/core/models';
|
||||
import { expect, test } from '../test';
|
||||
|
||||
test('installer is shown', async ({ page, worker }) => {
|
||||
await worker.use(
|
||||
// Override the server status to be "must-install"
|
||||
rest.get(umbracoPath('/server/status'), (_req, res, ctx) => {
|
||||
return res(
|
||||
// Respond with a 200 status code
|
||||
ctx.status(200),
|
||||
ctx.json<StatusResponse>({
|
||||
serverStatus: 'must-install',
|
||||
test.describe('installer tests', () => {
|
||||
test.beforeEach(async ({ page, worker }) => {
|
||||
await worker.use(
|
||||
// Override the server status to be "must-install"
|
||||
rest.get(umbracoPath('/server/status'), (_req, res, ctx) => {
|
||||
return res(
|
||||
// Respond with a 200 status code
|
||||
ctx.status(200),
|
||||
ctx.json<StatusResponse>({
|
||||
serverStatus: 'must-install',
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
await page.goto('/install');
|
||||
|
||||
await page.waitForSelector('[data-test="installer"]');
|
||||
});
|
||||
|
||||
test('installer is shown', async ({ page }) => {
|
||||
await expect(page).toHaveURL('/install');
|
||||
});
|
||||
|
||||
test.describe('test success and failure', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.waitForSelector('[data-test="installer-user"]');
|
||||
await page.fill('[aria-label="name"]', 'Test');
|
||||
await page.fill('[aria-label="email"]', 'test@umbraco');
|
||||
await page.fill('[aria-label="password"]', 'test123456');
|
||||
await page.click('[name="subscribeToNewsletter"]');
|
||||
|
||||
// Go to the next step
|
||||
await page.click('[aria-label="Next"]');
|
||||
|
||||
// Set telemetry
|
||||
await page.waitForSelector('[data-test="installer-telemetry"]');
|
||||
await page.waitForSelector('uui-slider[name="telemetryLevel"]');
|
||||
|
||||
// Click [aria-label="Next"]
|
||||
await page.click('[aria-label="Next"]');
|
||||
|
||||
// Database form
|
||||
await page.waitForSelector('[data-test="installer-database"]');
|
||||
});
|
||||
|
||||
test('installer completes successfully', async ({ page }) => {
|
||||
await page.click('[aria-label="Install"]');
|
||||
await page.waitForSelector('umb-backoffice', { timeout: 30000 });
|
||||
});
|
||||
|
||||
test('installer fails', async ({ page, worker }) => {
|
||||
await worker.use(
|
||||
// Override the server status to be "must-install"
|
||||
rest.post(umbracoPath('/install/setup'), (_req, res, ctx) => {
|
||||
return res(
|
||||
// Respond with a 200 status code
|
||||
ctx.status(400),
|
||||
ctx.json<ProblemDetails>({
|
||||
status: 400,
|
||||
type: 'validation',
|
||||
detail: 'Something went wrong',
|
||||
errors: {
|
||||
databaseName: ['The database name is required'],
|
||||
},
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
await page.goto('/install');
|
||||
await page.click('[aria-label="Install"]');
|
||||
|
||||
await page.waitForSelector('[data-test="installer"]');
|
||||
await page.waitForSelector('[data-test="installer-error"]');
|
||||
|
||||
await expect(page).toHaveURL('/install');
|
||||
await expect(page.locator('[data-test="error-message"]')).toHaveText('Something went wrong', {
|
||||
useInnerText: true,
|
||||
});
|
||||
|
||||
// Click reset button
|
||||
await page.click('#button-reset');
|
||||
|
||||
await page.waitForSelector('[data-test="installer-user"]');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
62
src/Umbraco.Web.UI.Client/e2e/upgrader.spec.ts
Normal file
62
src/Umbraco.Web.UI.Client/e2e/upgrader.spec.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { rest } from 'msw';
|
||||
|
||||
import umbracoPath from '../src/core/helpers/umbraco-path';
|
||||
import { ProblemDetails, StatusResponse } from '../src/core/models';
|
||||
import { expect, test } from '../test';
|
||||
|
||||
test.describe('upgrader tests', () => {
|
||||
test.beforeEach(async ({ page, worker }) => {
|
||||
await worker.use(
|
||||
// Override the server status to be "must-install"
|
||||
rest.get(umbracoPath('/server/status'), (_req, res, ctx) => {
|
||||
return res(
|
||||
// Respond with a 200 status code
|
||||
ctx.status(200),
|
||||
ctx.json<StatusResponse>({
|
||||
serverStatus: 'must-upgrade',
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
await page.goto('/upgrade');
|
||||
});
|
||||
|
||||
test('upgrader is shown', async ({ page }) => {
|
||||
await page.waitForSelector('[data-test="upgrader"]');
|
||||
await expect(page).toHaveURL('/upgrade');
|
||||
await expect(page.locator('h1')).toHaveText('Upgrading Umbraco', { useInnerText: true });
|
||||
});
|
||||
|
||||
test('upgrader has a "View Report" button', async ({ page }) => {
|
||||
await expect(page.locator('[data-test="view-report-button"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('upgrader completes successfully', async ({ page }) => {
|
||||
await page.click('[data-test="continue-button"]');
|
||||
await page.waitForSelector('umb-backoffice', { timeout: 30000 });
|
||||
});
|
||||
|
||||
test('upgrader fails and shows error', async ({ page, worker }) => {
|
||||
await worker.use(
|
||||
// Override the server status to be "must-install"
|
||||
rest.post(umbracoPath('/upgrade/authorize'), (_req, res, ctx) => {
|
||||
return res(
|
||||
// Respond with a 200 status code
|
||||
ctx.status(400),
|
||||
ctx.json<ProblemDetails>({
|
||||
status: 400,
|
||||
type: 'error',
|
||||
detail: 'Something went wrong',
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
await page.click('[data-test="continue-button"]');
|
||||
|
||||
await expect(page.locator('[data-test="error-message"]')).toHaveText('Something went wrong', {
|
||||
useInnerText: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
2376
src/Umbraco.Web.UI.Client/package-lock.json
generated
2376
src/Umbraco.Web.UI.Client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -36,9 +36,9 @@
|
||||
"npm": ">=8.0.0 < 9"
|
||||
},
|
||||
"dependencies": {
|
||||
"@umbraco-ui/uui": "^1.0.0-rc.2",
|
||||
"element-internals-polyfill": "^1.1.6",
|
||||
"lit": "^2.2.8",
|
||||
"@umbraco-ui/uui": "^1.0.0-rc.3",
|
||||
"element-internals-polyfill": "^1.1.9",
|
||||
"lit": "^2.3.1",
|
||||
"openapi-typescript-fetch": "^1.1.3",
|
||||
"router-slot": "^1.5.5",
|
||||
"rxjs": "^7.5.6",
|
||||
@@ -50,7 +50,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.18.10",
|
||||
"@mdx-js/react": "^2.1.2",
|
||||
"@mdx-js/react": "^2.1.3",
|
||||
"@open-wc/testing": "^3.1.6",
|
||||
"@playwright/test": "^1.25.0",
|
||||
"@storybook/addon-a11y": "^6.5.10",
|
||||
@@ -63,26 +63,26 @@
|
||||
"@types/chai": "^4.3.1",
|
||||
"@types/mocha": "^9.1.1",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.32.0",
|
||||
"@typescript-eslint/parser": "^5.32.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.33.1",
|
||||
"@typescript-eslint/parser": "^5.33.1",
|
||||
"@web/dev-server-esbuild": "^0.3.1",
|
||||
"@web/test-runner": "^0.14.0",
|
||||
"@web/test-runner-playwright": "^0.8.9",
|
||||
"babel-loader": "^8.2.5",
|
||||
"eslint": "^8.21.0",
|
||||
"eslint": "^8.22.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-import-resolver-typescript": "^3.4.0",
|
||||
"eslint-import-resolver-typescript": "^3.4.2",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-lit": "^1.6.1",
|
||||
"eslint-plugin-lit-a11y": "^2.2.2",
|
||||
"eslint-plugin-storybook": "^0.6.4",
|
||||
"lit-html": "^2.2.7",
|
||||
"msw": "^0.44.2",
|
||||
"lit-html": "^2.3.1",
|
||||
"msw": "^0.45.0",
|
||||
"msw-storybook-addon": "^1.6.3",
|
||||
"playwright-msw": "^1.0.0",
|
||||
"prettier": "2.7.1",
|
||||
"typescript": "^4.7.4",
|
||||
"vite": "^3.0.3"
|
||||
"vite": "^3.0.9"
|
||||
},
|
||||
"msw": {
|
||||
"workerDirectory": "public"
|
||||
|
||||
@@ -99,9 +99,9 @@ const config: PlaywrightTestConfig = {
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
command: 'npm run dev -- --mode e2e',
|
||||
port: 5173,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
command: 'npm run dev -- --port 8000 --mode e2e',
|
||||
port: 8000,
|
||||
reuseExistingServer: false,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
/* tslint:disable */
|
||||
|
||||
/**
|
||||
* Mock Service Worker (0.44.2).
|
||||
* Mock Service Worker (0.45.0).
|
||||
* @see https://github.com/mswjs/msw
|
||||
* - Please do NOT modify this file.
|
||||
* - Please do NOT serve this file on production.
|
||||
|
||||
@@ -2,12 +2,13 @@ import { defineElement } from '@umbraco-ui/uui-base/lib/registration';
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
|
||||
import { UmbContextProviderMixin } from '../core/context';
|
||||
import { UmbContextProviderMixin, UmbContextConsumerMixin } from '../core/context';
|
||||
import { UmbNotificationService } from '../core/services/notification';
|
||||
import { UmbModalService } from '../core/services/modal';
|
||||
import { UmbDataTypeStore } from '../core/stores/data-type.store';
|
||||
import { UmbDocumentTypeStore } from '../core/stores/document-type.store';
|
||||
import { UmbNodeStore } from '../core/stores/node.store';
|
||||
import { UmbSectionStore } from '../core/stores/section.store';
|
||||
|
||||
import './components/backoffice-header.element';
|
||||
import './components/backoffice-main.element';
|
||||
@@ -15,12 +16,13 @@ import './components/backoffice-notification-container.element';
|
||||
import './components/backoffice-modal-container.element';
|
||||
import './components/editor-property-layout.element';
|
||||
import './components/node-property.element';
|
||||
import './components/section-layout.element';
|
||||
import './components/section-sidebar.element';
|
||||
import './components/section-main.element';
|
||||
import './sections/shared/section-layout.element';
|
||||
import './sections/shared/section-sidebar.element';
|
||||
import './sections/shared/section-main.element';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@defineElement('umb-backoffice')
|
||||
export default class UmbBackoffice extends UmbContextProviderMixin(LitElement) {
|
||||
export default class UmbBackoffice extends UmbContextConsumerMixin(UmbContextProviderMixin(LitElement)) {
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
@@ -36,6 +38,9 @@ export default class UmbBackoffice extends UmbContextProviderMixin(LitElement) {
|
||||
`,
|
||||
];
|
||||
|
||||
private _umbSectionStore?: UmbSectionStore;
|
||||
private _currentSectionSubscription?: Subscription;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
@@ -44,6 +49,17 @@ export default class UmbBackoffice extends UmbContextProviderMixin(LitElement) {
|
||||
this.provideContext('umbDocumentTypeStore', new UmbDocumentTypeStore());
|
||||
this.provideContext('umbNotificationService', new UmbNotificationService());
|
||||
this.provideContext('umbModalService', new UmbModalService());
|
||||
|
||||
// TODO: how do we want to handle context aware DI?
|
||||
this.consumeContext('umbExtensionRegistry', (extensionRegistry) => {
|
||||
this._umbSectionStore = new UmbSectionStore(extensionRegistry);
|
||||
this.provideContext('umbSectionStore', this._umbSectionStore);
|
||||
});
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._currentSectionSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -3,14 +3,15 @@ import { css, CSSResultGroup, html, LitElement } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
import { when } from 'lit/directives/when.js';
|
||||
import { isPathActive, path } from 'router-slot';
|
||||
import { map, Subscription } from 'rxjs';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { getUserSections } from '../../core/api/fetcher';
|
||||
import { UmbContextConsumerMixin } from '../../core/context';
|
||||
import { UmbExtensionManifestSection, UmbExtensionRegistry } from '../../core/extension';
|
||||
import { UmbContextConsumerMixin, UmbContextProvider, UmbContextProviderMixin } from '../../core/context';
|
||||
import { UmbExtensionManifestSection } from '../../core/extension';
|
||||
import { UmbSectionStore } from '../../core/stores/section.store';
|
||||
import { UmbSectionContext } from '../sections/section.context';
|
||||
|
||||
@customElement('umb-backoffice-header-sections')
|
||||
export class UmbBackofficeHeaderSections extends UmbContextConsumerMixin(LitElement) {
|
||||
export class UmbBackofficeHeaderSections extends UmbContextProviderMixin(UmbContextConsumerMixin(LitElement)) {
|
||||
static styles: CSSResultGroup = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
@@ -40,9 +41,6 @@ export class UmbBackofficeHeaderSections extends UmbContextConsumerMixin(LitElem
|
||||
@state()
|
||||
private _open = false;
|
||||
|
||||
@state()
|
||||
private _allowedSection: Array<string> = [];
|
||||
|
||||
@state()
|
||||
private _sections: Array<UmbExtensionManifestSection> = [];
|
||||
|
||||
@@ -55,16 +53,18 @@ export class UmbBackofficeHeaderSections extends UmbContextConsumerMixin(LitElem
|
||||
@state()
|
||||
private _currentSectionAlias = '';
|
||||
|
||||
private _extensionRegistry?: UmbExtensionRegistry;
|
||||
private _sectionStore?: UmbSectionStore;
|
||||
|
||||
private _sectionSubscription?: Subscription;
|
||||
private _currentSectionSubscription?: Subscription;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.consumeContext('umbExtensionRegistry', (extensionRegistry: UmbExtensionRegistry) => {
|
||||
this._extensionRegistry = extensionRegistry;
|
||||
this.consumeContext('umbSectionStore', (sectionStore: UmbSectionStore) => {
|
||||
this._sectionStore = sectionStore;
|
||||
this._useSections();
|
||||
this._useCurrentSection();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -77,9 +77,11 @@ export class UmbBackofficeHeaderSections extends UmbContextConsumerMixin(LitElem
|
||||
const tab = e.currentTarget as HTMLElement;
|
||||
|
||||
// TODO: we need to be able to prevent the tab from setting the active state
|
||||
if (tab.id === 'moreTab') {
|
||||
return;
|
||||
}
|
||||
if (tab.id === 'moreTab') return;
|
||||
|
||||
if (!tab.dataset.alias) return;
|
||||
|
||||
this._sectionStore?.setCurrent(tab.dataset.alias);
|
||||
}
|
||||
|
||||
private _handleLabelClick() {
|
||||
@@ -89,19 +91,21 @@ export class UmbBackofficeHeaderSections extends UmbContextConsumerMixin(LitElem
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private async _useSections() {
|
||||
private _useSections() {
|
||||
this._sectionSubscription?.unsubscribe();
|
||||
|
||||
const { data } = await getUserSections({});
|
||||
this._allowedSection = data.sections;
|
||||
this._sectionSubscription = this._sectionStore?.getAllowed().subscribe((allowedSections) => {
|
||||
this._sections = allowedSections;
|
||||
this._visibleSections = this._sections;
|
||||
});
|
||||
}
|
||||
|
||||
this._sectionSubscription = this._extensionRegistry
|
||||
?.extensionsOfType('section')
|
||||
.pipe(map((extensions) => extensions.sort((a, b) => b.meta.weight - a.meta.weight)))
|
||||
.subscribe((sections) => {
|
||||
this._sections = sections.filter((section) => this._allowedSection.includes(section.alias));
|
||||
this._visibleSections = this._sections;
|
||||
});
|
||||
private _useCurrentSection() {
|
||||
this._currentSectionSubscription?.unsubscribe();
|
||||
|
||||
this._currentSectionSubscription = this._sectionStore?.currentAlias.subscribe((currentSectionAlias) => {
|
||||
this._currentSectionAlias = currentSectionAlias;
|
||||
});
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
@@ -109,15 +113,17 @@ export class UmbBackofficeHeaderSections extends UmbContextConsumerMixin(LitElem
|
||||
this._sectionSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
render() {
|
||||
private _renderSections() {
|
||||
return html`
|
||||
<uui-tab-group id="tabs">
|
||||
${this._visibleSections.map(
|
||||
(section: UmbExtensionManifestSection) => html`
|
||||
<uui-tab
|
||||
?active="${isPathActive(`/section/${section.meta.pathname}`, path())}"
|
||||
@click="${this._handleTabClick}"
|
||||
?active="${this._currentSectionAlias === section.alias}"
|
||||
href="${`/section/${section.meta.pathname}`}"
|
||||
label="${section.name}"></uui-tab>
|
||||
label="${section.name}"
|
||||
data-alias="${section.alias}"></uui-tab>
|
||||
`
|
||||
)}
|
||||
${this._renderExtraSections()}
|
||||
@@ -150,6 +156,10 @@ export class UmbBackofficeHeaderSections extends UmbContextConsumerMixin(LitElem
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return html` ${this._renderSections()} `;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -2,13 +2,16 @@ 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 { map, Subscription } from 'rxjs';
|
||||
import { IRoutingInfo } from 'router-slot';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { UmbContextConsumerMixin } from '../../core/context';
|
||||
import { createExtensionElement, UmbExtensionManifestSection, UmbExtensionRegistry } from '../../core/extension';
|
||||
import { UmbContextConsumerMixin, UmbContextProviderMixin } from '../../core/context';
|
||||
import { createExtensionElement, UmbExtensionManifestSection } from '../../core/extension';
|
||||
import { UmbSectionStore } from '../../core/stores/section.store';
|
||||
import { UmbSectionContext } from '../sections/section.context';
|
||||
|
||||
@defineElement('umb-backoffice-main')
|
||||
export class UmbBackofficeMain extends UmbContextConsumerMixin(LitElement) {
|
||||
export class UmbBackofficeMain extends UmbContextProviderMixin(UmbContextConsumerMixin(LitElement)) {
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
@@ -28,40 +31,61 @@ export class UmbBackofficeMain extends UmbContextConsumerMixin(LitElement) {
|
||||
@state()
|
||||
private _sections: Array<UmbExtensionManifestSection> = [];
|
||||
|
||||
private _extensionRegistry?: UmbExtensionRegistry;
|
||||
private _routePrefix = 'section/';
|
||||
private _sectionContext?: UmbSectionContext;
|
||||
private _sectionStore?: UmbSectionStore;
|
||||
private _sectionSubscription?: Subscription;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.consumeContext('umbExtensionRegistry', (_instance: UmbExtensionRegistry) => {
|
||||
this._extensionRegistry = _instance;
|
||||
this.consumeContext('umbSectionStore', (_instance: UmbSectionStore) => {
|
||||
this._sectionStore = _instance;
|
||||
this._useSections();
|
||||
});
|
||||
}
|
||||
|
||||
private _useSections() {
|
||||
private async _useSections() {
|
||||
this._sectionSubscription?.unsubscribe();
|
||||
|
||||
this._sectionSubscription = this._extensionRegistry
|
||||
?.extensionsOfType('section')
|
||||
.pipe(map((extensions) => extensions.sort((a, b) => b.meta.weight - a.meta.weight)))
|
||||
.subscribe((sections) => {
|
||||
this._routes = [];
|
||||
this._sections = sections as Array<UmbExtensionManifestSection>;
|
||||
this._sectionSubscription = this._sectionStore?.getAllowed().subscribe((sections) => {
|
||||
if (!sections) return;
|
||||
this._sections = sections;
|
||||
this._createRoutes();
|
||||
});
|
||||
}
|
||||
|
||||
this._routes = this._sections.map((section) => {
|
||||
return {
|
||||
path: 'section/' + section.meta.pathname,
|
||||
component: () => createExtensionElement(section),
|
||||
};
|
||||
});
|
||||
private _createRoutes() {
|
||||
this._routes = [];
|
||||
this._routes = this._sections.map((section) => {
|
||||
return {
|
||||
path: this._routePrefix + section.meta.pathname,
|
||||
component: () => createExtensionElement(section),
|
||||
setup: this._onRouteSetup,
|
||||
};
|
||||
});
|
||||
|
||||
this._routes.push({
|
||||
path: '**',
|
||||
redirectTo: 'section/' + this._sections[0].meta.pathname,
|
||||
});
|
||||
});
|
||||
this._routes.push({
|
||||
path: '**',
|
||||
redirectTo: this._routePrefix + this._sections?.[0]?.meta.pathname,
|
||||
});
|
||||
}
|
||||
|
||||
private _onRouteSetup = (_component: HTMLElement, info: IRoutingInfo) => {
|
||||
const currentPath = info.match.route.path;
|
||||
const section = this._sections.find((s) => this._routePrefix + s.meta.pathname === currentPath);
|
||||
if (!section) return;
|
||||
this._sectionStore?.setCurrent(section.alias);
|
||||
this._provideSectionContext(section);
|
||||
};
|
||||
|
||||
private _provideSectionContext(section: UmbExtensionManifestSection) {
|
||||
if (!this._sectionContext) {
|
||||
this._sectionContext = new UmbSectionContext(section);
|
||||
this.provideContext('umbSectionContext', this._sectionContext);
|
||||
} else {
|
||||
this._sectionContext.update(section);
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
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 { createExtensionElement, UmbExtensionManifestDashboard, UmbExtensionRegistry } from '../../core/extension';
|
||||
|
||||
@customElement('umb-section-dashboards')
|
||||
export class UmbSectionDashboards extends UmbContextConsumerMixin(LitElement) {
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#tabs {
|
||||
background-color: var(--uui-color-surface);
|
||||
height: 70px;
|
||||
}
|
||||
|
||||
#router-slot {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: var(--uui-size-space-5);
|
||||
display: block;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@state()
|
||||
private _dashboards: Array<UmbExtensionManifestDashboard> = [];
|
||||
|
||||
@state()
|
||||
private _current = '';
|
||||
|
||||
@state()
|
||||
private _routes: Array<any> = [];
|
||||
|
||||
private _extensionRegistry?: UmbExtensionRegistry;
|
||||
private _dashboardsSubscription?: Subscription;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.consumeContext('umbExtensionRegistry', (_instance: UmbExtensionRegistry) => {
|
||||
this._extensionRegistry = _instance;
|
||||
this._useDashboards();
|
||||
});
|
||||
}
|
||||
|
||||
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) => {
|
||||
this._dashboards = dashboards;
|
||||
this._routes = [];
|
||||
|
||||
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;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
this._routes.push({
|
||||
path: '**',
|
||||
redirectTo: this._dashboards[0].meta.pathname,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private _handleTabClick(e: PointerEvent, dashboard: UmbExtensionManifestDashboard) {
|
||||
// TODO: generate URL from context/location. Or use Router-link concept?
|
||||
history.pushState(null, '', `/section/content/dashboard/${dashboard.meta.pathname}`);
|
||||
this._current = dashboard.name;
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._dashboardsSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<uui-tab-group id="tabs">
|
||||
${this._dashboards.map(
|
||||
(dashboard: UmbExtensionManifestDashboard) => html`
|
||||
<uui-tab
|
||||
label=${dashboard.name}
|
||||
?active="${dashboard.meta.pathname === this._current}"
|
||||
@click="${(e: PointerEvent) => this._handleTabClick(e, dashboard)}"></uui-tab>
|
||||
`
|
||||
)}
|
||||
</uui-tab-group>
|
||||
<router-slot id="router-slot" .routes="${this._routes}"></router-slot>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export default UmbSectionDashboards;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-section-dashboards': UmbSectionDashboards;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
|
||||
@customElement('umb-dashboard-examine-management')
|
||||
export class UmbDashboardExamineManagementElement extends LitElement {
|
||||
static styles = [UUITextStyles, css``];
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<uui-box>
|
||||
<h1>Examine Management</h1>
|
||||
</uui-box>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-dashboard-examine-management': UmbDashboardExamineManagementElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
|
||||
@customElement('umb-dashboard-media-management')
|
||||
export class UmbDashboardMediaManagementElement extends LitElement {
|
||||
static styles = [UUITextStyles, css``];
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<uui-box>
|
||||
<h1>Media Management</h1>
|
||||
</uui-box>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-dashboard-media-management': UmbDashboardMediaManagementElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
|
||||
@customElement('umb-dashboard-models-builder')
|
||||
export class UmbDashboardModelsBuilderElement extends LitElement {
|
||||
static styles = [UUITextStyles, css``];
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<uui-box>
|
||||
<h1>Models Builder</h1>
|
||||
</uui-box>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-dashboard-models-builder': UmbDashboardModelsBuilderElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
|
||||
@customElement('umb-dashboard-settings-about')
|
||||
export class UmbDashboardSettingsAboutElement extends LitElement {
|
||||
static styles = [UUITextStyles, css``];
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<uui-box>
|
||||
<h1>Settings</h1>
|
||||
</uui-box>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-dashboard-settings-about': UmbDashboardSettingsAboutElement;
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ export class UmbContentSection extends LitElement {
|
||||
private _routes: Array<IRoute> = [
|
||||
{
|
||||
path: 'dashboard',
|
||||
component: () => import('../../components/section-dashboards.element'),
|
||||
component: () => import('../shared/section-dashboards.element'),
|
||||
setup: () => {
|
||||
this._currentNodeId = undefined;
|
||||
},
|
||||
|
||||
@@ -23,7 +23,7 @@ export class UmbMediaSection extends LitElement {
|
||||
private _routes: Array<IRoute> = [
|
||||
{
|
||||
path: 'dashboard',
|
||||
component: () => import('../../components/section-dashboards.element'),
|
||||
component: () => import('../shared/section-dashboards.element'),
|
||||
setup: () => {
|
||||
this._currentNodeId = undefined;
|
||||
},
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { UmbExtensionManifestSection } from '../../core/extension';
|
||||
|
||||
export class UmbSectionContext {
|
||||
// TODO: figure out how fine grained we want to make our observables.
|
||||
private _data: BehaviorSubject<UmbExtensionManifestSection> = new BehaviorSubject({
|
||||
type: 'section',
|
||||
alias: '',
|
||||
name: '',
|
||||
meta: {
|
||||
pathname: '',
|
||||
weight: 0,
|
||||
},
|
||||
});
|
||||
public readonly data: Observable<UmbExtensionManifestSection> = this._data.asObservable();
|
||||
|
||||
constructor(section: UmbExtensionManifestSection) {
|
||||
if (!section) return;
|
||||
this._data.next(section);
|
||||
}
|
||||
|
||||
// TODO: figure out how we want to update data
|
||||
public update(data: Partial<UmbExtensionManifestSection>) {
|
||||
this._data.next({ ...this._data.getValue(), ...data });
|
||||
}
|
||||
|
||||
public getData() {
|
||||
return this._data.getValue();
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ export class UmbSettingsSection extends UmbContextConsumerMixin(LitElement) {
|
||||
private _routes: Array<IRoute> = [
|
||||
{
|
||||
path: 'dashboard',
|
||||
component: () => import('../../components/section-dashboards.element'),
|
||||
component: () => import('../shared/section-dashboards.element'),
|
||||
},
|
||||
{
|
||||
path: 'extensions',
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
|
||||
import { css, html, LitElement, nothing } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
import { IRoutingInfo } from 'router-slot';
|
||||
import { map, Subscription, first } from 'rxjs';
|
||||
|
||||
import { UmbContextConsumerMixin } from '../../../core/context';
|
||||
import { createExtensionElement, UmbExtensionManifestDashboard, UmbExtensionRegistry } from '../../../core/extension';
|
||||
import { UmbSectionContext } from '../section.context';
|
||||
|
||||
@customElement('umb-section-dashboards')
|
||||
export class UmbSectionDashboards extends UmbContextConsumerMixin(LitElement) {
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#tabs {
|
||||
background-color: var(--uui-color-surface);
|
||||
height: 70px;
|
||||
}
|
||||
|
||||
#router-slot {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: var(--uui-size-space-5);
|
||||
display: block;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@state()
|
||||
private _dashboards: Array<UmbExtensionManifestDashboard> = [];
|
||||
|
||||
@state()
|
||||
private _currentDashboardPathname = '';
|
||||
|
||||
@state()
|
||||
private _routes: Array<any> = [];
|
||||
|
||||
@state()
|
||||
private _currentSectionPathname = '';
|
||||
|
||||
private _currentSectionAlias = '';
|
||||
|
||||
private _extensionRegistry?: UmbExtensionRegistry;
|
||||
private _dashboardsSubscription?: Subscription;
|
||||
|
||||
private _sectionContext?: UmbSectionContext;
|
||||
private _sectionContextSubscription?: Subscription;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// TODO: wait for more contexts
|
||||
this.consumeContext('umbExtensionRegistry', (_instance: UmbExtensionRegistry) => {
|
||||
this._extensionRegistry = _instance;
|
||||
});
|
||||
|
||||
this.consumeContext('umbSectionContext', (context: UmbSectionContext) => {
|
||||
this._sectionContext = context;
|
||||
this._useSectionContext();
|
||||
});
|
||||
}
|
||||
|
||||
private _useSectionContext() {
|
||||
this._sectionContextSubscription?.unsubscribe();
|
||||
|
||||
this._sectionContextSubscription = this._sectionContext?.data.pipe(first()).subscribe((section) => {
|
||||
this._currentSectionAlias = section.alias;
|
||||
this._currentSectionPathname = section.meta.pathname;
|
||||
this._useDashboards();
|
||||
});
|
||||
}
|
||||
|
||||
private _useDashboards() {
|
||||
if (!this._extensionRegistry || !this._currentSectionAlias) return;
|
||||
|
||||
this._dashboardsSubscription?.unsubscribe();
|
||||
|
||||
this._dashboardsSubscription = this._extensionRegistry
|
||||
?.extensionsOfType('dashboard')
|
||||
.pipe(
|
||||
map((extensions) =>
|
||||
extensions
|
||||
.filter((extension) => extension.meta.sections.includes(this._currentSectionAlias))
|
||||
.sort((a, b) => b.meta.weight - a.meta.weight)
|
||||
)
|
||||
)
|
||||
.subscribe((dashboards) => {
|
||||
if (dashboards?.length === 0) return;
|
||||
this._dashboards = dashboards;
|
||||
this._createRoutes();
|
||||
});
|
||||
}
|
||||
|
||||
private _createRoutes() {
|
||||
this._routes = [];
|
||||
|
||||
this._routes = this._dashboards.map((dashboard) => {
|
||||
return {
|
||||
path: `${dashboard.meta.pathname}`,
|
||||
component: () => createExtensionElement(dashboard),
|
||||
setup: (_element: UmbExtensionManifestDashboard, info: IRoutingInfo) => {
|
||||
this._currentDashboardPathname = info.match.route.path;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
this._routes.push({
|
||||
path: '**',
|
||||
redirectTo: this._dashboards?.[0]?.meta.pathname,
|
||||
});
|
||||
}
|
||||
|
||||
private _renderNavigation() {
|
||||
return html`
|
||||
${this._dashboards?.length > 1
|
||||
? html`
|
||||
<uui-tab-group id="tabs">
|
||||
${this._dashboards.map(
|
||||
(dashboard: UmbExtensionManifestDashboard) => html`
|
||||
<uui-tab
|
||||
href="${`/section/${this._currentSectionPathname}/dashboard/${dashboard.meta.pathname}`}"
|
||||
label=${dashboard.meta.label || dashboard.name}
|
||||
?active="${dashboard.meta.pathname === this._currentDashboardPathname}"></uui-tab>
|
||||
`
|
||||
)}
|
||||
</uui-tab-group>
|
||||
`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._dashboardsSubscription?.unsubscribe();
|
||||
this._sectionContextSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
${this._renderNavigation()}
|
||||
<router-slot id="router-slot" .routes="${this._routes}"></router-slot>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export default UmbSectionDashboards;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-section-dashboards': UmbSectionDashboards;
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,7 @@ export type UmbManifestPropertyActionMeta = {
|
||||
// Dashboard:
|
||||
export type UmbManifestDashboardMeta = {
|
||||
sections: Array<string>;
|
||||
label?: string;
|
||||
pathname: string; // TODO: how to we want to support pretty urls?
|
||||
weight: number;
|
||||
};
|
||||
|
||||
30
src/Umbraco.Web.UI.Client/src/core/stores/section.store.ts
Normal file
30
src/Umbraco.Web.UI.Client/src/core/stores/section.store.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { map, Observable, ReplaySubject } from 'rxjs';
|
||||
import { UmbExtensionRegistry } from '../extension';
|
||||
|
||||
export class UmbSectionStore {
|
||||
private _extensionRegistry: UmbExtensionRegistry;
|
||||
|
||||
private _currentAlias: ReplaySubject<string> = new ReplaySubject(1);
|
||||
public readonly currentAlias: Observable<string> = this._currentAlias.asObservable();
|
||||
|
||||
// TODO: how do we want to handle DI in contexts?
|
||||
constructor(extensionRegistry: UmbExtensionRegistry) {
|
||||
this._extensionRegistry = extensionRegistry;
|
||||
}
|
||||
|
||||
public getAllowed() {
|
||||
// TODO: implemented allowed filtering
|
||||
/*
|
||||
const { data } = await getUserSections({});
|
||||
this._allowedSection = data.sections;
|
||||
*/
|
||||
|
||||
return this._extensionRegistry
|
||||
?.extensionsOfType('section')
|
||||
.pipe(map((extensions) => extensions.sort((a, b) => b.meta.weight - a.meta.weight)));
|
||||
}
|
||||
|
||||
public setCurrent(alias: string) {
|
||||
this._currentAlias.next(alias);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '@umbraco-ui/uui-css/dist/uui-css.css';
|
||||
@import '../../node_modules/@umbraco-ui/uui-css/dist/uui-css.css';
|
||||
|
||||
:root {
|
||||
--uui-color-positive: #1c874c;
|
||||
|
||||
@@ -3,4 +3,5 @@ export * from './installer-database.element';
|
||||
export * from './installer-installing.element';
|
||||
export * from './installer-user.element';
|
||||
export * from './installer-layout.element';
|
||||
export * from './installer-error.element';
|
||||
export * from './installer.element';
|
||||
|
||||
@@ -125,7 +125,7 @@ export class UmbInstallerConsent extends UmbContextConsumerMixin(LitElement) {
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div id="container" class="uui-text">
|
||||
<div id="container" class="uui-text" data-test="installer-telemetry">
|
||||
<h1>Consent for telemetry data</h1>
|
||||
${this._renderSlider()}
|
||||
<div id="buttons">
|
||||
|
||||
@@ -4,11 +4,7 @@ import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { UmbContextConsumerMixin } from '../core/context';
|
||||
import {
|
||||
ProblemDetails,
|
||||
UmbracoInstallerDatabaseModel,
|
||||
UmbracoPerformInstallDatabaseConfiguration,
|
||||
} from '../core/models';
|
||||
import { UmbracoInstallerDatabaseModel, UmbracoPerformInstallDatabaseConfiguration } from '../core/models';
|
||||
import { UmbInstallerContext } from './installer-context';
|
||||
|
||||
@customElement('umb-installer-database')
|
||||
@@ -73,19 +69,12 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) {
|
||||
margin-left: auto;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
#error-message {
|
||||
color: var(--uui-color-error, red);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@query('#button-install')
|
||||
private _installButton!: UUIButtonElement;
|
||||
|
||||
@query('#error-message')
|
||||
private _errorMessage!: HTMLElement;
|
||||
|
||||
@property({ attribute: false })
|
||||
public databaseFormData!: UmbracoPerformInstallDatabaseConfiguration;
|
||||
|
||||
@@ -179,17 +168,10 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) {
|
||||
this._installerStore?.appendData({ database });
|
||||
}
|
||||
|
||||
this._installerStore?.requestInstall().then(this._handleFulfilled.bind(this), this._handleRejected.bind(this));
|
||||
this.dispatchEvent(new CustomEvent('submit', { bubbles: true, composed: true }));
|
||||
|
||||
this._installButton.state = 'waiting';
|
||||
};
|
||||
private _handleFulfilled() {
|
||||
this.dispatchEvent(new CustomEvent('next', { bubbles: true, composed: true }));
|
||||
this._installButton.state = undefined;
|
||||
}
|
||||
private _handleRejected(error: ProblemDetails) {
|
||||
this._installButton.state = 'failed';
|
||||
this._errorMessage.innerText = error.type;
|
||||
}
|
||||
|
||||
private _onBack() {
|
||||
this.dispatchEvent(new CustomEvent('previous', { bubbles: true, composed: true }));
|
||||
@@ -197,7 +179,6 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) {
|
||||
|
||||
private get selectedDatabase() {
|
||||
const id = this._installerStore?.getData().database?.id;
|
||||
console.log('selected id', id, this._databases);
|
||||
return this._databases.find((x) => x.id === id) ?? this._databases[0];
|
||||
}
|
||||
|
||||
@@ -333,7 +314,7 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) {
|
||||
`;
|
||||
|
||||
render() {
|
||||
return html` <div id="container" class="uui-text">
|
||||
return html` <div id="container" class="uui-text" data-test="installer-database">
|
||||
<h1 class="uui-h3">Database Configuration</h1>
|
||||
<uui-form>
|
||||
<form id="database-form" name="database" @submit="${this._handleSubmit}">
|
||||
@@ -341,9 +322,6 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) {
|
||||
? this._renderPreConfiguredDatabase(this._preConfiguredDatabase)
|
||||
: this._renderDatabaseSelection()}
|
||||
|
||||
<!-- TODO: Apply error message to the fields that has errors -->
|
||||
<p id="error-message"></p>
|
||||
|
||||
<div id="buttons">
|
||||
<uui-button label="Back" @click=${this._onBack} look="secondary"></uui-button>
|
||||
<uui-button id="button-install" type="submit" label="Install" look="primary" color="positive"></uui-button>
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
|
||||
import { ProblemDetails } from '../core/models';
|
||||
|
||||
@customElement('umb-installer-error')
|
||||
export class UmbInstallerError extends LitElement {
|
||||
static styles: CSSResultGroup = [
|
||||
css`
|
||||
:host,
|
||||
#container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#error-message {
|
||||
color: var(--uui-color-error, red);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@property({ type: Object })
|
||||
error?: ProblemDetails;
|
||||
|
||||
private _handleSubmit(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
this.dispatchEvent(new CustomEvent('reset', { bubbles: true, composed: true }));
|
||||
}
|
||||
|
||||
private _renderError(error: ProblemDetails) {
|
||||
return html`
|
||||
<p id="error-message" data-test="error-message">${error.detail ?? 'Unknown error'}</p>
|
||||
<hr />
|
||||
${error.errors ? this._renderErrors(error.errors) : nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderErrors(errors: Record<string, unknown>) {
|
||||
return html`
|
||||
<ul>
|
||||
${Object.keys(errors).map((key) => html` <li>${key}: ${(errors[key] as string[]).join(', ')}</li> `)}
|
||||
</ul>
|
||||
`;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html` <div id="container" class="uui-text" data-test="installer-error">
|
||||
<uui-form>
|
||||
<form id="installer-form" @submit="${this._handleSubmit}">
|
||||
<h1 class="uui-h3">Installing Umbraco</h1>
|
||||
<h2>Something went wrong</h2>
|
||||
${this.error ? this._renderError(this.error) : nothing}
|
||||
<div id="buttons">
|
||||
<uui-button
|
||||
id="button-reset"
|
||||
type="submit"
|
||||
label="Go back and try again"
|
||||
look="primary"
|
||||
color="positive"></uui-button>
|
||||
</div>
|
||||
</form>
|
||||
</uui-form>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-installer-error': UmbInstallerError;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { css, CSSResultGroup, html, LitElement, PropertyValueMap } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
import { css, CSSResultGroup, html, LitElement } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
|
||||
@customElement('umb-installer-installing')
|
||||
export class UmbInstallerInstalling extends LitElement {
|
||||
static styles: CSSResultGroup = [
|
||||
@@ -10,32 +11,10 @@ export class UmbInstallerInstalling extends LitElement {
|
||||
`,
|
||||
];
|
||||
|
||||
@state()
|
||||
private _installProgress = 0;
|
||||
|
||||
protected firstUpdated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
|
||||
super.firstUpdated(_changedProperties);
|
||||
|
||||
this._updateProgress();
|
||||
}
|
||||
|
||||
private async _updateProgress() {
|
||||
this._installProgress = Math.min(this._installProgress + (Math.random() + 1) * 10, 100);
|
||||
await new Promise((resolve) => setTimeout(resolve, (Math.random() + 1) * 1000));
|
||||
|
||||
if (this._installProgress >= 100) {
|
||||
// Redirect to backoffice
|
||||
history.replaceState(null, '', '/');
|
||||
return;
|
||||
}
|
||||
|
||||
this._updateProgress();
|
||||
}
|
||||
|
||||
render() {
|
||||
return html` <div class="uui-text">
|
||||
return html` <div class="uui-text" data-test="installer-installing">
|
||||
<h1 class="uui-h3">Installing Umbraco</h1>
|
||||
<uui-progress-bar progress=${this._installProgress}></uui-progress-bar>
|
||||
<uui-loader-bar></uui-loader-bar>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ export class UmbInstallerUser extends UmbContextConsumerMixin(LitElement) {
|
||||
};
|
||||
|
||||
render() {
|
||||
return html` <div id="container" class="uui-text">
|
||||
return html` <div id="container" class="uui-text" data-test="installer-user">
|
||||
<h1>Install Umbraco</h1>
|
||||
<uui-form>
|
||||
<form id="LoginForm" name="login" @submit="${this._handleSubmit}">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import './installer-consent.element';
|
||||
import './installer-database.element';
|
||||
import './installer-error.element';
|
||||
import './installer-installing.element';
|
||||
import './installer-layout.element';
|
||||
import './installer-user.element';
|
||||
@@ -7,7 +8,9 @@ import './installer-user.element';
|
||||
import { css, CSSResultGroup, html, LitElement } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
|
||||
import { postInstallSetup } from '../core/api/fetcher';
|
||||
import { UmbContextProviderMixin } from '../core/context';
|
||||
import { ProblemDetails } from '../core/models';
|
||||
import { UmbInstallerContext } from './installer-context';
|
||||
|
||||
@customElement('umb-installer')
|
||||
@@ -17,15 +20,21 @@ export class UmbInstaller extends UmbContextProviderMixin(LitElement) {
|
||||
@state()
|
||||
step = 1;
|
||||
|
||||
private _umbInstallerContext = new UmbInstallerContext();
|
||||
|
||||
private _error?: ProblemDetails;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.provideContext('umbInstallerContext', new UmbInstallerContext());
|
||||
this.provideContext('umbInstallerContext', this._umbInstallerContext);
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.addEventListener('next', () => this._handleNext());
|
||||
this.addEventListener('previous', () => this._goToPreviousStep());
|
||||
this.addEventListener('submit', () => this._handleSubmit());
|
||||
this.addEventListener('reset', () => this._handleReset());
|
||||
}
|
||||
|
||||
private _handleNext() {
|
||||
@@ -36,6 +45,36 @@ export class UmbInstaller extends UmbContextProviderMixin(LitElement) {
|
||||
this.step--;
|
||||
}
|
||||
|
||||
private _handleFulfilled() {
|
||||
console.warn('TODO: Set up real authentication');
|
||||
sessionStorage.setItem('is-authenticated', 'true');
|
||||
history.replaceState(null, '', '/content');
|
||||
}
|
||||
|
||||
private _handleRejected(e: unknown) {
|
||||
if (e instanceof postInstallSetup.Error) {
|
||||
const error = e.getActualType();
|
||||
if (error.status === 400) {
|
||||
this._error = error.data;
|
||||
}
|
||||
}
|
||||
this._handleNext();
|
||||
}
|
||||
|
||||
private _handleSubmit() {
|
||||
this._handleNext();
|
||||
|
||||
this._umbInstallerContext
|
||||
.requestInstall()
|
||||
.then(() => this._handleFulfilled())
|
||||
.catch((error) => this._handleRejected(error));
|
||||
}
|
||||
|
||||
private _handleReset() {
|
||||
this.step = 1;
|
||||
this._error = undefined;
|
||||
}
|
||||
|
||||
private _renderSection() {
|
||||
switch (this.step) {
|
||||
case 2:
|
||||
@@ -44,6 +83,8 @@ export class UmbInstaller extends UmbContextProviderMixin(LitElement) {
|
||||
return html`<umb-installer-database></umb-installer-database>`;
|
||||
case 4:
|
||||
return html`<umb-installer-installing></umb-installer-installing>`;
|
||||
case 5:
|
||||
return html`<umb-installer-error .error=${this._error}></umb-installer-error>`;
|
||||
|
||||
default:
|
||||
return html`<umb-installer-user></umb-installer-user>`;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import '../core/context/context-provider.element';
|
||||
import './installer-consent.element';
|
||||
import './installer-database.element';
|
||||
import './installer-error.element';
|
||||
import './installer-installing.element';
|
||||
import './installer-user.element';
|
||||
|
||||
@@ -8,10 +9,10 @@ import { Meta, Story } from '@storybook/web-components';
|
||||
import { html } from 'lit-html';
|
||||
import { rest } from 'msw';
|
||||
|
||||
import { UmbInstallerUser } from '.';
|
||||
import { UmbracoInstaller } from '../core/models';
|
||||
import { UmbInstallerContext } from './installer-context';
|
||||
|
||||
import type { UmbInstallerError, UmbInstallerUser } from '.';
|
||||
import type { UmbracoInstaller } from '../core/models';
|
||||
export default {
|
||||
title: 'Components/Installer/Steps',
|
||||
component: 'umb-installer',
|
||||
@@ -91,3 +92,27 @@ Step3DatabasePreconfigured.parameters = {
|
||||
|
||||
export const Step4Installing: Story = () => html`<umb-installer-installing></umb-installer-installing>`;
|
||||
Step4Installing.storyName = 'Step 4: Installing';
|
||||
|
||||
export const Step5Error: Story<UmbInstallerError> = ({ error }) =>
|
||||
html`<umb-installer-error .error=${error}></umb-installer-error>`;
|
||||
Step5Error.storyName = 'Step 5: Error';
|
||||
Step5Error.args = {
|
||||
error: {
|
||||
type: 'validation',
|
||||
status: 400,
|
||||
detail: 'The form did not pass validation',
|
||||
title: 'Validation error',
|
||||
errors: {
|
||||
'user.password': [
|
||||
'The password must be at least 6 characters long',
|
||||
'The password must contain at least one number',
|
||||
],
|
||||
databaseName: ['The database name is required'],
|
||||
},
|
||||
},
|
||||
};
|
||||
Step5Error.parameters = {
|
||||
actions: {
|
||||
handles: ['reset'],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -73,16 +73,18 @@ export const handlers = [
|
||||
);
|
||||
}),
|
||||
|
||||
rest.post<PostInstallRequest>(umbracoPath('/install/settings'), async (req, res, ctx) => {
|
||||
rest.post(umbracoPath('/install/setup'), async (req, res, ctx) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, (Math.random() + 1) * 1000)); // simulate a delay of 1-2 seconds
|
||||
const body = await req.json<PostInstallRequest>();
|
||||
|
||||
if (req.body.database?.name === 'fail') {
|
||||
if (body.database?.name === 'fail') {
|
||||
return res(
|
||||
// Respond with a 200 status code
|
||||
ctx.status(400),
|
||||
ctx.json<ProblemDetails>({
|
||||
type: 'validation',
|
||||
status: 400,
|
||||
detail: 'Something went wrong',
|
||||
errors: {
|
||||
name: ['Database name is invalid'],
|
||||
},
|
||||
|
||||
@@ -51,7 +51,7 @@ export const internalManifests: Array<UmbExtensionManifestCore> = [
|
||||
alias: 'Umb.Dashboard.Welcome',
|
||||
name: 'Welcome',
|
||||
elementName: 'umb-dashboard-welcome',
|
||||
js: () => import('./backoffice/dashboards/dashboard-welcome.element'),
|
||||
js: () => import('./backoffice/dashboards/welcome/dashboard-welcome.element'),
|
||||
meta: {
|
||||
sections: ['Umb.Section.Content'],
|
||||
pathname: 'welcome', // TODO: how to we want to support pretty urls?
|
||||
@@ -63,13 +63,62 @@ export const internalManifests: Array<UmbExtensionManifestCore> = [
|
||||
alias: 'Umb.Dashboard.RedirectManagement',
|
||||
name: 'Redirect Management',
|
||||
elementName: 'umb-dashboard-redirect-management',
|
||||
js: () => import('./backoffice/dashboards/dashboard-redirect-management.element'),
|
||||
js: () => import('./backoffice/dashboards/redirect-management/dashboard-redirect-management.element'),
|
||||
meta: {
|
||||
sections: ['Umb.Section.Content'],
|
||||
pathname: 'redirect-management', // TODO: how to we want to support pretty urls?
|
||||
weight: 10,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'dashboard',
|
||||
alias: 'Umb.Dashboard.SettingsAbout',
|
||||
name: 'Settings About',
|
||||
elementName: 'umb-dashboard-settings-about',
|
||||
js: () => import('./backoffice/dashboards/settings-about/dashboard-settings-about.element'),
|
||||
meta: {
|
||||
label: 'About',
|
||||
sections: ['Umb.Section.Settings'],
|
||||
pathname: 'about', // TODO: how to we want to support pretty urls?
|
||||
weight: 10,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'dashboard',
|
||||
alias: 'Umb.Dashboard.ExamineManagement',
|
||||
name: 'Examine Management',
|
||||
elementName: 'umb-dashboard-examine-management',
|
||||
js: () => import('./backoffice/dashboards/examine-management/dashboard-examine-management.element'),
|
||||
meta: {
|
||||
sections: ['Umb.Section.Settings'],
|
||||
pathname: 'examine-management', // TODO: how to we want to support pretty urls?
|
||||
weight: 10,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'dashboard',
|
||||
alias: 'Umb.Dashboard.ModelsBuilder',
|
||||
name: 'Models Builder',
|
||||
elementName: 'umb-dashboard-models-builder',
|
||||
js: () => import('./backoffice/dashboards/models-builder/dashboard-models-builder.element'),
|
||||
meta: {
|
||||
sections: ['Umb.Section.Settings'],
|
||||
pathname: 'models-builder', // TODO: how to we want to support pretty urls?
|
||||
weight: 10,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'dashboard',
|
||||
alias: 'Umb.Dashboard.MediaManagement',
|
||||
name: 'Media',
|
||||
elementName: 'umb-dashboard-media-management',
|
||||
js: () => import('./backoffice/dashboards/media-management/dashboard-media-management.element'),
|
||||
meta: {
|
||||
sections: ['Umb.Section.Media'],
|
||||
pathname: 'media-management', // TODO: how to we want to support pretty urls?
|
||||
weight: 10,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'propertyEditorUI',
|
||||
alias: 'Umb.PropertyEditorUI.Text',
|
||||
|
||||
@@ -55,6 +55,7 @@ export class UmbUpgraderView extends LitElement {
|
||||
? html`
|
||||
<p>
|
||||
<uui-button
|
||||
data-test="view-report-button"
|
||||
look="secondary"
|
||||
href="${this.settings.reportUrl}"
|
||||
target="_blank"
|
||||
@@ -69,6 +70,7 @@ export class UmbUpgraderView extends LitElement {
|
||||
<form id="authorizeUpgradeForm" @submit=${this._handleSubmit}>
|
||||
<p>
|
||||
<uui-button
|
||||
data-test="continue-button"
|
||||
id="authorizeUpgrade"
|
||||
type="submit"
|
||||
look="primary"
|
||||
@@ -84,7 +86,9 @@ export class UmbUpgraderView extends LitElement {
|
||||
}
|
||||
|
||||
private _renderError() {
|
||||
return html` ${this.errorMessage ? html`<p class="error">${this.errorMessage}</p>` : ''} `;
|
||||
return html`
|
||||
${this.errorMessage ? html`<p class="error" data-test="error-message">${this.errorMessage}</p>` : ''}
|
||||
`;
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -5,7 +5,8 @@ import { html, LitElement } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
|
||||
import { getUpgradeSettings, PostUpgradeAuthorize } from '../core/api/fetcher';
|
||||
import { UmbracoUpgrader } from '../core/models';
|
||||
|
||||
import type { UmbracoUpgrader } from '../core/models';
|
||||
|
||||
/**
|
||||
* @element umb-upgrader
|
||||
@@ -30,7 +31,7 @@ export class UmbUpgrader extends LitElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<umb-installer-layout>
|
||||
return html`<umb-installer-layout data-test="upgrader">
|
||||
<umb-upgrader-view
|
||||
.fetching=${this.fetching}
|
||||
.upgrading=${this.upgrading}
|
||||
@@ -49,7 +50,7 @@ export class UmbUpgrader extends LitElement {
|
||||
this.upgradeSettings = data;
|
||||
} catch (e) {
|
||||
if (e instanceof getUpgradeSettings.Error) {
|
||||
this.errorMessage = e.message;
|
||||
this.errorMessage = e.data.detail;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,18 @@ import { createWorkerFixture } from 'playwright-msw';
|
||||
import { handlers } from './src/mocks/e2e-handlers';
|
||||
|
||||
import type { MockServiceWorker } from 'playwright-msw';
|
||||
|
||||
const test = base.extend<{
|
||||
worker: MockServiceWorker;
|
||||
}>({
|
||||
worker: createWorkerFixture(...handlers),
|
||||
page: async ({ page }, use) => {
|
||||
// Set is-authenticated in sessionStorage to true
|
||||
await page.addInitScript(`window.sessionStorage.setItem('is-authenticated', 'true');`);
|
||||
|
||||
// Use signed-in page in all tests
|
||||
await use(page);
|
||||
},
|
||||
});
|
||||
|
||||
export { test, expect };
|
||||
|
||||
Reference in New Issue
Block a user