Merge branch 'main' into feature/tree-navigator

This commit is contained in:
Mads Rasmussen
2022-08-29 10:45:55 +02:00
12 changed files with 5682 additions and 1180 deletions

View File

@@ -0,0 +1,11 @@
{
"recommendations": [
"gruntfuggly.todo-tree",
"formulahendry.auto-rename-tag",
"mikestead.dotenv",
"dbaeumer.vscode-eslint",
"runem.lit-plugin",
"esbenp.prettier-vscode",
"hbenl.vscode-test-explorer"
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -36,7 +36,8 @@
"npm": ">=8.0.0 < 9"
},
"dependencies": {
"@umbraco-ui/uui": "^1.0.0-rc.3",
"@umbraco-ui/uui": "^1.0.0",
"@umbraco-ui/uui-css": "^1.0.0",
"element-internals-polyfill": "^1.1.9",
"lit": "^2.3.1",
"openapi-typescript-fetch": "^1.1.3",
@@ -49,10 +50,10 @@
"@umbraco-ui/uui-modal-sidebar": "file:umbraco-ui-uui-modal-sidebar-0.0.0.tgz"
},
"devDependencies": {
"@babel/core": "^7.18.10",
"@babel/core": "^7.18.13",
"@mdx-js/react": "^2.1.3",
"@open-wc/testing": "^3.1.6",
"@playwright/test": "^1.25.0",
"@playwright/test": "^1.25.1",
"@storybook/addon-a11y": "^6.5.10",
"@storybook/addon-actions": "^6.5.10",
"@storybook/addon-essentials": "^6.5.10",
@@ -63,15 +64,15 @@
"@types/chai": "^4.3.1",
"@types/mocha": "^9.1.1",
"@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "^5.33.1",
"@typescript-eslint/parser": "^5.33.1",
"@typescript-eslint/eslint-plugin": "^5.34.0",
"@typescript-eslint/parser": "^5.34.0",
"@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.22.0",
"eslint-config-prettier": "^8.5.0",
"eslint-import-resolver-typescript": "^3.4.2",
"eslint-import-resolver-typescript": "^3.5.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-lit": "^1.6.1",
"eslint-plugin-lit-a11y": "^2.2.2",

View File

@@ -46,7 +46,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/InstallValidateDatabaseRequest'
$ref: '#/components/schemas/InstallSetupDatabaseConfiguration'
required: true
responses:
'201':
@@ -341,13 +341,6 @@ components:
required:
- user
- telemetryLevel
InstallValidateDatabaseRequest:
type: object
properties:
database:
$ref: '#/components/schemas/InstallSetupDatabaseConfiguration'
required:
- database
ServerStatus:
type: string
enum:

View File

@@ -102,9 +102,6 @@ export interface components {
telemetryLevel: components["schemas"]["ConsentLevel"];
database?: components["schemas"]["InstallSetupDatabaseConfiguration"];
};
InstallValidateDatabaseRequest: {
database: components["schemas"]["InstallSetupDatabaseConfiguration"];
};
/** @enum {string} */
ServerStatus: "running" | "must-install" | "must-upgrade";
StatusResponse: {
@@ -184,7 +181,7 @@ export interface operations {
};
requestBody: {
content: {
"application/json": components["schemas"]["InstallValidateDatabaseRequest"];
"application/json": components["schemas"]["InstallSetupDatabaseConfiguration"];
};
};
};

View File

@@ -4,14 +4,15 @@ import 'router-slot';
import { UUIIconRegistryEssential } from '@umbraco-ui/uui';
import { css, html, LitElement } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { Guard, IRoute } from 'router-slot/model';
import { getServerStatus } from './core/api/fetcher';
import { UmbContextProviderMixin } from './core/context';
import { UmbExtensionManifest, UmbExtensionManifestCore, UmbExtensionRegistry } from './core/extension';
import { ServerStatus } from './core/models';
import { internalManifests } from './temp-internal-manifests';
import type { Guard, IRoute } from 'router-slot/model';
import type { ServerStatus } from './core/models';
@customElement('umb-app')
export class UmbApp extends UmbContextProviderMixin(LitElement) {
static styles = css`
@@ -49,8 +50,8 @@ export class UmbApp extends UmbContextProviderMixin(LitElement) {
},
];
private _extensionRegistry: UmbExtensionRegistry = new UmbExtensionRegistry();
private _iconRegistry: UUIIconRegistryEssential = new UUIIconRegistryEssential();
private _extensionRegistry = new UmbExtensionRegistry();
private _iconRegistry = new UUIIconRegistryEssential();
private _serverStatus: ServerStatus = 'running';
constructor() {

View File

@@ -1,28 +1,28 @@
import './components/backoffice-header.element';
import './components/backoffice-main.element';
import './components/backoffice-modal-container.element';
import './components/backoffice-notification-container.element';
import './components/editor-property-layout.element';
import './components/node-property.element';
import './sections/shared/section-layout.element';
import './sections/shared/section-main.element';
import './sections/shared/section-sidebar.element';
import './sections/shared/section.element';
import { defineElement } from '@umbraco-ui/uui-base/lib/registration';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { Subscription } from 'rxjs';
import type { Subscription } from 'rxjs';
import { UmbContextProviderMixin, UmbContextConsumerMixin } from '../core/context';
import { UmbNotificationService } from '../core/services/notification';
import { UmbContextConsumerMixin, UmbContextProviderMixin } from '../core/context';
import { UmbModalService } from '../core/services/modal';
import { UmbNotificationService } from '../core/services/notification';
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 { UmbEntityStore } from '../core/stores/entity.store';
import './components/backoffice-header.element';
import './components/backoffice-main.element';
import './components/backoffice-notification-container.element';
import './components/backoffice-modal-container.element';
import './components/editor-property-layout.element';
import './components/node-property.element';
import './sections/shared/section-layout.element';
import './sections/shared/section-sidebar.element';
import './sections/shared/section-main.element';
import './sections/shared/section.element';
@defineElement('umb-backoffice')
export default class UmbBackoffice extends UmbContextConsumerMixin(UmbContextProviderMixin(LitElement)) {
static styles = [

View File

@@ -1,8 +1,9 @@
import { UUIButtonElement } from '@umbraco-ui/uui';
import { css, CSSResultGroup, html, LitElement } from 'lit';
import { css, CSSResultGroup, html, LitElement, nothing } from 'lit';
import { customElement, property, query, state } from 'lit/decorators.js';
import { Subscription } from 'rxjs';
import { postInstallSetup, postInstallValidateDatabase } from '../core/api/fetcher';
import { UmbContextConsumerMixin } from '../core/context';
import { UmbracoInstallerDatabaseModel, UmbracoPerformInstallDatabaseConfiguration } from '../core/models';
import { UmbInstallerContext } from './installer-context';
@@ -56,7 +57,7 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) {
margin-bottom: var(--uui-size-layout-3);
}
h4 {
h2 {
margin: 0;
}
@@ -69,6 +70,11 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) {
margin-left: auto;
min-width: 120px;
}
.error {
color: var(--uui-color-danger);
padding: var(--uui-size-space-2) 0;
}
`,
];
@@ -90,6 +96,9 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) {
@state()
private _installerStore?: UmbInstallerContext;
@state()
private _validationErrorMessage = '';
private storeDataSubscription?: Subscription;
private storeSettingsSubscription?: Subscription;
@@ -136,15 +145,19 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) {
this._installerStore?.appendData({ database });
}
private _handleSubmit = (e: SubmitEvent) => {
e.preventDefault();
private _handleSubmit = async (evt: SubmitEvent) => {
evt.preventDefault();
const form = e.target as HTMLFormElement;
const form = evt.target as HTMLFormElement;
if (!form) return;
const isValid = form.checkValidity();
if (!isValid) return;
if (!this._installerStore) return;
this._installButton.state = 'waiting';
// Only append the database if it's not pre-configured
if (!this._preConfiguredDatabase) {
const formData = new FormData(form);
@@ -154,6 +167,39 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) {
const server = formData.get('server') as string;
const name = formData.get('name') as string;
const useIntegratedAuthentication = formData.has('useIntegratedAuthentication');
const connectionString = formData.get('connectionString') as string;
// Validate connection
const selectedDatabase = this._databases.find((x) => x.id === id);
if (selectedDatabase?.requiresConnectionTest) {
try {
let databaseDetails: UmbracoPerformInstallDatabaseConfiguration = {};
if (connectionString) {
databaseDetails.connectionString = connectionString;
} else {
databaseDetails = {
id,
username,
password,
server,
useIntegratedAuthentication,
name,
};
}
await postInstallValidateDatabase(databaseDetails);
} catch (e) {
if (e instanceof postInstallSetup.Error) {
const error = e.getActualType();
console.warn('Database validation failed', error.data);
this._validationErrorMessage = error.data.detail ?? 'Could not verify database connection';
} else {
this._validationErrorMessage = 'A server error happened when trying to validate the database';
}
this._installButton.state = 'failed';
return;
}
}
const database = {
...this._installerStore?.getData().database,
@@ -163,14 +209,13 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) {
server,
name,
useIntegratedAuthentication,
connectionString,
} as UmbracoPerformInstallDatabaseConfiguration;
this._installerStore?.appendData({ database });
}
this.dispatchEvent(new CustomEvent('submit', { bubbles: true, composed: true }));
this._installButton.state = 'waiting';
};
private _onBack() {
@@ -205,14 +250,15 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) {
}
private _renderServer = () => html`
<h4>Connection</h4>
<h2 class="uui-h4">Connection</h2>
<hr />
<uui-form-layout-item>
<uui-label for="server" slot="label" required>Server</uui-label>
<uui-label for="server" slot="label" required>Server address</uui-label>
<uui-input
type="text"
id="server"
name="server"
label="Server address"
@input=${this._handleChange}
.value=${this.databaseFormData.server ?? ''}
.placeholder=${this.selectedDatabase?.serverPlaceholder ?? ''}
@@ -228,6 +274,7 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) {
.value=${value}
id="database-name"
name="name"
label="Database name"
@input=${this._handleChange}
placeholder="umbraco"
required
@@ -235,16 +282,16 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) {
</uui-form-layout-item>`;
private _renderCredentials = () => html`
<h4>Credentials</h4>
<h2 class="uui-h4">Credentials</h2>
<hr />
<uui-form-layout-item>
<uui-checkbox
name="useIntegratedAuthentication"
label="use-integrated-authentication"
label="Use integrated authentication"
@change=${this._handleChange}
.checked=${this.databaseFormData.useIntegratedAuthentication || false}
>Use integrated authentication</uui-checkbox
>
.checked=${this.databaseFormData.useIntegratedAuthentication || false}>
Use integrated authentication
</uui-checkbox>
</uui-form-layout-item>
${!this.databaseFormData.useIntegratedAuthentication
@@ -255,6 +302,7 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) {
.value=${this.databaseFormData.username ?? ''}
id="username"
name="username"
label="Username"
@input=${this._handleChange}
required
required-message="Username is required"></uui-input>
@@ -267,6 +315,7 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) {
.value=${this.databaseFormData.password ?? ''}
id="password"
name="password"
label="Password"
@input=${this._handleChange}
autocomplete="new-password"
required
@@ -321,6 +370,7 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) {
${this._preConfiguredDatabase
? this._renderPreConfiguredDatabase(this._preConfiguredDatabase)
: this._renderDatabaseSelection()}
${this._validationErrorMessage ? html` <div class="error">${this._validationErrorMessage}</div> ` : nothing}
<div id="buttons">
<uui-button label="Back" @click=${this._onBack} look="secondary"></uui-button>

View File

@@ -48,7 +48,7 @@ export const Step3Database: Story = () => html`<umb-installer-database></umb-ins
Step3Database.storyName = 'Step 3: Database';
Step3Database.parameters = {
actions: {
handles: ['previous', 'next'],
handles: ['previous', 'submit'],
},
};
@@ -56,7 +56,7 @@ export const Step3DatabasePreconfigured: Story = () => html`<umb-installer-datab
Step3DatabasePreconfigured.storyName = 'Step 3: Database (preconfigured)';
Step3DatabasePreconfigured.parameters = {
actions: {
handles: ['previous', 'next'],
handles: ['previous', 'submit'],
},
msw: {
handlers: {

View File

@@ -1,7 +1,12 @@
import { rest } from 'msw';
import umbracoPath from '../../core/helpers/umbraco-path';
import { PostInstallRequest, ProblemDetails, UmbracoInstaller } from '../../core/models';
import {
PostInstallRequest,
ProblemDetails,
UmbracoInstaller,
UmbracoPerformInstallDatabaseConfiguration,
} from '../../core/models';
export const handlers = [
rest.get(umbracoPath('/install/settings'), (_req, res, ctx) => {
@@ -73,6 +78,26 @@ export const handlers = [
);
}),
rest.post(umbracoPath('/install/validateDatabase'), async (req, res, ctx) => {
const body = await req.json<UmbracoPerformInstallDatabaseConfiguration>();
if (body.name === 'validate') {
return res(
ctx.status(400),
ctx.json<ProblemDetails>({
type: 'connection',
status: 400,
detail: 'Database connection failed',
})
);
}
return res(
// Respond with a 200 status code
ctx.status(201)
);
}),
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>();

View File

@@ -3,103 +3,99 @@ import { body, defaultResponse, endpoint, request, response } from '@airtasker/s
import { ProblemDetails } from './models';
@endpoint({
method: 'GET',
path: '/install/settings',
method: 'GET',
path: '/install/settings',
})
export class GetInstallSettings {
@response({ status: 200 })
success(@body body: InstallSettingsResponse) {}
@response({ status: 200 })
success(@body body: InstallSettingsResponse) {}
@defaultResponse
default(@body body: ProblemDetails) {}
@defaultResponse
default(@body body: ProblemDetails) {}
}
@endpoint({
method: 'POST',
path: '/install/setup',
method: 'POST',
path: '/install/setup',
})
export class PostInstallSetup {
@request
request(@body body: InstallSetupRequest) {}
@request
request(@body body: InstallSetupRequest) {}
@response({ status: 201 })
success() {}
@response({ status: 201 })
success() {}
@response({ status: 400 })
badRequest(@body body: ProblemDetails) {}
@response({ status: 400 })
badRequest(@body body: ProblemDetails) {}
}
@endpoint({
method: 'POST',
path: '/install/validateDatabase',
method: 'POST',
path: '/install/validateDatabase',
})
export class PostInstallValidateDatabase {
@request
request(@body body: InstallValidateDatabaseRequest) {}
@request
request(@body body: InstallSetupDatabaseConfiguration) {}
@response({ status: 201 })
success() {}
@response({ status: 201 })
success() {}
@response({ status: 400 })
badRequest(@body body: ProblemDetails) {}
@response({ status: 400 })
badRequest(@body body: ProblemDetails) {}
}
export interface InstallSetupRequest {
user: InstallSetupUserConfiguration;
telemetryLevel: ConsentLevel;
database?: InstallSetupDatabaseConfiguration;
}
export interface InstallValidateDatabaseRequest {
database: InstallSetupDatabaseConfiguration;
user: InstallSetupUserConfiguration;
telemetryLevel: ConsentLevel;
database?: InstallSetupDatabaseConfiguration;
}
export interface InstallSettingsResponse {
user: InstallUserModel;
databases: InstallDatabaseModel[];
user: InstallUserModel;
databases: InstallDatabaseModel[];
}
export interface InstallUserModel {
minCharLength: number;
minNonAlphaNumericLength: number;
consentLevels: TelemetryModel[];
minCharLength: number;
minNonAlphaNumericLength: number;
consentLevels: TelemetryModel[];
}
export interface InstallSetupUserConfiguration {
name: string;
email: string;
password: string;
subscribeToNewsletter: boolean;
name: string;
email: string;
password: string;
subscribeToNewsletter: boolean;
}
export interface InstallSetupDatabaseConfiguration {
id?: string;
server?: string | null;
password?: string | null;
username?: string | null;
name?: string | null;
providerName?: string | null;
useIntegratedAuthentication?: boolean | null;
connectionString?: string | null;
id?: string;
server?: string | null;
password?: string | null;
username?: string | null;
name?: string | null;
providerName?: string | null;
useIntegratedAuthentication?: boolean | null;
connectionString?: string | null;
}
export interface TelemetryModel {
level: ConsentLevel;
description: string;
level: ConsentLevel;
description: string;
}
export interface InstallDatabaseModel {
id: string;
sortOrder: number;
displayName: string;
defaultDatabaseName: string;
providerName: null | string;
isConfigured: boolean;
requiresServer: boolean;
serverPlaceholder: null | string;
requiresCredentials: boolean;
supportsIntegratedAuthentication: boolean;
requiresConnectionTest: boolean;
id: string;
sortOrder: number;
displayName: string;
defaultDatabaseName: string;
providerName: null | string;
isConfigured: boolean;
requiresServer: boolean;
serverPlaceholder: null | string;
requiresCredentials: boolean;
supportsIntegratedAuthentication: boolean;
requiresConnectionTest: boolean;
}
export type ConsentLevel = 'Minimal' | 'Basic' | 'Detailed';

File diff suppressed because it is too large Load Diff