Merge branch 'main' into feature/media-types-repo

This commit is contained in:
Jesper Møller Jensen
2023-11-07 20:43:19 +13:00
204 changed files with 4287 additions and 2343 deletions

View File

@@ -45,6 +45,12 @@
"local-rules/umb-class-prefix": "error",
"local-rules/prefer-static-styles-last": "warn",
"local-rules/ensure-relative-import-use-js-extension": "error",
"local-rules/enforce-umbraco-external-imports": [
"error",
{
"exceptions": ["@umbraco-cms", "@open-wc/testing", "@storybook", "msw", "."]
}
],
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": "warn"

View File

@@ -1,49 +0,0 @@
name: Azure Static Web Apps CI/CD
on:
push:
branches:
- main
# pull_request:
# types: [opened, synchronize, reopened, closed]
# branches:
# - main
env:
NODE_OPTIONS: --max_old_space_size=16384
jobs:
build_and_deploy_job:
if: false && github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
runs-on: ubuntu-latest
name: Build and Deploy Job
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Build And Deploy
id: builddeploy
uses: Azure/static-web-apps-deploy@v1
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_ASHY_BAY_09F36A803 }}
repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
action: 'upload'
###### Repository/Build Configurations - These values can be configured to match your app requirements. ######
# For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
app_location: '/' # App source code path
api_location: 'api' # Api source code path - optional
output_location: 'dist-cms' # Built app content directory - optional
###### End of Repository/Build Configurations ######
close_pull_request_job:
if: github.event_name == 'pull_request' && github.event.action == 'closed'
runs-on: ubuntu-latest
name: Close Pull Request Job
steps:
- name: Close Pull Request
id: closepullrequest
uses: Azure/static-web-apps-deploy@v1
with:
app_location: '/'
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_ASHY_BAY_09F36A803 }}
action: 'close'

View File

@@ -5,9 +5,9 @@ name: Build and test
on:
push:
branches: [ main ]
branches: [main]
pull_request:
branches: [ main ]
branches: [main]
# Allows GitHub to use this workflow to validate the merge queue
merge_group:
@@ -20,33 +20,32 @@ env:
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x]
node-version: [20]
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci --no-audit --no-fund --prefer-offline
- run: npm run lint
- run: npm run build
- run: npm run generate:jsonschema:dist
- run: sudo npx playwright install-deps
- run: npm test
- name: Upload Code Coverage reports
uses: actions/upload-artifact@v3
if: always()
with:
name: code-coverage
path: coverage/
retention-days: 30
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci --no-audit --no-fund --prefer-offline
- run: npm run lint
- run: npm run build
- run: npm run generate:jsonschema:dist
- run: sudo npx playwright install-deps
- run: npm test
- name: Upload Code Coverage reports
uses: actions/upload-artifact@v3
if: always()
with:
name: code-coverage
path: coverage/
retention-days: 30
# Commented out since it is outdated and is quite spammy
# - name: Report code coverage
# uses: zgosalvez/github-actions-report-lcov@v2

View File

@@ -47,7 +47,7 @@ jobs:
ref: ${{ inputs.ref }}
- uses: actions/setup-node@v4
with:
node-version: 18
node-version: 20
cache: 'npm'
registry-url: https://registry.npmjs.org/
scope: '@umbraco-cms'

View File

@@ -1 +1 @@
18.16
20.9

View File

@@ -9,7 +9,7 @@
#root-inner {
height: 100%;
}
body {
padding: 0px !important;
}
@@ -23,7 +23,7 @@
line-height: 1.3em;
}
</style>
<script src="https://cdn.jsdelivr.net/npm/msw/lib/iife/index.js"></script>
<script src="umbraco/backoffice/msw/index.js"></script>
<script>
(function () {
window.addEventListener('load', () => {

View File

@@ -338,4 +338,52 @@ module.exports = {
};
},
},
/** @type {import('eslint').Rule.RuleModule}*/
'enforce-umbraco-external-imports': {
meta: {
type: 'problem',
docs: {
description: 'Ensures that the application strictly uses node_modules imports from `@umbraco-cms/backoffice/external`. This is needed to run the application in the browser.',
recommended: true,
},
fixable: 'code',
schema: {
type: "array",
minItems: 0,
maxItems: 1,
items: [
{
type: "object",
properties: {
exceptions: { type: "array" }
},
additionalProperties: false
}
]
}
},
create: (context) => {
return {
ImportDeclaration: (node) => {
const { source } = node;
const { value } = source;
const options = context.options[0] || {};
const exceptions = options.exceptions || [];
// If import starts with any of the following, then it's allowed
if (exceptions.some(v => value.startsWith(v))) {
return;
}
context.report({
node,
message: 'node_modules imports should be proxied through `@umbraco-cms/backoffice/external`. Please create it if it does not exist.',
fix: (fixer) => fixer.replaceText(source, `'@umbraco-cms/backoffice/external${value.startsWith('/') ? '' : '/'}${value}'`),
});
},
};
},
},
};

File diff suppressed because it is too large Load Diff

View File

@@ -101,7 +101,7 @@
"auth:test:e2e": "npx playwright test --config apps/auth/",
"backoffice:test:e2e": "npx playwright test",
"test:e2e": "npm run auth:test:e2e && npm run backoffice:test:e2e",
"lint": "eslint src apps e2e",
"lint": "eslint src",
"lint:errors": "npm run lint -- --quiet",
"lint:fix": "npm run lint -- --fix",
"format": "prettier 'src/**/*.ts' -- check",
@@ -122,8 +122,8 @@
"prepublishOnly": "node ./devops/publish/cleanse-pkg.js"
},
"engines": {
"node": ">=18.14 <19",
"npm": ">=9.5 < 10"
"node": ">=20.9 <21",
"npm": ">=10.1 < 11"
},
"dependencies": {
"@openid/appauth": "^1.3.1",
@@ -148,15 +148,15 @@
"@open-wc/testing": "^3.2.0",
"@playwright/test": "^1.37.1",
"@rollup/plugin-commonjs": "^25.0.4",
"@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-json": "^6.0.1",
"@rollup/plugin-node-resolve": "^15.2.1",
"@storybook/addon-a11y": "7.4.5",
"@storybook/addon-actions": "7.4.5",
"@storybook/addon-essentials": "7.4.5",
"@storybook/addon-links": "7.4.5",
"@storybook/addon-a11y": "7.5.2",
"@storybook/addon-actions": "7.5.2",
"@storybook/addon-essentials": "7.5.2",
"@storybook/addon-links": "7.5.2",
"@storybook/mdx2-csf": "^1.1.0",
"@storybook/web-components": "7.4.5",
"@storybook/web-components-vite": "7.4.5",
"@storybook/web-components": "7.5.2",
"@storybook/web-components-vite": "7.5.2",
"@types/chai": "^4.3.5",
"@types/lodash-es": "^4.17.8",
"@types/mocha": "^10.0.1",
@@ -171,16 +171,16 @@
"eslint-config-prettier": "^9.0.0",
"eslint-import-resolver-typescript": "^3.6.0",
"eslint-plugin-import": "^2.28.0",
"eslint-plugin-lit": "^1.8.3",
"eslint-plugin-lit": "^1.10.1",
"eslint-plugin-lit-a11y": "^4.1.0",
"eslint-plugin-local-rules": "^1.3.2",
"eslint-plugin-storybook": "^0.6.14",
"eslint-plugin-storybook": "^0.6.15",
"eslint-plugin-wc": "^1.5.0",
"msw": "^1.2.3",
"openapi-typescript-codegen": "^0.25.0",
"playwright-msw": "^2.2.1",
"plop": "^3.1.2",
"prettier": "3.0.1",
"prettier": "3.0.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"remark-gfm": "^3.0.1",
@@ -189,7 +189,7 @@
"rollup-plugin-esbuild": "^5.0.0",
"rollup-plugin-import-css": "^3.3.4",
"rollup-plugin-web-worker-loader": "^1.6.1",
"storybook": "7.4.5",
"storybook": "7.5.2",
"tiny-glob": "^0.2.9",
"tsc-alias": "^1.8.7",
"typescript": "^5.1.6",

View File

@@ -1,7 +1,7 @@
import type { UmbAppErrorElement } from './app-error.element.js';
import { UMB_APP, UmbAppContext } from './app.context.js';
import { umbLocalizationRegistry } from '@umbraco-cms/backoffice/localization';
import { UMB_AUTH, UmbAuthContext } from '@umbraco-cms/backoffice/auth';
import { UMB_AUTH_CONTEXT, UmbAuthContext } from '@umbraco-cms/backoffice/auth';
import { css, html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import { UUIIconRegistryEssential } from '@umbraco-cms/backoffice/external/uui';
import { UmbIconRegistry } from '@umbraco-cms/backoffice/icon';
@@ -111,7 +111,7 @@ export class UmbAppElement extends UmbLitElement {
this.#authContext = new UmbAuthContext(this, this.serverUrl, redirectUrl);
this.provideContext(UMB_AUTH, this.#authContext);
this.provideContext(UMB_AUTH_CONTEXT, this.#authContext);
this.provideContext(UMB_APP, new UmbAppContext({ backofficePath: this.backofficePath, serverUrl: this.serverUrl }));

View File

@@ -1,4 +1,3 @@
export * from './app-context-config.interface.js';
export * from './app-error.element.js';
export * from './app.element.js';
export * from './app.context.js';

View File

@@ -1,3 +1,4 @@
/* eslint local-rules/enforce-umbraco-external-imports: 0 */
import styles from 'monaco-editor/min/vs/editor/editor.main.css';
//eslint-disable-next-line
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker.js?worker';

View File

@@ -1,3 +1,4 @@
/* eslint local-rules/enforce-umbraco-external-imports: 0 */
import DOMPurify from 'dompurify';
const sanitizeHtml = DOMPurify.sanitize;

View File

@@ -1,3 +1,4 @@
/* eslint local-rules/enforce-umbraco-external-imports: 0 */
/**
* TinyMce is a CommonJS module, but in order to make @web/test-runner happy
* we need to load it as a module and then manually register it in the browser

View File

@@ -3,14 +3,8 @@ import type { UmbControllerAlias } from './controller-alias.type.js';
import { UmbControllerHostBaseMixin } from './controller-host-base.mixin.js';
import type { UmbControllerHost } from './controller-host.interface.js';
import type { UmbController } from './controller.interface.js';
import type { UmbLocalizeController } from '@umbraco-cms/backoffice/localization-api';
export declare class UmbControllerHostElement extends HTMLElement implements UmbControllerHost {
/**
* Use the UmbLocalizeController to localize your element.
* @see UmbLocalizeController
*/
localize: UmbLocalizeController;
hasController(controller: UmbController): boolean;
getControllers(filterMethod: (ctrl: UmbController) => boolean): UmbController[];
addController(controller: UmbController): void;

View File

@@ -33,6 +33,11 @@ export declare class UmbElement extends UmbControllerHostElement {
alias: string | UmbContextToken<BaseType, ResultType>,
callback: UmbContextCallback<ResultType>
): UmbContextConsumerController<BaseType, ResultType>;
/**
* Use the UmbLocalizeController to localize your element.
* @see UmbLocalizeController
*/
localize: UmbLocalizeController;
}
export const UmbElementMixin = <T extends HTMLElementConstructor>(superClass: T) => {

View File

@@ -1,7 +1,7 @@
import { expect } from '@open-wc/testing';
import { customElement } from 'lit/decorators.js';
import { ManifestElement, ManifestElementAndApi } from '../types.js';
import { createExtensionElement } from './create-extension-element.function.js';
import { customElement } from '@umbraco-cms/backoffice/external/lit';

View File

@@ -6,7 +6,7 @@ import * as manifestsHandlers from './handlers/manifests.handlers.js';
import { handlers as publishedStatusHandlers } from './handlers/published-status.handlers.js';
import * as serverHandlers from './handlers/server.handlers.js';
import { handlers as upgradeHandlers } from './handlers/upgrade.handlers.js';
import { handlers as userHandlers } from './handlers/user.handlers.js';
import { handlers as userHandlers } from './handlers/user/index.js';
import { handlers as telemetryHandlers } from './handlers/telemetry.handlers.js';
import { handlers as userGroupsHandlers } from './handlers/user-group/index.js';
import { handlers as examineManagementHandlers } from './handlers/examine-management.handlers.js';

View File

@@ -514,7 +514,7 @@ export const data: Array<DataTypeResponseModel | FolderTreeItemResponseModel> =
name: 'Rich Text Editor',
id: 'dt-richTextEditor',
parentId: null,
propertyEditorAlias: 'Umbraco.TinyMCE',
propertyEditorAlias: 'Umbraco.RichText',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.TinyMCE',
values: [
{

View File

@@ -1,7 +1,11 @@
import { UmbId } from '@umbraco-cms/backoffice/id';
import { UmbEntityData } from './entity.data.js';
import { umbUserGroupData } from './user-group.data.js';
import { UmbLoggedInUser } from '@umbraco-cms/backoffice/auth';
import {
CreateUserRequestModel,
CreateUserResponseModel,
InviteUserRequestModel,
UpdateUserGroupsOnUserRequestModel,
UserItemResponseModel,
UserResponseModel,
@@ -21,11 +25,52 @@ class UmbUserData extends UmbEntityData<UserResponseModel> {
super(data);
}
/**
* Create user
* @param {CreateUserRequestModel} data
* @memberof UmbUserData
*/
createUser = (data: CreateUserRequestModel): CreateUserResponseModel => {
const userId = UmbId.new();
const initialPassword = 'mocked-initial-password';
const user: UserResponseModel = {
id: userId,
languageIsoCode: null,
contentStartNodeIds: [],
mediaStartNodeIds: [],
avatarUrls: [],
state: UserStateModel.INACTIVE,
failedLoginAttempts: 0,
createDate: new Date().toUTCString(),
updateDate: new Date().toUTCString(),
lastLoginDate: null,
lastLockoutDate: null,
lastPasswordChangeDate: null,
...data,
};
this.insert(user);
return { userId, initialPassword };
};
/**
* Get user items
* @param {Array<string>} ids
* @return {*} {Array<UserItemResponseModel>}
* @memberof UmbUserData
*/
getItems(ids: Array<string>): Array<UserItemResponseModel> {
const items = this.data.filter((item) => ids.includes(item.id ?? ''));
return items.map((item) => createUserItem(item));
}
/**
* Set user groups
* @param {UpdateUserGroupsOnUserRequestModel} data
* @memberof UmbUserData
*/
setUserGroups(data: UpdateUserGroupsOnUserRequestModel): void {
const users = this.data.filter((user) => data.userIds?.includes(user.id ?? ''));
users.forEach((user) => {
@@ -33,6 +78,11 @@ class UmbUserData extends UmbEntityData<UserResponseModel> {
});
}
/**
* Get current user
* @return {*} {UmbLoggedInUser}
* @memberof UmbUserData
*/
getCurrentUser(): UmbLoggedInUser {
const firstUser = this.data[0];
const permissions = firstUser.userGroupIds?.length ? umbUserGroupData.getPermissions(firstUser.userGroupIds) : [];
@@ -51,6 +101,52 @@ class UmbUserData extends UmbEntityData<UserResponseModel> {
permissions,
};
}
/**
* Disable users
* @param {Array<string>} ids
* @memberof UmbUserData
*/
disable(ids: Array<string>): void {
const users = this.data.filter((user) => ids.includes(user.id ?? ''));
users.forEach((user) => {
user.state = UserStateModel.DISABLED;
});
}
/**
* Enable users
* @param {Array<string>} ids
* @memberof UmbUserData
*/
enable(ids: Array<string>): void {
const users = this.data.filter((user) => ids.includes(user.id ?? ''));
users.forEach((user) => {
user.state = UserStateModel.ACTIVE;
});
}
/**
* Unlock users
* @param {Array<string>} ids
* @memberof UmbUserData
*/
unlock(ids: Array<string>): void {
const users = this.data.filter((user) => ids.includes(user.id ?? ''));
users.forEach((user) => {
user.failedLoginAttempts = 0;
user.state = UserStateModel.ACTIVE;
});
}
invite(data: InviteUserRequestModel): void {
const invitedUser = {
status: UserStateModel.INVITED,
...data,
};
this.createUser(invitedUser);
}
}
export const data: Array<UserResponseModel & { type: string }> = [
@@ -78,17 +174,17 @@ export const data: Array<UserResponseModel & { type: string }> = [
{
id: '82e11d3d-b91d-43c9-9071-34d28e62e81d',
type: 'user',
contentStartNodeIds: [],
mediaStartNodeIds: [],
contentStartNodeIds: ['simple-document-id'],
mediaStartNodeIds: ['f2f81a40-c989-4b6b-84e2-057cecd3adc1'],
name: 'Amelie Walker',
email: 'awalker1@domain.com',
languageIsoCode: 'Japanese',
state: UserStateModel.INACTIVE,
lastLoginDate: '4/12/2023',
lastLockoutDate: '',
lastPasswordChangeDate: '4/1/2023',
updateDate: '4/12/2023',
createDate: '4/12/2023',
lastLoginDate: '2023-10-12T18:30:32.879Z',
lastLockoutDate: null,
lastPasswordChangeDate: '2023-10-12T18:30:32.879Z',
updateDate: '2023-10-12T18:30:32.879Z',
createDate: '2023-10-12T18:30:32.879Z',
failedLoginAttempts: 0,
userGroupIds: ['c630d49e-4e7b-42ea-b2bc-edc0edacb6b1'],
},
@@ -101,11 +197,11 @@ export const data: Array<UserResponseModel & { type: string }> = [
email: 'okim1@domain.com',
languageIsoCode: 'Russian',
state: UserStateModel.ACTIVE,
lastLoginDate: '4/11/2023',
lastLockoutDate: '',
lastPasswordChangeDate: '4/5/2023',
updateDate: '4/11/2023',
createDate: '4/11/2023',
lastLoginDate: '2023-10-12T18:30:32.879Z',
lastLockoutDate: null,
lastPasswordChangeDate: '2023-10-12T18:30:32.879Z',
updateDate: '2023-10-12T18:30:32.879Z',
createDate: '2023-10-12T18:30:32.879Z',
failedLoginAttempts: 0,
userGroupIds: ['c630d49e-4e7b-42ea-b2bc-edc0edacb6b1'],
},
@@ -118,11 +214,11 @@ export const data: Array<UserResponseModel & { type: string }> = [
email: 'enieves1@domain.com',
languageIsoCode: 'Spanish',
state: UserStateModel.INVITED,
lastLoginDate: '4/10/2023',
lastLockoutDate: '',
lastPasswordChangeDate: '4/6/2023',
updateDate: '4/10/2023',
createDate: '4/10/2023',
lastLoginDate: '2023-10-12T18:30:32.879Z',
lastLockoutDate: null,
lastPasswordChangeDate: null,
updateDate: '2023-10-12T18:30:32.879Z',
createDate: '2023-10-12T18:30:32.879Z',
failedLoginAttempts: 0,
userGroupIds: ['c630d49e-4e7b-42ea-b2bc-edc0edacb6b1'],
},
@@ -134,13 +230,13 @@ export const data: Array<UserResponseModel & { type: string }> = [
name: 'Jasmine Patel',
email: 'jpatel1@domain.com',
languageIsoCode: 'Hindi',
state: UserStateModel.DISABLED,
lastLoginDate: '4/9/2023',
lastLockoutDate: '',
lastPasswordChangeDate: '4/7/2023',
updateDate: '4/9/2023',
createDate: '4/9/2023',
failedLoginAttempts: 0,
state: UserStateModel.LOCKED_OUT,
lastLoginDate: '2023-10-12T18:30:32.879Z',
lastLockoutDate: '2023-10-12T18:30:32.879Z',
lastPasswordChangeDate: null,
updateDate: '2023-10-12T18:30:32.879Z',
createDate: '2023-10-12T18:30:32.879Z',
failedLoginAttempts: 25,
userGroupIds: ['c630d49e-4e7b-42ea-b2bc-edc0edacb6b1'],
},
];

View File

@@ -5,7 +5,7 @@ import * as manifestsHandlers from './handlers/manifests.handlers.js';
import { handlers as publishedStatusHandlers } from './handlers/published-status.handlers.js';
import * as serverHandlers from './handlers/server.handlers.js';
import { handlers as upgradeHandlers } from './handlers/upgrade.handlers.js';
import { handlers as userHandlers } from './handlers/user.handlers.js';
import { handlers as userHandlers } from './handlers/user/index.js';
import { handlers as telemetryHandlers } from './handlers/telemetry.handlers.js';
import { handlers as examineManagementHandlers } from './handlers/examine-management.handlers.js';
import { handlers as modelsBuilderHandlers } from './handlers/modelsbuilder.handlers.js';

View File

@@ -1,73 +0,0 @@
const { rest } = window.MockServiceWorker;
import { umbUsersData } from '../data/user.data.js';
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
const slug = '/user';
export const handlers = [
rest.get(umbracoPath(`${slug}/item`), (req, res, ctx) => {
const ids = req.url.searchParams.getAll('id');
if (!ids) return;
const items = umbUsersData.getItems(ids);
return res(ctx.status(200), ctx.json(items));
}),
rest.post(umbracoPath(`${slug}/set-user-groups`), async (req, res, ctx) => {
const data = await req.json();
if (!data) return;
umbUsersData.setUserGroups(data);
return res(ctx.status(200));
}),
rest.get(umbracoPath(`${slug}/filter`), (req, res, ctx) => {
//TODO: Implementer filter
const response = umbUsersData.getAll();
return res(ctx.status(200), ctx.json(response));
}),
rest.get(umbracoPath(`${slug}/current`), (_req, res, ctx) => {
const loggedInUser = umbUsersData.getCurrentUser();
return res(ctx.status(200), ctx.json(loggedInUser));
}),
rest.get(umbracoPath(`${slug}/sections`), (_req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
sections: ['Umb.Section.Content', 'Umb.Section.Media', 'Umb.Section.Settings', 'My.Section.Custom'],
}),
);
}),
rest.get(umbracoPath(`${slug}`), (req, res, ctx) => {
const response = umbUsersData.getAll();
return res(ctx.status(200), ctx.json(response));
}),
rest.get(umbracoPath(`${slug}/:id`), (req, res, ctx) => {
const id = req.params.id as string;
if (!id) return;
const user = umbUsersData.getById(id);
if (!user) return res(ctx.status(404));
return res(ctx.status(200), ctx.json(user));
}),
rest.put(umbracoPath(`${slug}/:id`), async (req, res, ctx) => {
const id = req.params.id as string;
if (!id) return;
const data = await req.json();
if (!data) return;
const saved = umbUsersData.save(id, data);
return res(ctx.status(200), ctx.json(saved));
}),
];

View File

@@ -0,0 +1,16 @@
const { rest } = window.MockServiceWorker;
import { slug } from './slug.js';
import { ChangePasswordUserRequestModel } from '@umbraco-cms/backoffice/backend-api';
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
export const handlers = [
rest.post<ChangePasswordUserRequestModel>(umbracoPath(`${slug}/change-password/:id`), async (req, res, ctx) => {
const data = await req.json();
if (!data) return;
if (!data.newPassword) return;
/* we don't have to update any mock data when a password is changed
so we just return a 200 */
return res(ctx.status(200));
}),
];

View File

@@ -0,0 +1,11 @@
const { rest } = window.MockServiceWorker;
import { umbUsersData } from '../../data/user.data.js';
import { slug } from './slug.js';
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
export const handlers = [
rest.get(umbracoPath(`${slug}/current`), (_req, res, ctx) => {
const loggedInUser = umbUsersData.getCurrentUser();
return res(ctx.status(200), ctx.json(loggedInUser));
}),
];

View File

@@ -0,0 +1,56 @@
const { rest } = window.MockServiceWorker;
import { umbUsersData } from '../../data/user.data.js';
import { slug } from './slug.js';
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
export const handlers = [
rest.post(umbracoPath(`${slug}`), async (req, res, ctx) => {
const data = await req.json();
if (!data) return;
const response = umbUsersData.createUser(data);
return res(ctx.status(200), ctx.json(response));
}),
rest.get(umbracoPath(`${slug}`), (req, res, ctx) => {
const response = umbUsersData.getAll();
return res(ctx.status(200), ctx.json(response));
}),
rest.get(umbracoPath(`${slug}/filter`), (req, res, ctx) => {
//TODO: Implementer filter
const response = umbUsersData.getAll();
return res(ctx.status(200), ctx.json(response));
}),
rest.get(umbracoPath(`${slug}/:id`), (req, res, ctx) => {
const id = req.params.id as string;
if (!id) return;
const item = umbUsersData.getById(id);
return res(ctx.status(200), ctx.json(item));
}),
rest.put(umbracoPath(`${slug}/:id`), async (req, res, ctx) => {
const id = req.params.id as string;
if (!id) return;
const data = await req.json();
if (!data) return;
umbUsersData.save(id, data);
return res(ctx.status(200));
}),
rest.delete<string>(umbracoPath(`${slug}/:id`), async (req, res, ctx) => {
const id = req.params.id as string;
if (!id) return;
umbUsersData.delete([id]);
return res(ctx.status(200));
}),
];

View File

@@ -0,0 +1,17 @@
const { rest } = window.MockServiceWorker;
import { umbUsersData } from '../../data/user.data.js';
import { slug } from './slug.js';
import { DisableUserRequestModel } from '@umbraco-cms/backoffice/backend-api';
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
export const handlers = [
rest.post<DisableUserRequestModel>(umbracoPath(`${slug}/disable`), async (req, res, ctx) => {
const data = await req.json();
if (!data) return;
if (!data.userIds) return;
umbUsersData.disable(data.userIds);
return res(ctx.status(200));
}),
];

View File

@@ -0,0 +1,17 @@
const { rest } = window.MockServiceWorker;
import { umbUsersData } from '../../data/user.data.js';
import { slug } from './slug.js';
import { EnableUserRequestModel } from '@umbraco-cms/backoffice/backend-api';
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
export const handlers = [
rest.post<EnableUserRequestModel>(umbracoPath(`${slug}/enable`), async (req, res, ctx) => {
const data = await req.json();
if (!data) return;
if (!data.userIds) return;
umbUsersData.enable(data.userIds);
return res(ctx.status(200));
}),
];

View File

@@ -0,0 +1,21 @@
import { handlers as detailHandlers } from './detail.handlers.js';
import { handlers as itemHandlers } from './item.handlers.js';
import { handlers as currentHandlers } from './current.handlers.js';
import { handlers as setUserGroupsHandlers } from './set-user-groups.handlers.js';
import { handlers as enableHandlers } from './enable.handlers.js';
import { handlers as disableHandlers } from './disable.handlers.js';
import { handlers as changePasswordHandlers } from './change-password.handlers.js';
import { handlers as unlockHandlers } from './unlock.handlers.js';
import { handlers as inviteHandlers } from './invite.handlers.js';
export const handlers = [
...itemHandlers,
...currentHandlers,
...enableHandlers,
...disableHandlers,
...setUserGroupsHandlers,
...changePasswordHandlers,
...unlockHandlers,
...detailHandlers,
...inviteHandlers,
];

View File

@@ -0,0 +1,23 @@
const { rest } = window.MockServiceWorker;
import { umbUsersData } from '../../data/user.data.js';
import { slug } from './slug.js';
import { InviteUserRequestModel } from '@umbraco-cms/backoffice/backend-api';
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
export const handlers = [
rest.post<InviteUserRequestModel>(umbracoPath(`${slug}/invite`), async (req, res, ctx) => {
const data = await req.json();
if (!data) return;
umbUsersData.invite(data);
return res(ctx.status(200));
}),
rest.post<any>(umbracoPath(`${slug}/invite/resend`), async (req, res, ctx) => {
const data = await req.json();
if (!data) return;
return res(ctx.status(200));
}),
];

View File

@@ -0,0 +1,13 @@
const { rest } = window.MockServiceWorker;
import { umbUsersData } from '../../data/user.data.js';
import { slug } from './slug.js';
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
export const handlers = [
rest.get(umbracoPath(`${slug}/item`), (req, res, ctx) => {
const ids = req.url.searchParams.getAll('id');
if (!ids) return;
const items = umbUsersData.getItems(ids);
return res(ctx.status(200), ctx.json(items));
}),
];

View File

@@ -0,0 +1,15 @@
const { rest } = window.MockServiceWorker;
import { umbUsersData } from '../../data/user.data.js';
import { slug } from './slug.js';
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
export const handlers = [
rest.post(umbracoPath(`${slug}/set-user-groups`), async (req, res, ctx) => {
const data = await req.json();
if (!data) return;
umbUsersData.setUserGroups(data);
return res(ctx.status(200));
}),
];

View File

@@ -0,0 +1 @@
export const slug = '/user';

View File

@@ -0,0 +1,17 @@
const { rest } = window.MockServiceWorker;
import { umbUsersData } from '../../data/user.data.js';
import { slug } from './slug.js';
import { UnlockUsersRequestModel } from '@umbraco-cms/backoffice/backend-api';
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
export const handlers = [
rest.post<UnlockUsersRequestModel>(umbracoPath(`${slug}/unlock`), async (req, res, ctx) => {
const data = await req.json();
if (!data) return;
if (!data.userIds) return;
umbUsersData.unlock(data.userIds);
return res(ctx.status(200));
}),
];

View File

@@ -1,4 +1,3 @@
import { UMB_COLLECTION_CONTEXT } from './collection.context.js';
import { UmbBaseController } from '@umbraco-cms/backoffice/controller-api';
import {

View File

@@ -1,22 +1,20 @@
import { UmbCollectionRepository } from '@umbraco-cms/backoffice/repository';
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import { UmbBaseController, type UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import {
UmbArrayState,
UmbNumberState,
UmbObjectState,
UmbObserverController,
} from '@umbraco-cms/backoffice/observable-api';
import { createExtensionApi } from '@umbraco-cms/backoffice/extension-api';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { ManifestCollectionView, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import type { UmbCollectionFilterModel } from '@umbraco-cms/backoffice/collection';
import { map } from '@umbraco-cms/backoffice/external/rxjs';
import { UmbSelectionManager } from '@umbraco-cms/backoffice/utils';
// TODO: Clean up the need for store as Media has switched to use Repositories(repository).
export class UmbCollectionContext<ItemType, FilterModelType extends UmbCollectionFilterModel> {
private _host: UmbControllerHostElement;
private _entityType: string;
protected _dataObserver?: UmbObserverController<ItemType[]>;
export class UmbCollectionContext<ItemType, FilterModelType extends UmbCollectionFilterModel> extends UmbBaseController {
protected entityType: string;
protected init;
#items = new UmbArrayState<ItemType>([]);
public readonly items = this.#items.asObservable();
@@ -24,113 +22,125 @@ export class UmbCollectionContext<ItemType, FilterModelType extends UmbCollectio
#total = new UmbNumberState(0);
public readonly total = this.#total.asObservable();
#selection = new UmbArrayState<string>([]);
public readonly selection = this.#selection.asObservable();
#selectionManager = new UmbSelectionManager();
public readonly selection = this.#selectionManager.selection;
#filter = new UmbObjectState<FilterModelType | object>({});
public readonly filter = this.#filter.asObservable();
repository?: UmbCollectionRepository;
#views = new UmbArrayState<ManifestCollectionView>([]);
public readonly views = this.#views.asObservable();
/*
TODO:
private _search = new StringState('');
public readonly search = this._search.asObservable();
*/
#currentView = new UmbObjectState<ManifestCollectionView | undefined>(undefined);
public readonly currentView = this.#currentView.asObservable();
repository?: UmbCollectionRepository;
collectionRootPathname: string;
constructor(host: UmbControllerHostElement, entityType: string, repositoryAlias: string) {
this._entityType = entityType;
this._host = host;
super(host);
this.entityType = entityType;
new UmbObserverController(
this._host,
umbExtensionsRegistry.getByTypeAndAlias('repository', repositoryAlias),
async (repositoryManifest) => {
if (repositoryManifest) {
const result = await createExtensionApi(repositoryManifest, [this._host]);
this.repository = result as UmbCollectionRepository;
this._onRepositoryReady();
}
}
);
this.#selectionManager.setMultiple(true);
const currentUrl = new URL(window.location.href);
this.collectionRootPathname = currentUrl.pathname.substring(0, currentUrl.pathname.lastIndexOf('/'));
this.init = Promise.all([
this.observe(
umbExtensionsRegistry.getByTypeAndAlias('repository', repositoryAlias),
async (repositoryManifest) => {
if (repositoryManifest) {
const result = await createExtensionApi(repositoryManifest, [this._host]);
this.repository = result as UmbCollectionRepository;
this.requestCollection();
}
},
'umbCollectionRepositoryObserver'
).asPromise(),
this.observe(umbExtensionsRegistry.extensionsOfType('collectionView').pipe(
map((extensions) => {
return extensions.filter((extension) => extension.conditions.entityType === this.getEntityType());
}),
),
(views) => {
this.#views.next(views);
this.#setCurrentView();
}, 'umbCollectionViewsObserver').asPromise(),
]);
this.provideContext(UMB_COLLECTION_CONTEXT, this);
}
/**
* Returns true if the given id is selected.
* @param {string} id
* @return {Boolean}
* @memberof UmbCollectionContext
*/
public isSelected(id: string) {
return this.#selection.getValue().includes(id);
return this.#selectionManager.isSelected(id);
}
public setSelection(value: Array<string>) {
if (!value) return;
this.#selection.next(value);
/**
* Sets the current selection.
* @param {Array<string>} selection
* @memberof UmbCollectionContext
*/
public setSelection(selection: Array<string>) {
this.#selectionManager.setSelection(selection);
}
/**
* Returns the current selection.
* @return {Array<string>}
* @memberof UmbCollectionContext
*/
public getSelection() {
this.#selection.getValue();
this.#selectionManager.getSelection();
}
/**
* Clears the current selection.
* @memberof UmbCollectionContext
*/
public clearSelection() {
this.#selection.next([]);
this.#selectionManager.clearSelection();
}
/**
* Appends the given id to the current selection.
* @param {string} id
* @memberof UmbCollectionContext
*/
public select(id: string) {
this.#selection.appendOne(id);
this.#selectionManager.select(id);
}
/**
* Removes the given id from the current selection.
* @param {string} id
* @memberof UmbCollectionContext
*/
public deselect(id: string) {
this.#selection.filter((k) => k !== id);
}
// TODO: how can we make sure to call this.
public destroy(): void {
this.#items.unsubscribe();
this.#selectionManager.deselect(id);
}
/**
* Returns the collection entity type
* @return {string}
* @memberof UmbCollectionContext
*/
public getEntityType() {
return this._entityType;
}
/*
public getData() {
return this.#data.getValue();
}
*/
/*
public update(data: Partial<DataType>) {
this._data.next({ ...this.getData(), ...data });
}
*/
// protected _onStoreSubscription(): void {
// if (!this._store) {
// return;
// }
// this._dataObserver?.destroy();
// if (this._entityId) {
// this._dataObserver = new UmbObserverController(
// this._host,
// this._store.getTreeItemChildren(this._entityId),
// (nodes) => {
// if (nodes) {
// this.#data.next(nodes);
// }
// }
// );
// } else {
// this._dataObserver = new UmbObserverController(this._host, this._store.getTreeRoot(), (nodes) => {
// if (nodes) {
// this.#data.next(nodes);
// }
// });
// }
// }
protected async _onRepositoryReady() {
if (!this.repository) return;
this.requestCollection();
return this.entityType;
}
/**
* Requests the collection from the repository.
* @return {*}
* @memberof UmbCollectionContext
*/
public async requestCollection() {
if (!this.repository) return;
@@ -143,11 +153,48 @@ export class UmbCollectionContext<ItemType, FilterModelType extends UmbCollectio
}
}
// TODO: find better name
setFilter(filter: Partial<FilterModelType>) {
/**
* Sets the filter for the collection and refreshes the collection.
* @param {Partial<FilterModelType>} filter
* @memberof UmbCollectionContext
*/
public setFilter(filter: Partial<FilterModelType>) {
this.#filter.next({ ...this.#filter.getValue(), ...filter });
this.requestCollection();
}
// Views
/**
* Sets the current view.
* @param {ManifestCollectionView} view
* @memberof UmbCollectionContext
*/
public setCurrentView(view: ManifestCollectionView) {
this.#currentView.next(view);
}
/**
* Returns the current view.
* @return {ManifestCollectionView}
* @memberof UmbCollectionContext
*/
public getCurrentView() {
return this.#currentView.getValue();
}
#setCurrentView() {
const currentUrl = new URL(window.location.href);
const lastPathSegment = currentUrl.pathname.split('/').pop();
const views = this.#views.getValue();
const viewMatch = views.find((view) => view.meta.pathName === lastPathSegment);
/* TODO: Find a way to figure out which layout it starts with and set _currentLayout to that instead of [0]. eg. '/table'
For document, media and members this will come as part of a data type configuration, but in other cases "users" we should find another way.
This should only happen if the current layout is not set in the URL.
*/
const currentView = viewMatch || views[0];
this.setCurrentView(currentView);
}
}
export const UMB_COLLECTION_CONTEXT = new UmbContextToken<UmbCollectionContext<any, any>>('UmbCollectionContext');

View File

@@ -1,26 +1,16 @@
import { UmbTextStyles } from "@umbraco-cms/backoffice/style";
import { css, html, nothing, customElement, state, property } from '@umbraco-cms/backoffice/external/lit';
import { map } from '@umbraco-cms/backoffice/external/rxjs';
import { UmbCollectionContext, UMB_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection';
import { UMB_COLLECTION_CONTEXT, UmbCollectionContext } from './collection.context.js';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, customElement, state, property } from '@umbraco-cms/backoffice/external/lit';
import { createExtensionElement } from '@umbraco-cms/backoffice/extension-api';
import { ManifestCollectionView, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { ManifestCollectionView } from '@umbraco-cms/backoffice/extension-registry';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import type { UmbObserverController } from '@umbraco-cms/backoffice/observable-api';
import type { UmbRoute } from '@umbraco-cms/backoffice/router';
import './collection-selection-actions.element.js';
import './collection-toolbar.element.js';
@customElement('umb-collection')
export class UmbCollectionElement extends UmbLitElement {
@state()
private _routes: Array<UmbRoute> = [];
@state()
private _selection?: Array<string> | null;
private _collectionContext?: UmbCollectionContext<any, any>;
private _entityType!: string;
@property({ type: String, attribute: 'entity-type' })
public get entityType(): string {
@@ -28,41 +18,24 @@ export class UmbCollectionElement extends UmbLitElement {
}
public set entityType(value: string) {
this._entityType = value;
this._observeCollectionViews();
}
private _collectionViewUnsubscribe?: UmbObserverController<Array<ManifestCollectionView>>;
protected collectionContext?: UmbCollectionContext<any, any>;
constructor() {
super();
this.consumeContext(UMB_COLLECTION_CONTEXT, (instance) => {
this._collectionContext = instance;
this._observeCollectionContext();
this.consumeContext(UMB_COLLECTION_CONTEXT, (context) => {
this.collectionContext = context;
this.#observeCollectionViews();
});
}
private _observeCollectionContext() {
if (!this._collectionContext) return;
this.observe(this._collectionContext.selection, (selection) => {
this._selection = selection;
});
}
private _observeCollectionViews() {
this._collectionViewUnsubscribe?.destroy();
this._collectionViewUnsubscribe = this.observe(
// TODO: could we make some helper methods for this scenario:
umbExtensionsRegistry?.extensionsOfType('collectionView').pipe(
map((extensions) => {
return extensions.filter((extension) => extension.conditions.entityType === this._entityType);
})
),
(views) => {
this._createRoutes(views);
}
);
#observeCollectionViews() {
this.observe(this.collectionContext!.views, (views) => {
this._createRoutes(views);
}),
'umbCollectionViewsObserver';
}
private _createRoutes(views: ManifestCollectionView[] | null) {
@@ -85,16 +58,22 @@ export class UmbCollectionElement extends UmbLitElement {
render() {
return html`
<umb-body-layout>
<umb-collection-toolbar slot="header"></umb-collection-toolbar>
<umb-body-layout header-transparent>
${this.renderToolbar()}
<umb-router-slot id="router-slot" .routes="${this._routes}"></umb-router-slot>
${this._selection && this._selection.length > 0
? html`<umb-collection-selection-actions slot="footer-info"></umb-collection-selection-actions>`
: nothing}
${this.renderSelectionActions()}
</umb-body-layout>
`;
}
protected renderToolbar() {
return html`<umb-collection-toolbar slot="header"></umb-collection-toolbar>`;
}
protected renderSelectionActions() {
return html`<umb-collection-selection-actions slot="footer-info"></umb-collection-selection-actions>`;
}
static styles = [
UmbTextStyles,
css`

View File

@@ -15,7 +15,7 @@ export class UmbCollectionSelectionActionsElement extends UmbLitElement {
@state()
private _extensionProps = {};
private _selection: Array<string> = [];
private _selection: Array<string | null> = [];
private _collectionContext?: UmbCollectionContext<any, any>;
@@ -46,7 +46,7 @@ export class UmbCollectionSelectionActionsElement extends UmbLitElement {
(mediaItems) => {
this._nodesLength = mediaItems.length;
},
'observeItem',
'umbItemsLengthObserver',
);
this.observe(
@@ -56,7 +56,7 @@ export class UmbCollectionSelectionActionsElement extends UmbLitElement {
this._selection = selection;
this._extensionProps = { selection: this._selection };
},
'observeSelection',
'umbSelectionObserver',
);
}

View File

@@ -1,4 +1,4 @@
import type { TooltipMenuItem } from '../components/tooltip-menu/index.js';
import type { TooltipMenuItem } from '../../components/tooltip-menu/index.js';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, nothing, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
import { map } from '@umbraco-cms/backoffice/external/rxjs';
@@ -43,46 +43,19 @@ export class UmbCollectionToolbarElement extends UmbLitElement {
constructor() {
super();
this._observeCollectionViews();
}
private _observeCollectionViews() {
this.observe<ManifestCollectionView[]>(
umbExtensionsRegistry.extensionsOfType('collectionView').pipe(
map((extensions) => {
return extensions.filter((extension) => extension.conditions.entityType === 'media');
})
),
(layouts) => {
this._layouts = layouts;
if (!this._currentLayout) {
//TODO: Find a way to figure out which layout it starts with and set _currentLayout to that instead of [0]. eg. '/table'
this._currentLayout = layouts[0];
}
}
);
}
private _changeLayout(path: string) {
history.pushState(null, '', 'section/media/dashboard/media-management/' + path);
}
private _toggleViewType() {
if (!this._currentLayout) return;
const index = this._layouts.indexOf(this._currentLayout);
this._currentLayout = this._layouts[(index + 1) % this._layouts.length];
this._changeLayout(this._currentLayout.meta.pathName);
}
private _updateSearch(e: InputEvent) {
this._search = (e.target as HTMLInputElement).value;
this.dispatchEvent(
new CustomEvent('search', {
detail: this._search,
})
}),
);
}
@@ -104,43 +77,11 @@ export class UmbCollectionToolbarElement extends UmbLitElement {
return nothing;
}
private _renderLayoutButton() {
if (!this._currentLayout) return;
if (this._layouts.length < 2 || !this._currentLayout.meta.icon) return nothing;
if (this._layouts.length === 2) {
return html`<uui-button @click=${this._toggleViewType} look="outline" compact>
<uui-icon .name=${this._currentLayout.meta.icon}></uui-icon>
</uui-button>`;
}
if (this._layouts.length > 2) {
return html`<uui-popover margin="8" .open=${this._viewTypesOpen} @close=${() => (this._viewTypesOpen = false)}>
<uui-button @click=${() => (this._viewTypesOpen = !this._viewTypesOpen)} slot="trigger" look="outline" compact>
<uui-icon .name=${this._currentLayout.meta.icon}></uui-icon>
</uui-button>
<umb-tooltip-menu
icon-only
slot="popover"
.items=${this._layouts.map((layout) => ({
label: layout.meta.label,
icon: layout.meta.icon,
action: () => {
this._changeLayout(layout.meta.pathName);
this._viewTypesOpen = false;
},
}))}></umb-tooltip-menu>
</uui-popover>`;
}
return nothing;
}
render() {
return html`
${this._renderCreateButton()}
<uui-input id="search" @input=${this._updateSearch}></uui-input>
${this._renderLayoutButton()}
<umb-collection-view-bundle></umb-collection-view-bundle>
`;
}

View File

@@ -0,0 +1,95 @@
import { UMB_COLLECTION_CONTEXT, UmbCollectionContext } from '../collection.context.js';
import { ManifestCollectionView } from '../../extension-registry/models/collection-view.model.js';
import { css, html, customElement, state, nothing } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
@customElement('umb-collection-view-bundle')
export class UmbCollectionViewBundleElement extends UmbLitElement {
@state()
_views: Array<ManifestCollectionView> = [];
@state()
_currentView?: ManifestCollectionView;
@state()
private _isOpen = false;
@state()
private _collectionRootPathname = '';
#collectionContext?: UmbCollectionContext<any, any>;
constructor() {
super();
this.consumeContext(UMB_COLLECTION_CONTEXT, (context) => {
this.#collectionContext = context;
if (!this.#collectionContext) return;
this._collectionRootPathname = this.#collectionContext.collectionRootPathname;
this.#observeViews();
this.#observeCurrentView();
});
}
#observeCurrentView() {
this.observe(this.#collectionContext!.currentView, (view) => {
this._currentView = view;
}, 'umbCurrentCollectionViewObserver');
}
#observeViews() {
this.observe(this.#collectionContext!.views, (views) => {
this._views = views;
}, 'umbCollectionViewsObserver');
}
#toggleDropdown() {
this._isOpen = !this._isOpen;
}
#closeDropdown() {
this._isOpen = false;
}
render() {
return html`${this.#renderLayoutButton()}`;
}
#renderLayoutButton() {
if (!this._currentView) return nothing;
return html` <umb-dropdown .open="${this._isOpen}" @close=${this.#closeDropdown}>
<uui-button slot="trigger" label="status" @click=${this.#toggleDropdown}
>${this.#renderItemDisplay(this._currentView)}</uui-button
>
<div slot="dropdown" class="filter-dropdown">${this._views.map((view) => this.#renderItem(view))}</div>
</umb-dropdown>`;
}
#renderItem(view: ManifestCollectionView) {
return html`<a href="${this._collectionRootPathname}/${view.meta.pathName}">${this.#renderItemDisplay(view)}</a>`;
}
#renderItemDisplay(view: ManifestCollectionView) {
return html`<span class="item"><uui-icon name=${view.meta.icon}></uui-icon> ${view.meta.label}</span>`;
}
static styles = [
UmbTextStyles,
css`
.item {
}
a {
display: block;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
'umb-collection-view-bundle': UmbCollectionViewBundleElement;
}
}

View File

@@ -0,0 +1,7 @@
import './collection-selection-actions.element.js';
import './collection-toolbar.element.js';
import './collection-view-bundle.element.js';
export * from './collection-selection-actions.element.js';
export * from './collection-toolbar.element.js';
export * from './collection-view-bundle.element.js';

View File

@@ -1,11 +1,9 @@
import { css, html, customElement, state, ifDefined } from '@umbraco-cms/backoffice/external/lit';
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { UMB_COLLECTION_CONTEXT, UmbCollectionContext } from '@umbraco-cms/backoffice/collection';
import type { ManifestDashboardCollection } from '@umbraco-cms/backoffice/extension-registry';
import type { FolderTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import '../collection.element.js';
@customElement('umb-dashboard-collection')
export class UmbDashboardCollectionElement extends UmbLitElement {
// TODO: Use the right type here:
@@ -28,7 +26,7 @@ export class UmbDashboardCollectionElement extends UmbLitElement {
}
render() {
return html`<umb-collection entity-type=${ifDefined(this._entityType)}></umb-collection>`;
return html`<umb-collection></umb-collection>`;
}
static styles = [

View File

@@ -1,4 +1,9 @@
import './collection.element.js';
import './components/index.js';
export * from './collection.element.js';
export * from './components/index.js';
export * from './collection.context.js';
export * from './collection-filter-model.interface.js';
export * from './collection-selection-actions.element.js';
export { type CollectionEntityTypeConditionConfig } from './collection-entity-type.condition.js';

View File

@@ -1,4 +1,4 @@
import { UmbTextStyles } from "@umbraco-cms/backoffice/style";
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, nothing, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
@@ -10,7 +10,7 @@ export class UmbDropdownElement extends UmbLitElement {
render() {
return html`
<uui-popover id="container" .open=${this.open}>
<uui-popover id="container" .open=${this.open} @close=${() => (this.open = false)}>
<slot name="trigger" slot="trigger"></slot>
${this.open ? this.#renderDropdown() : nothing}
</uui-popover>

View File

@@ -48,7 +48,7 @@ export class UmbEntityActionsBundleElement extends UmbLitElement {
(actions) => {
this._hasActions = actions.length > 0;
},
'observeEntityAction'
'umbEntityActionsObserver'
);
}

View File

@@ -4,7 +4,7 @@ import { availableLanguages } from './input-tiny-mce.languages.js';
import { uriAttributeSanitizer } from './input-tiny-mce.sanitizer.js';
import { FormControlMixin } from '@umbraco-cms/backoffice/external/uui';
import { renderEditor, type tinymce } from '@umbraco-cms/backoffice/external/tinymce';
import { UMB_AUTH, UmbLoggedInUser } from '@umbraco-cms/backoffice/auth';
import { UMB_AUTH_CONTEXT, UmbLoggedInUser } from '@umbraco-cms/backoffice/auth';
import { TinyMcePluginArguments, UmbTinyMcePluginBase } from '@umbraco-cms/backoffice/components';
import { ClassConstructor, hasDefaultExport, loadExtension } from '@umbraco-cms/backoffice/extension-api';
import { ManifestTinyMcePlugin, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
@@ -21,6 +21,8 @@ import { firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs';
import { UmbMediaHelper } from '@umbraco-cms/backoffice/utils';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
import { UMB_APP } from '@umbraco-cms/backoffice/app';
import { UmbStylesheetRepository } from '@umbraco-cms/backoffice/stylesheet';
// TODO => integrate macro picker, update stylesheet fetch when backend CLI exists (ref tinymce.service.js in existing backoffice)
@customElement('umb-input-tiny-mce')
@@ -33,9 +35,11 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) {
#mediaHelper = new UmbMediaHelper();
#currentUser?: UmbLoggedInUser;
#auth?: typeof UMB_AUTH.TYPE;
#auth?: typeof UMB_AUTH_CONTEXT.TYPE;
#plugins: Array<new (args: TinyMcePluginArguments) => UmbTinyMcePluginBase> = [];
#editorRef?: tinymce.Editor | null = null;
#stylesheetRepository?: UmbStylesheetRepository;
#serverUrl?: string;
protected getFormElement() {
return this._editorElement?.querySelector('iframe') ?? undefined;
@@ -47,6 +51,12 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) {
constructor() {
super();
this.consumeContext(UMB_APP, (instance) => {
this.#serverUrl = instance.getServerUrl();
});
this.#stylesheetRepository = new UmbStylesheetRepository(this);
// TODO => this breaks tests, removing for now will ignore user language
// and fall back to tinymce default language
// this.consumeContext(UMB_AUTH, (instance) => {
@@ -94,6 +104,61 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) {
}
}
async getFormatStyles(stylesheetPath: Array<string>) {
const rules: any[] = [];
stylesheetPath.forEach((path) => {
//TODO => Legacy path?
/**
* if (val.indexOf(Umbraco.Sys.ServerVariables.umbracoSettings.cssPath + "/") === 0) {
// current format (full path to stylesheet)
stylesheets.push(val);
}
else {
// legacy format (stylesheet name only) - must prefix with stylesheet folder and postfix with ".css"
stylesheets.push(Umbraco.Sys.ServerVariables.umbracoSettings.cssPath + "/" + val + ".css");
}
*/
this.#stylesheetRepository?.getStylesheetRules(path).then(({ data }) => {
data?.rules?.forEach((rule) => {
const r: {
title?: string;
inline?: string;
classes?: string;
attributes?: Record<string, string>;
block?: string;
} = {
title: rule.name,
};
if (!rule.selector) return;
if (rule.selector.startsWith('.')) {
r.inline = 'span';
r.classes = rule.selector.substring(1);
} else if (rule.selector.startsWith('#')) {
r.inline = 'span';
r.attributes = { id: rule.selector.substring(1) };
} else if (rule.selector.includes('.')) {
const [block, ...classes] = rule.selector.split('.');
r.block = block;
r.classes = classes.join(' ').replace(/\./g, ' ');
} else if (rule.selector.includes('#')) {
const [block, id] = rule.selector.split('#');
r.block = block;
r.classes = id;
} else {
r.block = rule.selector;
}
rules.push(r);
});
});
});
return rules;
}
async #setTinyConfig() {
// create an object by merging the configuration onto the fallback config
// TODO: Seems like a too tight coupling between DataTypeConfigCollection and TinyMceConfig, I would love it begin more explicit what we take from DataTypeConfigCollection and parse on, but I understand that this gives some flexibility. Is this flexibility on purpose?
@@ -102,6 +167,12 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) {
...(this.configuration ? this.configuration?.toObject() : {}),
};
// Map the stylesheets with server url
const stylesheets = configurationOptions.stylesheets.map(
(stylesheetPath: string) => `${this.#serverUrl}/css/${stylesheetPath.replace(/\\/g, '/')}`,
);
const styleFormats = await this.getFormatStyles(configurationOptions.stylesheets);
// no auto resize when a fixed height is set
if (!configurationOptions.dimensions?.height) {
configurationOptions.plugins ??= [];
@@ -129,13 +200,13 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) {
// extend with configuration values
this._tinyConfig = {
...this._tinyConfig,
content_css: configurationOptions.stylesheets.join(','),
content_css: stylesheets,
style_formats: styleFormats || defaultStyleFormats,
extended_valid_elements: defaultExtendedValidElements,
height: configurationOptions.height ?? 500,
invalid_elements: configurationOptions.invalidElements,
plugins: configurationOptions.plugins.map((x: any) => x.name),
toolbar: configurationOptions.toolbar.join(' '),
style_formats: defaultStyleFormats,
valid_elements: configurationOptions.validElements,
width: configurationOptions.width,
};

View File

@@ -1,6 +1,6 @@
import type { UserPermissionConditionConfig } from '@umbraco-cms/backoffice/user-permission';
import type { SectionAliasConditionConfig } from './section-alias.condition.js';
import type { SwitchConditionConfig } from './switch.condition.js';
import type { UserPermissionConditionConfig } from '@umbraco-cms/backoffice/user-permission';
import type {
WorkspaceAliasConditionConfig,
WorkspaceEntityTypeConditionConfig,

View File

@@ -1,6 +1,6 @@
import { Meta, StoryObj } from '@storybook/web-components';
import { html } from 'lit';
import type { UmbLocalizeElement } from '../localize.element.js';
import { html } from '@umbraco-cms/backoffice/external/lit';
import '../localize.element.js';
const meta: Meta<UmbLocalizeElement> = {

View File

@@ -1,5 +1,5 @@
import { UmbTreeElement } from '../../../tree/tree.element.js';
import { css, html, nothing, customElement, query, state } from '@umbraco-cms/backoffice/external/lit';
import { css, html, nothing, customElement, query, state, styleMap } from '@umbraco-cms/backoffice/external/lit';
import { UUIBooleanInputEvent, UUIInputElement } from '@umbraco-cms/backoffice/external/uui';
import {
UmbLinkPickerConfig,
@@ -36,6 +36,12 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement<UmbLinkPicker
ignoreUserStartNodes: false,
};
@state()
documentExpand = false;
@state()
mediaExpanded = false;
@query('#link-input')
private _linkInput!: UUIInputElement;
@@ -59,9 +65,17 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement<UmbLinkPicker
private _handleQueryString() {
if (!this._linkQueryInput) return;
const query = this._linkQueryInput.value as string;
//TODO: Handle query strings (add # etc)
this._link.queryString = query;
if (query.startsWith('#') || query.startsWith('?')) {
this._link.queryString = query;
return;
}
if (query.includes('=')) {
this._link.queryString = `?${query}`;
} else {
this._link.queryString = `#${query}`;
}
}
private _handleSelectionChange(e: CustomEvent, entityType: string) {
@@ -69,13 +83,20 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement<UmbLinkPicker
e.stopPropagation();
const element = e.target as UmbTreeElement;
const selectedKey = element.selection[element.selection.length - 1];
if (!selectedKey) return;
if (!selectedKey) {
this._link.url = '';
this._link.udi = undefined;
this._selectedKey = undefined;
this.requestUpdate();
return;
}
const udi = buildUdi(entityType, selectedKey);
this._selectedKey = selectedKey;
this._link.udi = udi;
this._link.url = udi; // TODO
this._link.url = udi;
this.requestUpdate();
}
@@ -93,22 +114,22 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement<UmbLinkPicker
<uui-box>
<div class="url-link">${this._renderLinkUrlInput()} ${this._renderAnchorInput()}</div>
<uui-label for="link-title-input">Link Title</uui-label>
<uui-label for="link-title-input">${this.localize.term('defaultdialogs_nodeNameLinkPicker')}</uui-label>
<uui-input
id="link-title-input"
placeholder="Enter a title"
label="link title"
placeholder=${this.localize.term('defaultdialogs_nodeNameLinkPicker')}
label=${this.localize.term('defaultdialogs_nodeNameLinkPicker')}
@input=${() => (this._link.name = this._linkTitleInput.value as string)}
.value="${this._link.name ?? ''}"></uui-input>
<uui-label>Target</uui-label>
<uui-label>${this.localize.term('content_target')}</uui-label>
<uui-toggle
id="#target-toggle"
label="Toggle if link should open in a new tab"
label=${this.localize.term('defaultdialogs_openInNewWindow')}
.checked="${this._link.target === '_blank' ? true : false}"
@change="${(e: UUIBooleanInputEvent) =>
e.target.checked ? (this._link.target = '_blank') : (this._link.target = '')}">
Open the link in a new tab
${this.localize.term('defaultdialogs_openInNewWindow')}
</uui-toggle>
<hr />
@@ -116,8 +137,12 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement<UmbLinkPicker
${this._renderTrees()}
</uui-box>
<div slot="actions">
<uui-button label="Close" @click=${this._close}></uui-button>
<uui-button label="Submit" look="primary" color="positive" @click=${this._submit}></uui-button>
<uui-button label=${this.localize.term('general_close')} @click=${this._close}></uui-button>
<uui-button
label=${this.localize.term('general_submit')}
look="primary"
color="positive"
@click=${this._submit}></uui-button>
</div>
</umb-body-layout>
`;
@@ -125,50 +150,67 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement<UmbLinkPicker
private _renderLinkUrlInput() {
return html`<span>
<uui-label for="link-input">Link</uui-label>
<uui-label for="link-input">${this.localize.term('defaultdialogs_link')}</uui-label>
<uui-input
id="link-input"
placeholder="URL"
label="URL"
placeholder=${this.localize.term('general_url')}
label=${this.localize.term('general_url')}
.value="${this._link.udi ?? this._link.url ?? ''}"
@input=${() => (this._link.url = this._linkInput.value as string)}
.disabled="${this._link.udi ? true : false}"></uui-input>
?disabled="${this._link.udi ? true : false}"></uui-input>
</span>`;
}
private _renderAnchorInput() {
if (this._layout.hideAnchor) return nothing;
return html`<span>
<uui-label for="anchor-input">Anchor / querystring</uui-label>
<uui-label for="anchor-input">${this.localize.term('defaultdialogs_anchorLinkPicker')}</uui-label>
<uui-input
id="anchor-input"
placeholder="#value or ?key=value"
label="#value or ?key=value"
placeholder=${this.localize.term('placeholders_anchor')}
label=${this.localize.term('placeholders_anchor')}
@input=${this._handleQueryString}
.value="${this._link.queryString ?? ''}"></uui-input>
</span>`;
}
private _renderTrees() {
return html`<uui-label for="search-input">Link to page</uui-label>
<uui-input id="search-input" placeholder="Type to search" label="Type to search"></uui-input>
<umb-tree
?multiple=${false}
alias="Umb.Tree.Documents"
@selection-change=${(event: CustomEvent) => this._handleSelectionChange(event, 'document')}
.selection=${[this._selectedKey ?? '']}
selectable></umb-tree>
//TODO: Make search work
return html`
<uui-symbol-expand
id="document-expand"
@click=${() => (this.documentExpand = !this.documentExpand)}
.open=${!this.documentExpand}></uui-symbol-expand>
<uui-label for="document-expand">${this.localize.term('defaultdialogs_linkToPage')}</uui-label>
<div style="${styleMap({ display: !this.documentExpand ? 'block' : 'none' })}">
<uui-input
id="search-input"
placeholder=${this.localize.term('placeholders_search')}
label=${this.localize.term('placeholders_search')}></uui-input>
<umb-tree
?hide-tree-root=${true}
?multiple=${false}
alias="Umb.Tree.Documents"
@selection-change=${(event: CustomEvent) => this._handleSelectionChange(event, 'document')}
.selection=${[this._selectedKey ?? '']}
selectable></umb-tree>
</div>
<hr />
<uui-label>Link to media</uui-label>
<umb-tree
?multiple=${false}
alias="Umb.Tree.Media"
@selection-change=${(event: CustomEvent) => this._handleSelectionChange(event, 'media')}
.selection=${[this._selectedKey ?? '']}
selectable></umb-tree>`;
<uui-symbol-expand
id="media-expand"
@click=${() => (this.mediaExpanded = !this.mediaExpanded)}
.open=${!this.mediaExpanded}></uui-symbol-expand>
<uui-label for="media-expand">${this.localize.term('defaultdialogs_linkToMedia')}</uui-label>
<div style="${styleMap({ display: !this.mediaExpanded ? 'block' : 'none' })}">
<umb-tree
?hide-tree-root=${true}
?multiple=${false}
alias="Umb.Tree.Media"
@selection-change=${(event: CustomEvent) => this._handleSelectionChange(event, 'media')}
.selection=${[this._selectedKey ?? '']}
selectable></umb-tree>
</div>
`;
}
static styles = [
@@ -185,7 +227,7 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement<UmbLinkPicker
width: 100%;
}
uui-input,
uui-input:not(#search-input),
uui-label {
margin-bottom: var(--uui-size-space-6);
}

View File

@@ -1,6 +1,6 @@
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbSelectionManagerBase } from '@umbraco-cms/backoffice/utils';
import { UmbSelectionManager } from '@umbraco-cms/backoffice/utils';
import { ManifestSection, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import {
UmbSectionPickerModalData,
@@ -16,7 +16,7 @@ export class UmbSectionPickerModalElement extends UmbModalBaseElement<
@state()
private _sections: Array<ManifestSection> = [];
#selectionManager = new UmbSelectionManagerBase();
#selectionManager = new UmbSelectionManager();
#submit() {
this.modalContext?.submit({
@@ -38,7 +38,7 @@ export class UmbSectionPickerModalElement extends UmbModalBaseElement<
this.observe(
umbExtensionsRegistry.extensionsOfType('section'),
(sections: Array<ManifestSection>) => (this._sections = sections),
);
), 'umbSectionsObserver';
}
render() {

View File

@@ -30,11 +30,15 @@ type OptionalSubmitArgumentIfUndefined<T> = T extends undefined
submit: (arg: T) => void;
};
export interface UmbModalRejectReason {
type: string;
}
// TODO: consider splitting this into two separate handlers
export class UmbModalContextClass<ModalPreset extends object = object, ModalValue = unknown> extends EventTarget {
#submitPromise: Promise<ModalValue>;
#submitResolver?: (value: ModalValue) => void;
#submitRejecter?: () => void;
#submitRejecter?: (reason?: UmbModalRejectReason) => void;
public readonly key: string;
public readonly data: ModalPreset;
@@ -90,8 +94,8 @@ export class UmbModalContextClass<ModalPreset extends object = object, ModalValu
* @public
* @memberof UmbModalContext
*/
public reject() {
this.#submitRejecter?.();
public reject(reason?: UmbModalRejectReason) {
this.#submitRejecter?.(reason);
}
/**

View File

@@ -1,9 +1,17 @@
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
export interface UmbChangePasswordModalData {
requireOldPassword: boolean;
userId: string;
}
export const UMB_CHANGE_PASSWORD_MODAL = new UmbModalToken<UmbChangePasswordModalData>('Umb.Modal.ChangePassword', {
type: 'dialog',
});
export interface UmbChangePasswordModalValue {
oldPassword: string;
newPassword: string;
}
export const UMB_CHANGE_PASSWORD_MODAL = new UmbModalToken<UmbChangePasswordModalData, UmbChangePasswordModalValue>(
'Umb.Modal.ChangePassword',
{
type: 'dialog',
},
);

View File

@@ -1,6 +1,6 @@
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
export const UMB_CREATE_USER_MODAL = new UmbModalToken('Umb.Modal.CreateUser', {
export const UMB_CREATE_USER_MODAL = new UmbModalToken('Umb.Modal.User.Create', {
type: 'dialog',
size: 'small',
});

View File

@@ -0,0 +1,16 @@
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
export interface UmbCreateUserSuccessModalData {
userId: string;
initialPassword: string;
}
export type UmbCreateUserSuccessModalValue = undefined;
export const UMB_CREATE_USER_SUCCESS_MODAL = new UmbModalToken<
UmbCreateUserSuccessModalData,
UmbCreateUserSuccessModalValue
>('Umb.Modal.User.CreateSuccess', {
type: 'dialog',
size: 'small',
});

View File

@@ -1,6 +1,6 @@
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
export const UMB_CURRENT_USER_MODAL = new UmbModalToken('Umb.Modal.CurrentUser', {
export const UMB_CURRENT_USER_MODAL = new UmbModalToken('Umb.Modal.User.Current', {
type: 'sidebar',
size: 'small',
});

View File

@@ -5,6 +5,7 @@ export * from './code-editor-modal.token.js';
export * from './confirm-modal.token.js';
export * from './create-dictionary-modal.token.js';
export * from './create-user-modal.token.js';
export * from './create-user-success-modal.token.js';
export * from './current-user-modal.token.js';
export * from './debug-modal.token.js';
export * from './document-picker-modal.token.js';
@@ -35,3 +36,4 @@ export * from './data-type-picker-flow-modal.token.js';
export * from './data-type-picker-flow-data-type-picker-modal.token.js';
export * from './entity-user-permission-settings-modal.token.js';
export * from './permissions-modal.token.js';
export * from './resend-invite-to-user-modal.token.js';

View File

@@ -1,6 +1,6 @@
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
export const UMB_INVITE_USER_MODAL = new UmbModalToken('Umb.Modal.InviteUser', {
export const UMB_INVITE_USER_MODAL = new UmbModalToken('Umb.Modal.User.Invite', {
type: 'dialog',
size: 'small',
});

View File

@@ -0,0 +1,15 @@
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
export type UmbResendInviteToUserModalData = {
userId: string;
};
export type UmbResendInviteToUserModalValue = undefined;
export const UMB_RESEND_INVITE_TO_USER_MODAL = new UmbModalToken<
UmbResendInviteToUserModalData,
UmbResendInviteToUserModalValue
>('Umb.Modal.User.ResendInvite', {
type: 'dialog',
size: 'small',
});

View File

@@ -8,7 +8,7 @@ export interface UmbUserPickerModalValue {
}
export const UMB_USER_PICKER_MODAL = new UmbModalToken<UmbUserPickerModalData, UmbUserPickerModalValue>(
'Umb.Modal.UserPicker',
'Umb.Modal.User.Picker',
{
type: 'sidebar',
size: 'small',

View File

@@ -3,7 +3,7 @@ import type { ManifestPropertyEditorSchema } from '@umbraco-cms/backoffice/exten
export const manifest: ManifestPropertyEditorSchema = {
type: 'propertyEditorSchema',
name: 'Rich Text',
alias: 'Umbraco.TinyMCE',
alias: 'Umbraco.RichText',
meta: {
defaultPropertyEditorUiAlias: 'Umb.PropertyEditorUi.TinyMCE',
settings: {

View File

@@ -1,4 +1,4 @@
import { UmbTextStyles } from "@umbraco-cms/backoffice/style";
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { customElement, html, property } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
@@ -11,8 +11,18 @@ export class UmbPropertyEditorUITinyMceDimensionsConfigurationElement extends Um
value: { width?: number; height?: number } = {};
render() {
return html`<uui-input type="number" placeholder="Width" .value=${this.value.width}></uui-input> x
<uui-input type="number" placeholder="Height" .value=${this.value.height}></uui-input> pixels`;
return html`<uui-input
type="number"
label=${this.localize.term('general_width')}
placeholder=${this.localize.term('general_width')}
.value=${this.value?.width}></uui-input>
x
<uui-input
type="number"
label=${this.localize.term('general_height')}
placeholder=${this.localize.term('general_height')}
.value=${this.value?.height}></uui-input>
pixels`;
}
static styles = [UmbTextStyles];

View File

@@ -8,7 +8,7 @@ const configurationManifests: Array<ManifestPropertyEditorUi> = [
loader: () => import('./toolbar/property-editor-ui-tiny-mce-toolbar-configuration.element.js'),
meta: {
label: 'TinyMCE Toolbar Configuration',
propertyEditorSchemaAlias: 'Umbraco.TinyMCE.Configuration',
propertyEditorSchemaAlias: 'Umbraco.RichText.Configuration',
icon: 'umb:autofill',
group: 'common',
},
@@ -20,7 +20,7 @@ const configurationManifests: Array<ManifestPropertyEditorUi> = [
loader: () => import('./stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.element.js'),
meta: {
label: 'TinyMCE Stylesheets Configuration',
propertyEditorSchemaAlias: 'Umbraco.TinyMCE.Configuration',
propertyEditorSchemaAlias: 'Umbraco.RichText.Configuration',
icon: 'umb:autofill',
group: 'common',
},
@@ -32,7 +32,7 @@ const configurationManifests: Array<ManifestPropertyEditorUi> = [
loader: () => import('./dimensions/property-editor-ui-tiny-mce-dimensions-configuration.element.js'),
meta: {
label: 'TinyMCE Dimensions Configuration',
propertyEditorSchemaAlias: 'Umbraco.TinyMCE.Configuration',
propertyEditorSchemaAlias: 'Umbraco.RichText.Configuration',
icon: 'umb:autofill',
group: 'common',
},
@@ -44,7 +44,7 @@ const configurationManifests: Array<ManifestPropertyEditorUi> = [
loader: () => import('./max-image-size/property-editor-ui-tiny-mce-maximagesize-configuration.element.js'),
meta: {
label: 'TinyMCE Max Image Size Configuration',
propertyEditorSchemaAlias: 'Umbraco.TinyMCE.Configuration',
propertyEditorSchemaAlias: 'Umbraco.RichText.Configuration',
icon: 'umb:autofill',
group: 'common',
},

View File

@@ -1,21 +1,73 @@
import { UmbTextStyles } from "@umbraco-cms/backoffice/style";
import { css, customElement, html, property } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, customElement, html, property, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { UmbStylesheetRepository } from '@umbraco-cms/backoffice/stylesheet';
import { StylesheetOverviewResponseModel } from '@umbraco-cms/backoffice/backend-api';
/**
* @element umb-property-editor-ui-tiny-mce-stylesheets-configuration
*/
@customElement('umb-property-editor-ui-tiny-mce-stylesheets-configuration')
export class UmbPropertyEditorUITinyMceStylesheetsConfigurationElement extends UmbLitElement {
@property()
@property({ type: Array })
value: string[] = [];
@property({ type: Array, attribute: false })
public config = [];
@state()
stylesheetList: Array<StylesheetOverviewResponseModel & Partial<{ selected: boolean }>> = [];
#repository;
constructor() {
super();
this.#repository = new UmbStylesheetRepository(this);
this.#getAllStylesheets();
}
async #getAllStylesheets() {
const { data } = await this.#repository.getAll();
if (!data) return;
const styles = data.items;
this.stylesheetList = styles.map((stylesheet) => ({
...stylesheet,
selected: this.value?.some((path) => path === stylesheet.path),
}));
}
#onChange(event: CustomEvent) {
const checkbox = event.target as HTMLInputElement;
if (checkbox.checked) {
if (this.value) {
this.value = [...this.value, checkbox.value];
} else {
this.value = [checkbox.value];
}
} else {
this.value = this.value.filter((v) => v !== checkbox.value);
}
this.dispatchEvent(new CustomEvent('property-value-change'));
}
render() {
return html`<ul>
${this.value?.map((v) => html`<li><uui-checkbox value=${v}>${v}</uui-checkbox></li>`)}
${this.stylesheetList.map(
(stylesheet) =>
html`<li>
<uui-checkbox
.label=${stylesheet.name}
.value=${stylesheet.path ?? ''}
@change=${this.#onChange}
?checked=${stylesheet.selected}>
${stylesheet.name}
</uui-checkbox>
</li>`,
)}
</ul>`;
}

View File

@@ -1,4 +1,4 @@
import { UmbTextStyles } from "@umbraco-cms/backoffice/style";
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { customElement, css, html, property, map, state, PropertyValueMap } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { UmbPropertyEditorUiElement, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
@@ -112,12 +112,13 @@ export class UmbPropertyEditorUITinyMceToolbarConfigurationElement
return html`<ul>
${map(
this._toolbarConfig,
(v) => html`<li>
<uui-checkbox value=${v.alias} ?checked=${v.selected} @change=${this.onChange}>
<uui-icon .svg=${tinyIconSet?.icons[v.icon ?? 'alignjustify']}></uui-icon>
${v.label}
</uui-checkbox>
</li>`
(v) =>
html`<li>
<uui-checkbox label=${v.label} value=${v.alias} ?checked=${v.selected} @change=${this.onChange}>
<uui-icon .svg=${tinyIconSet?.icons[v.icon ?? 'alignjustify']}></uui-icon>
${v.label}
</uui-checkbox>
</li>`,
)}
</ul>`;
}

View File

@@ -8,7 +8,7 @@ const manifest: ManifestPropertyEditorUi = {
loader: () => import('./property-editor-ui-tiny-mce.element.js'),
meta: {
label: 'Rich Text Editor',
propertyEditorSchemaAlias: 'Umbraco.TinyMCE',
propertyEditorSchemaAlias: 'Umbraco.RichText',
icon: 'umb:browser-window',
group: 'richText',
settings: {

View File

@@ -5,7 +5,7 @@ import {
UmbModalManagerContext,
UMB_MODAL_MANAGER_CONTEXT_TOKEN,
} from '@umbraco-cms/backoffice/modal';
import { UMB_AUTH, UmbLoggedInUser } from '@umbraco-cms/backoffice/auth';
import { UMB_AUTH_CONTEXT, UmbLoggedInUser } from '@umbraco-cms/backoffice/auth';
interface MediaPickerTargetData {
altText?: string;
@@ -28,7 +28,7 @@ export default class UmbTinyMceMediaPickerPlugin extends UmbTinyMcePluginBase {
#mediaHelper: UmbMediaHelper;
#currentUser?: UmbLoggedInUser;
#modalContext?: UmbModalManagerContext;
#auth?: typeof UMB_AUTH.TYPE;
#auth?: typeof UMB_AUTH_CONTEXT.TYPE;
constructor(args: TinyMcePluginArguments) {
super(args);

View File

@@ -7,4 +7,4 @@ export * from './item-repository.interface.js';
export * from './move-repository.interface.js';
export * from './copy-repository.interface.js';
export * from './repository-items.manager.js';
export * from './repository.interface.js';
export * from './repository-base.js';

View File

@@ -0,0 +1,7 @@
import { UmbBaseController, UmbControllerHost } from "@umbraco-cms/backoffice/controller-api";
export class UmbRepositoryBase extends UmbBaseController {
constructor(host: UmbControllerHost) {
super(host);
}
}

View File

@@ -1,10 +0,0 @@
export interface UmbRepository<EntityType = unknown> {
/**
* Get the type of the entity
*
* @public
* @type {EntityType}
* @returns undefined
*/
readonly ENTITY_TYPE: EntityType;
}

View File

@@ -3,6 +3,5 @@ export * from './section-sidebar/index.js';
export * from './section-sidebar-context-menu/index.js';
export * from './section-sidebar-menu/index.js';
export * from './section-sidebar-menu-with-entity-actions/index.js';
export * from './section-main-views/index.js';
export * from './section-default.element.js';
export * from './section.context.js';

View File

@@ -1,7 +1,7 @@
import { Meta, Story } from '@storybook/web-components';
import { html } from 'lit';
import type { UmbSectionSidebarMenuWithEntityActionsElement } from './section-sidebar-menu-with-entity-actions.element.js';
import { html } from '@umbraco-cms/backoffice/external/lit';
import './section-sidebar-menu-with-entity-actions.element.js';
export default {

View File

@@ -1,6 +1,6 @@
import { Meta, StoryObj } from '@storybook/web-components';
import { html } from 'lit';
import type UmbTestSorterControllerElement from './test-sorter-controller.element.js';
import { html } from '@umbraco-cms/backoffice/external/lit';
import './test-sorter-controller.element.js';
const meta: Meta<UmbTestSorterControllerElement> = {

View File

@@ -5,7 +5,7 @@ import { UmbBooleanState } from '@umbraco-cms/backoffice/observable-api';
import { UmbBaseController, UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import { createExtensionApi } from '@umbraco-cms/backoffice/extension-api';
import { ProblemDetails, TreeItemPresentationModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbSelectionManagerBase } from '@umbraco-cms/backoffice/utils';
import { UmbSelectionManager } from '@umbraco-cms/backoffice/utils';
import { UmbSelectionChangeEvent } from '@umbraco-cms/backoffice/event';
// TODO: update interface
@@ -31,7 +31,7 @@ export class UmbTreeContextBase<TreeItemType extends TreeItemPresentationModel>
extends UmbBaseController
implements UmbTreeContext<TreeItemType>
{
#selectionManager = new UmbSelectionManagerBase();
#selectionManager = new UmbSelectionManager();
#selectable = new UmbBooleanState(false);
public readonly selectable = this.#selectable.asObservable();

View File

@@ -1,5 +1,5 @@
import { css, html, customElement, ifDefined } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from "@umbraco-cms/backoffice/style";
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbCollectionContext, UMB_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import type { FolderTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api';
@@ -39,7 +39,7 @@ export class UmbWorkspaceViewCollectionElement extends UmbLitElement {
}
render() {
return html`<umb-collection entity-type=${ifDefined(this._workspaceContext?.getEntityType())}></umb-collection>`;
return html`<umb-collection></umb-collection>`;
}
static styles = [

View File

@@ -48,7 +48,7 @@ export class UmbDocumentTableCollectionViewElement extends UmbLitElement {
private _tableItems: Array<UmbTableItem> = [];
@state()
private _selection: Array<string> = [];
private _selection: Array<string | null> = [];
private _collectionContext?: UmbCollectionContext<EntityType, any>;

View File

@@ -1,4 +1,3 @@
import { t } from 'msw/lib/glossary-de6278a9.js';
import { UmbDocumentPermissionServerDataSource } from './document-permission.server.data.js';
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';

View File

@@ -10,7 +10,7 @@ export class UmbMediaGridCollectionViewElement extends UmbLitElement {
private _mediaItems?: Array<EntityTreeItemResponseModel>;
@state()
private _selection: Array<string> = [];
private _selection: Array<string | null> = [];
private _collectionContext?: UmbCollectionContext<EntityTreeItemResponseModel, any>;

View File

@@ -36,7 +36,7 @@ export class UmbMediaTableCollectionViewElement extends UmbLitElement {
private _tableItems: Array<UmbTableItem> = [];
@state()
private _selection: Array<string> = [];
private _selection: Array<string | null> = [];
private _collectionContext?: UmbCollectionContext<MediaDetails, any>;

View File

@@ -1,6 +1,7 @@
import { ContentTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api';
export * from './components/index.js';
export * from './repository/index.js';
// Content
export interface ContentProperty {

View File

@@ -0,0 +1 @@
export * from './media.repository.js';

View File

@@ -2,7 +2,7 @@ import { UmbLanguageRepository } from '../../repository/language.repository.js';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, customElement, state, repeat } from '@umbraco-cms/backoffice/external/lit';
import { LanguageResponseModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbSelectionManagerBase } from '@umbraco-cms/backoffice/utils';
import { UmbSelectionManager } from '@umbraco-cms/backoffice/utils';
import {
UmbLanguagePickerModalValue,
UmbLanguagePickerModalData,
@@ -18,7 +18,7 @@ export class UmbLanguagePickerModalElement extends UmbModalBaseElement<
private _languages: Array<LanguageResponseModel> = [];
#languageRepository = new UmbLanguageRepository(this);
#selectionManager = new UmbSelectionManagerBase();
#selectionManager = new UmbSelectionManager();
connectedCallback(): void {
super.connectedCallback();

View File

@@ -1,4 +1,3 @@
import { v4 as uuidv4 } from 'uuid';
import { TagResource } from '@umbraco-cms/backoffice/backend-api';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';

View File

@@ -1,4 +1,3 @@
import _ from 'lodash';
import { UmbScriptsWorkspaceContext } from './scripts-workspace.context.js';
import type { UmbCodeEditorElement } from '@umbraco-cms/backoffice/code-editor';
import { UUITextStyles, UUIInputElement } from '@umbraco-cms/backoffice/external/uui';

View File

@@ -1,5 +1,5 @@
import { UmbScriptsWorkspaceContext } from './scripts-workspace.context.js';
import { UUITextStyles } from '@umbraco-cms/backoffice/external/uui';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { UmbRoute, IRoutingInfo, PageComponent } from '@umbraco-cms/backoffice/router';
@@ -40,7 +40,7 @@ export class UmbScriptsWorkspaceElement extends UmbLitElement {
return html`<umb-router-slot .routes=${this._routes}></umb-router-slot>`;
}
static styles = [UUITextStyles, css``];
static styles = [UmbTextStyles, css``];
}
export default UmbScriptsWorkspaceElement;

View File

@@ -46,6 +46,16 @@ export class UmbStylesheetServerDataSource
if (!path) throw new Error('Path is missing');
return tryExecuteAndNotify(this.#host, StylesheetResource.getStylesheet({ path }));
}
/**
* Fetches all stylesheets from the server
* @return {*}
* @memberof UmbStylesheetServerDataSource
*/
async getAll(skip: number = 0, take: number = 9999) {
return tryExecuteAndNotify(this.#host, StylesheetResource.getStylesheetAll({ skip, take }));
}
/**
* Creates a new Stylesheet
*
@@ -85,7 +95,7 @@ export class UmbStylesheetServerDataSource
* @memberof UmbStylesheetServerDataSource
*/
getStylesheetRichTextRules(
path: string
path: string,
): Promise<DataSourceResponse<RichTextStylesheetRulesResponseModel | ExtractRichTextStylesheetRulesResponseModel>> {
return tryExecuteAndNotify(this.#host, StylesheetResource.getStylesheetRichTextRules({ path }));
}
@@ -97,11 +107,11 @@ export class UmbStylesheetServerDataSource
* @memberof UmbStylesheetServerDataSource
*/
postStylesheetRichTextExtractRules(
data: ExtractRichTextStylesheetRulesRequestModel
data: ExtractRichTextStylesheetRulesRequestModel,
): Promise<DataSourceResponse<ExtractRichTextStylesheetRulesResponseModel>> {
return tryExecuteAndNotify(
this.#host,
StylesheetResource.postStylesheetRichTextExtractRules({ requestBody: data })
StylesheetResource.postStylesheetRichTextExtractRules({ requestBody: data }),
);
}
/**
@@ -112,11 +122,11 @@ export class UmbStylesheetServerDataSource
* @memberof UmbStylesheetServerDataSource
*/
postStylesheetRichTextInterpolateRules(
data: InterpolateRichTextStylesheetRequestModel
data: InterpolateRichTextStylesheetRequestModel,
): Promise<DataSourceResponse<InterpolateRichTextStylesheetResponseModel>> {
return tryExecuteAndNotify(
this.#host,
StylesheetResource.postStylesheetRichTextInterpolateRules({ requestBody: data })
StylesheetResource.postStylesheetRichTextInterpolateRules({ requestBody: data }),
);
}
}

View File

@@ -1,4 +1,3 @@
import { Observable } from 'rxjs';
import { StylesheetDetails } from '../index.js';
import { UmbStylesheetTreeStore, UMB_STYLESHEET_TREE_STORE_CONTEXT_TOKEN } from './stylesheet.tree.store.js';
import { UmbStylesheetTreeServerDataSource } from './sources/stylesheet.tree.server.data.js';
@@ -7,6 +6,7 @@ import {
StylesheetGetFolderResponse,
UmbStylesheetFolderServerDataSource,
} from './sources/stylesheet.folder.server.data.js';
import type { Observable } from '@umbraco-cms/backoffice/external/rxjs';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api';
import {
@@ -27,6 +27,7 @@ import {
FolderResponseModel,
InterpolateRichTextStylesheetRequestModel,
InterpolateRichTextStylesheetResponseModel,
PagedStylesheetOverviewResponseModel,
ProblemDetails,
RichTextStylesheetRulesResponseModel,
TextFileResponseModelBaseModel,
@@ -143,6 +144,10 @@ export class UmbStylesheetRepository
return promise;
}
async getAll(skip?: number, take?: number): Promise<DataSourceResponse<PagedStylesheetOverviewResponseModel>> {
return this.#dataSource.getAll(skip, take);
}
getStylesheetRules(
path: string,
): Promise<DataSourceResponse<RichTextStylesheetRulesResponseModel | ExtractRichTextStylesheetRulesResponseModel>> {

View File

@@ -2,14 +2,22 @@ import { UmbStylesheetRepository } from '../repository/stylesheet.repository.js'
import { StylesheetDetails } from '../index.js';
import { UmbSaveableWorkspaceContextInterface, UmbWorkspaceContext } from '@umbraco-cms/backoffice/workspace';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import { UmbArrayState, UmbBooleanState, UmbObjectState, createObservablePart } from '@umbraco-cms/backoffice/observable-api';
import {
UmbArrayState,
UmbBooleanState,
UmbObjectState,
createObservablePart,
} from '@umbraco-cms/backoffice/observable-api';
import { loadCodeEditor } from '@umbraco-cms/backoffice/code-editor';
import { RichTextRuleModel, UpdateStylesheetRequestModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
export type RichTextRuleModelSortable = RichTextRuleModel & { sortOrder?: number };
export class UmbStylesheetWorkspaceContext extends UmbWorkspaceContext<UmbStylesheetRepository, StylesheetDetails> implements UmbSaveableWorkspaceContextInterface {
export class UmbStylesheetWorkspaceContext
extends UmbWorkspaceContext<UmbStylesheetRepository, StylesheetDetails>
implements UmbSaveableWorkspaceContextInterface
{
#data = new UmbObjectState<StylesheetDetails | undefined>(undefined);
#rules = new UmbArrayState<RichTextRuleModelSortable>([], (rule) => rule.name);
data = this.#data.asObservable();
@@ -55,7 +63,6 @@ export class UmbStylesheetWorkspaceContext extends UmbWorkspaceContext<UmbStyles
updateRule(unique: string, rule: RichTextRuleModelSortable) {
this.#rules.updateOne(unique, rule);
this.sendRulesGetContent();
}
setRules(rules: RichTextRuleModelSortable[]) {
@@ -103,7 +110,6 @@ export class UmbStylesheetWorkspaceContext extends UmbWorkspaceContext<UmbStyles
}
async sendContentGetRules() {
if (!this.getData()?.content) return;
const requestBody = {
@@ -135,7 +141,7 @@ export class UmbStylesheetWorkspaceContext extends UmbWorkspaceContext<UmbStyles
const createRequestBody = {
name: stylesheet.name,
content: stylesheet.content,
parentPath: stylesheet.path === 'null' ? '' : stylesheet.path + '/',
parentPath: stylesheet.path ?? '',
};
this.repository.create(createRequestBody);
@@ -166,7 +172,10 @@ export class UmbStylesheetWorkspaceContext extends UmbWorkspaceContext<UmbStyles
}
}
export const UMB_STYLESHEET_WORKSPACE_CONTEXT = new UmbContextToken<UmbSaveableWorkspaceContextInterface, UmbStylesheetWorkspaceContext>(
export const UMB_STYLESHEET_WORKSPACE_CONTEXT = new UmbContextToken<
UmbSaveableWorkspaceContextInterface,
UmbStylesheetWorkspaceContext
>(
'UmbWorkspaceContext',
(context): context is UmbStylesheetWorkspaceContext => context.getEntityType?.() === 'stylesheet'
(context): context is UmbStylesheetWorkspaceContext => context.getEntityType?.() === 'stylesheet',
);

View File

@@ -1,7 +1,6 @@
import { serverFilePathFromUrlFriendlyPath } from '../../utils.js';
import { UmbStylesheetWorkspaceEditorElement } from './stylesheet-workspace-editor.element.js';
import { UmbStylesheetWorkspaceContext } from './stylesheet-workspace.context.js';
import { UmbTextStyles } from "@umbraco-cms/backoffice/style";
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import type { UmbRoute } from '@umbraco-cms/backoffice/router';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
@@ -10,7 +9,6 @@ import { UmbWorkspaceIsNewRedirectController } from '@umbraco-cms/backoffice/wor
@customElement('umb-stylesheet-workspace')
export class UmbStylesheetWorkspaceElement extends UmbLitElement {
#workspaceContext = new UmbStylesheetWorkspaceContext(this);
#element = new UmbStylesheetWorkspaceEditorElement();
@state()
_routes: UmbRoute[] = [

View File

@@ -1,9 +1,8 @@
import { UUITextStyles } from '@umbraco-ui/uui-css';
import { css, html } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { UMB_STYLESHEET_WORKSPACE_CONTEXT, UmbStylesheetWorkspaceContext } from '../../stylesheet-workspace.context.js';
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { UmbCodeEditorElement } from '@umbraco-cms/backoffice/code-editor';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
@customElement('umb-stylesheet-workspace-view-code-editor')
export class UmbStylesheetWorkspaceViewCodeEditorElement extends UmbLitElement {
@@ -62,7 +61,7 @@ export class UmbStylesheetWorkspaceViewCodeEditorElement extends UmbLitElement {
}
static styles = [
UUITextStyles,
UmbTextStyles,
css`
:host {
display: block;

View File

@@ -1,11 +1,11 @@
import { UmbStylesheetWorkspaceContext } from '../../stylesheet-workspace.context.js';
import { UMB_MODAL_TEMPLATING_STYLESHEET_RTF_STYLE_SIDEBAR_MODAL } from './stylesheet-workspace-view-rich-text-editor.element.js';
import { UUITextStyles } from '@umbraco-cms/backoffice/external/uui';
import { css, html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import { RichTextRuleModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace';
import { UMB_MODAL_MANAGER_CONTEXT_TOKEN, UmbModalManagerContext } from '@umbraco-cms/backoffice/modal';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
@customElement('umb-stylesheet-rich-text-editor-rule')
export default class UmbStylesheetRichTextEditorRuleElement extends UmbLitElement {
@@ -56,7 +56,7 @@ export default class UmbStylesheetRichTextEditorRuleElement extends UmbLitElemen
}
static styles = [
UUITextStyles,
UmbTextStyles,
css`
:host {
display: flex;

View File

@@ -1,5 +1,5 @@
import { RichTextRuleModelSortable } from '../../stylesheet-workspace.context.js';
import { UUITextStyles } from '@umbraco-cms/backoffice/external/uui';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, customElement, ifDefined, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
import { RichTextRuleModel } from '@umbraco-cms/backoffice/backend-api';
@@ -128,7 +128,7 @@ export default class UmbStylesheetRichTextEditorStyleModalElement extends UmbMod
}
static styles = [
UUITextStyles,
UmbTextStyles,
css`
:host {
display: block;

View File

@@ -1,18 +1,16 @@
import { UUITextStyles } from '@umbraco-ui/uui-css';
import { css, html } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { RichTextRuleModelSortable, UmbStylesheetWorkspaceContext } from '../../stylesheet-workspace.context.js';
import { UMB_MODAL_TEMPLATING_STYLESHEET_RTF_STYLE_SIDEBAR } from '../../manifests.js';
import {
StylesheetRichTextEditorStyleModalData,
UmbStylesheetRichTextEditorStyleModalValue,
} from './stylesheet-workspace-view-rich-text-editor-style-sidebar.element.js';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace';
import { UMB_MODAL_MANAGER_CONTEXT_TOKEN, UmbModalManagerContext, UmbModalToken } from '@umbraco-cms/backoffice/modal';
import { RichTextRuleModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbSorterConfig, UmbSorterController } from '@umbraco-cms/backoffice/sorter';
import { ifDefined, repeat } from '@umbraco-cms/backoffice/external/lit';
import { css, html, customElement, state, ifDefined, repeat } from '@umbraco-cms/backoffice/external/lit';
import './stylesheet-workspace-view-rich-text-editor-rule.element.js';
@@ -111,7 +109,7 @@ export class UmbStylesheetWorkspaceViewRichTextEditorElement extends UmbLitEleme
}
static styles = [
UUITextStyles,
UmbTextStyles,
css`
:host {
display: block;

View File

@@ -2,8 +2,8 @@ import { UmbTemplateRepository } from '../../repository/template.repository.js';
import type { UmbQueryBuilderFilterElement } from './query-builder-filter.element.js';
import { UUIComboboxListElement } from '@umbraco-cms/backoffice/external/uui';
import { css, html, customElement, state, query, queryAll } from '@umbraco-cms/backoffice/external/lit';
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
import {
UmbModalBaseElement ,
UMB_DOCUMENT_PICKER_MODAL,
UMB_MODAL_MANAGER_CONTEXT_TOKEN,
UmbModalManagerContext,

View File

@@ -1,18 +1,18 @@
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UMB_AUTH } from '@umbraco-cms/backoffice/auth';
import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth';
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
@customElement('umb-umbraco-news-dashboard')
export class UmbUmbracoNewsDashboardElement extends UmbLitElement {
#auth?: typeof UMB_AUTH.TYPE;
#auth?: typeof UMB_AUTH_CONTEXT.TYPE;
@state()
private name = '';
constructor() {
super();
this.consumeContext(UMB_AUTH, (instance) => {
this.consumeContext(UMB_AUTH_CONTEXT, (instance) => {
this.#auth = instance;
this.#observeCurrentUser();
});

View File

@@ -1,4 +1,4 @@
import { UmbTextStyles } from "@umbraco-cms/backoffice/style";
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, CSSResultGroup, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import {
UmbModalManagerContext,
@@ -6,14 +6,14 @@ import {
UMB_CURRENT_USER_MODAL,
} from '@umbraco-cms/backoffice/modal';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { UMB_AUTH, type UmbLoggedInUser } from '@umbraco-cms/backoffice/auth';
import { UMB_AUTH_CONTEXT, type UmbLoggedInUser } from '@umbraco-cms/backoffice/auth';
@customElement('umb-current-user-header-app')
export class UmbCurrentUserHeaderAppElement extends UmbLitElement {
@state()
private _currentUser?: UmbLoggedInUser;
private _auth?: typeof UMB_AUTH.TYPE;
private _auth?: typeof UMB_AUTH_CONTEXT.TYPE;
private _modalContext?: UmbModalManagerContext;
constructor() {
@@ -23,7 +23,7 @@ export class UmbCurrentUserHeaderAppElement extends UmbLitElement {
this._modalContext = instance;
});
this.consumeContext(UMB_AUTH, (instance) => {
this.consumeContext(UMB_AUTH_CONTEXT, (instance) => {
this._auth = instance;
this._observeCurrentUser();
});
@@ -34,7 +34,7 @@ export class UmbCurrentUserHeaderAppElement extends UmbLitElement {
this.observe(this._auth.currentUser, (currentUser) => {
this._currentUser = currentUser;
});
}, 'umbCurrentUserObserver');
}
private _handleUserClick() {

View File

@@ -1,2 +1,3 @@
// TODO:Do not export store, but instead export future repository
export * from './current-user-history.store.js';
export * from './utils/index.js';

View File

@@ -1,109 +0,0 @@
import { UmbTextStyles } from "@umbraco-cms/backoffice/style";
import { css, CSSResultGroup, html, nothing, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import { UmbModalContext, UmbChangePasswordModalData } from '@umbraco-cms/backoffice/modal';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
@customElement('umb-change-password-modal')
export class UmbChangePasswordModalElement extends UmbLitElement {
@property({ attribute: false })
modalContext?: UmbModalContext;
@property()
data?: UmbChangePasswordModalData;
private _close() {
this.modalContext?.submit();
}
private _handleSubmit(e: SubmitEvent) {
e.preventDefault();
const form = e.target as HTMLFormElement;
if (!form) return;
const isValid = form.checkValidity();
if (!isValid) return;
const formData = new FormData(form);
const oldPassword = formData.get('oldPassword') as string;
const newPassword = formData.get('newPassword') as string;
const confirmPassword = formData.get('confirmPassword') as string;
console.log('IMPLEMENT SUBMIT', { oldPassword, newPassword, confirmPassword });
this._close();
}
private _renderOldPasswordInput() {
return html`
<uui-form-layout-item style="margin-bottom: var(--uui-size-layout-2)">
<uui-label id="oldPasswordLabel" for="oldPassword" slot="label" required>Old password</uui-label>
<uui-input-password
id="oldPassword"
name="oldPassword"
required
required-message="Old password is required"></uui-input-password>
</uui-form-layout-item>
`;
}
render() {
return html`
<uui-dialog-layout class="uui-text" headline="Change password">
<uui-form>
<form id="LoginForm" name="login" @submit="${this._handleSubmit}">
${this.data?.requireOldPassword ? this._renderOldPasswordInput() : nothing}
<uui-form-layout-item>
<uui-label id="newPasswordLabel" for="newPassword" slot="label" required>New password</uui-label>
<uui-input-password
id="newPassword"
name="newPassword"
required
required-message="New password is required"></uui-input-password>
</uui-form-layout-item>
<uui-form-layout-item>
<uui-label id="confirmPasswordLabel" for="confirmPassword" slot="label" required
>Confirm password</uui-label
>
<uui-input-password
id="confirmPassword"
name="confirmPassword"
required
required-message="Confirm password is required"></uui-input-password>
</uui-form-layout-item>
<div id="actions">
<uui-button @click=${this._close} label="Cancel"></uui-button>
<uui-button type="submit" label="Confirm" look="primary" color="positive"></uui-button>
</div>
</form>
</uui-form>
</uui-dialog-layout>
`;
}
static styles: CSSResultGroup = [
UmbTextStyles,
css`
:host {
display: block;
width: 400px;
}
uui-input-password {
width: 100%;
}
#actions {
display: flex;
justify-content: flex-end;
margin-top: var(--uui-size-layout-2);
}
`,
];
}
export default UmbChangePasswordModalElement;
declare global {
interface HTMLElementTagNameMap {
'umb-change-password-modal': UmbChangePasswordModalElement;
}
}

View File

@@ -1,5 +1,5 @@
import { UMB_APP } from '@umbraco-cms/backoffice/app';
import { UMB_AUTH, type UmbLoggedInUser } from '@umbraco-cms/backoffice/auth';
import { UMB_AUTH_CONTEXT, type UmbLoggedInUser } from '@umbraco-cms/backoffice/auth';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, CSSResultGroup, html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbModalContext } from '@umbraco-cms/backoffice/modal';
@@ -13,14 +13,14 @@ export class UmbCurrentUserModalElement extends UmbLitElement {
@state()
private _currentUser?: UmbLoggedInUser;
#authContext?: typeof UMB_AUTH.TYPE;
#authContext?: typeof UMB_AUTH_CONTEXT.TYPE;
#appContext?: typeof UMB_APP.TYPE;
constructor() {
super();
this.consumeContext(UMB_AUTH, (instance) => {
this.consumeContext(UMB_AUTH_CONTEXT, (instance) => {
this.#authContext = instance;
this._observeCurrentUser();
});
@@ -35,7 +35,7 @@ export class UmbCurrentUserModalElement extends UmbLitElement {
this.observe(this.#authContext.currentUser, (currentUser) => {
this._currentUser = currentUser;
});
}, 'umbCurrentUserObserver');
}
private _close() {

View File

@@ -3,7 +3,7 @@ import type { ManifestModal } from '@umbraco-cms/backoffice/extension-registry';
const modals: Array<ManifestModal> = [
{
type: 'modal',
alias: 'Umb.Modal.CurrentUser',
alias: 'Umb.Modal.User.Current',
name: 'Current User Modal',
loader: () => import('./current-user/current-user-modal.element.js'),
},

View File

@@ -27,7 +27,7 @@ export class UmbUserProfileAppHistoryElement extends UmbLitElement {
if (this.#currentUserHistoryStore) {
this.observe(this.#currentUserHistoryStore.latestHistory, (history) => {
this._history = history;
});
}, 'umbCurrentUserHistoryObserver');
}
}

View File

@@ -1,12 +1,12 @@
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from "@umbraco-cms/backoffice/style";
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import {
UmbModalManagerContext,
UMB_CHANGE_PASSWORD_MODAL,
UMB_MODAL_MANAGER_CONTEXT_TOKEN,
} from '@umbraco-cms/backoffice/modal';
import { UMB_AUTH, type UmbLoggedInUser } from '@umbraco-cms/backoffice/auth';
import { UMB_AUTH_CONTEXT, type UmbLoggedInUser } from '@umbraco-cms/backoffice/auth';
@customElement('umb-user-profile-app-profile')
export class UmbUserProfileAppProfileElement extends UmbLitElement {
@@ -14,7 +14,7 @@ export class UmbUserProfileAppProfileElement extends UmbLitElement {
private _currentUser?: UmbLoggedInUser;
private _modalContext?: UmbModalManagerContext;
private _auth?: typeof UMB_AUTH.TYPE;
private _auth?: typeof UMB_AUTH_CONTEXT.TYPE;
constructor() {
super();
@@ -23,7 +23,7 @@ export class UmbUserProfileAppProfileElement extends UmbLitElement {
this._modalContext = instance;
});
this.consumeContext(UMB_AUTH, (instance) => {
this.consumeContext(UMB_AUTH_CONTEXT, (instance) => {
this._auth = instance;
this._observeCurrentUser();
});
@@ -34,21 +34,20 @@ export class UmbUserProfileAppProfileElement extends UmbLitElement {
this.observe(this._auth.currentUser, (currentUser) => {
this._currentUser = currentUser;
});
}, 'umbCurrentUserObserver');
}
private _edit() {
if (!this._currentUser) return;
history.pushState(null, '', 'section/users/view/users/user/' + this._currentUser.id); //TODO Change to a tag with href and make dynamic
history.pushState(null, '', 'section/user-management/view/users/user/' + this._currentUser.id); //TODO Change to a tag with href and make dynamic
//TODO Implement modal routing for the current-user-modal, so that the modal closes when navigating to the edit profile page
}
private _changePassword() {
if (!this._modalContext) return;
// TODO: check if current user is admin
this._modalContext.open(UMB_CHANGE_PASSWORD_MODAL, {
requireOldPassword: false,
userId: this._currentUser?.id ?? '',
});
}

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