This commit is contained in:
Lone Iversen
2022-12-19 15:28:25 +01:00
296 changed files with 16204 additions and 7069 deletions

View File

@@ -1,4 +1,4 @@
# Copy this to .env.local and change what you want to test.
VITE_UMBRACO_USE_MSW=on # on = turns on MSW, off = disables all mock handlers
VITE_UMBRACO_API_URL=http://localhost:9000
VITE_UMBRACO_API_URL=http://localhost:11000
VITE_UMBRACO_INSTALL_STATUS=running # running or must-install or must-upgrade

View File

@@ -1,2 +1,2 @@
VITE_UMBRACO_INSTALL_STATUS=running # running or must-install or must-upgrade
VITE_UMBRACO_USE_MSW=off
VITE_UMBRACO_API_URL=

View File

@@ -42,7 +42,7 @@ jobs:
# path: coverage/
# retention-days: 30
- name: Report code coverage
uses: zgosalvez/github-actions-report-lcov@v1
uses: zgosalvez/github-actions-report-lcov@v2
if: always()
continue-on-error: true
with:

View File

@@ -27,7 +27,11 @@ jobs:
- name: Run DevSkim scanner
uses: microsoft/DevSkim-Action@v1
with:
directory-to-scan: src
should-scan-archives: false
ignore-globs: "**/.git/**,*.md,*.mdx,*.stories.ts,*.js"
- name: Upload DevSkim scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v2
with:

View File

@@ -19,6 +19,8 @@ types
# Editor directories and files
.vscode/*
!.vscode/extensions.json
!.vscode/*.code-snippets
!.vscode/settings.json
.idea
.DS_Store
*.suo

View File

@@ -0,0 +1,3 @@
{
"type": "commonjs"
}

View File

@@ -9,9 +9,8 @@ import { initialize, mswDecorator } from 'msw-storybook-addon';
import { setCustomElements } from '@storybook/web-components';
import customElementManifests from '../custom-elements.json';
import { UmbDataTypeStore } from '../src/core/stores/data-type/data-type.store';
import { UmbDocumentTypeStore } from '../src/core/stores/document-type.store';
import { UmbNodeStore } from '../src/core/stores/node.store';
import { UmbDataTypesStore } from '../src/core/stores/data-types/data-types.store';
import { UmbDocumentTypeStore } from '../src/core/stores/document-type/document-type.store';
import { UmbIconStore } from '../src/core/stores/icon/icon.store';
import { onUnhandledRequest } from '../src/core/mocks/browser';
import { handlers } from '../src/core/mocks/browser-handlers';
@@ -22,14 +21,15 @@ import { manifests as sectionManifests } from '../src/backoffice/sections/manife
import { manifests as propertyEditorModelManifests } from '../src/backoffice/property-editor-models/manifests';
import { manifests as propertyEditorUIManifests } from '../src/backoffice/property-editor-uis/manifests';
import { manifests as treeManifests } from '../src/backoffice/trees/manifests';
import { manifests as editorManifests } from '../src/backoffice/editors/manifests';
import { manifests as workspaceManifests } from '../src/backoffice/workspaces/manifests';
import { manifests as propertyActionManifests } from '../src/backoffice/property-actions/manifests';
import { umbExtensionsRegistry } from '../src/core/extensions-registry';
import '../src/core/context-api/provide/context-provider.element';
import '../src/core/css/custom-properties.css';
import '../src/backoffice/components/backoffice-modal-container.element';
import '../src/backoffice/components/backoffice-frame/backoffice-modal-container.element';
import '../src/backoffice/components/shared/code-block.element';
class UmbStoryBookElement extends LitElement {
_umbIconStore = new UmbIconStore();
@@ -40,7 +40,7 @@ class UmbStoryBookElement extends LitElement {
this._registerExtensions(sectionManifests);
this._registerExtensions(treeManifests);
this._registerExtensions(editorManifests);
this._registerExtensions(workspaceManifests);
this._registerExtensions(propertyEditorModelManifests);
this._registerExtensions(propertyEditorUIManifests);
this._registerExtensions(propertyActionManifests);
@@ -62,12 +62,8 @@ customElements.define('umb-storybook', UmbStoryBookElement);
const storybookProvider = (story) => html` <umb-storybook>${story()}</umb-storybook> `;
const nodeStoreProvider = (story) => html`
<umb-context-provider key="umbNodeStore" .value=${new UmbNodeStore()}>${story()}</umb-context-provider>
`;
const dataTypeStoreProvider = (story) => html`
<umb-context-provider key="umbDataTypeStore" .value=${new UmbDataTypeStore()}>${story()}</umb-context-provider>
<umb-context-provider key="umbDataTypeStore" .value=${new UmbDataTypesStore()}>${story()}</umb-context-provider>
`;
const documentTypeStoreProvider = (story) => html`
@@ -90,7 +86,6 @@ initialize({ onUnhandledRequest });
export const decorators = [
mswDecorator,
storybookProvider,
nodeStoreProvider,
dataTypeStoreProvider,
documentTypeStoreProvider,
modalServiceProvider,

View File

@@ -6,6 +6,7 @@
"dbaeumer.vscode-eslint",
"runem.lit-plugin",
"esbenp.prettier-vscode",
"hbenl.vscode-test-explorer"
"hbenl.vscode-test-explorer",
"vunguyentuan.vscode-css-variables"
]
}

View File

@@ -0,0 +1,27 @@
{
"Create Lit Component": {
"prefix": "lit new",
"scope": "typescript",
"body": [
"import { UUITextStyles } from '@umbraco-ui/uui-css';",
"import { css, html, LitElement } from 'lit';",
"import { customElement } from 'lit/decorators.js';",
"",
"@customElement('umb-${TM_FILENAME_BASE/(.*)\\..+$/$1/}')",
"export class Umb${TM_FILENAME_BASE/(.*)\\..+$/${1:/pascalcase}/}Element extends LitElement {",
"\tstatic styles = [UUITextStyles, css``];",
"",
"\trender() {",
"\t\treturn html`${0:umb-${TM_FILENAME_BASE/(.*)\\..+$/$1/}}`;",
"\t}",
"}",
"",
"declare global {",
"\tinterface HTMLElementTagNameMap {",
"\t\t'umb-${TM_FILENAME_BASE/(.*)\\..+$/$1/}': Umb${TM_FILENAME_BASE/(.*)\\..+$/${1:/pascalcase}/}Element;",
"\t}",
"}"
],
"description": "Create a new Lit Component"
}
}

View File

@@ -0,0 +1,3 @@
{
"cssVariables.lookupFiles": ["node_modules/@umbraco-ui/uui-css/dist/custom-properties.css"]
}

View File

@@ -1,8 +1,8 @@
import { rest } from 'msw';
import { expect, test } from '../test';
import { umbracoPath } from '@umbraco-cms/utils';
import { ProblemDetails, RuntimeLevel, ServerStatus } from '@umbraco-cms/backend-api';
import { expect, test } from './test';
test.describe('installer tests', () => {
test.beforeEach(async ({ page, worker }) => {

View File

@@ -1,4 +1,4 @@
import { expect, test } from '../test';
import { expect, test } from './test';
test('login', async ({ page }) => {
// Go to /login

View File

@@ -0,0 +1,4 @@
{
"name": "backoffice-e2e",
"type": "commonjs"
}

View File

@@ -2,7 +2,7 @@ import { expect, test as base } from '@playwright/test';
import { createWorkerFixture } from 'playwright-msw';
import type { MockServiceWorker } from 'playwright-msw';
import { handlers } from './src/core/mocks/e2e-handlers';
import { handlers } from '../src/core/mocks/e2e-handlers';
const test = base.extend<{
worker: MockServiceWorker;

View File

@@ -1,7 +1,7 @@
import { rest } from 'msw';
import { expect, test } from '../test';
import { umbracoPath } from '@umbraco-cms/utils';
import { ProblemDetails, RuntimeLevel, ServerStatus } from '@umbraco-cms/backend-api';
import { expect, test } from './test';
test.describe('upgrader tests', () => {
test.beforeEach(async ({ page, worker }) => {

View File

@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Umbraco</title>
<script type="module" src="/src/index.ts"></script>

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,16 @@
"license": "MIT",
"private": true,
"version": "0.0.0",
"type": "module",
"main": "dist/main.js",
"exports": {
".": "./dist/main.js"
},
"types": "types/src/app.d.ts",
"files": [
"dist",
"types"
],
"repository": {
"url": "https://github.com/umbraco/Umbraco.CMS.Backoffice",
"type": "git"
@@ -18,7 +28,9 @@
"scripts": {
"dev": "vite",
"build": "tsc && vite build --mode staging",
"build:production": "tsc && vite build",
"build:for:static": "tsc && vite build",
"build:for:cms": "tsc && vite build -c vite.cms.config.ts",
"build:for:cms:watch": "npm run build:for:cms -- --watch",
"preview": "vite preview --open",
"test": "web-test-runner --coverage",
"test:watch": "web-test-runner --watch",
@@ -31,9 +43,9 @@
"generate:api-dev": "openapi --input http://localhost:9000/umbraco/swagger/v1/swagger.json --output src/core/backend-api --postfix Resource --useOptions",
"storybook": "npm run wc-analyze && start-storybook -p 6006",
"build-storybook": "npm run wc-analyze && build-storybook",
"generate:icons": "node ./devops/icons/index.mjs",
"generate:icons": "node ./devops/icons/index.js",
"wc-analyze": "wca **/*.element.ts --outFile custom-elements.json",
"new-extension": "plop --plopfile ./devops/plop/plop.mjs",
"new-extension": "plop --plopfile ./devops/plop/plop.js",
"compile": "tsc"
},
"engines": {
@@ -49,57 +61,57 @@
"@umbraco-ui/uui-modal-sidebar": "file:umbraco-ui-uui-modal-sidebar-0.0.0.tgz",
"@umbraco-ui/uui-color-swatches": "file:umbraco-ui-uui-color-swatches-2.0.0.tgz",
"@umbraco-ui/uui-color-swatch": "file:umbraco-ui-uui-color-swatch-0.0.0.tgz",
"element-internals-polyfill": "^1.1.16",
"lit": "^2.4.1",
"element-internals-polyfill": "^1.1.17",
"lit": "^2.5.0",
"lodash": "^4.17.21",
"openapi-typescript-fetch": "^1.1.3",
"router-slot": "^1.5.5",
"rxjs": "^7.5.7",
"rxjs": "^7.8.0",
"uuid": "^9.0.0"
},
"devDependencies": {
"@babel/core": "^7.20.2",
"@mdx-js/react": "^2.1.5",
"@babel/core": "^7.20.5",
"@mdx-js/react": "^2.2.1",
"@open-wc/testing": "^3.1.7",
"@playwright/test": "^1.28.0",
"@storybook/addon-a11y": "^6.5.13",
"@storybook/addon-actions": "^6.5.13",
"@storybook/addon-essentials": "^6.5.13",
"@playwright/test": "^1.28.1",
"@storybook/addon-a11y": "^6.5.14",
"@storybook/addon-actions": "^6.5.14",
"@storybook/addon-essentials": "^6.5.14",
"@storybook/addon-links": "^6.5.13",
"@storybook/builder-vite": "^0.2.5",
"@storybook/builder-vite": "^0.2.6",
"@storybook/mdx2-csf": "^0.0.3",
"@storybook/web-components": "^6.5.13",
"@storybook/web-components": "^6.5.14",
"@types/chai": "^4.3.4",
"@types/lodash-es": "^4.17.6",
"@types/mocha": "^10.0.0",
"@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "^5.43.0",
"@typescript-eslint/parser": "^5.43.0",
"@types/uuid": "^9.0.0",
"@typescript-eslint/eslint-plugin": "^5.46.1",
"@typescript-eslint/parser": "^5.46.1",
"@web/dev-server-esbuild": "^0.3.3",
"@web/dev-server-import-maps": "^0.0.7",
"@web/test-runner": "^0.15.0",
"@web/test-runner-playwright": "^0.9.0",
"babel-loader": "^9.1.0",
"eslint": "^8.27.0",
"eslint": "^8.29.0",
"eslint-config-prettier": "^8.5.0",
"eslint-import-resolver-typescript": "^3.5.2",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-lit": "^1.6.1",
"eslint-plugin-lit": "^1.7.1",
"eslint-plugin-lit-a11y": "^2.3.0",
"eslint-plugin-local-rules": "^1.3.2",
"eslint-plugin-storybook": "^0.6.7",
"lit-html": "^2.4.0",
"msw": "^0.48.3",
"eslint-plugin-storybook": "^0.6.8",
"lit-html": "^2.5.0",
"msw": "^0.49.2",
"msw-storybook-addon": "^1.6.3",
"openapi-typescript-codegen": "^0.23.0",
"playwright-msw": "^2.0.1",
"playwright-msw": "^2.1.0",
"plop": "^3.1.1",
"prettier": "2.7.1",
"prettier": "2.8.1",
"tiny-glob": "^0.2.9",
"typescript": "^4.9.3",
"typescript": "^4.9.4",
"vite": "^3.2.4",
"vite-plugin-static-copy": "^0.12.0",
"vite-tsconfig-paths": "^3.5.2",
"vite-plugin-static-copy": "^0.13.0",
"vite-tsconfig-paths": "^4.0.3",
"web-component-analyzer": "^2.0.0-next.4"
},
"msw": {

View File

@@ -1,6 +1,6 @@
const template = document.createElement('template');
template.innerHTML = `
<umb-editor-layout role="aside">
<umb-body-layout role="aside">
<h1 slot="header">My package view</h1>
<uui-box>
@@ -10,7 +10,7 @@ template.innerHTML = `
<uui-action-bar slot="footer">
<uui-button look="primary" type="button">Close</uui-button>
</uui-action-bar>
</umb-editor-layout>
</umb-body-layout>
`;
export default class MyPackageViewCustom extends HTMLElement {

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -2,7 +2,7 @@
/* tslint:disable */
/**
* Mock Service Worker (0.48.3).
* Mock Service Worker (0.49.2).
* @see https://github.com/mswjs/msw
* - Please do NOT modify this file.
* - Please do NOT serve this file on production.

View File

@@ -6,13 +6,14 @@ import '@umbraco-ui/uui-modal-sidebar';
import '@umbraco-ui/uui-color-swatch';
import '@umbraco-ui/uui-color-swatches';
import 'router-slot';
import 'element-internals-polyfill';
// TODO: remove these imports when they are part of UUI
import type { Guard, IRoute } from 'router-slot/model';
import { UUIIconRegistryEssential } from '@umbraco-ui/uui';
import { css, html, LitElement } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import {customElement, property, state} from 'lit/decorators.js';
import { OpenAPI, RuntimeLevel, ServerResource } from '@umbraco-cms/backend-api';
import { UmbContextProviderMixin } from '@umbraco-cms/context-api';
@@ -32,6 +33,9 @@ export class UmbApp extends UmbContextProviderMixin(LitElement) {
}
`;
@property({ type: String })
private umbracoUrl?: string;
@state()
private _routes: IRoute[] = [
{
@@ -66,9 +70,11 @@ export class UmbApp extends UmbContextProviderMixin(LitElement) {
async connectedCallback() {
super.connectedCallback();
OpenAPI.BASE = import.meta.env.VITE_UMBRACO_USE_MSW === 'on' ? '' : import.meta.env.VITE_UMBRACO_API_URL;
OpenAPI.BASE = import.meta.env.VITE_UMBRACO_USE_MSW === 'on' ? '' : this.umbracoUrl ?? import.meta.env.VITE_UMBRACO_API_URL ?? '';
OpenAPI.WITH_CREDENTIALS = true;
this.provideContext('UMBRACOBASE', OpenAPI.BASE);
await this._setInitStatus();
await this._registerExtensionManifestsFromServer();
this._redirect();
@@ -114,7 +120,8 @@ export class UmbApp extends UmbContextProviderMixin(LitElement) {
}
private _isAuthorized(): boolean {
return sessionStorage.getItem('is-authenticated') === 'true';
return true; // TODO: Return true for now, until new login page is up and running
//return sessionStorage.getItem('is-authenticated') === 'true';
}
private _isAuthorizedGuard(redirectTo?: string): Guard {
@@ -123,13 +130,14 @@ export class UmbApp extends UmbContextProviderMixin(LitElement) {
return true;
}
let returnPath = '/login';
let returnPath = `${OpenAPI.BASE}/umbraco/login`;
if (redirectTo) {
returnPath += `?redirectTo=${redirectTo}`;
}
history.replaceState(null, '', returnPath);
// Redirect user completely to login page
location.href = returnPath;
return false;
};
}

View File

@@ -11,7 +11,7 @@ export class UmbAuthLayout extends LitElement {
background-position: 50%;
background-repeat: no-repeat;
background-size: cover;
background-image: url('/login.jpeg');
background-image: url('login.jpeg');
width: 100vw;
height: 100vh;
}

View File

@@ -0,0 +1,4 @@
export default function() {
sessionStorage.setItem('is-authenticated', 'true');
history.replaceState(null, '', 'section');
}

View File

@@ -47,7 +47,7 @@ export default class UmbLogin extends LitElement {
this._loggingIn = false;
let { redirectTo } = query();
if (!redirectTo) {
redirectTo = '/section';
redirectTo = 'section';
}
sessionStorage.setItem('is-authenticated', 'true');
history.pushState(null, '', redirectTo);

View File

@@ -1,10 +1,14 @@
//TODO: we need to figure out what components should be available for extensions and load them upfront
import './components/backoffice-header.element';
import './components/backoffice-main.element';
import './components/backoffice-modal-container.element';
import './components/backoffice-notification-container.element';
import './components/node-property/node-property.element';
import './workspaces/shared/workspace-entity-layout/workspace-entity-layout.element';
import './components/ref-property-editor-ui/ref-property-editor-ui.element';
import './components/backoffice-frame/backoffice-header.element';
import './components/backoffice-frame/backoffice-main.element';
import './components/backoffice-frame/backoffice-modal-container.element';
import './components/backoffice-frame/backoffice-notification-container.element';
import './components/content-property/content-property.element';
import './components/table/table.element';
import './components/shared/code-block.element';
import './components/extension-slot/extension-slot.element';
import './sections/shared/section-main/section-main.element';
import './sections/shared/section-sidebar/section-sidebar.element';
import './sections/shared/section.element';
@@ -17,23 +21,29 @@ import { css, html, LitElement } from 'lit';
import { UmbModalService } from '../core/services/modal';
import { UmbNotificationService } from '../core/services/notification';
import { UmbDataTypeStore } from '../core/stores/data-type/data-type.store';
import { UmbDocumentTypeStore } from '../core/stores/document-type.store';
import { UmbNodeStore } from '../core/stores/node.store';
import { UmbDataTypesStore } from '../core/stores/data-types/data-types.store';
import { UmbDocumentTypeStore } from '../core/stores/document-type/document-type.store';
import { UmbMediaTypeStore } from '../core/stores/media-type/media-type.store';
import { UmbMemberTypeStore } from '../core/stores/member-type/member-type.store';
import { UmbDocumentStore } from '../core/stores/document/document.store';
import { UmbMediaStore } from '../core/stores/media/media.store';
import { UmbMemberGroupStore } from '../core/stores/member-group/member-group.store';
import { UmbSectionStore } from '../core/stores/section.store';
import { UmbEntityStore } from '../core/stores/entity.store';
import { UmbUserStore } from '../core/stores/user/user.store';
import { UmbIconStore } from '../core/stores/icon/icon.store';
import { UmbUserGroupStore } from '../core/stores/user/user-group.store';
import { UmbCurrentUserHistoryStore } from '../core/stores/current-user-history/current-user-history.store';
import { manifests as sectionManifests } from './sections/manifests';
import { manifests as propertyEditorModelManifests } from './property-editor-models/manifests';
import { manifests as propertyEditorUIManifests } from './property-editor-uis/manifests';
import { manifests as treeManifests } from './trees/manifests';
import { manifests as editorManifests } from './editors/manifests';
import { manifests as editorManifests } from './workspaces/manifests';
import { manifests as propertyActionManifests } from './property-actions/manifests';
import { manifests as externalLoginProviderManifests } from './external-login-providers/manifests';
import { manifests as userDashboards } from './user-dashboards/manifests';
import { UmbContextConsumerMixin, UmbContextProviderMixin } from '@umbraco-cms/context-api';
import { umbExtensionsRegistry } from '@umbraco-cms/extensions-registry';
import type { ManifestTypes, ManifestWithLoader } from '@umbraco-cms/models';
import type { ManifestTypes } from '@umbraco-cms/models';
@defineElement('umb-backoffice')
export class UmbBackofficeElement extends UmbContextConsumerMixin(UmbContextProviderMixin(LitElement)) {
@@ -53,7 +63,6 @@ export class UmbBackofficeElement extends UmbContextConsumerMixin(UmbContextProv
];
private _umbIconRegistry = new UmbIconStore();
private _umbEntityStore = new UmbEntityStore();
constructor() {
super();
@@ -64,21 +73,27 @@ export class UmbBackofficeElement extends UmbContextConsumerMixin(UmbContextProv
this._registerExtensions(propertyEditorModelManifests);
this._registerExtensions(propertyEditorUIManifests);
this._registerExtensions(propertyActionManifests);
this._registerExtensions(externalLoginProviderManifests);
this._registerExtensions(userDashboards);
this._umbIconRegistry.attach(this);
this.provideContext('umbEntityStore', this._umbEntityStore);
this.provideContext('umbNodeStore', new UmbNodeStore(this._umbEntityStore));
this.provideContext('umbDataTypeStore', new UmbDataTypeStore(this._umbEntityStore));
this.provideContext('umbDocumentTypeStore', new UmbDocumentTypeStore(this._umbEntityStore));
this.provideContext('umbUserStore', new UmbUserStore(this._umbEntityStore));
this.provideContext('umbUserGroupStore', new UmbUserGroupStore(this._umbEntityStore));
this.provideContext('umbDocumentStore', new UmbDocumentStore());
this.provideContext('umbMediaStore', new UmbMediaStore());
this.provideContext('umbDataTypeStore', new UmbDataTypesStore());
this.provideContext('umbDocumentTypeStore', new UmbDocumentTypeStore());
this.provideContext('umbMediaTypeStore', new UmbMediaTypeStore());
this.provideContext('umbMemberTypeStore', new UmbMemberTypeStore());
this.provideContext('umbUserStore', new UmbUserStore());
this.provideContext('umbUserGroupStore', new UmbUserGroupStore());
this.provideContext('umbMemberGroupStore', new UmbMemberGroupStore());
this.provideContext('umbNotificationService', new UmbNotificationService());
this.provideContext('umbModalService', new UmbModalService());
this.provideContext('umbSectionStore', new UmbSectionStore());
this.provideContext('umbCurrentUserHistoryStore', new UmbCurrentUserHistoryStore());
}
private _registerExtensions(manifests: Array<ManifestWithLoader<ManifestTypes>> | Array<ManifestTypes>) {
private _registerExtensions(manifests: Array<ManifestTypes> | Array<ManifestTypes>) {
manifests.forEach((manifest) => {
if (umbExtensionsRegistry.isRegistered(manifest.alias)) return;
umbExtensionsRegistry.register(manifest);

View File

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

View File

@@ -2,7 +2,7 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, CSSResultGroup, html, LitElement } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { when } from 'lit/directives/when.js';
import { UmbSectionStore } from '../../core/stores/section.store';
import { UmbSectionStore } from '../../../core/stores/section.store';
import { UmbObserverMixin } from '@umbraco-cms/observable-api';
import { UmbContextConsumerMixin, UmbContextProviderMixin } from '@umbraco-cms/context-api';
import type { ManifestSection } from '@umbraco-cms/models';
@@ -112,7 +112,7 @@ export class UmbBackofficeHeaderSections extends UmbContextProviderMixin(
<uui-tab
@click="${this._handleTabClick}"
?active="${this._currentSectionAlias === section.alias}"
href="${`/section/${section.meta.pathname}`}"
href="${`section/${section.meta.pathname}`}"
label="${section.meta.label || section.name}"
data-alias="${section.alias}"></uui-tab>
`

View File

@@ -3,7 +3,7 @@ import { css, CSSResultGroup, html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import './backoffice-header-sections.element';
import './backoffice-header-tools.element';
import './backoffice-header-apps.element';
@customElement('umb-backoffice-header')
export class UmbBackofficeHeader extends LitElement {
@@ -43,11 +43,11 @@ export class UmbBackofficeHeader extends LitElement {
return html`
<div id="appHeader">
<uui-button id="logo" look="primary" label="Umbraco" compact>
<img src="/umbraco_logomark_white.svg" alt="Umbraco" />
<img src="umbraco_logomark_white.svg" alt="Umbraco" />
</uui-button>
<umb-backoffice-header-sections id="sections"></umb-backoffice-header-sections>
<umb-backoffice-header-tools></umb-backoffice-header-tools>
<umb-backoffice-header-apps></umb-backoffice-header-apps>
</div>
`;
}

View File

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

View File

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

View File

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

View File

@@ -1,43 +0,0 @@
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, CSSResultGroup, html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
@customElement('umb-backoffice-header-tools')
export class UmbBackofficeHeaderTools extends LitElement {
static styles: CSSResultGroup = [
UUITextStyles,
css`
#tools {
display: flex;
align-items: center;
gap: var(--uui-size-space-2);
}
.tool {
font-size: 18px;
}
`,
];
render() {
return html`
<div id="tools">
<uui-button class="tool" look="primary" label="Search" compact>
<uui-icon name="search"></uui-icon>
</uui-button>
<uui-button class="tool" look="primary" label="Help" compact>
<uui-icon name="favorite"></uui-icon>
</uui-button>
<uui-button look="primary" style="font-size: 14px;" label="User" compact>
<uui-avatar name="Mr Rabbit"></uui-avatar>
</uui-button>
</div>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'umb-backoffice-header-tools': UmbBackofficeHeaderTools;
}
}

View File

@@ -0,0 +1,114 @@
import { css, html, LitElement, nothing } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement, property } from 'lit/decorators.js';
@customElement('umb-body-layout')
export class UmbBodyLayout extends LitElement {
static styles = [
UUITextStyles,
css`
:host {
display: flex;
background-color: var(--uui-color-background);
width: 100%;
height: 100%;
flex-direction: column;
}
#header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
min-height: 60px;
background-color: var(--uui-color-surface);
border-bottom: 1px solid var(--uui-color-border);
box-sizing: border-box;
}
#headline {
display: block;
margin: 0 var(--uui-size-layout-1);
}
#tabs {
margin-left: auto;
}
#main {
display: flex;
flex: 1;
flex-direction: column;
}
#footer {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 54px; /* TODO: missing var(--uui-size-18);*/
border-top: 1px solid var(--uui-color-border);
background-color: var(--uui-color-surface);
box-sizing: border-box;
}
#actions {
display: flex;
gap: 6px;
margin: 0 var(--uui-size-layout-1);
margin-left: auto;
}
`,
];
connectedCallback() {
super.connectedCallback();
this.shadowRoot?.removeEventListener('slotchange', this._slotChanged);
this.shadowRoot?.addEventListener('slotchange', this._slotChanged);
}
disconnectedCallback() {
super.disconnectedCallback();
this.shadowRoot?.removeEventListener('slotchange', this._slotChanged);
}
private _slotChanged = (e: Event) => {
(e.target as any).style.display =
(e.target as HTMLSlotElement).assignedNodes({ flatten: true }).length > 0 ? '' : 'none';
};
/**
* Renders a headline in the header.
* @public
* @type {string}
* @attr
* @default ''
*/
@property()
public headline = '';
render() {
return html`
<div id="header">
${this.headline ? html`<h3 id="headline">${this.headline}</h3>` : nothing}
<slot name="header"></slot>
<slot id="tabs" name="tabs"></slot>
</div>
<uui-scroll-container id="main">
<slot></slot>
</uui-scroll-container>
<div id="footer">
<slot name="footer"></slot>
<slot id="actions" name="actions"></slot>
</div>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'umb-body-layout': UmbBodyLayout;
}
}

View File

@@ -1,19 +1,19 @@
import './editor-layout.element';
import './body-layout.element';
import { Meta, Story } from '@storybook/web-components';
import { html } from 'lit-html';
import type { UmbEditorLayout } from './editor-layout.element';
import type { UmbBodyLayout } from './body-layout.element';
export default {
title: 'Editors/Shared/Editor Layout',
component: 'umb-editor-layout',
id: 'umb-editor-layout',
title: 'Workspaces/Shared/Workspace Layout',
component: 'umb-body-layout',
id: 'umb-body-layout',
} as Meta;
export const AAAOverview: Story<UmbEditorLayout> = () => html` <umb-editor-layout>
export const AAAOverview: Story<UmbBodyLayout> = () => html` <umb-body-layout>
<div slot="header"><uui-button color="" look="placeholder">Header slot</uui-button></div>
<uui-button color="" look="placeholder">Main slot</uui-button>
<div slot="footer"><uui-button color="" look="placeholder">Footer slot</uui-button></div>
</umb-editor-layout>`;
</umb-body-layout>`;
AAAOverview.storyName = 'Overview';

View File

@@ -4,17 +4,16 @@ import { ifDefined } from 'lit-html/directives/if-defined.js';
import { customElement, property, state } from 'lit/decorators.js';
import { EMPTY, of, switchMap } from 'rxjs';
import { UmbDataTypeStore } from '../../../core/stores/data-type/data-type.store';
import { NodeProperty } from '../../../core/mocks/data/node.data';
import { UmbDataTypesStore } from '../../../core/stores/data-types/data-types.store';
import { UmbObserverMixin } from '@umbraco-cms/observable-api';
import type { ManifestTypes } from '@umbraco-cms/models';
import type { ContentProperty, ManifestTypes } from '@umbraco-cms/models';
import { umbExtensionsRegistry } from '@umbraco-cms/extensions-registry';
import { UmbContextConsumerMixin } from '@umbraco-cms/context-api';
import '../entity-property/entity-property.element';
@customElement('umb-node-property')
export class UmbNodePropertyElement extends UmbContextConsumerMixin(UmbObserverMixin(LitElement)) {
@customElement('umb-content-property')
export class UmbContentPropertyElement extends UmbContextConsumerMixin(UmbObserverMixin(LitElement)) {
static styles = [
UUITextStyles,
css`
@@ -24,12 +23,12 @@ export class UmbNodePropertyElement extends UmbContextConsumerMixin(UmbObserverM
`,
];
private _property?: NodeProperty;
private _property?: ContentProperty;
@property({ type: Object, attribute: false })
public get property(): NodeProperty | undefined {
public get property(): ContentProperty | undefined {
return this._property;
}
public set property(value: NodeProperty | undefined) {
public set property(value: ContentProperty | undefined) {
this._property = value;
this._observeDataType();
}
@@ -43,7 +42,7 @@ export class UmbNodePropertyElement extends UmbContextConsumerMixin(UmbObserverM
@state()
private _dataTypeData?: any;
private _dataTypeStore?: UmbDataTypeStore;
private _dataTypeStore?: UmbDataTypesStore;
constructor() {
super();
@@ -86,6 +85,6 @@ export class UmbNodePropertyElement extends UmbContextConsumerMixin(UmbObserverM
declare global {
interface HTMLElementTagNameMap {
'umb-node-property': UmbNodePropertyElement;
'umb-content-property': UmbContentPropertyElement;
}
}

View File

@@ -8,7 +8,7 @@ import { UmbContextConsumerMixin } from '@umbraco-cms/context-api';
import type { ManifestPropertyEditorUI, ManifestTypes } from '@umbraco-cms/models';
import '../../property-actions/shared/property-action-menu/property-action-menu.element';
import '../../editors/shared/editor-property-layout/editor-property-layout.element';
import '../../workspaces/shared/workspace-property-layout/workspace-property-layout.element';
/**
* @element umb-entity-property
@@ -174,11 +174,10 @@ export class UmbEntityPropertyElement extends UmbContextConsumerMixin(UmbObserve
render() {
return html`
<umb-editor-property-layout id="layout" label="${this.label}" description="${this.description}">
<umb-workspace-property-layout id="layout" label="${this.label}" description="${this.description}">
${this._renderPropertyActionMenu()}
<div slot="editor">${this._element}</div>
</umb-editor-property-layout>
<hr />
</umb-workspace-property-layout>
`;
}

View File

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

View File

@@ -0,0 +1,78 @@
import { html, LitElement } from 'lit';
import { property } from 'lit/decorators.js';
import { UUIModalSidebarSize } from '@umbraco-ui/uui-modal-sidebar';
import { UmbPickerData } from '../../../core/services/modal/layouts/modal-layout-picker-base';
import { UmbContextConsumerMixin } from '@umbraco-cms/context-api';
//TODO: These should probably be imported dynamically.
import '../../../core/services/modal/layouts/picker-section/picker-layout-section.element';
import '../../../core/services/modal/layouts/picker-user-group/picker-layout-user-group.element';
import '../../../core/services/modal/layouts/picker-user/picker-layout-user.element';
import { UmbModalService, UmbModalType } from '@umbraco-cms/services';
/** TODO: Make use of UUI FORM Mixin, to make it easily take part of a form. */
export class UmbInputListBase extends UmbContextConsumerMixin(LitElement) {
@property({ type: Array })
public value: Array<string> = [];
@property({ type: Boolean })
public multiple = true;
@property({ type: String })
public modalType: UmbModalType = 'sidebar';
@property({ type: String })
public modalSize: UUIModalSidebarSize = 'small';
protected pickerLayout?: string;
private _modalService?: UmbModalService;
constructor() {
super();
this.consumeContext('umbModalService', (modalService: UmbModalService) => {
this._modalService = modalService;
});
}
private _openPicker() {
if (!this.pickerLayout) return;
const modalHandler = this._modalService?.open(this.pickerLayout, {
type: this.modalType,
size: this.modalSize,
data: {
multiple: this.multiple,
selection: this.value,
},
});
modalHandler?.onClose().then((data: UmbPickerData<string>) => {
if (data) {
this.value = data.selection;
this.selectionUpdated();
}
});
}
protected removeFromSelection(key: string) {
this.value = this.value.filter((k) => k !== key);
this.selectionUpdated();
}
protected selectionUpdated() {
// override this method to react to selection changes
}
protected renderButton() {
return html`<uui-button id="add-button" look="placeholder" @click=${this._openPicker} label="open">
Add
</uui-button>`;
}
protected renderContent() {
return html``;
}
render() {
return html`${this.renderContent()}${this.renderButton()}`;
}
}

View File

@@ -0,0 +1,90 @@
import { UUITextStyles } from '@umbraco-ui/uui-css';
import { css, html, nothing } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { UmbInputListBase } from '../input-list-base/input-list-base';
import type { ManifestSection } from '@umbraco-cms/models';
import { umbExtensionsRegistry } from '@umbraco-cms/extensions-registry';
@customElement('umb-input-section')
export class UmbInputPickerSectionElement extends UmbInputListBase {
static styles = [
UUITextStyles,
css`
:host {
display: flex;
flex-direction: column;
gap: var(--uui-size-space-4);
}
#user-group-list {
display: flex;
flex-direction: column;
gap: var(--uui-size-space-4);
}
.user-group {
display: flex;
align-items: center;
gap: var(--uui-size-space-2);
}
.user-group div {
display: flex;
align-items: center;
gap: var(--uui-size-4);
}
.user-group uui-button {
margin-left: auto;
}
`,
];
@state()
private _sections: Array<ManifestSection> = [];
connectedCallback(): void {
super.connectedCallback();
this.pickerLayout = 'umb-picker-layout-section';
this._observeSections();
}
private _observeSections() {
if (this.value.length > 0) {
umbExtensionsRegistry.extensionsOfType('section').subscribe((sections: Array<ManifestSection>) => {
this._sections = sections.filter((section) => this.value.includes(section.alias));
});
} else {
this._sections = [];
}
}
selectionUpdated() {
this._observeSections();
this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true }));
}
renderContent() {
if (this._sections.length === 0) return html`${nothing}`;
return html`
<div id="user-list">
${this._sections.map(
(section) => html`
<div class="user-group">
<div>
<span>${section.meta.label}</span>
</div>
<uui-button
@click=${() => this.removeFromSelection(section.alias)}
label="remove"
color="danger"></uui-button>
</div>
`
)}
</div>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'umb-input-section': UmbInputPickerSectionElement;
}
}

View File

@@ -0,0 +1,19 @@
import { expect, fixture, html } from '@open-wc/testing';
//TODO: Test has been commented out while we figure out how to setup import maps for the test environment
// import { UmbPickerSectionElement } from './picker-section.element';
// import { defaultA11yConfig } from '@umbraco-cms/test-utils';
// describe('UmbPickerSectionElement', () => {
// let element: UmbPickerSectionElement;
// beforeEach(async () => {
// element = await fixture(html`<umb-input-section></umb-input-section>`);
// });
// it('is defined with its own instance', () => {
// expect(element).to.be.instanceOf(UmbPickerSectionElement);
// });
// it('passes the a11y audit', async () => {
// await expect(element).shadowDom.to.be.accessible(defaultA11yConfig);
// });
// });

View File

@@ -0,0 +1,100 @@
import { UUITextStyles } from '@umbraco-ui/uui-css';
import { css, html, nothing } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { UmbInputListBase } from '../input-list-base/input-list-base';
import type { UserGroupEntity } from '@umbraco-cms/models';
import { UmbObserverMixin } from '@umbraco-cms/observable-api';
import { UmbUserGroupStore } from '@umbraco-cms/stores/user/user-group.store';
@customElement('umb-input-user-group')
export class UmbInputPickerUserGroupElement extends UmbObserverMixin(UmbInputListBase) {
static styles = [
UUITextStyles,
css`
:host {
display: flex;
flex-direction: column;
gap: var(--uui-size-space-4);
}
#user-group-list {
display: flex;
flex-direction: column;
gap: var(--uui-size-space-4);
}
.user-group {
display: flex;
align-items: center;
gap: var(--uui-size-space-2);
}
.user-group div {
display: flex;
align-items: center;
gap: var(--uui-size-4);
}
.user-group uui-button {
margin-left: auto;
}
`,
];
@state()
private _userGroups: Array<UserGroupEntity> = [];
private _userGroupStore?: UmbUserGroupStore;
connectedCallback(): void {
super.connectedCallback();
this.pickerLayout = 'umb-picker-layout-user-group';
this.consumeContext('umbUserGroupStore', (usersContext: UmbUserGroupStore) => {
this._userGroupStore = usersContext;
this._observeUserGroups();
});
}
private _observeUserGroups() {
if (this.value.length > 0 && this._userGroupStore) {
this.observe<Array<UserGroupEntity>>(
this._userGroupStore.getByKeys(this.value),
(userGroups) => (this._userGroups = userGroups)
);
} else {
this._userGroups = [];
}
}
selectionUpdated() {
this._observeUserGroups();
this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true }));
}
private _renderUserGroupList() {
if (this._userGroups.length === 0) return nothing;
return html`<div id="user-list">
${this._userGroups.map(
(userGroup) => html`
<div class="user-group">
<div>
<uui-icon .name=${userGroup.icon}></uui-icon>
<span>${userGroup.name}</span>
</div>
<uui-button
@click=${() => this.removeFromSelection(userGroup.key)}
label="remove"
color="danger"></uui-button>
</div>
`
)}
</div> `;
}
renderContent() {
return html`${this._renderUserGroupList()}`;
}
}
declare global {
interface HTMLElementTagNameMap {
'umb-input-user-group': UmbInputPickerUserGroupElement;
}
}

View File

@@ -0,0 +1,19 @@
import { expect, fixture, html } from '@open-wc/testing';
//TODO: Test has been commented out while we figure out how to setup import maps for the test environment
// import { UmbPickerUserGroupElement } from './picker-user-group.element';
// import { defaultA11yConfig } from '@umbraco-cms/test-utils';
// describe('UmbPickerLayoutUserGroupElement', () => {
// let element: UmbPickerUserGroupElement;
// beforeEach(async () => {
// element = await fixture(html`<umb-input-user-group></umb-input-user-group>`);
// });
// it('is defined with its own instance', () => {
// expect(element).to.be.instanceOf(UmbPickerUserGroupElement);
// });
// it('passes the a11y audit', async () => {
// await expect(element).shadowDom.to.be.accessible(defaultA11yConfig);
// });
// });

View File

@@ -0,0 +1,94 @@
import { UUITextStyles } from '@umbraco-ui/uui-css';
import { css, html, nothing, PropertyValueMap } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { UmbInputListBase } from '../input-list-base/input-list-base';
import { UmbObserverMixin } from '@umbraco-cms/observable-api';
import type { UserEntity } from '@umbraco-cms/models';
import { UmbUserStore } from '@umbraco-cms/stores/user/user.store';
@customElement('umb-input-user')
export class UmbPickerUserElement extends UmbObserverMixin(UmbInputListBase) {
static styles = [
UUITextStyles,
css`
:host {
display: flex;
flex-direction: column;
gap: var(--uui-size-space-4);
}
#user-list {
display: flex;
flex-direction: column;
gap: var(--uui-size-space-4);
}
.user {
display: flex;
align-items: center;
gap: var(--uui-size-space-2);
}
.user uui-button {
margin-left: auto;
}
`,
];
@state()
private _users: Array<UserEntity> = [];
private _userStore?: UmbUserStore;
connectedCallback(): void {
super.connectedCallback();
this.pickerLayout = 'umb-picker-layout-user';
this.consumeContext('umbUserStore', (userStore: UmbUserStore) => {
this._userStore = userStore;
this._observeUser();
});
}
protected updated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
super.updated(_changedProperties);
if (_changedProperties.has('value')) {
this._observeUser(); // TODO: This works, but it makes the value change twice.
}
}
private _observeUser() {
if (!this._userStore) return;
this.observe<Array<UserEntity>>(this._userStore.getByKeys(this.value), (users) => {
this._users = users;
});
}
selectionUpdated() {
this._observeUser();
this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true }));
}
private _renderUserList() {
if (this._users.length === 0) return nothing;
return html`<div id="user-list">
${this._users.map(
(user) => html`
<div class="user">
<uui-avatar .name=${user.name}></uui-avatar>
<div>${user.name}</div>
<uui-button @click=${() => this.removeFromSelection(user.key)} label="remove" color="danger"></uui-button>
</div>
`
)}
</div> `;
}
renderContent() {
return html`${this._renderUserList()}`;
}
}
declare global {
interface HTMLElementTagNameMap {
'umb-input-user': UmbPickerUserElement;
}
}

View File

@@ -0,0 +1,19 @@
import { expect, fixture, html } from '@open-wc/testing';
//TODO: Test has been commented out while we figure out how to setup import maps for the test environment
// import { UmbPickerUserElement } from './picker-user.element';
// import { defaultA11yConfig } from '@umbraco-cms/test-utils';
// describe('UmbPickerUserElement', () => {
// let element: UmbPickerUserElement;
// beforeEach(async () => {
// element = await fixture(html`<umb-input-user></umb-input-user>`);
// });
// it('is defined with its own instance', () => {
// expect(element).to.be.instanceOf(UmbPickerUserElement);
// });
// it('passes the a11y audit', async () => {
// await expect(element).shadowDom.to.be.accessible(defaultA11yConfig);
// });
// });

View File

@@ -0,0 +1,53 @@
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
/**
* A simple styled box for showing code-based error messages.
* @slot the full message
*
*/
@customElement('uui-code-block')
export class UUICodeBlock extends LitElement {
static styles = [
UUITextStyles,
css`
:host {
display: block;
font-family: monospace;
}
#container {
border: 1px solid var(--uui-color-divider-emphasis);
color: var(--uui-color-text-alt);
background-color: var(--uui-color-divider-standalone);
padding: var(--uui-size-space-2);
border-radius: var(--uui-border-radius);
line-height: var(--uui-size-10);
}
:host uui-scroll-container {
max-height: 500px;
overflow-y: auto;
overflow-wrap: anywhere;
}
`,
];
render() {
return html`<div id="container">
<uui-scroll-container>
<pre>
<code>
<slot></slot>
</code>
</pre>
</uui-scroll-container>
</div> `;
}
}
declare global {
interface HTMLElementTagNameMap {
'uui-code-block': UUICodeBlock;
}
}

View File

@@ -0,0 +1 @@
export * from './table.element';

View File

@@ -48,17 +48,24 @@ export class UmbDashboardExamineManagementElement extends UmbContextConsumerMixi
@state()
private _currentPath?: string;
/**
*
*/
constructor() {
super();
}
private _onRouteChange() {
this._currentPath = path();
}
private get backbutton(): boolean {
return this._currentPath != '/section/settings/dashboard/examine-management/' || !this._currentPath ? true : false;
return !(this._currentPath?.endsWith('examine-management/'));
}
render() {
return html` ${this.backbutton
? html` <a href="/section/settings/dashboard/examine-management/"> &larr; Back to overview </a> `
? html` <a href="section/settings/dashboard/examine-management"> &larr; Back to overview </a> `
: nothing}
<router-slot @changestate="${this._onRouteChange}" .routes=${this._routes}></router-slot>`;
}

View File

@@ -1,38 +0,0 @@
export interface SearcherModel {
name: string;
providerProperties: unknown; //TODO
}
export interface IndexModel {
name: string;
canRebuild: boolean;
healthStatus: string;
isHealthy: boolean;
providerProperties: ProviderPropertiesModel;
}
export interface ProviderPropertiesModel {
CommitCount: number;
DefaultAnalyzer: string;
DocumentCount: number;
FieldCount: number;
LuceneDirectory: string;
LuceneIndexFolder: string;
DirectoryFactory: string;
EnableDefaultEventHandler: boolean;
PublishedValuesOnly: boolean;
SupportProtectedContent: boolean;
IncludeFields?: string[];
}
export interface FieldViewModel {
name: string;
values: string[];
}
export interface SearchResultsModel {
id: number;
name: string;
fields: FieldViewModel[];
score: number;
}

View File

@@ -1,11 +1,11 @@
import { html, css } from 'lit';
import { html, css, nothing } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement } from 'lit/decorators.js';
import { UmbModalLayoutElement } from '@umbraco-cms/services';
import { SearchResultsModel } from 'src/backoffice/dashboards/examine-management/examine-extension';
import { SearchResult } from '@umbraco-cms/backend-api';
@customElement('umb-modal-layout-fields-viewer')
export class UmbModalLayoutFieldsViewerElement extends UmbModalLayoutElement<SearchResultsModel> {
export class UmbModalLayoutFieldsViewerElement extends UmbModalLayoutElement<SearchResult & { name: string }> {
static styles = [
UUITextStyles,
css`
@@ -47,31 +47,31 @@ export class UmbModalLayoutFieldsViewerElement extends UmbModalLayoutElement<Sea
}
render() {
if (this.data) {
return html`
<uui-dialog-layout class="uui-text" headline="${this.data.name}">
<uui-scroll-container id="field-viewer">
<span>
<uui-table>
<uui-table-head>
<uui-table-head-cell> Field </uui-table-head-cell>
<uui-table-head-cell> Value </uui-table-head-cell>
</uui-table-head>
${Object.values(this.data.fields).map((cell) => {
return html`<uui-table-row>
<uui-table-cell> ${cell.name} </uui-table-cell>
<uui-table-cell> ${cell.values.join(', ')} </uui-table-cell>
</uui-table-row>`;
})}
</uui-table>
</span>
</uui-scroll-container>
<div>
<uui-button look="primary" @click="${this._handleClose}">Close</uui-button>
</div>
</uui-dialog-layout>
`;
} else return html``;
if (!this.data) return nothing;
return html`
<uui-dialog-layout class="uui-text" headline="${this.data.name}">
<uui-scroll-container id="field-viewer">
<span>
<uui-table>
<uui-table-head>
<uui-table-head-cell> Field </uui-table-head-cell>
<uui-table-head-cell> Value </uui-table-head-cell>
</uui-table-head>
${Object.values(this.data.fields ?? []).map((cell) => {
return html`<uui-table-row>
<uui-table-cell> ${cell.name} </uui-table-cell>
<uui-table-cell> ${cell.values?.join(', ')} </uui-table-cell>
</uui-table-row>`;
})}
</uui-table>
</span>
</uui-scroll-container>
<div>
<uui-button look="primary" @click="${this._handleClose}">Close</uui-button>
</div>
</uui-dialog-layout>
`;
}
}

View File

@@ -1,15 +1,15 @@
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { customElement, state, property } from 'lit/decorators.js';
import {UUITextStyles} from '@umbraco-ui/uui-css/lib';
import {css, html, LitElement, nothing} from 'lit';
import {customElement, property, state} from 'lit/decorators.js';
import { UUIButtonState } from '@umbraco-ui/uui-button';
import {UUIButtonState} from '@umbraco-ui/uui-button';
import { UmbModalService, UmbNotificationService, UmbNotificationDefaultData } from '@umbraco-cms/services';
import {UmbModalService, UmbNotificationDefaultData, UmbNotificationService} from '@umbraco-cms/services';
import { UmbContextConsumerMixin } from '@umbraco-cms/context-api';
import {UmbContextConsumerMixin} from '@umbraco-cms/context-api';
import './section-view-examine-searchers';
import { ApiError, ProblemDetails, Index, SearchResource } from '@umbraco-cms/backend-api';
import {ApiError, Index, IndexerResource, ProblemDetails} from '@umbraco-cms/backend-api';
@customElement('umb-dashboard-examine-index')
export class UmbDashboardExamineIndexElement extends UmbContextConsumerMixin(LitElement) {
@@ -86,24 +86,14 @@ export class UmbDashboardExamineIndexElement extends UmbContextConsumerMixin(Lit
private _buttonState?: UUIButtonState = undefined;
@state()
private _indexData!: Index;
private _indexData?: Index;
@state()
private _loading = true;
private _notificationService?: UmbNotificationService;
private _modalService?: UmbModalService;
private async _getIndexData() {
try {
const index = await SearchResource.getSearchIndexByIndexName({ indexName: this.indexName });
this._indexData = index;
} catch (e) {
if (e instanceof ApiError) {
const error = e as ProblemDetails;
const data: UmbNotificationDefaultData = { message: error.message ?? 'Could not fetch index' };
this._notificationService?.peek('danger', { data });
}
}
}
constructor() {
super();
@@ -113,9 +103,25 @@ export class UmbDashboardExamineIndexElement extends UmbContextConsumerMixin(Lit
});
}
connectedCallback(): void {
private async _getIndexData() {
try {
this._indexData = await IndexerResource.getIndexerByIndexName({indexName: this.indexName});
if (!this._indexData?.isHealthy) {
this._buttonState = 'waiting';
}
} catch (e) {
if (e instanceof ApiError) {
const error = e as ProblemDetails;
const data: UmbNotificationDefaultData = { message: error.message ?? 'Could not fetch index' };
this._notificationService?.peek('danger', { data });
}
}
this._loading = false;
}
async connectedCallback() {
super.connectedCallback();
this._getIndexData();
await this._getIndexData();
}
private async _onRebuildHandler() {
@@ -136,45 +142,56 @@ export class UmbDashboardExamineIndexElement extends UmbContextConsumerMixin(Lit
}
private async _rebuild() {
this._buttonState = 'waiting';
if (this._indexData.name)
try {
await SearchResource.postSearchIndexByIndexNameRebuild({ indexName: this._indexData.name });
this._buttonState = 'success';
} catch (e) {
this._buttonState = 'failed';
if (e instanceof ApiError) {
const error = e as ProblemDetails;
const data: UmbNotificationDefaultData = { message: error.message ?? 'Rebuild error' };
this._notificationService?.peek('danger', { data });
}
try {
await IndexerResource.postIndexerByIndexNameRebuild({ indexName: this.indexName });
this._buttonState = 'success';
await this._getIndexData();
} catch (e) {
this._buttonState = 'failed';
if (e instanceof ApiError) {
const error = e as ProblemDetails;
const data: UmbNotificationDefaultData = { message: error.message ?? 'Rebuild error' };
this._notificationService?.peek('danger', { data });
}
}
}
render() {
if (this._indexData) {
return html` <uui-box headline="${this.indexName}">
<p>
<strong>Health Status</strong><br />
The health status of the ${this._indexData.name} and if it can be read
</p>
<div>
<uui-icon-essentials>
<uui-icon
name=${this._indexData.isHealthy ? `check` : `wrong`}
class=${this._indexData.isHealthy ? 'positive' : 'danger'}>
</uui-icon>
</uui-icon-essentials>
${this._indexData.healthStatus}
</div>
</uui-box>
<umb-dashboard-examine-searcher searcherName="${this.indexName}"></umb-dashboard-examine-searcher>
${this.renderPropertyList()} ${this.renderTools()}`;
} else return html``;
if (!this._indexData || this._loading) return html`
<uui-loader-bar></uui-loader-bar>`;
return html`
<uui-box headline="${this.indexName}">
<p>
<strong>Health Status</strong><br/>
The health status of the ${this.indexName} and if it can be read
</p>
<div>
<uui-icon-essentials>
<uui-icon
name=${this._indexData.isHealthy ? `check` : `wrong`}
class=${this._indexData.isHealthy ? 'positive' : 'danger'}>
</uui-icon>
</uui-icon-essentials>
${this._indexData.healthStatus}
</div>
</uui-box>
${this.renderIndexSearch()}
${this.renderPropertyList()}
${this.renderTools()}
`;
}
private renderIndexSearch() {
if (!this._indexData || !this._indexData.isHealthy) return nothing;
return html`<umb-dashboard-examine-searcher searcherName="${this.indexName}"></umb-dashboard-examine-searcher>`;
}
private renderPropertyList() {
if (!this._indexData) return nothing;
return html`<uui-box headline="Index info">
<p>Lists the properties of the ${this._indexData.name}</p>
<p>Lists the properties of the ${this.indexName}</p>
<uui-table class="info">
<uui-table-row>
<uui-table-cell style="width:0px; font-weight: bold;"> documentCount </uui-table-cell>
@@ -187,7 +204,7 @@ export class UmbDashboardExamineIndexElement extends UmbContextConsumerMixin(Lit
${this._indexData.providerProperties
? Object.entries(this._indexData.providerProperties).map((entry) => {
return html`<uui-table-row>
<uui-table-cell style="width:0px; font-weight: bold;"> ${entry[0]} </uui-table-cell>
<uui-table-cell style="width:0; font-weight: bold;"> ${entry[0]} </uui-table-cell>
<uui-table-cell clip-text> ${entry[1]} </uui-table-cell>
</uui-table-row>`;
})
@@ -198,13 +215,13 @@ export class UmbDashboardExamineIndexElement extends UmbContextConsumerMixin(Lit
private renderTools() {
return html` <uui-box headline="Tools">
<p>Tools to manage the ${this._indexData.name}</p>
<p>Tools to manage the ${this.indexName}</p>
<uui-button
color="danger"
look="primary"
.state="${this._buttonState}"
@click="${this._onRebuildHandler}"
.disabled="${!this._indexData?.canRebuild}"
.disabled="${!this._indexData?.canRebuild ?? true}"
label="Rebuild index">
Rebuild
</uui-button>

View File

@@ -1,11 +1,11 @@
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { css, html, LitElement, nothing } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { UmbModalService, UmbNotificationService, UmbNotificationDefaultData } from '@umbraco-cms/services';
import { UmbNotificationService, UmbNotificationDefaultData } from '@umbraco-cms/services';
import { UmbContextConsumerMixin } from '@umbraco-cms/context-api';
import { ApiError, ProblemDetails, Searcher, Index, SearchResource } from '@umbraco-cms/backend-api';
import { ApiError, ProblemDetails, Searcher, Index, IndexerResource, SearcherResource } from '@umbraco-cms/backend-api';
@customElement('umb-dashboard-examine-overview')
export class UmbDashboardExamineOverviewElement extends UmbContextConsumerMixin(LitElement) {
@@ -58,12 +58,32 @@ export class UmbDashboardExamineOverviewElement extends UmbContextConsumerMixin(
@state()
private _searchers?: Searcher[];
@state()
private _loadingIndexers = false;
@state()
private _loadingSearchers = false;
private _notificationService?: UmbNotificationService;
private _modalService?: UmbModalService;
constructor() {
super();
this.consumeAllContexts(['umbNotificationService'], (instances) => {
this._notificationService = instances['umbNotificationService'];
});
}
connectedCallback() {
super.connectedCallback();
this._getIndexers();
this._getSearchers();
}
private async _getIndexers() {
this._loadingIndexers = true;
try {
const indexers = await SearchResource.getSearchIndex({ take: 9999, skip: 0 });
const indexers = await IndexerResource.getIndexer({ take: 9999, skip: 0 });
this._indexers = indexers.items;
} catch (e) {
if (e instanceof ApiError) {
@@ -72,11 +92,13 @@ export class UmbDashboardExamineOverviewElement extends UmbContextConsumerMixin(
this._notificationService?.peek('danger', { data });
}
}
this._loadingIndexers = false;
}
private async _getSearchers() {
this._loadingSearchers = true;
try {
const searchers = await SearchResource.getSearchSearcher({ take: 9999, skip: 0 });
const searchers = await SearcherResource.getSearcher({ take: 9999, skip: 0 });
this._searchers = searchers.items;
} catch (e) {
if (e instanceof ApiError) {
@@ -85,17 +107,7 @@ export class UmbDashboardExamineOverviewElement extends UmbContextConsumerMixin(
this._notificationService?.peek('danger', { data });
}
}
}
constructor() {
super();
this._getIndexers();
this._getSearchers();
this.consumeAllContexts(['umbNotificationService', 'umbModalService'], (instances) => {
this._notificationService = instances['umbNotificationService'];
this._modalService = instances['umbModalService'];
});
this._loadingSearchers = false;
}
render() {
@@ -118,7 +130,8 @@ export class UmbDashboardExamineOverviewElement extends UmbContextConsumerMixin(
}
private renderIndexersList() {
if (!this._indexers) return;
if (this._loadingIndexers) return html`<uui-loader></uui-loader>`;
if (!this._indexers) return nothing;
return html` <uui-table class="overview">
${this._indexers.map((index) => {
return html`
@@ -142,7 +155,8 @@ export class UmbDashboardExamineOverviewElement extends UmbContextConsumerMixin(
}
private renderSearchersList() {
if (!this._searchers) return html`<span class="not-found-message">No searchers were found</span>`;
if (this._loadingSearchers) return html`<uui-loader></uui-loader>`;
if (!this._searchers) return nothing;
return html`
<uui-table class="overview2">
${this._searchers.map((searcher) => {

View File

@@ -1,12 +1,12 @@
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { css, html, LitElement, nothing } from 'lit';
import { customElement, state, query, property } from 'lit/decorators.js';
import { UmbModalService, UmbNotificationService, UmbNotificationDefaultData } from '@umbraco-cms/services';
import { UmbContextConsumerMixin } from '@umbraco-cms/context-api';
import { ApiError, ProblemDetails, SearchResult, SearchResource, Field } from '@umbraco-cms/backend-api';
import { ApiError, ProblemDetails, SearchResult, SearcherResource, Field } from '@umbraco-cms/backend-api';
import './modal-views/fields-viewer.element';
import './modal-views/fields-settings.element';
@@ -113,6 +113,9 @@ export class UmbDashboardExamineSearcherElement extends UmbContextConsumerMixin(
@state()
private _exposedFields?: ExposedSearchResultField[];
@state()
private _searchLoading = false;
@query('#search-input')
private _searchInput!: HTMLInputElement;
@@ -125,7 +128,7 @@ export class UmbDashboardExamineSearcherElement extends UmbContextConsumerMixin(
}
private _onNameClick() {
const data: UmbNotificationDefaultData = { message: 'TODO: Open editor for this' }; // TODO
const data: UmbNotificationDefaultData = { message: 'TODO: Open workspace for this' }; // TODO
this._notificationService?.peek('warning', { data });
}
@@ -135,11 +138,12 @@ export class UmbDashboardExamineSearcherElement extends UmbContextConsumerMixin(
private async _onSearch() {
if (!this._searchInput.value.length) return;
this._searchLoading = true;
try {
const res = await SearchResource.getSearchSearcherBySearcherNameSearch({
const res = await SearcherResource.getSearcherBySearcherNameQuery({
searcherName: this.searcherName,
query: this._searchInput.value,
take: 9999,
term: this._searchInput.value,
take: 100,
skip: 0,
});
this._searchResults = res.items;
@@ -151,6 +155,7 @@ export class UmbDashboardExamineSearcherElement extends UmbContextConsumerMixin(
this._notificationService?.peek('danger', { data });
}
}
this._searchLoading = false;
}
private _updateFieldFilter() {
@@ -204,59 +209,64 @@ export class UmbDashboardExamineSearcherElement extends UmbContextConsumerMixin(
`;
}
// Find the field named 'nodeName' and return its value if it exists in the fields array
private getSearchResultNodeName(searchResult: SearchResult): string {
const nodeNameField = searchResult.fields?.find((field) => field.name?.toUpperCase() === 'NODENAME');
return nodeNameField?.values?.join(', ') ?? '';
}
private renderSearchResults() {
if (this._searchResults?.length) {
return html`<div class="table-container">
<uui-scroll-container>
<uui-table class="search">
<uui-table-head>
<uui-table-head-cell style="width:0">Score</uui-table-head-cell>
<uui-table-head-cell style="width:0">Id</uui-table-head-cell>
<uui-table-head-cell>Name</uui-table-head-cell>
<uui-table-head-cell>Fields</uui-table-head-cell>
${this.renderHeadCells()}
</uui-table-head>
${this._searchResults?.map((rowData) => {
return html`<uui-table-row>
<uui-table-cell> ${rowData.score} </uui-table-cell>
<uui-table-cell> ${rowData.id} </uui-table-cell>
<uui-table-cell>
<uui-button look="secondary" label="Open editor for this document" @click="${this._onNameClick}">
${rowData.fields?.find((field) => {
if (field.name?.toUpperCase() === 'NODENAME') return field.values;
else return;
})?.values}
</uui-button>
</uui-table-cell>
<uui-table-cell>
<uui-button
class="bright"
look="secondary"
label="Open sidebar to see all fields"
@click="${() =>
this._modalService?.open('umb-modal-layout-fields-viewer', {
type: 'sidebar',
size: 'medium',
data: { ...rowData },
})}">
${rowData.fields ? Object.keys(rowData.fields).length : ''} fields
</uui-button>
</uui-table-cell>
${rowData.fields ? this.renderBodyCells(rowData.fields) : ''}
</uui-table-row>`;
})}
</uui-table>
</uui-scroll-container>
<button class="field-adder" @click="${this._onFieldFilterClick}">
<uui-icon-registry-essential>
<uui-tag look="secondary">
<uui-icon name="add"></uui-icon>
</uui-tag>
</uui-icon-registry-essential>
</button>
</div>`;
if (this._searchLoading) return html`<uui-loader></uui-loader>`;
if (!this._searchResults) return nothing;
if (!this._searchResults.length) {
return html`<p>No results found</p>`;
}
return;
return html`<div class="table-container">
<uui-scroll-container>
<uui-table class="search">
<uui-table-head>
<uui-table-head-cell style="width:0">Score</uui-table-head-cell>
<uui-table-head-cell style="width:0">Id</uui-table-head-cell>
<uui-table-head-cell>Name</uui-table-head-cell>
<uui-table-head-cell>Fields</uui-table-head-cell>
${this.renderHeadCells()}
</uui-table-head>
${this._searchResults?.map((rowData) => {
return html`<uui-table-row>
<uui-table-cell> ${rowData.score} </uui-table-cell>
<uui-table-cell> ${rowData.id} </uui-table-cell>
<uui-table-cell>
<uui-button look="secondary" label="Open workspace for this document" @click="${this._onNameClick}">
${this.getSearchResultNodeName(rowData)}
</uui-button>
</uui-table-cell>
<uui-table-cell>
<uui-button
class="bright"
look="secondary"
label="Open sidebar to see all fields"
@click="${() =>
this._modalService?.open('umb-modal-layout-fields-viewer', {
type: 'sidebar',
size: 'medium',
data: { ...rowData, name: this.getSearchResultNodeName(rowData) },
})}">
${rowData.fields ? Object.keys(rowData.fields).length : ''} fields
</uui-button>
</uui-table-cell>
${rowData.fields ? this.renderBodyCells(rowData.fields) : ''}
</uui-table-row>`;
})}
</uui-table>
</uui-scroll-container>
<button class="field-adder" @click="${this._onFieldFilterClick}">
<uui-icon-registry-essential>
<uui-tag look="secondary">
<uui-icon name="add"></uui-icon>
</uui-tag>
</uui-icon-registry-essential>
</button>
</div>`;
}
renderHeadCells() {

View File

@@ -1,18 +1,181 @@
import { UUIButtonState } from '@umbraco-ui/uui';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import { css, html, LitElement, nothing } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { ApiError, ModelsBuilder, ModelsBuilderResource, ModelsMode, ProblemDetails } from '@umbraco-cms/backend-api';
import { UmbContextConsumerMixin } from '@umbraco-cms/context-api';
import { UmbNotificationDefaultData, UmbNotificationService } from '@umbraco-cms/services';
@customElement('umb-dashboard-models-builder')
export class UmbDashboardModelsBuilderElement extends LitElement {
static styles = [UUITextStyles, css``];
export class UmbDashboardModelsBuilderElement extends UmbContextConsumerMixin(LitElement) {
static styles = [
UUITextStyles,
css`
.headline {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.models-description ul {
list-style-type: square;
margin: 0;
padding-left: var(--uui-size-layout-1);
}
span.out-of-date {
display: block;
padding-block-end: var(--uui-size-space-4);
}
.error {
font-weight: bold;
color: var(--uui-color-danger);
}
p.models-actions {
margin-bottom: 0;
}
`,
];
private _notificationService?: UmbNotificationService;
@state()
private _modelsBuilder?: ModelsBuilder;
@state()
private _buttonStateBuild: UUIButtonState = undefined;
@state()
private _buttonStateReload: UUIButtonState = undefined;
constructor() {
super();
this._getDashboardData();
this.consumeAllContexts(['umbNotificationService'], (instances) => {
this._notificationService = instances['umbNotificationService'];
});
}
private async _getDashboardData() {
try {
const modelsBuilder = await ModelsBuilderResource.getModelsBuilderDashboard();
this._modelsBuilder = modelsBuilder;
return true;
} catch (e) {
if (e instanceof ApiError) {
const error = e as ProblemDetails;
const data: UmbNotificationDefaultData = {
message: error.message ?? 'Something went wrong',
};
this._notificationService?.peek('danger', { data });
}
return false;
}
}
private async _onGenerateModels() {
this._buttonStateBuild = 'waiting';
const status = await this._postGenerateModels();
this._buttonStateBuild = status ? 'success' : 'failed';
}
private async _postGenerateModels() {
try {
await ModelsBuilderResource.postModelsBuilderBuild();
this._getDashboardData();
return true;
} catch (e) {
if (e instanceof ApiError) {
const error = e as ProblemDetails;
const data: UmbNotificationDefaultData = {
message: error.message ?? 'Model generation failed',
};
this._notificationService?.peek('danger', { data });
}
return false;
}
}
private async _onDashboardReload() {
this._buttonStateReload = 'waiting';
const status = await this._getDashboardData();
this._buttonStateReload = status ? 'success' : 'failed';
}
render() {
return html`
<uui-box>
<h1>Models Builder</h1>
<div class="headline">
<h1>Models Builder</h1>
<uui-button .state="${this._buttonStateReload}" look="secondary" @click="${this._onDashboardReload}">
Reload
</uui-button>
</div>
<p>Version: ${this._modelsBuilder?.version}</p>
<div class="models-description">
<p>ModelsBuilder is enabled with the following configuration:</p>
<ul>
${this._modelsBuilder?.mode
? html`<li>
The <strong>ModelsMode</strong> is '${this._modelsBuilder.mode}'. ${this.renderModelsMode()}
</li> `
: nothing}
${this.renderList()}
</ul>
</div>
<p class="models-actions">
${this._modelsBuilder?.outOfDateModels
? html`<span class="out-of-date">Models are <strong>out-of-date</strong></span>`
: nothing}
${this._modelsBuilder?.canGenerate
? html` <uui-button
.state="${this._buttonStateBuild}"
look="primary"
label="Generate models"
@click="${this._onGenerateModels}">
Generate models
</uui-button>`
: nothing}
</p>
${this._modelsBuilder?.lastError
? html`<p class="error">Last generation failed with the following error:</p>
<uui-code-block>${this._modelsBuilder.lastError}</uui-code-block>`
: nothing}
</uui-box>
`;
}
private renderList() {
if (this._modelsBuilder?.mode !== ModelsMode.NOTHING) {
return html`${this._modelsBuilder?.modelsNamespace
? html`<li>The <strong>models namespace</strong> is ${this._modelsBuilder.modelsNamespace}.</li>`
: nothing}
${this._modelsBuilder?.trackingOutOfDateModels === true
? html`<li>Tracking of <strong>out-of-date models</strong> is enabled.</li>`
: this._modelsBuilder?.trackingOutOfDateModels === false
? html`<li>Tracking of <strong>out-of-date models</strong> is not enabled.</li>`
: nothing}`;
}
return nothing;
}
renderModelsMode() {
switch (this._modelsBuilder?.mode) {
case ModelsMode.IN_MEMORY_AUTO:
return 'Strongly typed models are re-generated on startup and anytime schema changes (i.e. Content Type) are made. No recompilation necessary but the generated models are not available to code outside of Razor.';
case ModelsMode.SOURCE_CODE_MANUAL:
return 'Strongly typed models are generated on demand. Recompilation is necessary and models are available to all CSharp code.';
case ModelsMode.SOURCE_CODE_AUTO:
return 'Strong typed models are generated on demand and anytime schema changes (i.e. Content Type) are made. Recompilation is necessary and models are available to all CSharp code.';
case ModelsMode.NOTHING:
return 'Strongly typed models are not generated. All content and cache will operate from instance of IPublishedContent only.';
default:
return;
}
}
}
declare global {

View File

@@ -1,15 +1,117 @@
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import { customElement, state } from 'lit/decorators.js';
import { ApiError, ProblemDetails, ProfilingResource } from '@umbraco-cms/backend-api';
import { UmbContextConsumerMixin } from '@umbraco-cms/context-api';
import { UmbNotificationDefaultData, UmbNotificationService } from '@umbraco-cms/services';
@customElement('umb-dashboard-performance-profiling')
export class UmbDashboardPerformanceProfilingElement extends LitElement {
static styles = [UUITextStyles, css``];
export class UmbDashboardPerformanceProfilingElement extends UmbContextConsumerMixin(LitElement) {
static styles = [
UUITextStyles,
css`
uui-toggle {
font-weight: bold;
}
h4 {
margin-bottom: 0;
}
h4 + p {
margin-top: 0;
}
`,
];
@state()
private _profilingStatus?: boolean;
@state()
private _profilingPerfomance = false;
private _notificationService?: UmbNotificationService;
private async _getProfilingStatus() {
try {
const status = await ProfilingResource.getProfilingStatus();
this._profilingStatus = status.enabled;
} catch (e) {
if (e instanceof ApiError) {
const error = e as ProblemDetails;
const data: UmbNotificationDefaultData = { message: error.message ?? 'Something went wrong' };
this._notificationService?.peek('danger', { data });
}
}
}
constructor() {
super();
this.consumeAllContexts(['umbNotificationService'], (instances) => {
this._notificationService = instances['umbNotificationService'];
});
}
connectedCallback(): void {
super.connectedCallback();
this._getProfilingStatus();
this._profilingPerfomance = localStorage.getItem('profilingPerformance') === 'true';
}
private _changeProfilingPerformance() {
this._profilingPerfomance = !this._profilingPerfomance;
localStorage.setItem('profilingPerformance', this._profilingPerfomance.toString());
}
private renderProfilingStatus() {
return this._profilingStatus
? html`
<p>
Umbraco currently runs in debug mode. This means you can use the built-in performance profiler to assess the
performance when rendering pages.
</p>
<p>
If you want to activate the profiler for a specific page rendering, simply add
<strong>umbDebug=true</strong> to the querystring when requesting the page.
</p>
<p>
If you want the profiler to be activated by default for all page renderings, you can use the toggle below.
It will set a cookie in your browser, which then activates the profiler automatically. In other words, the
profiler will only be active by default in your browser - not everyone else's.
</p>
<uui-toggle
label="Activate the profiler by default"
label-position="left"
.checked="${this._profilingPerfomance}"
@change="${this._changeProfilingPerformance}"></uui-toggle>
<h4>Friendly reminder</h4>
<p>
You should never let a production site run in debug mode. Debug mode is turned off by setting
Umbraco:CMS:Hosting:Debug to false in appsettings.json, appsettings.{Environment}.json or via an environment
variable.
</p>
`
: html`
<p>
Umbraco currently does not run in debug mode, so you can't use the built-in profiler. This is how it should
be for a production site.
</p>
<p>
Debug mode is turned on by setting <b>debug="true"</b> on the <b>&lt;compilation /&gt;</b> element in
web.config.
</p>
`;
}
render() {
return html`
<uui-box>
<h1>Performance Profiling</h1>
${typeof this._profilingStatus === 'undefined' ? html`<uui-loader></uui-loader>` : this.renderProfilingStatus()}
</uui-box>
`;
}

View File

@@ -1,11 +1,10 @@
import { UUIButtonState } from '@umbraco-ui/uui';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
import { customElement, state } from 'lit/decorators.js';
import { ApiError, ProblemDetails, Telemetry, TelemetryLevel, TelemetryResource } from '@umbraco-cms/backend-api';
export type SettingOption = 'Minimal' | 'Basic' | 'Detailed';
@customElement('umb-dashboard-telemetry')
export class UmbDashboardTelemetryElement extends LitElement {
static styles = [
@@ -26,18 +25,21 @@ export class UmbDashboardTelemetryElement extends LitElement {
@state()
private _errorMessage = '';
@state()
private _buttonState: UUIButtonState | undefined = undefined;
constructor() {
super();
}
connectedCallback(): void {
async connectedCallback() {
super.connectedCallback();
this._setup();
await this._setup();
}
private async _setup() {
try {
const consentLevels = await TelemetryResource.getTelemetry({});
const consentLevels = await TelemetryResource.getTelemetry({skip: 0, take: 3});
this._telemetryLevels = consentLevels.items ?? [];
} catch (e) {
if (e instanceof ApiError) {
@@ -59,11 +61,14 @@ export class UmbDashboardTelemetryElement extends LitElement {
private _handleSubmit = async (e: CustomEvent<SubmitEvent>) => {
e.stopPropagation();
this._buttonState = 'waiting';
try {
await TelemetryResource.postTelemetryLevel({
requestBody: { telemetryLevel: this._telemetryFormData },
});
this._buttonState = 'success';
} catch (e) {
this._buttonState = 'failed';
if (e instanceof ApiError) {
const error = e.body as ProblemDetails;
if (e.status === 400) {
@@ -75,10 +80,6 @@ export class UmbDashboardTelemetryElement extends LitElement {
}
};
disconnectedCallback(): void {
super.disconnectedCallback();
}
private _handleChange(e: InputEvent) {
const target = e.target as HTMLInputElement;
this._telemetryFormData = this._telemetryLevels[parseInt(target.value) - 1].telemetryLevel ?? TelemetryLevel.BASIC;
@@ -145,7 +146,7 @@ export class UmbDashboardTelemetryElement extends LitElement {
will be fully anonymized.
</p>
${this._renderSettingSlider()}
<uui-button look="primary" color="positive" label="Save telemetry settings" @click="${this._handleSubmit}">
<uui-button look="primary" color="positive" label="Save telemetry settings" @click="${this._handleSubmit}" .state=${this._buttonState}>
Save
</uui-button>
</div>

View File

@@ -1,18 +0,0 @@
import './editor-data-type.element';
import { Meta, Story } from '@storybook/web-components';
import { html } from 'lit-html';
import { data } from '../../../core/mocks/data/data-type.data';
import type { UmbEditorDataTypeElement } from './editor-data-type.element';
export default {
title: 'Editors/Data Type',
component: 'umb-editor-data-type',
id: 'umb-editor-data-type',
} as Meta;
export const AAAOverview: Story<UmbEditorDataTypeElement> = () =>
html` <umb-editor-data-type id="${data[0].key}"></umb-editor-data-type>`;
AAAOverview.storyName = 'Overview';

View File

@@ -1,18 +0,0 @@
import './editor-document-type.element';
import { Meta, Story } from '@storybook/web-components';
import { html } from 'lit-html';
import { data } from '../../../core/mocks/data/document-type.data';
import type { UmbEditorDocumentTypeElement } from './editor-document-type.element';
export default {
title: 'Editors/Document Type',
component: 'umb-editor-document-type',
id: 'umb-editor-document-type',
} as Meta;
export const AAAOverview: Story<UmbEditorDocumentTypeElement> = () =>
html` <umb-editor-document-type id="${data[0].key}"></umb-editor-document-type>`;
AAAOverview.storyName = 'Overview';

View File

@@ -1,25 +0,0 @@
import './editor-view-document-type-design.element';
import { Meta, Story } from '@storybook/web-components';
import { html } from 'lit-html';
import { data } from '../../../../../core/mocks/data/document-type.data';
import { UmbDocumentTypeContext } from '../../document-type.context';
import type { UmbEditorViewDocumentTypeDesignElement } from './editor-view-document-type-design.element';
export default {
title: 'Editors/Document Type/Views/Design',
component: 'umb-editor-view-document-type-design',
id: 'umb-editor-view-document-type-design',
decorators: [
(story) =>
html` <umb-context-provider key="umbDocumentTypeContext" .value=${new UmbDocumentTypeContext(data[0])}>
${story()}
</umb-context-provider>`,
],
} as Meta;
export const AAAOverview: Story<UmbEditorViewDocumentTypeDesignElement> = () =>
html` <umb-editor-view-document-type-design></umb-editor-view-document-type-design>`;
AAAOverview.storyName = 'Overview';

View File

@@ -1,78 +0,0 @@
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { umbExtensionsRegistry } from '@umbraco-cms/extensions-registry';
import type { ManifestEditorView, ManifestWithLoader } from '@umbraco-cms/models';
import '../shared/node/editor-node.element';
@customElement('umb-editor-document')
export class UmbEditorDocumentElement extends LitElement {
static styles = [
UUITextStyles,
css`
:host {
display: block;
width: 100%;
height: 100%;
}
`,
];
@property()
entityKey!: string;
constructor() {
super();
this._registerEditorViews();
}
private _registerEditorViews() {
const dashboards: Array<ManifestWithLoader<ManifestEditorView>> = [
{
type: 'editorView',
alias: 'Umb.EditorView.Document.Edit',
name: 'Document Editor Edit View',
loader: () => import('../shared/node/views/edit/editor-view-node-edit.element'),
weight: 200,
meta: {
editors: ['Umb.Editor.Document'],
label: 'Info',
pathname: 'content',
icon: 'document',
},
},
{
type: 'editorView',
alias: 'Umb.EditorView.Document.Info',
name: 'Document Editor Info View',
loader: () => import('../shared/node/views/info/editor-view-node-info.element'),
weight: 100,
meta: {
editors: ['Umb.Editor.Document'],
label: 'Info',
pathname: 'info',
icon: 'info',
},
},
];
dashboards.forEach((dashboard) => {
if (umbExtensionsRegistry.isRegistered(dashboard.alias)) return;
umbExtensionsRegistry.register(dashboard);
});
}
render() {
return html`<umb-editor-node .entityKey=${this.entityKey} alias="Umb.Editor.Document"></umb-editor-node>`;
}
}
export default UmbEditorDocumentElement;
declare global {
interface HTMLElementTagNameMap {
'umb-editor-document': UmbEditorDocumentElement;
}
}

View File

@@ -1,20 +0,0 @@
import './editor-document.element';
import { Meta, Story } from '@storybook/web-components';
import { html } from 'lit-html';
import { data } from '../../../core/mocks/data/node.data';
import type { UmbEditorDocumentElement } from './editor-document.element';
export default {
title: 'Editors/Document',
component: 'umb-editor-document',
id: 'umb-editor-document',
} as Meta;
const documentNodes = data.filter((node) => node.type === 'document');
export const AAAOverview: Story<UmbEditorDocumentElement> = () =>
html` <umb-editor-document id="${documentNodes[0].key}"></umb-editor-document>`;
AAAOverview.storyName = 'Overview';

View File

@@ -1,16 +0,0 @@
import './editor-extensions.element';
import { Meta, Story } from '@storybook/web-components';
import { html } from 'lit-html';
import type { UmbEditorExtensionsElement } from './editor-extensions.element';
export default {
title: 'Editors/Extensions',
component: 'umb-editor-extensions',
id: 'umb-editor-extensions',
} as Meta;
export const AAAOverview: Story<UmbEditorExtensionsElement> = () =>
html` <umb-editor-extensions></umb-editor-extensions>`;
AAAOverview.storyName = 'Overview';

View File

@@ -1,103 +0,0 @@
import type { ManifestEditor, ManifestWithLoader } from '@umbraco-cms/models';
export const manifests: Array<ManifestWithLoader<ManifestEditor>> = [
{
type: 'editor',
alias: 'Umb.Editor.Member',
name: 'Member Editor',
loader: () => import('./member/editor-member.element'),
meta: {
entityType: 'member',
},
},
{
type: 'editor',
alias: 'Umb.Editor.MemberGroup',
name: 'Member Group Editor',
loader: () => import('./member-group/editor-member-group.element'),
meta: {
entityType: 'memberGroup',
},
},
{
type: 'editor',
alias: 'Umb.Editor.DataType',
name: 'Data Type Editor',
loader: () => import('./data-type/editor-data-type.element'),
meta: {
entityType: 'dataType',
},
},
{
type: 'editor',
alias: 'Umb.Editor.DocumentType',
name: 'Document Type Editor',
loader: () => import('./document-type/editor-document-type.element'),
meta: {
entityType: 'documentType',
},
},
{
type: 'editor',
alias: 'Umb.Editor.Extensions',
name: 'Extensions Editor',
loader: () => import('./extensions/editor-extensions.element'),
meta: {
entityType: 'extensionsList',
},
},
{
type: 'editor',
alias: 'Umb.Editor.Media',
name: 'Media Editor',
loader: () => import('./media/editor-media.element'),
meta: {
entityType: 'media',
},
},
{
type: 'editor',
alias: 'Umb.Editor.Document',
name: 'Content Editor',
loader: () => import('./document/editor-document.element'),
meta: {
entityType: 'document',
},
},
{
type: 'editor',
alias: 'Umb.Editor.User',
name: 'User Editor',
loader: () => import('./user/editor-user.element'),
meta: {
entityType: 'user',
},
},
{
type: 'editor',
alias: 'Umb.Editor.UserGroup',
name: 'User Group Editor',
loader: () => import('./user-group/editor-user-group.element'),
meta: {
entityType: 'userGroup',
},
},
{
type: 'editor',
alias: 'Umb.Editor.Package',
name: 'Package Editor',
loader: () => import('./package/editor-package.element'),
meta: {
entityType: 'package',
},
},
{
type: 'editor',
alias: 'Umb.Editor.PackageBuilder',
name: 'Package Builder Editor',
loader: () => import('./package-builder/editor-package-builder.element'),
meta: {
entityType: 'packageBuilder',
},
},
];

View File

@@ -1,78 +0,0 @@
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import type { ManifestEditorView, ManifestWithLoader } from '@umbraco-cms/models';
import { umbExtensionsRegistry } from '@umbraco-cms/extensions-registry';
import '../shared/node/editor-node.element';
@customElement('umb-editor-media')
export class UmbEditorMediaElement extends LitElement {
static styles = [
UUITextStyles,
css`
:host {
display: block;
width: 100%;
height: 100%;
}
`,
];
@property()
entityKey!: string;
constructor() {
super();
this._registerEditorViews();
}
private _registerEditorViews() {
const dashboards: Array<ManifestWithLoader<ManifestEditorView>> = [
{
type: 'editorView',
alias: 'Umb.EditorView.Media.Edit',
name: 'Media Editor Edit View',
loader: () => import('../shared/node/views/edit/editor-view-node-edit.element'),
weight: 200,
meta: {
editors: ['Umb.Editor.Media'],
label: 'Media',
pathname: 'media',
icon: 'umb:picture',
},
},
{
type: 'editorView',
alias: 'Umb.EditorView.Media.Info',
name: 'Media Editor Info View',
loader: () => import('../shared/node/views/info/editor-view-node-info.element'),
weight: 100,
meta: {
editors: ['Umb.Editor.Media'],
label: 'Info',
pathname: 'info',
icon: 'info',
},
},
];
dashboards.forEach((dashboard) => {
if (umbExtensionsRegistry.isRegistered(dashboard.alias)) return;
umbExtensionsRegistry.register(dashboard);
});
}
render() {
return html`<umb-editor-node .entityKey=${this.entityKey} alias="Umb.Editor.Media"></umb-editor-node>`;
}
}
export default UmbEditorMediaElement;
declare global {
interface HTMLElementTagNameMap {
'umb-editor-media': UmbEditorMediaElement;
}
}

View File

@@ -1,20 +0,0 @@
import './editor-media.element';
import { Meta, Story } from '@storybook/web-components';
import { html } from 'lit-html';
import { data } from '../../../core/mocks/data/node.data';
import type { UmbEditorMediaElement } from './editor-media.element';
export default {
title: 'Editors/Media',
component: 'umb-editor-media',
id: 'umb-editor-media',
} as Meta;
const mediaNodes = data.filter((node) => node.type === 'media');
export const AAAOverview: Story<UmbEditorMediaElement> = () =>
html` <umb-editor-media id="${mediaNodes[0].key}"></umb-editor-media>`;
AAAOverview.storyName = 'Overview';

View File

@@ -1,33 +0,0 @@
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import '../shared/editor-entity-layout/editor-entity-layout.element';
@customElement('umb-editor-package-builder')
export class UmbEditorPackageBuilderElement extends LitElement {
static styles = [
UUITextStyles,
css`
:host {
display: block;
width: 100%;
height: 100%;
}
`,
];
render() {
return html`<umb-editor-entity-layout alias="Umb.Editor.PackageBuilder"
>PACKAGE BUILDER</umb-editor-entity-layout
> `;
}
}
export default UmbEditorPackageBuilderElement;
declare global {
interface HTMLElementTagNameMap {
'umb-editor-package-builder': UmbEditorPackageBuilderElement;
}
}

View File

@@ -1,31 +0,0 @@
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import '../shared/editor-entity-layout/editor-entity-layout.element';
@customElement('umb-editor-package')
export class UmbEditorPackageElement extends LitElement {
static styles = [
UUITextStyles,
css`
:host {
display: block;
width: 100%;
height: 100%;
}
`,
];
render() {
return html`<umb-editor-entity-layout alias="Umb.Editor.Package">PACKAGE EDITOR</umb-editor-entity-layout> `;
}
}
export default UmbEditorPackageElement;
declare global {
interface HTMLElementTagNameMap {
'umb-editor-package': UmbEditorPackageElement;
}
}

View File

@@ -1,46 +0,0 @@
import { UUITextStyles } from '@umbraco-ui/uui';
import { CSSResultGroup, html, LitElement } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { createExtensionElement } from '@umbraco-cms/extensions-api';
import type { ManifestEditorAction } from '@umbraco-cms/models';
@customElement('umb-editor-action-extension')
export class UmbEditorActionExtensionElement extends LitElement {
static styles: CSSResultGroup = [UUITextStyles];
private _editorAction?: ManifestEditorAction;
@property({ type: Object })
public get editorAction(): ManifestEditorAction | undefined {
return this._editorAction;
}
public set editorAction(value: ManifestEditorAction | undefined) {
this._editorAction = value;
this._createElement();
}
@state()
private _element?: any;
private async _createElement() {
if (!this.editorAction) return;
try {
this._element = await createExtensionElement(this.editorAction);
if (!this._element) return;
this._element.editorAction = this.editorAction;
} catch (error) {
// TODO: loading JS failed so we should do some nice UI. (This does only happen if extension has a js prop, otherwise we concluded that no source was needed resolved the load.)
}
}
render() {
return html`${this._element}`;
}
}
declare global {
interface HTMLElementTagNameMap {
'umb-editor-action-extension': UmbEditorActionExtensionElement;
}
}

View File

@@ -1,91 +0,0 @@
import { css, html, LitElement } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement, property, state } from 'lit/decorators.js';
import { map } from 'rxjs';
import { UmbObserverMixin } from '@umbraco-cms/observable-api';
import { createExtensionElement } from '@umbraco-cms/extensions-api';
import { umbExtensionsRegistry } from '@umbraco-cms/extensions-registry';
import { UmbContextConsumerMixin } from '@umbraco-cms/context-api';
import type { ManifestEditor } from '@umbraco-cms/models';
@customElement('umb-editor-entity')
export class UmbEditorEntityElement extends UmbContextConsumerMixin(UmbObserverMixin(LitElement)) {
static styles = [
UUITextStyles,
css`
:host {
display: block;
width: 100%;
height: 100%;
}
`,
];
@property()
public entityKey!: string;
private _entityType = '';
@property()
public get entityType(): string {
return this._entityType;
}
public set entityType(value: string) {
this._entityType = value;
this._observeEditors();
}
@state()
private _element?: any;
private _currentEditorAlias = '';
connectedCallback(): void {
super.connectedCallback();
this._observeEditors();
}
private _observeEditors() {
this.observe<ManifestEditor>(
umbExtensionsRegistry
.extensionsOfType('editor')
.pipe(map((editors) => editors.find((editor) => editor.meta.entityType === this.entityType))),
(editor) => {
// don't rerender editor if it's the same
if (this._currentEditorAlias === editor.alias) return;
this._currentEditorAlias = editor.alias;
this._createElement(editor);
}
);
}
private async _createElement(editor?: ManifestEditor) {
// TODO: implement fallback editor
const fallbackEditor = document.createElement('div');
fallbackEditor.innerHTML = '<p>No editor found</p>';
if (!editor) {
this._element = fallbackEditor;
return;
}
try {
this._element = (await createExtensionElement(editor)) as any;
this._element.entityKey = this.entityKey;
} catch (error) {
this._element = fallbackEditor;
}
}
render() {
return html`${this._element}`;
}
}
export default UmbEditorEntityElement;
declare global {
interface HTMLElementTagNameMap {
'umb-editor-entity': UmbEditorEntityElement;
}
}

View File

@@ -1,74 +0,0 @@
import { css, html, LitElement } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement } from 'lit/decorators.js';
@customElement('umb-editor-layout')
export class UmbEditorLayout extends LitElement {
static styles = [
UUITextStyles,
css`
:host {
display: block;
width: 100%;
height: 100%;
}
#editor-frame {
background-color: var(--uui-color-background);
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
#header {
background-color: var(--uui-color-surface);
width: 100%;
border-bottom: 1px solid var(--uui-color-border);
box-sizing: border-box;
padding: 0 var(--uui-size-6);
}
#main {
/* padding: 0 var(--uui-size-6); */
display: flex;
flex: 1;
flex-direction: column;
}
#footer {
display: flex;
align-items: center;
height: 70px;
width: 100%;
padding: 0 var(--uui-size-6);
border-top: 1px solid var(--uui-color-border);
background-color: var(--uui-color-surface);
box-sizing: border-box;
}
`,
];
render() {
return html`
<div id="editor-frame">
<div id="header">
<slot name="header"></slot>
</div>
<uui-scroll-container id="main">
<slot></slot>
</uui-scroll-container>
<div id="footer">
<!-- only show footer if slot has elements -->
<slot name="footer"></slot>
</div>
</div>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'umb-editor-layout': UmbEditorLayout;
}
}

View File

@@ -1,20 +0,0 @@
import { Meta, Story } from '@storybook/web-components';
import { html } from 'lit-html';
import type { UmbEditorPropertyLayoutElement } from './editor-property-layout.element';
import './editor-property-layout.element';
export default {
title: 'Editors/Shared/Editor Property Layout',
component: 'umb-editor-property-layout',
id: 'umb-editor-property-layout',
} as Meta;
export const AAAOverview: Story<UmbEditorPropertyLayoutElement> = () => html` <umb-editor-property-layout
label="Label"
description="Description">
<div slot="property-action-menu"><uui-button color="" look="placeholder">Menu</uui-button></div>
<div slot="editor"><uui-button color="" look="placeholder">Editor</uui-button></div>
</umb-editor-property-layout>`;
AAAOverview.storyName = 'Overview';

View File

@@ -1,18 +0,0 @@
import './editor-node.element';
import { Meta, Story } from '@storybook/web-components';
import { html } from 'lit-html';
import { data } from '../../../../core/mocks/data/node.data';
import type { UmbEditorNodeElement } from './editor-node.element';
export default {
title: 'Editors/Shared/Node',
component: 'umb-editor-node',
id: 'umb-editor-node',
} as Meta;
export const AAAOverview: Story<UmbEditorNodeElement> = () =>
html` <umb-editor-node id="${data[0].key}"></umb-editor-node>`;
AAAOverview.storyName = 'Overview';

View File

@@ -1,63 +0,0 @@
import { html, LitElement } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement, state } from 'lit/decorators.js';
import { distinctUntilChanged } from 'rxjs';
import { NodeEntity, NodeProperty, NodePropertyData } from '../../../../../../core/mocks/data/node.data';
import { UmbNodeContext } from '../../node.context';
import { UmbContextConsumerMixin } from '@umbraco-cms/context-api';
import '../../../../../components/node-property/node-property.element';
import { UmbObserverMixin } from '@umbraco-cms/observable-api';
@customElement('umb-editor-view-node-edit')
export class UmbEditorViewNodeEditElement extends UmbContextConsumerMixin(UmbObserverMixin(LitElement)) {
static styles = [UUITextStyles];
@state()
_properties: NodeProperty[] = [];
@state()
_data: NodePropertyData[] = [];
private _nodeContext?: UmbNodeContext;
constructor() {
super();
this.consumeContext('umbNodeContext', (nodeContext) => {
this._nodeContext = nodeContext;
this._observeNode();
});
}
private _observeNode() {
if (!this._nodeContext) return;
this.observe<NodeEntity>(this._nodeContext.data.pipe(distinctUntilChanged()), (node) => {
this._properties = node.properties;
this._data = node.data;
});
}
render() {
return html`
<uui-box>
${this._properties.map(
(property: NodeProperty) => html`
<umb-node-property
.property=${property}
.value=${this._data.find((data) => data.alias === property.alias)?.value}></umb-node-property>
`
)}
</uui-box>
`;
}
}
export default UmbEditorViewNodeEditElement;
declare global {
interface HTMLElementTagNameMap {
'umb-editor-view-node-edit': UmbEditorViewNodeEditElement;
}
}

View File

@@ -1,25 +0,0 @@
import './editor-view-node-edit.element';
import { Meta, Story } from '@storybook/web-components';
import { html } from 'lit-html';
import { data } from '../../../../../../core/mocks/data/node.data';
import { UmbNodeContext } from '../../node.context';
import type { UmbEditorViewNodeEditElement } from './editor-view-node-edit.element';
export default {
title: 'Editors/Shared/Node/Views/Edit',
component: 'umb-editor-view-node-edit',
id: 'umb-editor-view-node-edit',
decorators: [
(story) =>
html` <umb-context-provider key="umbNodeContext" .value=${new UmbNodeContext(data[0])}>
${story()}
</umb-context-provider>`,
],
} as Meta;
export const AAAOverview: Story<UmbEditorViewNodeEditElement> = () =>
html` <umb-editor-view-node-edit></umb-editor-view-node-edit>`;
AAAOverview.storyName = 'Overview';

View File

@@ -1,25 +0,0 @@
import './editor-view-node-info.element';
import { Meta, Story } from '@storybook/web-components';
import { html } from 'lit-html';
import { data } from '../../../../../../core/mocks/data/node.data';
import { UmbNodeContext } from '../../node.context';
import type { UmbEditorViewNodeInfoElement } from './editor-view-node-info.element';
export default {
title: 'Editors/Shared/Node/Views/Info',
component: 'umb-editor-view-node-info',
id: 'umb-editor-view-node-info',
decorators: [
(story) =>
html` <umb-context-provider key="umbNodeContext" .value=${new UmbNodeContext(data[0])}>
${story()}
</umb-context-provider>`,
],
} as Meta;
export const AAAOverview: Story<UmbEditorViewNodeInfoElement> = () =>
html` <umb-editor-view-node-info></umb-editor-view-node-info>`;
AAAOverview.storyName = 'Overview';

View File

@@ -1,283 +0,0 @@
import { UUIInputElement, UUIInputEvent } from '@umbraco-ui/uui';
import { UUITextStyles } from '@umbraco-ui/uui-css';
import { css, html, LitElement, nothing } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
@customElement('umb-editor-user-group')
export class UmbEditorUserGroupElement extends LitElement {
static styles = [
UUITextStyles,
css`
:host {
display: block;
height: 100%;
}
#main {
display: grid;
grid-template-columns: 1fr 350px;
gap: var(--uui-size-space-6);
padding: var(--uui-size-space-6);
}
#left-column {
display: flex;
flex-direction: column;
gap: var(--uui-size-space-4);
}
#right-column > uui-box > div {
display: flex;
flex-direction: column;
gap: var(--uui-size-space-2);
}
hr {
border: none;
border-bottom: 1px solid var(--uui-color-divider);
width: 100%;
}
uui-input {
width: 100%;
}
.faded-text {
color: var(--uui-color-text-alt);
font-size: 0.8rem;
}
#default-permissions {
display: flex;
flex-direction: column;
gap: var(--uui-size-space-4);
}
.default-permission {
display: flex;
align-items: center;
gap: var(--uui-size-space-4);
padding: var(--uui-size-space-2);
}
.default-permission:not(:last-child) {
border-bottom: 1px solid var(--uui-color-divider);
}
.permission-info {
display: flex;
flex-direction: column;
}
`,
];
@state()
private _userName = '';
@property({ type: String })
entityKey = '';
defaultPermissions: Array<{
name: string;
permissions: Array<{ name: string; description: string; value: boolean }>;
}> = [
{
name: 'Administration',
permissions: [
{
name: 'Culture and Hostnames',
description: 'Allow access to assign culture and hostnames',
value: false,
},
{
name: 'Restrict Public Access',
description: 'Allow access to set and change access restrictions for a node',
value: false,
},
{
name: 'Rollback',
description: 'Allow access to roll back a node to a previous state',
value: false,
},
],
},
{
name: 'Content',
permissions: [
{
name: 'Browse Node',
description: 'Allow access to view a node',
value: false,
},
{
name: 'Create Content Template',
description: 'Allow access to create a Content Template',
value: false,
},
{
name: 'Delete',
description: 'Allow access to delete nodes',
value: false,
},
{
name: 'Create',
description: 'Allow access to create nodes',
value: false,
},
{
name: 'Publish',
description: 'Allow access to publish nodes',
value: false,
},
{
name: 'Permissions',
description: 'Allow access to change permissions for a node',
value: false,
},
{
name: 'Send To Publish',
description: 'Allow access to send a node for approval before publishing',
value: false,
},
{
name: 'Unpublish',
description: 'Allow access to unpublish a node',
value: false,
},
{
name: 'Update',
description: 'Allow access to save a node',
value: false,
},
{
name: 'Full restore',
description: 'Allow the user to restore items',
value: false,
},
{
name: 'Partial restore',
description: 'Allow the user to partial restore items',
value: false,
},
{
name: 'Queue for transfer',
description: 'Allow the user to queue item(s)',
value: false,
},
],
},
{
name: 'Structure',
permissions: [
{
name: 'Copy',
description: 'Allow access to copy a node',
value: false,
},
{
name: 'Move',
description: 'Allow access to move a node',
value: false,
},
{
name: 'Sort',
description: 'Allow access to change the sort order for nodes',
value: false,
},
],
},
];
private renderLeftColumn() {
return html` <uui-box>
<div slot="headline">Assign access</div>
<div>
<b>Sections</b>
<div class="faded-text">Add sections to give users access</div>
</div>
<div>
<b>Content start nodes</b>
<div class="faded-text">Limit the content tree to specific start nodes</div>
<umb-property-editor-ui-content-picker></umb-property-editor-ui-content-picker>
</div>
<div>
<b>Media start nodes</b>
<div class="faded-text">Limit the media library to specific start nodes</div>
<umb-property-editor-ui-content-picker></umb-property-editor-ui-content-picker>
</div>
<b>Content</b>
<div class="access-content">
<uui-icon name="folder"></uui-icon>
<span>Content Root</span>
</div>
<b>Media</b>
<div class="access-content">
<uui-icon name="folder"></uui-icon>
<span>Media Root</span>
</div>
</uui-box>
<uui-box>
<div slot="headline">Default Permissions</div>
<div id="default-permissions">
${repeat(
this.defaultPermissions,
(defaultPermission) => html`
<div>
<b>${defaultPermission.name}</b>
${repeat(
defaultPermission.permissions,
(permission) => html`
<div class="default-permission">
<uui-toggle
.checked=${permission.value}
@change=${(e: Event) => {
permission.value = (e.target as HTMLInputElement).checked;
}}></uui-toggle>
<div class="permission-info">
<b>${permission.name}</b>
<span class="faded-text">${permission.description}</span>
</div>
</div>
`
)}
</div>
`
)}
</div>
</uui-box>
<uui-box>
<div slot="headline">Granular permissions</div>
</uui-box>`;
}
private renderRightColumn() {
return html`<uui-box>
<div slot="headline">Users</div>
</uui-box>`;
}
// TODO. find a way where we don't have to do this for all editors.
private _handleInput(event: UUIInputEvent) {
if (event instanceof UUIInputEvent) {
const target = event.composedPath()[0] as UUIInputElement;
console.log('input', target.value);
}
}
render() {
return html`
<umb-editor-entity-layout alias="Umb.Editor.UserGroup">
<uui-input id="name" slot="name" .value=${this._userName} @input="${this._handleInput}"></uui-input>
<div id="main">
<div id="left-column">${this.renderLeftColumn()}</div>
<div id="right-column">${this.renderRightColumn()}</div>
</div>
</umb-editor-entity-layout>
`;
}
}
export default UmbEditorUserGroupElement;
declare global {
interface HTMLElementTagNameMap {
'umb-editor-user-group': UmbEditorUserGroupElement;
}
}

View File

@@ -1,335 +0,0 @@
import { UUIInputElement, UUIInputEvent } from '@umbraco-ui/uui';
import { css, html, LitElement, nothing } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement, property, state } from 'lit/decorators.js';
import { Subscription } from 'rxjs';
import { ifDefined } from 'lit-html/directives/if-defined.js';
import { UmbUserStore } from '../../../core/stores/user/user.store';
import { getTagLookAndColor } from '../../sections/users/user-extensions';
import { UmbUserContext } from './user.context';
import { UmbContextProviderMixin, UmbContextConsumerMixin } from '@umbraco-cms/context-api';
import type { ManifestEditorAction, ManifestWithLoader, UserDetails } from '@umbraco-cms/models';
import '../../property-editor-uis/content-picker/property-editor-ui-content-picker.element';
import '../shared/editor-entity-layout/editor-entity-layout.element';
import { umbExtensionsRegistry } from '@umbraco-cms/extensions-registry';
@customElement('umb-editor-user')
export class UmbEditorUserElement extends UmbContextProviderMixin(UmbContextConsumerMixin(LitElement)) {
static styles = [
UUITextStyles,
css`
:host {
display: block;
height: 100%;
}
#main {
display: grid;
grid-template-columns: 1fr 350px;
gap: var(--uui-size-space-6);
padding: var(--uui-size-space-6);
}
#left-column {
display: flex;
flex-direction: column;
gap: var(--uui-size-space-4);
}
#right-column > uui-box > div {
display: flex;
flex-direction: column;
gap: var(--uui-size-space-2);
}
uui-avatar {
font-size: var(--uui-size-16);
place-self: center;
}
hr {
border: none;
border-bottom: 1px solid var(--uui-color-divider);
width: 100%;
}
uui-input {
width: 100%;
}
.faded-text {
color: var(--uui-color-text-alt);
font-size: 0.8rem;
}
uui-tag {
width: fit-content;
}
#user-info {
display: flex;
gap: var(--uui-size-space-6);
}
#user-info > div {
display: flex;
flex-direction: column;
}
#assign-access {
display: flex;
flex-direction: column;
gap: var(--uui-size-space-4);
}
.access-content {
margin-top: var(--uui-size-space-1);
margin-bottom: var(--uui-size-space-4);
display: flex;
align-items: center;
line-height: 1;
gap: var(--uui-size-space-3);
}
.access-content > span {
align-self: end;
}
`,
];
@state()
private _user?: UserDetails | null;
@state()
private _userName = '';
@property({ type: String })
entityKey = '';
protected _userStore?: UmbUserStore;
protected _usersSubscription?: Subscription;
private _userContext?: UmbUserContext;
private _userNameSubscription?: Subscription;
private _languages = []; //TODO Add languages
constructor() {
super();
this._registerEditorActions();
}
private _registerEditorActions() {
const manifests: Array<ManifestWithLoader<ManifestEditorAction>> = [
{
type: 'editorAction',
alias: 'Umb.EditorAction.User.Save',
name: 'EditorActionUserSave',
loader: () => import('./actions/editor-action-user-save.element'),
meta: {
editors: ['Umb.Editor.User'],
},
},
];
manifests.forEach((manifest) => {
if (umbExtensionsRegistry.isRegistered(manifest.alias)) return;
umbExtensionsRegistry.register(manifest);
});
}
connectedCallback(): void {
super.connectedCallback();
this.consumeContext('umbUserStore', (usersContext: UmbUserStore) => {
this._userStore = usersContext;
this._observeUser();
});
}
private _observeUser() {
this._usersSubscription?.unsubscribe();
this._usersSubscription = this._userStore?.getByKey(this.entityKey).subscribe((user) => {
this._user = user;
if (!this._user) return;
if (!this._userContext) {
this._userContext = new UmbUserContext(this._user);
this.provideContext('umbUserContext', this._userContext);
} else {
this._userContext.update(this._user);
}
this._userNameSubscription = this._userContext.data.subscribe((user) => {
if (user && user.name !== this._userName) {
this._userName = user.name;
}
});
});
}
disconnectedCallback(): void {
super.disconnectedCallback();
this._usersSubscription?.unsubscribe();
this._userNameSubscription?.unsubscribe();
}
private _updateUserStatus() {
if (!this._user || !this._userStore) return;
const isDisabled = this._user.status === 'disabled';
isDisabled ? this._userStore.enableUsers([this._user.key]) : this._userStore.disableUsers([this._user.key]);
}
private _deleteUser() {
if (!this._user || !this._userStore) return;
this._userStore.deleteUsers([this._user.key]);
history.pushState(null, '', '/section/users/view/users/overview');
}
private renderLeftColumn() {
if (!this._user) return nothing;
return html` <uui-box>
<div slot="headline">Profile</div>
<uui-form-layout-item style="margin-top: 0">
<uui-label for="email">Email</uui-label>
<uui-input name="email" label="email" readonly value=${this._user.email}></uui-input>
</uui-form-layout-item>
<uui-form-layout-item style="margin-bottom: 0">
<uui-label for="language">Language</uui-label>
<uui-select name="language" label="language" .options=${this._languages}> </uui-select>
</uui-form-layout-item>
</uui-box>
<uui-box>
<div id="assign-access">
<div slot="headline">Assign access</div>
<div>
<b>Groups</b>
<div class="faded-text">Add groups to assign access and permissions</div>
</div>
<div>
<b>Content start nodes</b>
<div class="faded-text">Limit the content tree to specific start nodes</div>
<umb-property-editor-ui-content-picker></umb-property-editor-ui-content-picker>
</div>
<div>
<b>Media start nodes</b>
<div class="faded-text">Limit the media library to specific start nodes</div>
<umb-property-editor-ui-content-picker></umb-property-editor-ui-content-picker>
</div>
</div>
</uui-box>
<uui-box>
<div slot="headline">Access</div>
<div slot="header" class="faded-text">
Based on the assigned groups and start nodes, the user has access to the following nodes
</div>
<b>Content</b>
<div class="access-content">
<uui-icon name="folder"></uui-icon>
<span>Content Root</span>
</div>
<b>Media</b>
<div class="access-content">
<uui-icon name="folder"></uui-icon>
<span>Media Root</span>
</div>
</uui-box>`;
}
private renderRightColumn() {
if (!this._user || !this._userStore) return nothing;
const statusLook = getTagLookAndColor(this._user.status);
return html` <uui-box>
<div id="user-info">
<uui-avatar .name=${this._user?.name || ''}></uui-avatar>
<uui-button label="Change photo"></uui-button>
<hr />
${this._user?.status !== 'invited'
? html`
<uui-button
@click=${this._updateUserStatus}
look="primary"
color="${this._user.status === 'disabled' ? 'positive' : 'warning'}"
label="${this._user.status === 'disabled' ? 'Enable' : 'Disable'}"></uui-button>
`
: nothing}
<uui-button @click=${this._deleteUser} look="primary" color="danger" label="Delete User"></uui-button>
<div>
<b>Status:</b>
<uui-tag look="${ifDefined(statusLook?.look)}" color="${ifDefined(statusLook?.color)}">
${this._user.status}
</uui-tag>
</div>
${this._user?.status === 'invited'
? html`
<uui-textarea placeholder="Enter a message..."> </uui-textarea>
<uui-button look="primary" label="Resend invitation"></uui-button>
`
: nothing}
<div>
<b>Last login:</b>
<span>${this._user.lastLoginDate || `${this._user.name} has not logged in yet`}</span>
</div>
<div>
<b>Failed login attempts</b>
<span>${this._user.failedLoginAttempts}</span>
</div>
<div>
<b>Last lockout date:</b>
<span>${this._user.lastLockoutDate || `${this._user.name} has not been locked out`}</span>
</div>
<div>
<b>Password last changed:</b>
<span>${this._user.lastLoginDate || `${this._user.name} has not changed password`}</span>
</div>
<div>
<b>User created:</b>
<span>${this._user.createDate}</span>
</div>
<div>
<b>User last updated:</b>
<span>${this._user.updateDate}</span>
</div>
<div>
<b>Key:</b>
<span>${this._user.key}</span>
</div>
</div>
</uui-box>`;
}
// TODO. find a way where we don't have to do this for all editors.
private _handleInput(event: UUIInputEvent) {
if (event instanceof UUIInputEvent) {
const target = event.composedPath()[0] as UUIInputElement;
if (typeof target?.value === 'string') {
this._userContext?.update({ name: target.value });
}
}
}
render() {
if (!this._user) return html`User not found`;
return html`
<umb-editor-entity-layout alias="Umb.Editor.User">
<uui-input id="name" slot="name" .value=${this._userName} @input="${this._handleInput}"></uui-input>
<div id="main">
<div id="left-column">${this.renderLeftColumn()}</div>
<div id="right-column">${this.renderRightColumn()}</div>
</div>
</umb-editor-entity-layout>
`;
}
}
export default UmbEditorUserElement;
declare global {
interface HTMLElementTagNameMap {
'umb-editor-view-users-user-details': UmbEditorUserElement;
}
}

View File

@@ -0,0 +1,44 @@
import { CSSResultGroup, html, LitElement } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement, property, state } from 'lit/decorators.js';
import { createExtensionElement } from '@umbraco-cms/extensions-api';
import type { ManifestExternalLoginProvider } from '@umbraco-cms/models';
@customElement('umb-external-login-provider-extension')
export class UmbExternalLoginProviderExtensionElement extends LitElement {
static styles: CSSResultGroup = [UUITextStyles];
private _externalLoginProvider?: ManifestExternalLoginProvider;
@property({ type: Object })
public get externalLoginProvider(): ManifestExternalLoginProvider | undefined {
return this._externalLoginProvider;
}
public set externalLoginProvider(value: ManifestExternalLoginProvider | undefined) {
this._externalLoginProvider = value;
this._createElement();
}
@state()
private _element?: any;
private async _createElement() {
if (!this.externalLoginProvider) return;
try {
this._element = (await createExtensionElement(this.externalLoginProvider)) as any | undefined;
} catch (error) {
// TODO: loading JS failed so we should do some nice UI. (This does only happen if extension has a js prop, otherwise we concluded that no source was needed resolved the load.)
}
}
render() {
return html`${this._element}`;
}
}
declare global {
interface HTMLElementTagNameMap {
'umb-external-login-provider-extension': UmbExternalLoginProviderExtensionElement;
}
}

View File

@@ -0,0 +1,44 @@
import { css, html, LitElement } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement } from 'lit/decorators.js';
import { UmbContextProviderMixin, UmbContextConsumerMixin } from '@umbraco-cms/context-api';
import { UmbObserverMixin } from '@umbraco-cms/observable-api';
@customElement('umb-external-login-provider-test')
export class UmbExternalLoginProviderTestElement extends UmbContextProviderMixin(
UmbContextConsumerMixin(UmbObserverMixin(LitElement))
) {
static styles = [
UUITextStyles,
css`
:host {
display: flex;
flex-direction: column;
gap: var(--uui-size-space-4);
padding: var(--uui-size-space-5);
border: 1px solid var(--uui-color-border);
background: var(--uui-color-surface-alt);
border-radius: var(--uui-border-radius);
}
p {
margin: 0;
}
`,
];
render() {
return html`
<b>Custom External Login Provider</b>
<p>This is an example of a custom external login provider using the external login provider extension point</p>
<uui-button label="My custom login provider" look="primary"></uui-button>
`;
}
}
export default UmbExternalLoginProviderTestElement;
declare global {
interface HTMLElementTagNameMap {
'umb-external-login-provider-test': UmbExternalLoginProviderTestElement;
}
}

View File

@@ -0,0 +1,57 @@
import { css, html, LitElement } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement } from 'lit/decorators.js';
import { UmbContextProviderMixin, UmbContextConsumerMixin } from '@umbraco-cms/context-api';
import { UmbObserverMixin } from '@umbraco-cms/observable-api';
@customElement('umb-external-login-provider-test2')
export class UmbExternalLoginProviderTest2Element extends UmbContextProviderMixin(
UmbContextConsumerMixin(UmbObserverMixin(LitElement))
) {
static styles = [
UUITextStyles,
css`
:host {
display: flex;
flex-direction: column;
gap: var(--uui-size-space-4);
padding: var(--uui-size-space-5);
border: 1px solid var(--uui-color-border);
background: var(--uui-color-surface-alt);
border-radius: var(--uui-border-radius);
}
p {
margin: 0;
}
uui-input {
width: 100%;
}
`,
];
render() {
return html`
<b>Another Custom External Login Provider</b>
<p>This is an example of another custom external login provider</p>
<uui-form-layout-item>
<uui-label id="emailLabel" for="email" slot="label" required>Email</uui-label>
<uui-input
type="email"
id="email"
name="email"
placeholder="Enter your email..."
required
required-message="Email is required"></uui-input>
</uui-form-layout-item>
<uui-button label="Custom login" look="primary"></uui-button>
`;
}
}
export default UmbExternalLoginProviderTest2Element;
declare global {
interface HTMLElementTagNameMap {
'umb-external-login-provider-test2': UmbExternalLoginProviderTest2Element;
}
}

View File

@@ -0,0 +1,28 @@
import type { ManifestExternalLoginProvider } from '@umbraco-cms/models';
export const manifests: Array<ManifestExternalLoginProvider> = [
{
type: 'externalLoginProvider',
alias: 'Umb.ExternalLoginProvider.Test',
name: 'Test External Login Provider',
elementName: 'umb-external-login-provider-test',
loader: () => import('./external-login-provider-test.element'),
weight: 2,
meta: {
label: 'Test External Login Provider',
pathname: 'test/test/test',
},
},
{
type: 'externalLoginProvider',
alias: 'Umb.ExternalLoginProvider.Test2',
name: 'Test External Login Provider 2',
elementName: 'umb-external-login-provider-test2',
loader: () => import('./external-login-provider-test2.element'),
weight: 1,
meta: {
label: 'Test External Login Provider 2',
pathname: 'test/test/test',
},
},
];

View File

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

View File

@@ -0,0 +1,61 @@
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, CSSResultGroup, html, LitElement } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { UmbContextConsumerMixin } from '@umbraco-cms/context-api';
import { UmbObserverMixin } from '@umbraco-cms/observable-api';
import type { UserDetails } from '@umbraco-cms/models';
import { UmbModalService } from '@umbraco-cms/services';
import { umbCurrentUserService } from 'src/core/services/current-user';
@customElement('umb-header-app-current-user')
export class UmbHeaderAppCurrentUser extends UmbContextConsumerMixin(UmbObserverMixin(LitElement)) {
static styles: CSSResultGroup = [
UUITextStyles,
css`
uui-button {
font-size: 14px;
}
`,
];
@state()
private _currentUser?: UserDetails;
private _modalService?: UmbModalService;
constructor() {
super();
this.consumeAllContexts(['umbUserStore', 'umbModalService'], (instances) => {
this._modalService = instances['umbModalService'];
this._observeCurrentUser();
});
}
private async _observeCurrentUser() {
this.observe<UserDetails>(umbCurrentUserService.currentUser, (currentUser) => {
this._currentUser = currentUser;
});
}
private _handleUserClick() {
this._modalService?.userSettings();
}
render() {
return html`
<uui-button @click=${this._handleUserClick} look="primary" label="${this._currentUser?.name || ''}" compact>
<uui-avatar name="${this._currentUser?.name || ''}"></uui-avatar>
</uui-button>
`;
}
}
export default UmbHeaderAppCurrentUser;
declare global {
interface HTMLElementTagNameMap {
'umb-header-app-current-user': UmbHeaderAppCurrentUser;
}
}

View File

@@ -1,6 +1,6 @@
import type { ManifestPropertyAction, ManifestWithLoader } from '@umbraco-cms/models';
import type { ManifestPropertyAction } from '@umbraco-cms/models';
export const manifests: Array<ManifestWithLoader<ManifestPropertyAction>> = [
export const manifests: Array<ManifestPropertyAction> = [
{
type: 'propertyAction',
alias: 'Umb.PropertyAction.Copy',

View File

@@ -1,13 +1,14 @@
import { css, html, LitElement, nothing } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement, property, state } from 'lit/decorators.js';
import { ifDefined } from 'lit-html/directives/if-defined.js';
import type { UmbModalService } from '../../../core/services/modal';
import type { UmbEntityStore } from '../../../core/stores/entity.store';
import type { Entity } from '../../../core/mocks/data/entities';
import { UmbObserverMixin } from '@umbraco-cms/observable-api';
import { UmbContextConsumerMixin } from '@umbraco-cms/context-api';
import { UmbDocumentStore } from 'src/core/stores/document/document.store';
import { FolderTreeItem } from '@umbraco-cms/backend-api';
// TODO: rename to Document Picker
@customElement('umb-property-editor-ui-content-picker')
export class UmbPropertyEditorUIContentPickerElement extends UmbContextConsumerMixin(UmbObserverMixin(LitElement)) {
static styles = [
@@ -41,36 +42,37 @@ export class UmbPropertyEditorUIContentPickerElement extends UmbContextConsumerM
public config = [];
@state()
private _items: Array<Entity> = [];
private _items: Array<FolderTreeItem> = [];
private _modalService?: UmbModalService;
private _entityStore?: UmbEntityStore;
private _documentStore?: UmbDocumentStore;
constructor() {
super();
this.consumeAllContexts(['umbEntityStore', 'umbModalService'], (instances) => {
this._entityStore = instances['umbEntityStore'];
this.consumeAllContexts(['umbDocumentStore', 'umbModalService'], (instances) => {
this._documentStore = instances['umbDocumentStore'];
this._modalService = instances['umbModalService'];
this._observePickedEntities();
this._observePickedDocuments();
});
}
private _observePickedEntities() {
if (!this._entityStore) return;
this.observe<Entity[]>(this._entityStore.getByKeys(this.value), (entities) => {
this._items = entities;
private _observePickedDocuments() {
if (!this._documentStore) return;
// TODO: consider changing this to the list data endpoint when it is available
this.observe<FolderTreeItem[]>(this._documentStore.getTreeItems(this.value), (items) => {
this._items = items;
});
}
private _openPicker() {
const modalHandler = this._modalService?.contentPicker({ multiple: true, selection: this.value });
modalHandler?.onClose().then(({ selection }: any) => {
this._setValue([...this.value, ...selection]);
this._setValue([...selection]);
});
}
private _removeItem(item: Entity) {
private _removeItem(item: FolderTreeItem) {
const modalHandler = this._modalService?.confirm({
color: 'danger',
headline: `Remove ${item.name}?`,
@@ -88,14 +90,17 @@ export class UmbPropertyEditorUIContentPickerElement extends UmbContextConsumerM
private _setValue(newValue: Array<string>) {
this.value = newValue;
this._observePickedEntities();
this._observePickedDocuments();
this.dispatchEvent(new CustomEvent('property-editor-change', { bubbles: true, composed: true }));
}
private _renderItem(item: Entity) {
private _renderItem(item: FolderTreeItem) {
// TODO: remove when we have a way to handle trashed items
const tempItem = item as FolderTreeItem & { isTrashed: boolean };
return html`
<uui-ref-node name=${item.name} detail=${item.key}>
${item.isTrashed ? html` <uui-tag size="s" slot="tag" color="danger">Trashed</uui-tag> ` : nothing}
<uui-ref-node name=${ifDefined(item.name === null ? undefined : item.name)} detail=${ifDefined(item.key)}>
${tempItem.isTrashed ? html` <uui-tag size="s" slot="tag" color="danger">Trashed</uui-tag> ` : nothing}
<uui-action-bar slot="actions">
<uui-button @click=${() => this._removeItem(item)}>Remove</uui-button>
</uui-action-bar>

View File

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

View File

@@ -1,6 +1,6 @@
import type { ManifestPropertyEditorUI, ManifestWithLoader } from '@umbraco-cms/models';
import type { ManifestPropertyEditorUI } from '@umbraco-cms/models';
export const manifests: Array<ManifestWithLoader<ManifestPropertyEditorUI>> = [
export const manifests: Array<ManifestPropertyEditorUI> = [
{
type: 'propertyEditorUI',
alias: 'Umb.PropertyEditorUI.BlockList',

View File

@@ -1,7 +1,7 @@
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import type { ManifestDashboard, ManifestWithLoader } from '@umbraco-cms/models';
import type { ManifestDashboard } from '@umbraco-cms/models';
import { umbExtensionsRegistry } from '@umbraco-cms/extensions-registry';
@customElement('umb-content-section')
@@ -15,7 +15,7 @@ export class UmbContentSection extends LitElement {
}
private _registerDashboards() {
const dashboards: Array<ManifestWithLoader<ManifestDashboard>> = [
const dashboards: Array<ManifestDashboard> = [
{
type: 'dashboard',
alias: 'Umb.Dashboard.Welcome',

View File

@@ -1,6 +1,6 @@
import type { ManifestSection, ManifestWithLoader } from '@umbraco-cms/models';
import type { ManifestSection } from '@umbraco-cms/models';
export const manifests: Array<ManifestWithLoader<ManifestSection>> = [
export const manifests: Array<ManifestSection> = [
{
type: 'section',
alias: 'Umb.Section.Content',

View File

@@ -2,7 +2,7 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import { umbExtensionsRegistry } from '@umbraco-cms/extensions-registry';
import type { ManifestDashboard, ManifestWithLoader } from '@umbraco-cms/models';
import type { ManifestDashboard } from '@umbraco-cms/models';
@customElement('umb-media-section')
export class UmbMediaSection extends LitElement {
@@ -14,7 +14,7 @@ export class UmbMediaSection extends LitElement {
}
private _registerDashboards() {
const dashboards: Array<ManifestWithLoader<ManifestDashboard>> = [
const dashboards: Array<ManifestDashboard> = [
{
type: 'dashboard',
alias: 'Umb.Dashboard.MediaManagement',

View File

@@ -2,7 +2,7 @@ import { html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import { umbExtensionsRegistry } from '@umbraco-cms/extensions-registry';
import { UmbContextConsumerMixin } from '@umbraco-cms/context-api';
import type { ManifestSectionView, ManifestWithLoader } from '@umbraco-cms/models';
import type { ManifestSectionView } from '@umbraco-cms/models';
@customElement('umb-section-packages')
export class UmbSectionPackages extends UmbContextConsumerMixin(LitElement) {
@@ -13,7 +13,7 @@ export class UmbSectionPackages extends UmbContextConsumerMixin(LitElement) {
}
private _registerSectionViews() {
const manifests: Array<ManifestWithLoader<ManifestSectionView>> = [
const manifests: Array<ManifestSectionView> = [
{
type: 'sectionView',
alias: 'Umb.SectionView.Packages.Repo',

View File

@@ -1,7 +1,7 @@
import { html, LitElement } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { IRoute, IRoutingInfo } from 'router-slot';
import { UmbEditorEntityElement } from '../../../../editors/shared/editor-entity/editor-entity.element';
import { UmbWorkspaceEntityElement } from '../../../../workspaces/shared/workspace-entity/workspace-entity.element';
@customElement('umb-section-view-packages-created')
export class UmbSectionViewPackagesCreatedElement extends LitElement {
@@ -13,16 +13,16 @@ export class UmbSectionViewPackagesCreatedElement extends LitElement {
},
{
path: `:entityType/:key`,
component: () => import('../../../../editors/shared/editor-entity/editor-entity.element'),
component: () => import('../../../../workspaces/shared/workspace-entity/workspace-entity.element'),
setup: (component: HTMLElement, info: IRoutingInfo) => {
const element = component as UmbEditorEntityElement;
const element = component as UmbWorkspaceEntityElement;
element.entityKey = info.match.params.key;
element.entityType = info.match.params.entityType;
},
},
{
path: '**',
redirectTo: '/section/packages/view/created/overview', //TODO: this should be dynamic
redirectTo: 'section/packages/view/created/overview', //TODO: this should be dynamic
},
];

View File

@@ -1,7 +1,7 @@
import { html, LitElement } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { IRoute, IRoutingInfo } from 'router-slot';
import { UmbEditorEntityElement } from '../../../../editors/shared/editor-entity/editor-entity.element';
import { UmbWorkspaceEntityElement } from '../../../../workspaces/shared/workspace-entity/workspace-entity.element';
@customElement('umb-section-view-packages-installed')
export class UmbSectionViewPackagesInstalledElement extends LitElement {
@@ -13,16 +13,16 @@ export class UmbSectionViewPackagesInstalledElement extends LitElement {
},
{
path: `:entityType/:key`,
component: () => import('../../../../editors/shared/editor-entity/editor-entity.element'),
component: () => import('../../../../workspaces/shared/workspace-entity/workspace-entity.element'),
setup: (component: HTMLElement, info: IRoutingInfo) => {
const element = component as UmbEditorEntityElement;
const element = component as UmbWorkspaceEntityElement;
element.entityKey = info.match.params.key;
element.entityType = info.match.params.entityType;
},
},
{
path: '**',
redirectTo: '/section/packages/view/installed/overview', //TODO: this should be dynamic
redirectTo: 'section/packages/view/installed/overview', //TODO: this should be dynamic
},
];

View File

@@ -3,6 +3,7 @@ import { customElement } from 'lit/decorators.js';
@customElement('umb-section-view-packages-repo')
export class UmbSectionViewPackagesRepoElement extends LitElement {
render() {
return html`
<uui-box headline="Popular"></uui-box>

View File

@@ -1,7 +1,5 @@
import { BehaviorSubject, ReplaySubject } from 'rxjs';
import { Entity } from '../../core/mocks/data/entities';
import type { ManifestSection, ManifestSectionView, ManifestTree } from '@umbraco-cms/models';
import type { Entity, ManifestSection, ManifestSectionView, ManifestTree } from '@umbraco-cms/models';
export class UmbSectionContext {
// TODO: figure out how fine grained we want to make our observables.

View File

@@ -1,7 +1,7 @@
import { html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import { umbExtensionsRegistry } from '@umbraco-cms/extensions-registry';
import type { ManifestDashboard, ManifestWithLoader } from '@umbraco-cms/models';
import type { ManifestDashboard } from '@umbraco-cms/models';
@customElement('umb-section-settings')
export class UmbSectionSettingsElement extends LitElement {
@@ -12,7 +12,7 @@ export class UmbSectionSettingsElement extends LitElement {
}
private _registerDashboards() {
const dashboards: Array<ManifestWithLoader<ManifestDashboard>> = [
const dashboards: Array<ManifestDashboard> = [
{
type: 'dashboard',
alias: 'Umb.Dashboard.SettingsWelcome',
@@ -65,6 +65,19 @@ export class UmbSectionSettingsElement extends LitElement {
pathname: 'published-status',
},
},
{
type: 'dashboard',
alias: 'Umb.Dashboard.Profiling',
name: 'Profiling',
elementName: 'umb-dashboard-performance-profiling',
loader: () => import('../../dashboards/performance-profiling/dashboard-performance-profiling.element'),
weight: 101,
meta: {
label: 'Profiling',
sections: ['Umb.Section.Settings'],
pathname: 'profiling',
},
},
{
type: 'dashboard',
alias: 'Umb.Dashboard.Telemetry',

View File

@@ -122,7 +122,7 @@ export class UmbSectionDashboardsElement extends UmbContextConsumerMixin(UmbObse
${this._dashboards.map(
(dashboard) => html`
<uui-tab
href="${`/section/${this._currentSectionPathname}/dashboard/${dashboard.meta.pathname}`}"
href="${`section/${this._currentSectionPathname}/dashboard/${dashboard.meta.pathname}`}"
label=${dashboard.meta.label || dashboard.name}
?active="${dashboard.meta.pathname === this._currentDashboardPathname}"></uui-tab>
`

View File

@@ -59,7 +59,7 @@ export class UmbSectionSidebarElement extends UmbContextConsumerMixin(UmbObserve
return html`
<umb-tree-context-menu-service>
<uui-scroll-container>
<a href="${`/section/${this._sectionPathname}`}">
<a href="${`section/${this._sectionPathname}`}">
<h3>${this._sectionLabel}</h3>
</a>

Some files were not shown because too many files have changed in this diff Show More