Merge branch 'v15/dev' into v15/feature/user-sidebar-menu
This commit is contained in:
@@ -5,11 +5,13 @@ on:
|
||||
branches:
|
||||
- main
|
||||
- release/*
|
||||
- v*/dev
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, closed]
|
||||
branches:
|
||||
- main
|
||||
- release/*
|
||||
- v*/dev
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
issue_number:
|
||||
|
||||
@@ -8,10 +8,12 @@ on:
|
||||
branches:
|
||||
- main
|
||||
- release/*
|
||||
- v*/dev
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- release/*
|
||||
- v*/dev
|
||||
|
||||
# Allows GitHub to use this workflow to validate the merge queue
|
||||
merge_group:
|
||||
|
||||
@@ -16,10 +16,12 @@ on:
|
||||
branches:
|
||||
- main
|
||||
- release/*
|
||||
- v*/dev
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- release/*
|
||||
- v*/dev
|
||||
schedule:
|
||||
- cron: '33 2 * * 1'
|
||||
|
||||
|
||||
@@ -83,6 +83,7 @@
|
||||
"./store": "./dist-cms/packages/core/store/index.js",
|
||||
"./style": "./dist-cms/packages/core/style/index.js",
|
||||
"./stylesheet": "./dist-cms/packages/templating/stylesheets/index.js",
|
||||
"./sysinfo": "./dist-cms/packages/sysinfo/index.js",
|
||||
"./tags": "./dist-cms/packages/tags/index.js",
|
||||
"./template": "./dist-cms/packages/templating/templates/index.js",
|
||||
"./temporary-file": "./dist-cms/packages/core/temporary-file/index.js",
|
||||
|
||||
@@ -33,6 +33,7 @@ const CORE_PACKAGES = [
|
||||
import('../../packages/search/umbraco-package.js'),
|
||||
import('../../packages/settings/umbraco-package.js'),
|
||||
import('../../packages/static-file/umbraco-package.js'),
|
||||
import('../../packages/sysinfo/umbraco-package.js'),
|
||||
import('../../packages/tags/umbraco-package.js'),
|
||||
import('../../packages/telemetry/umbraco-package.js'),
|
||||
import('../../packages/templating/umbraco-package.js'),
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { isCurrentUserAnAdmin } from '@umbraco-cms/backoffice/current-user';
|
||||
import { UMB_BACKOFFICE_CONTEXT } from '../backoffice.context.js';
|
||||
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||
import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
|
||||
import { UMB_SYSINFO_MODAL } from '@umbraco-cms/backoffice/sysinfo';
|
||||
|
||||
@customElement('umb-backoffice-header-logo')
|
||||
export class UmbBackofficeHeaderLogoElement extends UmbLitElement {
|
||||
@state()
|
||||
private _version?: string;
|
||||
|
||||
@state()
|
||||
private _isUserAdmin = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
@@ -21,6 +27,12 @@ export class UmbBackofficeHeaderLogoElement extends UmbLitElement {
|
||||
'_observeVersion',
|
||||
);
|
||||
});
|
||||
|
||||
this.#isAdmin();
|
||||
}
|
||||
|
||||
async #isAdmin() {
|
||||
this._isUserAdmin = await isCurrentUserAnAdmin(this);
|
||||
}
|
||||
|
||||
override render() {
|
||||
@@ -31,15 +43,35 @@ export class UmbBackofficeHeaderLogoElement extends UmbLitElement {
|
||||
<uui-popover-container id="logo-popover" placement="bottom-start">
|
||||
<umb-popover-layout>
|
||||
<div id="modal">
|
||||
<img src="/umbraco/backoffice/assets/umbraco_logo_blue.svg" alt="Umbraco" loading="lazy" />
|
||||
<img
|
||||
src="/umbraco/backoffice/assets/umbraco_logo_blue.svg"
|
||||
alt="Umbraco"
|
||||
width="300"
|
||||
height="82"
|
||||
loading="lazy" />
|
||||
<span>${this._version}</span>
|
||||
<a href="https://umbraco.com" target="_blank" rel="noopener">Umbraco.com</a>
|
||||
|
||||
${this._isUserAdmin
|
||||
? html`<uui-button
|
||||
@click=${this.#openSystemInformation}
|
||||
look="secondary"
|
||||
label="System information"></uui-button>`
|
||||
: ''}
|
||||
</div>
|
||||
</umb-popover-layout>
|
||||
</uui-popover-container>
|
||||
`;
|
||||
}
|
||||
|
||||
async #openSystemInformation() {
|
||||
const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT);
|
||||
modalManager
|
||||
.open(this, UMB_SYSINFO_MODAL)
|
||||
.onSubmit()
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
UmbTextStyles,
|
||||
css`
|
||||
|
||||
@@ -1831,6 +1831,9 @@ export default {
|
||||
administrators: 'Administrator',
|
||||
categoryField: 'Kategorifelt',
|
||||
createDate: 'Bruger oprettet',
|
||||
createUserHeadline: (kind: string) => {
|
||||
return kind === 'Api' ? 'Opret API bruger' : 'Opret bruger';
|
||||
},
|
||||
changePassword: 'Skift dit kodeord',
|
||||
changePhoto: 'Skift billede',
|
||||
newPassword: 'Nyt kodeord',
|
||||
@@ -1857,6 +1860,7 @@ export default {
|
||||
inviteAnotherUser: 'Invitér anden bruger',
|
||||
inviteUserHelp:
|
||||
'Invitér nye brugere til at give dem adgang til Umbraco. En invitation vil blive sendt\n via e-mail til brugeren med oplysninger om, hvordan man logger ind i Umbraco.\n ',
|
||||
kind: 'Slags',
|
||||
language: 'Sprog',
|
||||
languageHelp: 'Indstil det sprog, du vil se i menuer og dialoger',
|
||||
lastLockoutDate: 'Senest låst ude',
|
||||
@@ -1952,6 +1956,8 @@ export default {
|
||||
sortNameDescending: 'Navn (Å-A)',
|
||||
sortCreateDateAscending: 'Nyeste',
|
||||
sortCreateDateDescending: 'Ældste',
|
||||
userKindDefault: 'Bruger',
|
||||
userKindApi: 'API Bruger',
|
||||
sortLastLoginDateDescending: 'Sidst logget ind',
|
||||
noUserGroupsAdded: 'Ingen brugere er blevet tilføjet',
|
||||
'2faDisableText': 'Hvis du ønsker at slå denne totrinsbekræftelse fra, så skal du nu indtaste koden fra din enhed:',
|
||||
@@ -2223,6 +2229,7 @@ export default {
|
||||
labelForArrayOfItems: 'Samling af %0%',
|
||||
labelForRemoveAllEntries: 'Fjern alle elementer',
|
||||
labelForClearClipboard: 'Ryd udklipsholder',
|
||||
labelForCopyToClipboard: 'Kopier til udklipsholder',
|
||||
},
|
||||
propertyActions: {
|
||||
tooltipForPropertyActionsMenu: 'Åben egenskabshandlinger',
|
||||
|
||||
@@ -1967,6 +1967,14 @@ export default {
|
||||
selectAllLogLevelFilters: 'Wählen Sie Alle',
|
||||
deselectAllLogLevelFilters: 'Alle abwählen',
|
||||
},
|
||||
clipboard: {
|
||||
labelForCopyAllEntries: '%0% kopieren',
|
||||
labelForArrayOfItemsFrom: '%0% von %1%',
|
||||
labelForArrayOfItems: 'Sammlung von %0%',
|
||||
labelForRemoveAllEntries: 'Alle Elemente entfernen',
|
||||
labelForClearClipboard: 'Zwischenablage löschen',
|
||||
labelForCopyToClipboard: 'Kopieren in Zwischenablage',
|
||||
},
|
||||
formsDashboard: {
|
||||
formsHeadline: 'Umbraco Forms',
|
||||
formsDescription:
|
||||
|
||||
@@ -1839,8 +1839,16 @@ export default {
|
||||
assignAccess: 'Assign access',
|
||||
administrators: 'Administrator',
|
||||
categoryField: 'Category field',
|
||||
createDate: 'User created',
|
||||
changePassword: 'Change your password',
|
||||
createDate: 'Created',
|
||||
createUserHeadline: (kind: string) => {
|
||||
return kind === 'Api' ? 'Create API user' : 'Create user';
|
||||
},
|
||||
createUserDescription: (kind: string) => {
|
||||
const defaultUserText = `Create a user to give them access to Umbraco. When a user is created a password will be generated that you can share with them.`;
|
||||
const apiUserText = `Create an Api User to allow external services to authenticate with the Umbraco Management API.`;
|
||||
return kind === 'Api' ? apiUserText : defaultUserText;
|
||||
},
|
||||
changePassword: 'Change password',
|
||||
changePhoto: 'Change photo',
|
||||
configureMfa: 'Configure MFA',
|
||||
emailRequired: 'Required - enter an email address for this user',
|
||||
@@ -1849,6 +1857,7 @@ export default {
|
||||
? 'The email address is used for notifications, password recovery, and as the username for logging in'
|
||||
: 'The email address is used for notifications and password recovery';
|
||||
},
|
||||
kind: 'Kind',
|
||||
newPassword: 'New password',
|
||||
newPasswordFormatLengthTip: 'Minimum %0% character(s) to go!',
|
||||
newPasswordFormatNonAlphaTip: 'There should be at least %0% special character(s) in there.',
|
||||
@@ -1935,7 +1944,7 @@ export default {
|
||||
startnodehelp: 'Limit the content tree to a specific start node',
|
||||
startnodes: 'Content start nodes',
|
||||
startnodeshelp: 'Limit the content tree to specific start nodes',
|
||||
updateDate: 'User last updated',
|
||||
updateDate: 'Updated',
|
||||
userCreated: 'has been created',
|
||||
userCreatedSuccessHelp:
|
||||
'The new user has successfully been created. To log in to Umbraco use the\n password below.\n ',
|
||||
@@ -1982,6 +1991,8 @@ export default {
|
||||
sortCreateDateDescending: 'Newest',
|
||||
sortCreateDateAscending: 'Oldest',
|
||||
sortLastLoginDateDescending: 'Last login',
|
||||
userKindDefault: 'User',
|
||||
userKindApi: 'API User',
|
||||
noUserGroupsAdded: 'No user groups have been added',
|
||||
'2faDisableText':
|
||||
'If you wish to disable this two-factor provider, then you must enter the code shown on your authentication device:',
|
||||
@@ -2288,6 +2299,7 @@ export default {
|
||||
labelForArrayOfItems: 'Collection of %0%',
|
||||
labelForRemoveAllEntries: 'Remove all items',
|
||||
labelForClearClipboard: 'Clear clipboard',
|
||||
labelForCopyToClipboard: 'Copy to clipboard',
|
||||
},
|
||||
propertyActions: {
|
||||
tooltipForPropertyActionsMenu: 'Open Property Actions',
|
||||
|
||||
@@ -2354,6 +2354,7 @@ export default {
|
||||
labelForArrayOfItems: 'Collection of %0%',
|
||||
labelForRemoveAllEntries: 'Remove all items',
|
||||
labelForClearClipboard: 'Clear clipboard',
|
||||
labelForCopyToClipboard: 'Copy to clipboard',
|
||||
},
|
||||
propertyActions: {
|
||||
tooltipForPropertyActionsMenu: 'Open Property Actions',
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
export type ApiRequestOptions<T = unknown> = {
|
||||
readonly body?: any;
|
||||
readonly cookies?: Record<string, unknown>;
|
||||
readonly errors?: Record<number | string, string>;
|
||||
readonly formData?: Record<string, unknown> | any[] | Blob | File;
|
||||
readonly headers?: Record<string, unknown>;
|
||||
readonly mediaType?: string;
|
||||
readonly method:
|
||||
| 'DELETE'
|
||||
| 'GET'
|
||||
| 'HEAD'
|
||||
| 'OPTIONS'
|
||||
| 'PATCH'
|
||||
| 'POST'
|
||||
| 'PUT';
|
||||
readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH';
|
||||
readonly url: string;
|
||||
readonly path?: Record<string, unknown>;
|
||||
readonly cookies?: Record<string, unknown>;
|
||||
readonly headers?: Record<string, unknown>;
|
||||
readonly query?: Record<string, unknown>;
|
||||
readonly formData?: Record<string, unknown>;
|
||||
readonly body?: any;
|
||||
readonly mediaType?: string;
|
||||
readonly responseHeader?: string;
|
||||
readonly responseTransformer?: (data: unknown) => Promise<T>;
|
||||
readonly url: string;
|
||||
readonly errors?: Record<number | string, string>;
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -72,7 +72,7 @@ const handlers = [
|
||||
...userGroupsHandlers,
|
||||
...userHandlers,
|
||||
...documentBlueprintHandlers,
|
||||
serverHandlers.serverInformationHandler,
|
||||
...serverHandlers.serverInformationHandlers,
|
||||
];
|
||||
|
||||
switch (import.meta.env.VITE_UMBRACO_INSTALL_STATUS) {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import type { MemberResponseModel, MemberItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
import {
|
||||
type MemberResponseModel,
|
||||
type MemberItemResponseModel,
|
||||
MemberKindModel,
|
||||
} from '@umbraco-cms/backoffice/external/backend-api';
|
||||
|
||||
export type UmbMockMemberModel = MemberResponseModel & MemberItemResponseModel;
|
||||
|
||||
@@ -26,6 +30,7 @@ export const data: Array<UmbMockMemberModel> = [
|
||||
updateDate: '2023-02-06T15:32:24.957009',
|
||||
},
|
||||
],
|
||||
kind: MemberKindModel.DEFAULT,
|
||||
},
|
||||
{
|
||||
email: 'member2@member.com',
|
||||
@@ -50,6 +55,7 @@ export const data: Array<UmbMockMemberModel> = [
|
||||
updateDate: '2023-02-06T15:32:24.957009',
|
||||
},
|
||||
],
|
||||
kind: MemberKindModel.DEFAULT,
|
||||
},
|
||||
{
|
||||
email: 'member3@member.com',
|
||||
@@ -74,5 +80,6 @@ export const data: Array<UmbMockMemberModel> = [
|
||||
updateDate: '2023-02-06T15:31:51.354764',
|
||||
},
|
||||
],
|
||||
kind: MemberKindModel.DEFAULT,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -6,10 +6,11 @@ import { UmbMockContentCollectionManager } from '../utils/content/content-collec
|
||||
import type { UmbMockMemberModel } from './member.data.js';
|
||||
import { data } from './member.data.js';
|
||||
import { UmbId } from '@umbraco-cms/backoffice/id';
|
||||
import type {
|
||||
CreateMemberRequestModel,
|
||||
MemberItemResponseModel,
|
||||
MemberResponseModel,
|
||||
import {
|
||||
MemberKindModel,
|
||||
type CreateMemberRequestModel,
|
||||
type MemberItemResponseModel,
|
||||
type MemberResponseModel,
|
||||
} from '@umbraco-cms/backoffice/external/backend-api';
|
||||
|
||||
class UmbMemberMockDB extends UmbEntityMockDbBase<UmbMockMemberModel> {
|
||||
@@ -39,6 +40,7 @@ const createDetailMockMapper = (request: CreateMemberRequestModel): UmbMockMembe
|
||||
lastLockoutDate: null,
|
||||
lastLoginDate: null,
|
||||
lastPasswordChangeDate: null,
|
||||
kind: MemberKindModel.DEFAULT,
|
||||
memberType: {
|
||||
id: memberType.id,
|
||||
icon: memberType.icon,
|
||||
@@ -66,6 +68,7 @@ const detailResponseMapper = (item: UmbMockMemberModel): MemberResponseModel =>
|
||||
isApproved: item.isApproved,
|
||||
isLockedOut: item.isLockedOut,
|
||||
isTwoFactorEnabled: item.isTwoFactorEnabled,
|
||||
kind: item.kind,
|
||||
lastLockoutDate: item.lastLockoutDate,
|
||||
lastLoginDate: item.lastLoginDate,
|
||||
lastPasswordChangeDate: item.lastPasswordChangeDate,
|
||||
@@ -79,6 +82,7 @@ const detailResponseMapper = (item: UmbMockMemberModel): MemberResponseModel =>
|
||||
const itemResponseMapper = (item: UmbMockMemberModel): MemberItemResponseModel => {
|
||||
return {
|
||||
id: item.id,
|
||||
kind: item.kind,
|
||||
memberType: item.memberType,
|
||||
variants: item.variants,
|
||||
};
|
||||
|
||||
@@ -3,115 +3,120 @@ import type {
|
||||
UserResponseModel,
|
||||
UserTwoFactorProviderModel,
|
||||
} from '@umbraco-cms/backoffice/external/backend-api';
|
||||
import { UserStateModel } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
import { UserKindModel, UserStateModel } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
|
||||
export type UmbMockUserModel = UserResponseModel & UserItemResponseModel;
|
||||
|
||||
export const data: Array<UmbMockUserModel> = [
|
||||
{
|
||||
id: 'bca6c733-a63d-4353-a271-9a8b6bcca8bd',
|
||||
documentStartNodeIds: [],
|
||||
hasDocumentRootAccess: true,
|
||||
mediaStartNodeIds: [],
|
||||
hasMediaRootAccess: true,
|
||||
name: 'Umbraco User',
|
||||
email: 'noreply@umbraco.com',
|
||||
languageIsoCode: 'en-us',
|
||||
state: UserStateModel.ACTIVE,
|
||||
lastLoginDate: '9/10/2022',
|
||||
lastLockoutDate: '11/23/2021',
|
||||
lastPasswordChangeDate: '1/10/2022',
|
||||
updateDate: '2/10/2022',
|
||||
avatarUrls: [],
|
||||
createDate: '3/13/2022',
|
||||
documentStartNodeIds: [],
|
||||
email: 'noreply@umbraco.com',
|
||||
failedLoginAttempts: 946,
|
||||
hasDocumentRootAccess: true,
|
||||
hasMediaRootAccess: true,
|
||||
id: 'bca6c733-a63d-4353-a271-9a8b6bcca8bd',
|
||||
isAdmin: true,
|
||||
kind: UserKindModel.DEFAULT,
|
||||
languageIsoCode: 'en-us',
|
||||
lastLockoutDate: '11/23/2021',
|
||||
lastLoginDate: '9/10/2022',
|
||||
lastPasswordChangeDate: '1/10/2022',
|
||||
mediaStartNodeIds: [],
|
||||
name: 'Umbraco User',
|
||||
state: UserStateModel.ACTIVE,
|
||||
updateDate: '2/10/2022',
|
||||
userGroupIds: [{ id: 'user-group-administrators-id' }, { id: 'user-group-editors-id' }],
|
||||
userName: '',
|
||||
avatarUrls: [],
|
||||
isAdmin: true,
|
||||
},
|
||||
{
|
||||
id: '82e11d3d-b91d-43c9-9071-34d28e62e81d',
|
||||
documentStartNodeIds: [{ id: 'simple-document-id' }],
|
||||
hasDocumentRootAccess: true,
|
||||
mediaStartNodeIds: [{ id: 'f2f81a40-c989-4b6b-84e2-057cecd3adc1' }],
|
||||
hasMediaRootAccess: true,
|
||||
name: 'Amelie Walker',
|
||||
email: 'awalker1@domain.com',
|
||||
languageIsoCode: 'da-dk',
|
||||
state: UserStateModel.INACTIVE,
|
||||
lastLoginDate: '2023-10-12T18:30:32.879Z',
|
||||
lastLockoutDate: null,
|
||||
lastPasswordChangeDate: '2023-10-12T18:30:32.879Z',
|
||||
updateDate: '2023-10-12T18:30:32.879Z',
|
||||
avatarUrls: [],
|
||||
createDate: '2023-10-12T18:30:32.879Z',
|
||||
documentStartNodeIds: [{ id: 'simple-document-id' }],
|
||||
email: 'awalker1@domain.com',
|
||||
failedLoginAttempts: 0,
|
||||
hasDocumentRootAccess: true,
|
||||
hasMediaRootAccess: true,
|
||||
id: '82e11d3d-b91d-43c9-9071-34d28e62e81d',
|
||||
isAdmin: true,
|
||||
kind: UserKindModel.DEFAULT,
|
||||
languageIsoCode: 'da-dk',
|
||||
lastLockoutDate: null,
|
||||
lastLoginDate: '2023-10-12T18:30:32.879Z',
|
||||
lastPasswordChangeDate: '2023-10-12T18:30:32.879Z',
|
||||
mediaStartNodeIds: [{ id: 'f2f81a40-c989-4b6b-84e2-057cecd3adc1' }],
|
||||
name: 'Amelie Walker',
|
||||
state: UserStateModel.INACTIVE,
|
||||
updateDate: '2023-10-12T18:30:32.879Z',
|
||||
userGroupIds: [{ id: 'user-group-administrators-id' }],
|
||||
userName: '',
|
||||
avatarUrls: [],
|
||||
isAdmin: true,
|
||||
},
|
||||
{
|
||||
id: 'aa1d83a9-bc7f-47d2-b288-58d8a31f5017',
|
||||
avatarUrls: [],
|
||||
createDate: '2023-10-12T18:30:32.879Z',
|
||||
documentStartNodeIds: [],
|
||||
mediaStartNodeIds: [],
|
||||
hasDocumentRootAccess: true,
|
||||
hasMediaRootAccess: true,
|
||||
name: 'Oliver Kim',
|
||||
email: 'okim1@domain.com',
|
||||
failedLoginAttempts: 0,
|
||||
hasDocumentRootAccess: true,
|
||||
hasMediaRootAccess: true,
|
||||
id: 'aa1d83a9-bc7f-47d2-b288-58d8a31f5017',
|
||||
isAdmin: false,
|
||||
kind: UserKindModel.DEFAULT,
|
||||
languageIsoCode: 'da-dk',
|
||||
state: UserStateModel.ACTIVE,
|
||||
lastLoginDate: '2023-10-12T18:30:32.879Z',
|
||||
lastLockoutDate: null,
|
||||
lastLoginDate: '2023-10-12T18:30:32.879Z',
|
||||
lastPasswordChangeDate: '2023-10-12T18:30:32.879Z',
|
||||
mediaStartNodeIds: [],
|
||||
name: 'Oliver Kim',
|
||||
state: UserStateModel.ACTIVE,
|
||||
updateDate: '2023-10-12T18:30:32.879Z',
|
||||
createDate: '2023-10-12T18:30:32.879Z',
|
||||
failedLoginAttempts: 0,
|
||||
userGroupIds: [{ id: 'user-group-editors-id' }],
|
||||
userName: '',
|
||||
avatarUrls: [],
|
||||
isAdmin: false,
|
||||
},
|
||||
{
|
||||
id: 'ff2f4a50-d3d4-4bc4-869d-c7948c160e54',
|
||||
avatarUrls: [],
|
||||
createDate: '2023-10-12T18:30:32.879Z',
|
||||
documentStartNodeIds: [],
|
||||
mediaStartNodeIds: [],
|
||||
hasDocumentRootAccess: true,
|
||||
hasMediaRootAccess: true,
|
||||
name: 'Eliana Nieves',
|
||||
email: 'enieves1@domain.com',
|
||||
languageIsoCode: 'en-us',
|
||||
state: UserStateModel.INVITED,
|
||||
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: [{ id: 'user-group-editors-id' }],
|
||||
userName: '',
|
||||
avatarUrls: [],
|
||||
isAdmin: false,
|
||||
},
|
||||
{
|
||||
id: 'c290c6d9-9f12-4838-8567-621b52a178de',
|
||||
documentStartNodeIds: [],
|
||||
mediaStartNodeIds: [],
|
||||
hasDocumentRootAccess: true,
|
||||
hasMediaRootAccess: true,
|
||||
name: 'Jasmine Patel',
|
||||
email: 'jpatel1@domain.com',
|
||||
id: 'ff2f4a50-d3d4-4bc4-869d-c7948c160e54',
|
||||
isAdmin: false,
|
||||
kind: UserKindModel.DEFAULT,
|
||||
languageIsoCode: 'en-us',
|
||||
state: UserStateModel.LOCKED_OUT,
|
||||
lastLockoutDate: null,
|
||||
lastLoginDate: '2023-10-12T18:30:32.879Z',
|
||||
lastLockoutDate: '2023-10-12T18:30:32.879Z',
|
||||
lastPasswordChangeDate: null,
|
||||
mediaStartNodeIds: [],
|
||||
name: 'Eliana Nieves',
|
||||
state: UserStateModel.INVITED,
|
||||
updateDate: '2023-10-12T18:30:32.879Z',
|
||||
userGroupIds: [{ id: 'user-group-editors-id' }],
|
||||
userName: '',
|
||||
},
|
||||
{
|
||||
avatarUrls: [],
|
||||
createDate: '2023-10-12T18:30:32.879Z',
|
||||
documentStartNodeIds: [],
|
||||
email: 'jpatel1@domain.com',
|
||||
failedLoginAttempts: 25,
|
||||
hasDocumentRootAccess: true,
|
||||
hasMediaRootAccess: true,
|
||||
id: 'c290c6d9-9f12-4838-8567-621b52a178de',
|
||||
isAdmin: false,
|
||||
kind: UserKindModel.DEFAULT,
|
||||
languageIsoCode: 'en-us',
|
||||
lastLockoutDate: '2023-10-12T18:30:32.879Z',
|
||||
lastLoginDate: '2023-10-12T18:30:32.879Z',
|
||||
lastPasswordChangeDate: null,
|
||||
mediaStartNodeIds: [],
|
||||
name: 'Jasmine Patel',
|
||||
state: UserStateModel.LOCKED_OUT,
|
||||
updateDate: '2023-10-12T18:30:32.879Z',
|
||||
userGroupIds: [{ id: 'user-group-editors-id' }, { id: 'user-group-sensitive-data-id' }],
|
||||
userName: '',
|
||||
avatarUrls: [],
|
||||
isAdmin: false,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -195,9 +195,10 @@ class UmbUserMockDB extends UmbEntityMockDbBase<UmbMockUserModel> {
|
||||
|
||||
const itemMapper = (item: UmbMockUserModel): UserItemResponseModel => {
|
||||
return {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
avatarUrls: item.avatarUrls,
|
||||
id: item.id,
|
||||
kind: item.kind,
|
||||
name: item.name,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -222,30 +223,32 @@ const createMockMapper = (item: CreateUserRequestModel): UmbMockUserModel => {
|
||||
lastLockoutDate: null,
|
||||
lastPasswordChangeDate: null,
|
||||
isAdmin: item.userGroupIds.map((reference) => reference.id).includes(umbUserGroupMockDb.getAll()[0].id),
|
||||
kind: item.kind,
|
||||
};
|
||||
};
|
||||
|
||||
const detailResponseMapper = (item: UmbMockUserModel): UserResponseModel => {
|
||||
return {
|
||||
email: item.email,
|
||||
userName: item.userName,
|
||||
name: item.name,
|
||||
userGroupIds: item.userGroupIds,
|
||||
id: item.id,
|
||||
languageIsoCode: item.languageIsoCode,
|
||||
avatarUrls: item.avatarUrls,
|
||||
createDate: item.createDate,
|
||||
documentStartNodeIds: item.documentStartNodeIds,
|
||||
mediaStartNodeIds: item.mediaStartNodeIds,
|
||||
email: item.email,
|
||||
failedLoginAttempts: item.failedLoginAttempts,
|
||||
hasDocumentRootAccess: item.hasDocumentRootAccess,
|
||||
hasMediaRootAccess: item.hasMediaRootAccess,
|
||||
avatarUrls: item.avatarUrls,
|
||||
state: item.state,
|
||||
failedLoginAttempts: item.failedLoginAttempts,
|
||||
createDate: item.createDate,
|
||||
updateDate: item.updateDate,
|
||||
lastLoginDate: item.lastLoginDate,
|
||||
lastLockoutDate: item.lastLockoutDate,
|
||||
lastPasswordChangeDate: item.lastPasswordChangeDate,
|
||||
id: item.id,
|
||||
isAdmin: item.isAdmin,
|
||||
kind: item.kind,
|
||||
languageIsoCode: item.languageIsoCode,
|
||||
lastLockoutDate: item.lastLockoutDate,
|
||||
lastLoginDate: item.lastLoginDate,
|
||||
lastPasswordChangeDate: item.lastPasswordChangeDate,
|
||||
mediaStartNodeIds: item.mediaStartNodeIds,
|
||||
name: item.name,
|
||||
state: item.state,
|
||||
updateDate: item.updateDate,
|
||||
userGroupIds: item.userGroupIds,
|
||||
userName: item.userName,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import { handlers as configHandlers } from './handlers/config.handlers.js';
|
||||
|
||||
export const handlers = [
|
||||
serverHandlers.serverRunningHandler,
|
||||
serverHandlers.serverInformationHandler,
|
||||
...serverHandlers.serverInformationHandlers,
|
||||
...manifestsHandlers.manifestEmptyHandlers,
|
||||
...installHandlers,
|
||||
...upgradeHandlers,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
const { rest } = window.MockServiceWorker;
|
||||
import type {
|
||||
ServerStatusResponseModel,
|
||||
ServerInformationResponseModel,
|
||||
import {
|
||||
type ServerStatusResponseModel,
|
||||
type ServerInformationResponseModel,
|
||||
type ServerTroubleshootingResponseModel,
|
||||
RuntimeLevelModel,
|
||||
RuntimeModeModel,
|
||||
} from '@umbraco-cms/backoffice/external/backend-api';
|
||||
import { RuntimeLevelModel, RuntimeModeModel } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
|
||||
|
||||
export const serverRunningHandler = rest.get(umbracoPath('/server/status'), (_req, res, ctx) => {
|
||||
@@ -36,15 +38,30 @@ export const serverMustUpgradeHandler = rest.get(umbracoPath('/server/status'),
|
||||
);
|
||||
});
|
||||
|
||||
export const serverInformationHandler = rest.get(umbracoPath('/server/information'), (_req, res, ctx) => {
|
||||
return res(
|
||||
// Respond with a 200 status code
|
||||
ctx.status(200),
|
||||
ctx.json<ServerInformationResponseModel>({
|
||||
version: '14.0.0-preview004',
|
||||
assemblyVersion: '14.0.0-preview004',
|
||||
baseUtcOffset: '01:00:00',
|
||||
runtimeMode: RuntimeModeModel.BACKOFFICE_DEVELOPMENT,
|
||||
}),
|
||||
);
|
||||
});
|
||||
export const serverInformationHandlers = [
|
||||
rest.get(umbracoPath('/server/information'), (_req, res, ctx) => {
|
||||
return res(
|
||||
// Respond with a 200 status code
|
||||
ctx.status(200),
|
||||
ctx.json<ServerInformationResponseModel>({
|
||||
version: '14.0.0-preview004',
|
||||
assemblyVersion: '14.0.0-preview004',
|
||||
baseUtcOffset: '01:00:00',
|
||||
runtimeMode: RuntimeModeModel.BACKOFFICE_DEVELOPMENT,
|
||||
}),
|
||||
);
|
||||
}),
|
||||
rest.get(umbracoPath('/server/troubleshooting'), (_req, res, ctx) => {
|
||||
return res(
|
||||
// Respond with a 200 status code
|
||||
ctx.status(200),
|
||||
ctx.json<ServerTroubleshootingResponseModel>({
|
||||
items: [
|
||||
{ name: 'Umbraco base url', data: location.origin },
|
||||
{ name: 'Mocked server', data: 'true' },
|
||||
{ name: 'Umbraco version', data: '14.0.0-preview004' },
|
||||
],
|
||||
}),
|
||||
);
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -159,6 +159,7 @@ export class UmbCollectionViewBundleElement extends UmbLitElement {
|
||||
:host {
|
||||
--uui-button-content-align: left;
|
||||
--uui-menu-item-flat-structure: 1;
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.filter-dropdown {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { css, customElement, html, property, state, when, LitElement } from '@um
|
||||
|
||||
/**
|
||||
* A simple styled box for showing code-based error messages or blocks od code.
|
||||
* @slot the full message
|
||||
* @slot - the default slot where the full message resides
|
||||
*/
|
||||
@customElement('umb-code-block')
|
||||
export class UmbCodeBlockElement extends LitElement {
|
||||
@@ -44,7 +44,7 @@ export class UmbCodeBlockElement extends LitElement {
|
||||
${when(
|
||||
this.copy,
|
||||
() => html`
|
||||
<uui-button compat color=${this._copyState === 'idle' ? 'default' : 'positive'} @click=${this.copyCode}>
|
||||
<uui-button color=${this._copyState === 'idle' ? 'default' : 'positive'} @click=${this.copyCode}>
|
||||
${when(
|
||||
this._copyState === 'idle',
|
||||
() => html`<uui-icon name="copy"></uui-icon> <umb-localize key="general_copy">Copy</umb-localize>`,
|
||||
@@ -58,7 +58,7 @@ export class UmbCodeBlockElement extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
static override readonly styles = [
|
||||
UmbTextStyles,
|
||||
css`
|
||||
:host {
|
||||
@@ -86,7 +86,7 @@ export class UmbCodeBlockElement extends LitElement {
|
||||
pre,
|
||||
code {
|
||||
word-wrap: normal;
|
||||
white-space: pre;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
#header {
|
||||
|
||||
@@ -2308,6 +2308,10 @@
|
||||
"name": "icon-unlocked",
|
||||
"file": "lock-open.svg"
|
||||
},
|
||||
{
|
||||
"name": "icon-unplug",
|
||||
"file": "unplug.svg"
|
||||
},
|
||||
{
|
||||
"name": "icon-untitled",
|
||||
"file": "box.svg",
|
||||
|
||||
@@ -1967,6 +1967,10 @@ name: "icon-unlocked",
|
||||
|
||||
path: () => import("./icons/icon-unlocked.js"),
|
||||
},{
|
||||
name: "icon-unplug",
|
||||
|
||||
path: () => import("./icons/icon-unplug.js"),
|
||||
},{
|
||||
name: "icon-untitled",
|
||||
legacy: true,
|
||||
path: () => import("./icons/icon-untitled.js"),
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
export default `<!-- @license lucide-static v0.424.0 - ISC -->
|
||||
<svg
|
||||
class="lucide lucide-unplug"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.75"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m19 5 3-3" />
|
||||
<path d="m2 22 3-3" />
|
||||
<path d="M6.3 20.3a2.4 2.4 0 0 0 3.4 0L12 18l-6-6-2.3 2.3a2.4 2.4 0 0 0 0 3.4Z" />
|
||||
<path d="M7.5 13.5 10 11" />
|
||||
<path d="M10.5 16.5 13 14" />
|
||||
<path d="m12 6 6 6 2.3-2.3a2.4 2.4 0 0 0 0-3.4l-2.6-2.6a2.4 2.4 0 0 0-3.4 0Z" />
|
||||
</svg>
|
||||
`;
|
||||
@@ -3,7 +3,7 @@ import type { UmbDocumentAuditLogModel } from '../../../audit-log/types.js';
|
||||
import { UmbDocumentAuditLogRepository } from '../../../audit-log/index.js';
|
||||
import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from '../../document-workspace.context-token.js';
|
||||
import { TimeOptions, getDocumentHistoryTagStyleAndText } from './utils.js';
|
||||
import { css, html, customElement, state, nothing, repeat, ifDefined } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { css, html, customElement, state, nothing, repeat } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||
import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
|
||||
@@ -120,13 +120,15 @@ export class UmbDocumentWorkspaceViewInfoHistoryElement extends UmbLitElement {
|
||||
(item) => {
|
||||
const { text, style } = getDocumentHistoryTagStyleAndText(item.logType);
|
||||
const user = this.#userMap.get(item.user.unique);
|
||||
const userName = user?.name ?? 'Unknown';
|
||||
const avatarUrl = user && Array.isArray(user.avatarUrls) ? user.avatarUrls[1] : undefined;
|
||||
|
||||
return html`<umb-history-item
|
||||
.name=${userName}
|
||||
.name=${user?.name ?? 'Unknown'}
|
||||
.detail=${this.localize.date(item.timestamp, TimeOptions)}>
|
||||
<uui-avatar slot="avatar" .name="${userName}" img-src=${ifDefined(avatarUrl)}></uui-avatar>
|
||||
<umb-user-avatar
|
||||
slot="avatar"
|
||||
.name=${user?.name}
|
||||
.kind=${user?.kind}
|
||||
.imgUrls=${user?.avatarUrls ?? []}></umb-user-avatar>
|
||||
|
||||
<span class="log-type">
|
||||
<uui-tag look=${style.look} color=${style.color}>
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { UmbMediaAuditLogModel } from '../../../audit-log/types.js';
|
||||
import { UmbMediaAuditLogRepository } from '../../../audit-log/index.js';
|
||||
import { UMB_MEDIA_WORKSPACE_CONTEXT } from '../../media-workspace.context-token.js';
|
||||
import { TimeOptions, getMediaHistoryTagStyleAndText } from './utils.js';
|
||||
import { css, html, customElement, state, nothing, repeat, ifDefined } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { css, html, customElement, state, nothing, repeat } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||
import { UmbPaginationManager } from '@umbraco-cms/backoffice/utils';
|
||||
@@ -102,13 +102,15 @@ export class UmbMediaWorkspaceViewInfoHistoryElement extends UmbLitElement {
|
||||
(item) => {
|
||||
const { text, style } = getMediaHistoryTagStyleAndText(item.logType);
|
||||
const user = this.#userMap.get(item.user.unique);
|
||||
const userName = user?.name ?? 'Unknown';
|
||||
const avatarUrl = user && Array.isArray(user.avatarUrls) ? user.avatarUrls[1] : undefined;
|
||||
|
||||
return html`<umb-history-item
|
||||
.name=${user?.name ?? 'Unknown'}
|
||||
.detail=${this.localize.date(item.timestamp, TimeOptions)}>
|
||||
<uui-avatar slot="avatar" .name="${userName}" img-src=${ifDefined(avatarUrl)}></uui-avatar>
|
||||
<umb-user-avatar
|
||||
slot="avatar"
|
||||
.name=${user?.name}
|
||||
.kind=${user?.kind}
|
||||
.imgUrls=${user?.avatarUrls ?? []}></umb-user-avatar>
|
||||
|
||||
<span class="log-type">
|
||||
<uui-tag look=${style.look} color=${style.color}>
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import { UmbSysinfoRepository } from '../repository/sysinfo.repository.js';
|
||||
import { css, customElement, html, state, when } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
|
||||
import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification';
|
||||
import type { UUIButtonState } from '@umbraco-cms/backoffice/external/uui';
|
||||
|
||||
type ServerKeyValue = {
|
||||
name: string;
|
||||
data: string;
|
||||
};
|
||||
|
||||
@customElement('umb-sysinfo')
|
||||
export class UmbSysinfoElement extends UmbModalBaseElement {
|
||||
@state()
|
||||
private _systemInformation = '';
|
||||
|
||||
@state()
|
||||
private _loading = false;
|
||||
|
||||
@state()
|
||||
private _buttonState?: UUIButtonState;
|
||||
|
||||
#serverKeyValues: Array<ServerKeyValue> = [];
|
||||
#sysinfoRepository = new UmbSysinfoRepository(this);
|
||||
#notificationContext?: typeof UMB_NOTIFICATION_CONTEXT.TYPE;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.consumeContext(UMB_NOTIFICATION_CONTEXT, (context) => {
|
||||
this.#notificationContext = context;
|
||||
});
|
||||
|
||||
this.#populate();
|
||||
}
|
||||
|
||||
async #populate() {
|
||||
this._loading = true;
|
||||
this.#serverKeyValues = [];
|
||||
|
||||
const [serverTroubleshooting, serverInformation] = await Promise.all([
|
||||
this.#sysinfoRepository.requestTroubleShooting(),
|
||||
this.#sysinfoRepository.requestServerInformation(),
|
||||
]);
|
||||
|
||||
if (serverTroubleshooting) {
|
||||
this.#serverKeyValues = serverTroubleshooting.items;
|
||||
}
|
||||
|
||||
if (serverInformation) {
|
||||
this.#serverKeyValues.push({ name: 'Umbraco build version', data: serverInformation.version });
|
||||
this.#serverKeyValues.push({ name: 'Server time offset', data: serverInformation.baseUtcOffset });
|
||||
this.#serverKeyValues.push({ name: 'Runtime mode', data: serverInformation.runtimeMode });
|
||||
}
|
||||
|
||||
// Browser information
|
||||
this.#serverKeyValues.push({ name: 'Browser (user agent)', data: navigator.userAgent });
|
||||
this.#serverKeyValues.push({ name: 'Browser language', data: navigator.language });
|
||||
this.#serverKeyValues.push({ name: 'Browser location', data: location.href });
|
||||
|
||||
this._systemInformation = this.#renderServerKeyValues();
|
||||
this._loading = false;
|
||||
}
|
||||
|
||||
#renderServerKeyValues() {
|
||||
return this.#serverKeyValues
|
||||
.map((serverKeyValue) => {
|
||||
return `${serverKeyValue.name}: ${serverKeyValue.data}`;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<uui-dialog>
|
||||
<uui-dialog-layout headline="System information">
|
||||
${when(
|
||||
this._loading,
|
||||
() => html`<uui-loader-bar></uui-loader-bar>`,
|
||||
() => html` <umb-code-block id="codeblock"> ${this._systemInformation} </umb-code-block> `,
|
||||
)}
|
||||
|
||||
<uui-button
|
||||
@click=${this._submitModal}
|
||||
slot="actions"
|
||||
look="secondary"
|
||||
label=${this.localize.term('general_close')}></uui-button>
|
||||
|
||||
<uui-button
|
||||
@click=${this.#copyToClipboard}
|
||||
.state=${this._buttonState}
|
||||
slot="actions"
|
||||
look="primary"
|
||||
color="positive"
|
||||
label=${this.localize.term('clipboard_labelForCopyToClipboard')}></uui-button>
|
||||
</uui-dialog-layout>
|
||||
</uui-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
async #copyToClipboard() {
|
||||
try {
|
||||
this._buttonState = 'waiting';
|
||||
const text = `Umbraco system information
|
||||
--------------------------------
|
||||
${this._systemInformation}`;
|
||||
const textAsCode = `\`\`\`\n${text}\n\`\`\`\n`;
|
||||
await navigator.clipboard.writeText(textAsCode);
|
||||
|
||||
setTimeout(() => {
|
||||
this.#notificationContext?.peek('positive', {
|
||||
data: {
|
||||
headline: 'System information',
|
||||
message: this.localize.term('speechBubbles_copySuccessMessage'),
|
||||
},
|
||||
});
|
||||
this._buttonState = 'success';
|
||||
}, 250);
|
||||
} catch {
|
||||
this._buttonState = 'failed';
|
||||
this.#notificationContext?.peek('danger', {
|
||||
data: {
|
||||
headline: 'System information',
|
||||
message: this.localize.term('speechBubbles_cannotCopyInformation'),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static override readonly styles = [
|
||||
UmbTextStyles,
|
||||
css`
|
||||
#code-block {
|
||||
max-height: 300px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
export default UmbSysinfoElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-sysinfo': UmbSysinfoElement;
|
||||
}
|
||||
}
|
||||
3
src/Umbraco.Web.UI.Client/src/packages/sysinfo/index.ts
Normal file
3
src/Umbraco.Web.UI.Client/src/packages/sysinfo/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './components/sysinfo.element.js';
|
||||
export * from './modals/index.js';
|
||||
export * from './repository/index.js';
|
||||
12
src/Umbraco.Web.UI.Client/src/packages/sysinfo/manifests.ts
Normal file
12
src/Umbraco.Web.UI.Client/src/packages/sysinfo/manifests.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { ManifestModal, ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
|
||||
|
||||
export const UMB_SYSINFO_MODAL_ALIAS = 'Umb.Modal.Sysinfo';
|
||||
|
||||
const modalManifest: ManifestModal = {
|
||||
type: 'modal',
|
||||
alias: UMB_SYSINFO_MODAL_ALIAS,
|
||||
name: 'Sysinfo Modal',
|
||||
js: () => import('./components/sysinfo.element.js'),
|
||||
};
|
||||
|
||||
export const manifests: Array<ManifestTypes> = [modalManifest];
|
||||
@@ -0,0 +1 @@
|
||||
export * from './sysinfo-modal.token.js';
|
||||
@@ -0,0 +1,9 @@
|
||||
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
|
||||
import { UMB_SYSINFO_MODAL_ALIAS } from '../manifests.js';
|
||||
|
||||
export const UMB_SYSINFO_MODAL = new UmbModalToken(UMB_SYSINFO_MODAL_ALIAS, {
|
||||
modal: {
|
||||
type: 'dialog',
|
||||
size: 'medium',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "@umbraco-backoffice/sysinfo",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "vite build"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './sysinfo.repository.js';
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository';
|
||||
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
|
||||
import { ServerService } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
|
||||
export class UmbSysinfoRepository extends UmbRepositoryBase {
|
||||
constructor(host: UmbControllerHost) {
|
||||
super(host, 'Umb.Repository.Sysinfo');
|
||||
}
|
||||
|
||||
async requestTroubleShooting() {
|
||||
const { data } = await tryExecuteAndNotify(this, ServerService.getServerTroubleshooting());
|
||||
return data;
|
||||
}
|
||||
|
||||
async requestServerInformation() {
|
||||
const { data } = await tryExecuteAndNotify(this, ServerService.getServerInformation());
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export const name = 'Umbraco.Core.Sysinfo';
|
||||
export const version = '0.0.1';
|
||||
export const extensions = [
|
||||
{
|
||||
name: 'Sysinfo Bundle',
|
||||
alias: 'Umb.Bundle.Sysinfo',
|
||||
type: 'bundle',
|
||||
js: () => import('./manifests.js'),
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,12 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { rmSync } from 'fs';
|
||||
import { getDefaultConfig } from '../../vite-config-base';
|
||||
|
||||
const dist = '../../../dist-cms/packages/sysinfo';
|
||||
|
||||
// delete the unbundled dist folder
|
||||
rmSync(dist, { recursive: true, force: true });
|
||||
|
||||
export default defineConfig({
|
||||
...getDefaultConfig({ dist }),
|
||||
});
|
||||
@@ -14,6 +14,11 @@ export const manifests: Array<ManifestTypes | UmbBackofficeManifestKind> = [
|
||||
icon: 'icon-key',
|
||||
label: '#user_changePassword',
|
||||
},
|
||||
conditions: [
|
||||
{
|
||||
alias: 'Umb.Condition.User.AllowChangePassword',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'modal',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { UMB_CURRENT_USER_MODAL } from './modals/current-user/current-user-modal.token.js';
|
||||
import type { CSSResultGroup } from '@umbraco-cms/backoffice/external/lit';
|
||||
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_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
|
||||
import { UMB_CURRENT_USER_CONTEXT, type UmbCurrentUserModel } from '@umbraco-cms/backoffice/current-user';
|
||||
import { UmbHeaderAppButtonElement } from '@umbraco-cms/backoffice/components';
|
||||
@@ -10,9 +10,6 @@ export class UmbCurrentUserHeaderAppElement extends UmbHeaderAppButtonElement {
|
||||
@state()
|
||||
private _currentUser?: UmbCurrentUserModel;
|
||||
|
||||
@state()
|
||||
private _userAvatarUrls: Array<{ url: string; descriptor: string }> = [];
|
||||
|
||||
#currentUserContext?: typeof UMB_CURRENT_USER_CONTEXT.TYPE;
|
||||
|
||||
constructor() {
|
||||
@@ -31,8 +28,6 @@ export class UmbCurrentUserHeaderAppElement extends UmbHeaderAppButtonElement {
|
||||
this.#currentUserContext.currentUser,
|
||||
(currentUser) => {
|
||||
this._currentUser = currentUser;
|
||||
if (!currentUser) return;
|
||||
this.#setUserAvatarUrls(currentUser);
|
||||
},
|
||||
'umbCurrentUserObserver',
|
||||
);
|
||||
@@ -43,41 +38,6 @@ export class UmbCurrentUserHeaderAppElement extends UmbHeaderAppButtonElement {
|
||||
modalManager.open(this, UMB_CURRENT_USER_MODAL);
|
||||
}
|
||||
|
||||
#setUserAvatarUrls = async (user: UmbCurrentUserModel | undefined) => {
|
||||
if (!user || !user.avatarUrls || user.avatarUrls.length === 0) {
|
||||
this._userAvatarUrls = [];
|
||||
return;
|
||||
}
|
||||
|
||||
this._userAvatarUrls = [
|
||||
{
|
||||
descriptor: '1x',
|
||||
url: user.avatarUrls?.[0],
|
||||
},
|
||||
{
|
||||
descriptor: '2x',
|
||||
url: user.avatarUrls?.[1],
|
||||
},
|
||||
{
|
||||
descriptor: '3x',
|
||||
url: user.avatarUrls?.[2],
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
#getAvatarSrcset() {
|
||||
let string = '';
|
||||
|
||||
this._userAvatarUrls?.forEach((url) => {
|
||||
string += `${url.url} ${url.descriptor},`;
|
||||
});
|
||||
return string;
|
||||
}
|
||||
|
||||
#hasAvatar() {
|
||||
return this._userAvatarUrls.length > 0;
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<uui-button
|
||||
@@ -85,11 +45,10 @@ export class UmbCurrentUserHeaderAppElement extends UmbHeaderAppButtonElement {
|
||||
look="primary"
|
||||
label="${this.localize.term('visuallyHiddenTexts_openCloseBackofficeProfileOptions')}"
|
||||
compact>
|
||||
<uui-avatar
|
||||
<umb-user-avatar
|
||||
id="Avatar"
|
||||
.name=${this._currentUser?.name || 'Unknown'}
|
||||
img-src=${ifDefined(this.#hasAvatar() ? this._userAvatarUrls[0].url : undefined)}
|
||||
img-srcset=${ifDefined(this.#hasAvatar() ? this.#getAvatarSrcset() : undefined)}></uui-avatar>
|
||||
.name=${this._currentUser?.name}
|
||||
.imgUrls=${this._currentUser?.avatarUrls || []}></umb-user-avatar>
|
||||
</uui-button>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { UmbCurrentUserRepository } from './repository/index.js';
|
||||
import { UMB_CURRENT_USER_CONTEXT } from './current-user.context.token.js';
|
||||
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs';
|
||||
import { filter, firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs';
|
||||
import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth';
|
||||
import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
|
||||
import { umbLocalizationRegistry } from '@umbraco-cms/backoffice/localization';
|
||||
@@ -14,7 +14,7 @@ import { ensurePathEndsWithSlash } from '@umbraco-cms/backoffice/utils';
|
||||
|
||||
export class UmbCurrentUserContext extends UmbContextBase<UmbCurrentUserContext> {
|
||||
#currentUser = new UmbObjectState<UmbCurrentUserModel | undefined>(undefined);
|
||||
readonly currentUser = this.#currentUser.asObservable();
|
||||
readonly currentUser = this.#currentUser.asObservable().pipe(filter((user) => !!user));
|
||||
readonly allowedSections = this.#currentUser.asObservablePart((user) => user?.allowedSections);
|
||||
readonly avatarUrls = this.#currentUser.asObservablePart((user) => user?.avatarUrls);
|
||||
readonly documentStartNodeUniques = this.#currentUser.asObservablePart((user) => user?.documentStartNodeUniques);
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { UMB_SECTION_PATH_PATTERN } from '@umbraco-cms/backoffice/section';
|
||||
|
||||
export const UMB_USER_SECTION_PATHNAME = 'user-management';
|
||||
|
||||
export const UMB_USER_SECTION_PATH = UMB_SECTION_PATH_PATTERN.generateAbsolute({
|
||||
sectionName: UMB_USER_SECTION_PATHNAME,
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import { UMB_USER_GROUP_WORKSPACE_PATH } from '../../paths.js';
|
||||
import { css, html, LitElement, customElement, property } from '@umbraco-cms/backoffice/external/lit';
|
||||
import type { UmbTableItem } from '@umbraco-cms/backoffice/components';
|
||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||
@@ -11,7 +12,7 @@ export class UmbUserGroupTableNameColumnLayoutElement extends LitElement {
|
||||
value!: any;
|
||||
|
||||
override render() {
|
||||
const href = `section/user-management/view/user-groups/user-group/edit/${this.item.id}`;
|
||||
const href = UMB_USER_GROUP_WORKSPACE_PATH + '/edit/' + this.item.id;
|
||||
return html`<a href=${href}>${this.value.name}</a>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { UMB_USER_SECTION_PATHNAME } from '../user-section/paths.js';
|
||||
import { UMB_USER_GROUP_ENTITY_TYPE } from './entity.js';
|
||||
import { UMB_WORKSPACE_PATH_PATTERN } from '@umbraco-cms/backoffice/workspace';
|
||||
|
||||
export const UMB_USER_GROUP_WORKSPACE_PATH = UMB_WORKSPACE_PATH_PATTERN.generateAbsolute({
|
||||
sectionName: UMB_USER_SECTION_PATHNAME,
|
||||
entityType: UMB_USER_GROUP_ENTITY_TYPE,
|
||||
});
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { UmbUserGroupDetailModel } from '../index.js';
|
||||
import { UMB_USER_GROUP_ENTITY_TYPE } from '../index.js';
|
||||
import { UMB_USER_GROUP_WORKSPACE_CONTEXT } from './user-group-workspace.context-token.js';
|
||||
import type { UUIBooleanInputEvent } from '@umbraco-cms/backoffice/external/uui';
|
||||
import { css, html, nothing, customElement, state, ifDefined } from '@umbraco-cms/backoffice/external/lit';
|
||||
@@ -174,11 +173,7 @@ export class UmbUserGroupWorkspaceEditorElement extends UmbLitElement {
|
||||
alias="Umb.Workspace.UserGroup"
|
||||
class="uui-text"
|
||||
back-path="/section/user-management/view/user-groups">
|
||||
${this.#renderHeader()}
|
||||
<div id="main">
|
||||
<div id="left-column">${this.#renderLeftColumn()}</div>
|
||||
<div id="right-column">${this.#renderRightColumn()}</div>
|
||||
</div>
|
||||
${this.#renderHeader()} ${this.#renderMain()}
|
||||
</umb-workspace-editor>
|
||||
`;
|
||||
}
|
||||
@@ -225,40 +220,46 @@ export class UmbUserGroupWorkspaceEditorElement extends UmbLitElement {
|
||||
${umbFocus()}>
|
||||
</umb-input-with-alias>
|
||||
</div>
|
||||
|
||||
<umb-workspace-entity-action-menu slot="action-menu"></umb-workspace-entity-action-menu>
|
||||
`;
|
||||
}
|
||||
|
||||
#renderLeftColumn() {
|
||||
#renderMain() {
|
||||
if (!this._unique) return nothing;
|
||||
|
||||
return html`
|
||||
<uui-box>
|
||||
<div slot="headline"><umb-localize key="user_assignAccess"></umb-localize></div>
|
||||
<div id="main">
|
||||
<umb-stack>
|
||||
<uui-box>
|
||||
<div slot="headline"><umb-localize key="user_assignAccess"></umb-localize></div>
|
||||
|
||||
<umb-property-layout
|
||||
label=${this.localize.term('main_sections')}
|
||||
description=${this.localize.term('user_sectionsHelp')}>
|
||||
<umb-input-section
|
||||
slot="editor"
|
||||
.selection=${this._sections}
|
||||
@change=${this.#onSectionsChange}></umb-input-section>
|
||||
</umb-property-layout>
|
||||
<umb-property-layout
|
||||
label=${this.localize.term('main_sections')}
|
||||
description=${this.localize.term('user_sectionsHelp')}>
|
||||
<umb-input-section
|
||||
slot="editor"
|
||||
.selection=${this._sections}
|
||||
@change=${this.#onSectionsChange}></umb-input-section>
|
||||
</umb-property-layout>
|
||||
|
||||
${this.#renderLanguageAccess()} ${this.#renderDocumentAccess()} ${this.#renderMediaAccess()}
|
||||
</uui-box>
|
||||
${this.#renderLanguageAccess()} ${this.#renderDocumentAccess()} ${this.#renderMediaAccess()}
|
||||
</uui-box>
|
||||
|
||||
<uui-box>
|
||||
<div slot="headline"><umb-localize key="user_permissionsDefault"></umb-localize></div>
|
||||
<uui-box>
|
||||
<div slot="headline"><umb-localize key="user_permissionsDefault"></umb-localize></div>
|
||||
|
||||
<umb-property-layout label="Entity permissions" description="Assign permissions for an entity type">
|
||||
<umb-user-group-entity-user-permission-list slot="editor"></umb-user-group-entity-user-permission-list>
|
||||
</umb-property-layout>
|
||||
</uui-box>
|
||||
<umb-property-layout label="Entity permissions" description="Assign permissions for an entity type">
|
||||
<umb-user-group-entity-user-permission-list slot="editor"></umb-user-group-entity-user-permission-list>
|
||||
</umb-property-layout>
|
||||
</uui-box>
|
||||
|
||||
<uui-box>
|
||||
<div slot="headline"><umb-localize key="user_permissionsGranular"></umb-localize></div>
|
||||
<umb-user-group-granular-permission-list></umb-user-group-granular-permission-list>
|
||||
</uui-box>
|
||||
<uui-box>
|
||||
<div slot="headline"><umb-localize key="user_permissionsGranular"></umb-localize></div>
|
||||
<umb-user-group-granular-permission-list></umb-user-group-granular-permission-list>
|
||||
</uui-box>
|
||||
</umb-stack>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -338,15 +339,6 @@ export class UmbUserGroupWorkspaceEditorElement extends UmbLitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
#renderRightColumn() {
|
||||
return html`
|
||||
<uui-box headline="Actions">
|
||||
<umb-entity-action-list .entityType=${UMB_USER_GROUP_ENTITY_TYPE} .unique=${this._unique}>
|
||||
</umb-entity-action-list>
|
||||
</uui-box>
|
||||
`;
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
UmbTextStyles,
|
||||
css`
|
||||
@@ -375,25 +367,9 @@ export class UmbUserGroupWorkspaceEditorElement extends UmbLitElement {
|
||||
}
|
||||
|
||||
#main {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 350px;
|
||||
gap: var(--uui-size-layout-1);
|
||||
padding: var(--uui-size-layout-1);
|
||||
}
|
||||
|
||||
#left-column,
|
||||
#right-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--uui-size-space-4);
|
||||
}
|
||||
|
||||
#right-column > uui-box > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--uui-size-space-2);
|
||||
}
|
||||
|
||||
uui-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { manifests as modalManifests } from './modal/manifests.js';
|
||||
|
||||
import type { ManifestTypes, UmbBackofficeManifestKind } from '@umbraco-cms/backoffice/extension-registry';
|
||||
|
||||
export const manifests: Array<ManifestTypes | UmbBackofficeManifestKind> = [...modalManifests];
|
||||
@@ -0,0 +1 @@
|
||||
export const UMB_CREATE_USER_CLIENT_CREDENTIAL_MODAL_ALIAS = 'Umb.Modal.User.ClientCredential.Create';
|
||||
@@ -0,0 +1,117 @@
|
||||
import type { UmbCreateUserClientCredentialRequestArgs } from '../../repository/index.js';
|
||||
import { UmbUserClientCredentialRepository } from '../../repository/index.js';
|
||||
import type {
|
||||
UmbCreateUserClientCredentialModalData,
|
||||
UmbCreateUserClientCredentialModalValue,
|
||||
} from './create-user-client-credential-modal.token.js';
|
||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||
import { css, html, customElement, query } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
|
||||
|
||||
const elementName = 'umb-create-user-client-credential-modal';
|
||||
@customElement(elementName)
|
||||
export class UmbCreateUserModalElement extends UmbModalBaseElement<
|
||||
UmbCreateUserClientCredentialModalData,
|
||||
UmbCreateUserClientCredentialModalValue
|
||||
> {
|
||||
@query('#CreateUserClientCredentialForm')
|
||||
_form?: HTMLFormElement;
|
||||
|
||||
#userClientCredentialRepository = new UmbUserClientCredentialRepository(this);
|
||||
|
||||
#uniquePrefix = 'umbraco-back-office-';
|
||||
|
||||
async #onSubmit(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.data?.user?.unique === undefined) throw new Error('User unique is required');
|
||||
|
||||
const form = e.target as HTMLFormElement;
|
||||
if (!form) return;
|
||||
|
||||
const isValid = form.checkValidity();
|
||||
if (!isValid) return;
|
||||
|
||||
const formData = new FormData(form);
|
||||
|
||||
const unique = formData.get('unique') as string;
|
||||
const secret = formData.get('secret') as string;
|
||||
|
||||
const payload: UmbCreateUserClientCredentialRequestArgs = {
|
||||
user: { unique: this.data.user.unique },
|
||||
client: { unique, secret },
|
||||
};
|
||||
|
||||
// TODO: figure out when to use email or username
|
||||
const { data } = await this.#userClientCredentialRepository.requestCreate(payload);
|
||||
|
||||
if (data) {
|
||||
this.updateValue({ client: { unique: data.unique, secret } });
|
||||
this._submitModal();
|
||||
}
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`<uui-dialog-layout headline="Create client credential">
|
||||
${this.#renderForm()}
|
||||
<uui-button @click=${this._rejectModal} slot="actions" label="Cancel" look="secondary"></uui-button>
|
||||
<uui-button
|
||||
form="CreateUserClientCredentialForm"
|
||||
slot="actions"
|
||||
type="submit"
|
||||
label="Create"
|
||||
look="primary"
|
||||
color="positive"></uui-button>
|
||||
</uui-dialog-layout>`;
|
||||
}
|
||||
|
||||
#renderForm() {
|
||||
return html` <uui-form>
|
||||
<form id="CreateUserClientCredentialForm" name="form" @submit="${this.#onSubmit}">
|
||||
<uui-form-layout-item>
|
||||
<uui-label id="uniqueLabel" slot="label" for="unique" required>Id</uui-label>
|
||||
<uui-input id="unique" label="unique" type="text" name="unique" required>
|
||||
<div class="prepend" slot="prepend">${this.#uniquePrefix}</div>
|
||||
</uui-input>
|
||||
</uui-form-layout-item>
|
||||
|
||||
<uui-form-layout-item>
|
||||
<div slot="description">The secret cannot be retrieved again.</div>
|
||||
<uui-label id="secretLabel" slot="label" for="secret" required>Secret</uui-label>
|
||||
<uui-input-password id="secret" label="secret" name="secret" required></uui-input-password>
|
||||
</uui-form-layout-item>
|
||||
</form>
|
||||
</uui-form>`;
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
UmbTextStyles,
|
||||
css`
|
||||
uui-input,
|
||||
uui-input-password {
|
||||
width: 580px;
|
||||
}
|
||||
|
||||
.prepend {
|
||||
user-select: none;
|
||||
height: 100%;
|
||||
padding: 0 var(--uui-size-3);
|
||||
border-right: 1px solid var(--uui-input-border-color, var(--uui-color-border));
|
||||
background: #f3f3f3;
|
||||
color: grey;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
export { UmbCreateUserModalElement as element };
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
[elementName]: UmbCreateUserModalElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { UMB_CREATE_USER_CLIENT_CREDENTIAL_MODAL_ALIAS } from './constants.js';
|
||||
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
|
||||
|
||||
export interface UmbCreateUserClientCredentialModalData {
|
||||
user: {
|
||||
unique: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface UmbCreateUserClientCredentialModalValue {
|
||||
client: {
|
||||
unique: string;
|
||||
secret: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const UMB_CREATE_USER_CLIENT_CREDENTIAL_MODAL = new UmbModalToken<
|
||||
UmbCreateUserClientCredentialModalData,
|
||||
UmbCreateUserClientCredentialModalValue
|
||||
>(UMB_CREATE_USER_CLIENT_CREDENTIAL_MODAL_ALIAS, {
|
||||
modal: {
|
||||
type: 'dialog',
|
||||
size: 'small',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { UMB_CREATE_USER_CLIENT_CREDENTIAL_MODAL_ALIAS } from './constants.js';
|
||||
import type { ManifestTypes, UmbBackofficeManifestKind } from '@umbraco-cms/backoffice/extension-registry';
|
||||
|
||||
export const manifests: Array<ManifestTypes | UmbBackofficeManifestKind> = [
|
||||
{
|
||||
type: 'modal',
|
||||
alias: UMB_CREATE_USER_CLIENT_CREDENTIAL_MODAL_ALIAS,
|
||||
name: 'Create User Client Credential Modal',
|
||||
js: () => import('./create-user-client-credential-modal.element.js'),
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1 @@
|
||||
export * from './repository/index.js';
|
||||
@@ -0,0 +1,5 @@
|
||||
import { manifests as createManifests } from './create/manifests.js';
|
||||
import { manifests as repositoryManifests } from './repository/manifests.js';
|
||||
import type { ManifestTypes, UmbBackofficeManifestKind } from '@umbraco-cms/backoffice/extension-registry';
|
||||
|
||||
export const manifests: Array<ManifestTypes | UmbBackofficeManifestKind> = [...createManifests, ...repositoryManifests];
|
||||
@@ -0,0 +1 @@
|
||||
export const UMB_USER_CLIENT_CREDENTIAL_REPOSITORY_ALIAS = 'Umb.Repository.User.ClientCredential';
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './types.js';
|
||||
export * from './user-client-credential.server.data-source.js';
|
||||
@@ -0,0 +1,13 @@
|
||||
import type {
|
||||
UmbCreateUserClientCredentialRequestArgs,
|
||||
UmbDeleteUserClientCredentialRequestArgs,
|
||||
UmbUserClientCredentialModel,
|
||||
UmbUserClientCredentialRequestArgs,
|
||||
} from '../types.js';
|
||||
import type { UmbDataSourceErrorResponse, UmbDataSourceResponse } from '@umbraco-cms/backoffice/repository';
|
||||
|
||||
export interface UmbUserClientCredentialDataSource {
|
||||
create(args: UmbCreateUserClientCredentialRequestArgs): Promise<UmbDataSourceResponse<UmbUserClientCredentialModel>>;
|
||||
read(args: UmbUserClientCredentialRequestArgs): Promise<UmbDataSourceResponse<Array<UmbUserClientCredentialModel>>>;
|
||||
delete: (args: UmbDeleteUserClientCredentialRequestArgs) => Promise<UmbDataSourceErrorResponse>;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import type {
|
||||
UmbCreateUserClientCredentialRequestArgs,
|
||||
UmbDeleteUserClientCredentialRequestArgs,
|
||||
UmbUserClientCredentialRequestArgs,
|
||||
} from '../types.js';
|
||||
import type { UmbUserClientCredentialDataSource } from './types.js';
|
||||
import { UserService } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
|
||||
|
||||
/**
|
||||
* Server data source for user client credentials
|
||||
* @export
|
||||
* @class UmbUserClientCredentialServerDataSource
|
||||
* @implements {UmbUserClientCredentialDataSource}
|
||||
*/
|
||||
export class UmbUserClientCredentialServerDataSource implements UmbUserClientCredentialDataSource {
|
||||
#host: UmbControllerHost;
|
||||
|
||||
constructor(host: UmbControllerHost) {
|
||||
this.#host = host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new client credentials for a user
|
||||
* @param {UmbCreateUserClientCredentialRequestArgs} args - The user and client to create the credentials for
|
||||
* @returns {*}
|
||||
* @memberof UmbUserClientCredentialServerDataSource
|
||||
*/
|
||||
async create(args: UmbCreateUserClientCredentialRequestArgs) {
|
||||
const { error } = await tryExecuteAndNotify(
|
||||
this.#host,
|
||||
UserService.postUserByIdClientCredentials({
|
||||
id: args.user.unique,
|
||||
requestBody: {
|
||||
clientId: args.client.unique,
|
||||
clientSecret: args.client.secret,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
if (!error) {
|
||||
return { data: { unique: args.client.unique } };
|
||||
}
|
||||
|
||||
return { error };
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the client credentials for a user
|
||||
* @param {UmbUserClientCredentialRequestArgs} args - The user to read the credentials for
|
||||
* @returns {*}
|
||||
* @memberof UmbUserClientCredentialServerDataSource
|
||||
*/
|
||||
async read(args: UmbUserClientCredentialRequestArgs) {
|
||||
const { data, error } = await tryExecuteAndNotify(
|
||||
this.#host,
|
||||
UserService.getUserByIdClientCredentials({
|
||||
id: args.user.unique,
|
||||
}),
|
||||
);
|
||||
|
||||
if (data) {
|
||||
const credentials = data.map((clientId) => ({
|
||||
unique: clientId,
|
||||
}));
|
||||
|
||||
return { data: credentials };
|
||||
}
|
||||
|
||||
return { error };
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the client credentials for a user
|
||||
* @param {UmbDeleteUserClientCredentialRequestArgs} args - The user and client unique to delete the credentials for
|
||||
* @returns {*}
|
||||
* @memberof UmbUserClientCredentialServerDataSource
|
||||
*/
|
||||
delete(args: UmbDeleteUserClientCredentialRequestArgs) {
|
||||
return tryExecuteAndNotify(
|
||||
this.#host,
|
||||
UserService.deleteUserByIdClientCredentialsByClientId({
|
||||
id: args.user.unique,
|
||||
clientId: args.client.unique,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './user-client-credential.repository.js';
|
||||
export * from './constants.js';
|
||||
export * from './types.js';
|
||||
@@ -0,0 +1,11 @@
|
||||
import { UMB_USER_CLIENT_CREDENTIAL_REPOSITORY_ALIAS } from './constants.js';
|
||||
import type { ManifestTypes, UmbBackofficeManifestKind } from '@umbraco-cms/backoffice/extension-registry';
|
||||
|
||||
export const manifests: Array<ManifestTypes | UmbBackofficeManifestKind> = [
|
||||
{
|
||||
type: 'repository',
|
||||
alias: UMB_USER_CLIENT_CREDENTIAL_REPOSITORY_ALIAS,
|
||||
name: 'User Client Credentials Repository',
|
||||
api: () => import('./user-client-credential.repository.js'),
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,17 @@
|
||||
export interface UmbCreateUserClientCredentialRequestArgs {
|
||||
user: { unique: string };
|
||||
client: { unique: string; secret: string };
|
||||
}
|
||||
|
||||
export interface UmbUserClientCredentialRequestArgs {
|
||||
user: { unique: string };
|
||||
}
|
||||
|
||||
export interface UmbDeleteUserClientCredentialRequestArgs {
|
||||
user: { unique: string };
|
||||
client: { unique: string };
|
||||
}
|
||||
|
||||
export interface UmbUserClientCredentialModel {
|
||||
unique: string;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { UmbUserClientCredentialDataSource } from './data-source/index.js';
|
||||
import { UmbUserClientCredentialServerDataSource } from './data-source/user-client-credential.server.data-source.js';
|
||||
import type {
|
||||
UmbCreateUserClientCredentialRequestArgs,
|
||||
UmbDeleteUserClientCredentialRequestArgs,
|
||||
UmbUserClientCredentialRequestArgs,
|
||||
} from './types.js';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository';
|
||||
|
||||
/**
|
||||
* UmbUserClientCredentialRepository
|
||||
* @export
|
||||
* @class UmbUserClientCredentialRepository
|
||||
* @extends {UmbRepositoryBase}
|
||||
*/
|
||||
export class UmbUserClientCredentialRepository extends UmbRepositoryBase {
|
||||
#source: UmbUserClientCredentialDataSource;
|
||||
|
||||
/**
|
||||
* Creates an instance of UmbUserClientCredentialRepository.
|
||||
* @param {UmbControllerHost} host - The controller host
|
||||
* @memberof UmbUserClientCredentialRepository
|
||||
*/
|
||||
constructor(host: UmbControllerHost) {
|
||||
super(host);
|
||||
this.#source = new UmbUserClientCredentialServerDataSource(host);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new client credentials for a user
|
||||
* @param {UmbCreateUserClientCredentialRequestArgs} args - The user and client to create the credentials for
|
||||
* @returns {*}
|
||||
* @memberof UmbUserClientCredentialRepository
|
||||
*/
|
||||
async requestCreate(args: UmbCreateUserClientCredentialRequestArgs) {
|
||||
return this.#source.create(args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the client credentials for a user
|
||||
* @param {UmbUserClientCredentialRequestArgs} args - The user to read the credentials for
|
||||
* @returns {*}
|
||||
* @memberof UmbUserClientCredentialRepository
|
||||
*/
|
||||
async requestClientCredentials(args: UmbUserClientCredentialRequestArgs) {
|
||||
return this.#source.read(args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the client credentials for a user
|
||||
* @param {UmbDeleteUserClientCredentialRequestArgs} args - The user and client unique to delete the credentials for
|
||||
* @returns {*}
|
||||
* @memberof UmbUserClientCredentialRepository
|
||||
*/
|
||||
async requestDelete(args: UmbDeleteUserClientCredentialRequestArgs) {
|
||||
return this.#source.delete(args);
|
||||
}
|
||||
}
|
||||
|
||||
export { UmbUserClientCredentialRepository as api };
|
||||
@@ -0,0 +1,103 @@
|
||||
import { UMB_CREATE_USER_MODAL } from '../../modals/create/create-user-modal.token.js';
|
||||
import type { UmbUserKindType } from '../../utils/index.js';
|
||||
import { UmbUserKind } from '../../utils/index.js';
|
||||
import { html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
|
||||
import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity';
|
||||
import { UMB_ENTITY_CONTEXT } from '@umbraco-cms/backoffice/entity';
|
||||
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
|
||||
import { UmbRequestReloadChildrenOfEntityEvent } from '@umbraco-cms/backoffice/entity-action';
|
||||
|
||||
const elementName = 'umb-create-user-collection-action-button';
|
||||
@customElement(elementName)
|
||||
export class UmbCollectionActionButtonElement extends UmbLitElement {
|
||||
@state()
|
||||
private _popoverOpen = false;
|
||||
|
||||
async #onClick(event: Event, kind: UmbUserKindType) {
|
||||
event.stopPropagation();
|
||||
const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT);
|
||||
const entityContext = await this.getContext(UMB_ENTITY_CONTEXT);
|
||||
|
||||
const unique = entityContext.getUnique();
|
||||
const entityType = entityContext.getEntityType();
|
||||
|
||||
if (unique === undefined) throw new Error('Missing unique');
|
||||
if (!entityType) throw new Error('Missing entityType');
|
||||
|
||||
const modalContext = modalManager.open(this, UMB_CREATE_USER_MODAL, {
|
||||
data: {
|
||||
user: {
|
||||
kind,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
modalContext
|
||||
?.onSubmit()
|
||||
.then(() => {
|
||||
this.#requestReloadChildrenOfEntity({ entityType, unique });
|
||||
})
|
||||
.catch(async () => {
|
||||
// modal is closed after creation instead of navigating to the new user.
|
||||
// We therefore need to reload the children of the entity
|
||||
this.#requestReloadChildrenOfEntity({ entityType, unique });
|
||||
});
|
||||
}
|
||||
|
||||
async #requestReloadChildrenOfEntity({ entityType, unique }: UmbEntityModel) {
|
||||
const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
|
||||
const event = new UmbRequestReloadChildrenOfEntityEvent({
|
||||
entityType,
|
||||
unique,
|
||||
});
|
||||
|
||||
eventContext.dispatchEvent(event);
|
||||
}
|
||||
|
||||
#onPopoverToggle(event: ToggleEvent) {
|
||||
// TODO: This ignorer is just neede for JSON SCHEMA TO WORK, As its not updated with latest TS jet.
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
this._popoverOpen = event.newState === 'open';
|
||||
}
|
||||
|
||||
override render() {
|
||||
const label = this.localize.term('general_create');
|
||||
|
||||
return html`
|
||||
<uui-button popovertarget="collection-action-menu-popover" label=${label} color="default" look="outline">
|
||||
${label}
|
||||
<uui-symbol-expand .open=${this._popoverOpen}></uui-symbol-expand>
|
||||
</uui-button>
|
||||
<uui-popover-container
|
||||
id="collection-action-menu-popover"
|
||||
placement="bottom-start"
|
||||
@toggle=${this.#onPopoverToggle}>
|
||||
<umb-popover-layout>
|
||||
<uui-scroll-container>
|
||||
<uui-menu-item
|
||||
label=${this.localize.term('user_userKindDefault')}
|
||||
@click=${(event: Event) => this.#onClick(event, UmbUserKind.DEFAULT)}>
|
||||
<umb-icon slot="icon" name="icon-user"></umb-icon>
|
||||
</uui-menu-item>
|
||||
<uui-menu-item
|
||||
label=${this.localize.term('user_userKindApi')}
|
||||
@click=${(event: Event) => this.#onClick(event, UmbUserKind.API)}>
|
||||
<umb-icon slot="icon" name="icon-unplug"></umb-icon>
|
||||
</uui-menu-item>
|
||||
</uui-scroll-container>
|
||||
</umb-popover-layout>
|
||||
</uui-popover-container>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export { UmbCollectionActionButtonElement as element };
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
[elementName]: UmbCollectionActionButtonElement;
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { UMB_CREATE_USER_MODAL } from '../../modals/create/create-user-modal.token.js';
|
||||
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
|
||||
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
|
||||
import { UMB_ENTITY_CONTEXT } from '@umbraco-cms/backoffice/entity';
|
||||
import { UmbRequestReloadChildrenOfEntityEvent } from '@umbraco-cms/backoffice/entity-action';
|
||||
import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
|
||||
|
||||
export class UmbCreateUserCollectionAction extends UmbControllerBase {
|
||||
async execute() {
|
||||
const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT);
|
||||
const entityContext = await this.getContext(UMB_ENTITY_CONTEXT);
|
||||
|
||||
const unique = entityContext.getUnique();
|
||||
const entityType = entityContext.getEntityType();
|
||||
|
||||
if (unique === undefined) throw new Error('Missing unique');
|
||||
if (!entityType) throw new Error('Missing entityType');
|
||||
|
||||
const modalContext = modalManager.open(this, UMB_CREATE_USER_MODAL);
|
||||
modalContext?.onSubmit().catch(async () => {
|
||||
// modal is closed after creation instead of navigating to the new user.
|
||||
// We therefore need to reload the children of the entity
|
||||
const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
|
||||
const event = new UmbRequestReloadChildrenOfEntityEvent({
|
||||
entityType,
|
||||
unique,
|
||||
});
|
||||
|
||||
eventContext.dispatchEvent(event);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { UmbCreateUserCollectionAction } from './create-user.collection-action.js';
|
||||
@@ -1,17 +1,12 @@
|
||||
import { UmbCreateUserCollectionAction } from './create-user.collection-action.js';
|
||||
import { UMB_COLLECTION_ALIAS_CONDITION } from '@umbraco-cms/backoffice/collection';
|
||||
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
|
||||
|
||||
export const createManifest: ManifestTypes = {
|
||||
type: 'collectionAction',
|
||||
kind: 'button',
|
||||
name: 'Create User Collection Action',
|
||||
alias: 'Umb.CollectionAction.User.Create',
|
||||
api: UmbCreateUserCollectionAction,
|
||||
element: () => import('./create-user-collection-action.element.js'),
|
||||
weight: 200,
|
||||
meta: {
|
||||
label: '#general_create',
|
||||
},
|
||||
conditions: [
|
||||
{
|
||||
alias: UMB_COLLECTION_ALIAS_CONDITION,
|
||||
|
||||
@@ -86,6 +86,7 @@ export class UmbUserCollectionServerDataSource implements UmbCollectionDataSourc
|
||||
lastLockoutDate: item.lastLockoutDate || null,
|
||||
lastPasswordChangeDate: item.lastPasswordChangeDate || null,
|
||||
isAdmin: item.isAdmin,
|
||||
kind: item.kind,
|
||||
};
|
||||
|
||||
return userDetail;
|
||||
|
||||
@@ -2,12 +2,14 @@ import { getDisplayStateFromUserStatus } from '../../../utils.js';
|
||||
import type { UmbUserCollectionContext } from '../../user-collection.context.js';
|
||||
import type { UmbUserDetailModel } from '../../../types.js';
|
||||
import { UMB_USER_COLLECTION_CONTEXT } from '../../user-collection.context-token.js';
|
||||
import { UMB_USER_WORKSPACE_PATH } from '../../../paths.js';
|
||||
import { css, html, nothing, customElement, state, repeat, ifDefined } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { UserStateModel } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
import type { UmbUserGroupDetailModel } from '@umbraco-cms/backoffice/user-group';
|
||||
import { UmbUserGroupCollectionRepository } from '@umbraco-cms/backoffice/user-group';
|
||||
import { UmbUserKind } from '../../../utils/index.js';
|
||||
|
||||
@customElement('umb-user-grid-collection-view')
|
||||
export class UmbUserGridCollectionViewElement extends UmbLitElement {
|
||||
@@ -50,12 +52,6 @@ export class UmbUserGridCollectionViewElement extends UmbLitElement {
|
||||
this._loading = false;
|
||||
}
|
||||
|
||||
//TODO How should we handle url stuff?
|
||||
private _handleOpenCard(unique: string) {
|
||||
//TODO this will not be needed when cards works as links with href
|
||||
history.pushState(null, '', 'section/user-management/view/users/user/edit/' + unique); //TODO Change to a tag with href and make dynamic
|
||||
}
|
||||
|
||||
#onSelect(user: UmbUserDetailModel) {
|
||||
this.#collectionContext?.selection.select(user.unique ?? '');
|
||||
}
|
||||
@@ -78,44 +74,24 @@ export class UmbUserGridCollectionViewElement extends UmbLitElement {
|
||||
}
|
||||
|
||||
#renderUserCard(user: UmbUserDetailModel) {
|
||||
const avatarUrls = [
|
||||
{
|
||||
scale: '1x',
|
||||
url: user.avatarUrls?.[1],
|
||||
},
|
||||
{
|
||||
scale: '2x',
|
||||
url: user.avatarUrls?.[2],
|
||||
},
|
||||
{
|
||||
scale: '3x',
|
||||
url: user.avatarUrls?.[3],
|
||||
},
|
||||
];
|
||||
|
||||
let avatarSrcset = '';
|
||||
|
||||
avatarUrls.forEach((url) => {
|
||||
avatarSrcset += `${url.url} ${url.scale},`;
|
||||
});
|
||||
const href = UMB_USER_WORKSPACE_PATH + '/edit/' + user.unique;
|
||||
|
||||
return html`
|
||||
<uui-card-user
|
||||
.name=${user.name ?? 'Unnamed user'}
|
||||
href=${href}
|
||||
selectable
|
||||
?select-only=${this._selection.length > 0}
|
||||
?selected=${this.#collectionContext?.selection.isSelected(user.unique)}
|
||||
@open=${() => this._handleOpenCard(user.unique)}
|
||||
@selected=${() => this.#onSelect(user)}
|
||||
@deselected=${() => this.#onDeselect(user)}>
|
||||
${this.#renderUserTag(user)} ${this.#renderUserGroupNames(user)} ${this.#renderUserLoginDate(user)}
|
||||
|
||||
<uui-avatar
|
||||
style="font-size: 1.6rem;"
|
||||
<umb-user-avatar
|
||||
slot="avatar"
|
||||
.name=${user.name || 'Unknown'}
|
||||
img-src=${ifDefined(user.avatarUrls.length > 0 ? avatarUrls[0].url : undefined)}
|
||||
img-srcset=${ifDefined(user.avatarUrls.length > 0 ? avatarSrcset : undefined)}></uui-avatar>
|
||||
.name=${user.name}
|
||||
.kind=${user.kind}
|
||||
.imgUrls=${user.avatarUrls}
|
||||
style="font-size: 1.6rem;"></umb-user-avatar>
|
||||
</uui-card-user>
|
||||
`;
|
||||
}
|
||||
@@ -145,12 +121,14 @@ export class UmbUserGridCollectionViewElement extends UmbLitElement {
|
||||
}
|
||||
|
||||
#renderUserLoginDate(user: UmbUserDetailModel) {
|
||||
if (user.kind === UmbUserKind.API) return nothing;
|
||||
|
||||
if (!user.lastLoginDate) {
|
||||
return html`<div class="user-login-time">${`${user.name} ${this.localize.term('user_noLogin')}`}</div>`;
|
||||
}
|
||||
|
||||
return html`<div class="user-login-time">
|
||||
<umb-localize key="user_lastLogin"></umb-localize><br />
|
||||
<umb-localize key="user_lastLogin"></umb-localize>
|
||||
${this.localize.date(user.lastLoginDate)}
|
||||
</div>`;
|
||||
}
|
||||
@@ -172,6 +150,8 @@ export class UmbUserGridCollectionViewElement extends UmbLitElement {
|
||||
uui-card-user {
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
justify-content: normal;
|
||||
padding-top: var(--uui-size-space-5);
|
||||
}
|
||||
|
||||
.user-login-time {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { html, LitElement, customElement, property, ifDefined } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UMB_USER_WORKSPACE_PATH } from '../../../../../paths.js';
|
||||
import { html, LitElement, customElement, property } from '@umbraco-cms/backoffice/external/lit';
|
||||
import type { UmbTableColumn, UmbTableItem } from '@umbraco-cms/backoffice/components';
|
||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||
|
||||
@customElement('umb-user-table-name-column-layout')
|
||||
export class UmbUserTableNameColumnLayoutElement extends LitElement {
|
||||
@@ -13,38 +15,18 @@ export class UmbUserTableNameColumnLayoutElement extends LitElement {
|
||||
value!: any;
|
||||
|
||||
override render() {
|
||||
const avatarUrls = [
|
||||
{
|
||||
scale: '1x',
|
||||
url: this.value.avatarUrls?.[0],
|
||||
},
|
||||
{
|
||||
scale: '2x',
|
||||
url: this.value.avatarUrls?.[1],
|
||||
},
|
||||
{
|
||||
scale: '3x',
|
||||
url: this.value.avatarUrls?.[2],
|
||||
},
|
||||
];
|
||||
|
||||
let avatarSrcset = '';
|
||||
|
||||
avatarUrls.forEach((url) => {
|
||||
avatarSrcset += `${url.url} ${url.scale},`;
|
||||
});
|
||||
const href = UMB_USER_WORKSPACE_PATH + '/edit/' + this.value.unique;
|
||||
|
||||
return html` <div style="display: flex; align-items: center;">
|
||||
<uui-avatar
|
||||
<umb-user-avatar
|
||||
style="margin-right: var(--uui-size-space-3);"
|
||||
.name=${this.value.name || 'Unknown'}
|
||||
img-src=${ifDefined(this.value.avatarUrls.length > 0 ? avatarUrls[0].url : undefined)}
|
||||
img-srcset=${ifDefined(this.value.avatarUrls.length > 0 ? avatarSrcset : undefined)}></uui-avatar>
|
||||
<a style="font-weight: bold;" href="section/user-management/view/users/user/edit/${this.value.unique}"
|
||||
>${this.value.name}</a
|
||||
>
|
||||
name=${this.value.name}
|
||||
kind=${this.value.kind}
|
||||
.imgUrls=${this.value.avatarUrls}></umb-user-avatar>
|
||||
<a style="font-weight: bold;" href="${href}">${this.value.name}</a>
|
||||
</div>`;
|
||||
}
|
||||
static override styles = [UmbTextStyles];
|
||||
}
|
||||
|
||||
export default UmbUserTableNameColumnLayoutElement;
|
||||
|
||||
@@ -19,6 +19,7 @@ import type { UmbReferenceByUnique } from '@umbraco-cms/backoffice/models';
|
||||
|
||||
import './column-layouts/name/user-table-name-column-layout.element.js';
|
||||
import './column-layouts/status/user-table-status-column-layout.element.js';
|
||||
import { UmbUserKind } from '../../../utils/index.js';
|
||||
|
||||
@customElement('umb-user-table-collection-view')
|
||||
export class UmbUserTableCollectionViewElement extends UmbLitElement {
|
||||
@@ -114,7 +115,7 @@ export class UmbUserTableCollectionViewElement extends UmbLitElement {
|
||||
this._tableItems = this._users.map((user) => {
|
||||
return {
|
||||
id: user.unique,
|
||||
icon: 'icon-user',
|
||||
icon: user.kind === UmbUserKind.API ? 'icon-unplug' : 'icon-user',
|
||||
data: [
|
||||
{
|
||||
columnAlias: 'userName',
|
||||
@@ -122,6 +123,7 @@ export class UmbUserTableCollectionViewElement extends UmbLitElement {
|
||||
unique: user.unique,
|
||||
name: user.name,
|
||||
avatarUrls: user.avatarUrls,
|
||||
kind: user.kind,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import './user-input/user-input.element.js';
|
||||
import './user-avatar/user-avatar.element.js';
|
||||
import './user-document-start-node/user-document-start-node.element.js';
|
||||
import './user-input/user-input.element.js';
|
||||
import './user-media-start-node/user-media-start-node.element.js';
|
||||
|
||||
export * from './user-input/index.js';
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import type { UmbUserKindType } from '../../utils/index.js';
|
||||
import { UmbUserKind } from '../../utils/index.js';
|
||||
import type { UUIAvatarElement } from '@umbraco-cms/backoffice/external/uui';
|
||||
import {
|
||||
css,
|
||||
html,
|
||||
customElement,
|
||||
property,
|
||||
ifDefined,
|
||||
state,
|
||||
classMap,
|
||||
query,
|
||||
} from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
|
||||
const elementName = 'umb-user-avatar';
|
||||
@customElement(elementName)
|
||||
export class UmbUserAvatarElement extends UmbLitElement {
|
||||
@property({ type: String })
|
||||
name?: string;
|
||||
|
||||
@property({ type: String })
|
||||
kind?: UmbUserKindType = UmbUserKind.DEFAULT;
|
||||
|
||||
@property({ type: Array, attribute: false })
|
||||
public get imgUrls(): Array<string> {
|
||||
return this.#imgUrls;
|
||||
}
|
||||
public set imgUrls(value: Array<string>) {
|
||||
this.#imgUrls = value;
|
||||
this.hasImgUrls = value.length > 0;
|
||||
this.#setImgSrcSizes();
|
||||
}
|
||||
#imgUrls: Array<string> = [];
|
||||
|
||||
@state()
|
||||
private _imgSrcSizes: Array<{ w: number; url: string }> = [];
|
||||
|
||||
@state()
|
||||
private _imgSrc = '';
|
||||
|
||||
@state()
|
||||
private hasImgUrls = false;
|
||||
|
||||
@query('uui-avatar')
|
||||
avatarElement!: UUIAvatarElement;
|
||||
|
||||
#setImgSrcSizes() {
|
||||
if (this.#imgUrls.length === 0) {
|
||||
this._imgSrcSizes = [];
|
||||
return;
|
||||
}
|
||||
|
||||
this._imgSrcSizes = [
|
||||
{
|
||||
w: 30,
|
||||
url: this.#imgUrls[0],
|
||||
},
|
||||
{
|
||||
w: 60,
|
||||
url: this.#imgUrls[1],
|
||||
},
|
||||
{
|
||||
w: 90,
|
||||
url: this.#imgUrls[2],
|
||||
},
|
||||
{
|
||||
w: 150,
|
||||
url: this.#imgUrls[3],
|
||||
},
|
||||
{
|
||||
w: 300,
|
||||
url: this.#imgUrls[4],
|
||||
},
|
||||
];
|
||||
|
||||
this.#setImgSrc();
|
||||
}
|
||||
|
||||
protected override firstUpdated(): void {
|
||||
this.#setImgSrc();
|
||||
}
|
||||
|
||||
async #setImgSrc() {
|
||||
if (!this.hasImgUrls) return;
|
||||
if (!this.avatarElement) return;
|
||||
|
||||
setTimeout(() => {
|
||||
// TODO: look into img sizes="auto" to let the browser handle the correct image size based on the element size
|
||||
const elementSize = this.avatarElement.getBoundingClientRect();
|
||||
const elementWidth = elementSize.width;
|
||||
|
||||
const matchingSizes = this._imgSrcSizes.filter((size) => {
|
||||
// we multiply the element width to make sure we have a good quality image
|
||||
return elementWidth * 1.5 <= size.w;
|
||||
});
|
||||
|
||||
// We use the smallest image that is larger than the element width
|
||||
this._imgSrc = matchingSizes[0]?.url;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
override render() {
|
||||
const classes = {
|
||||
default: this.kind === UmbUserKind.API,
|
||||
api: this.kind === UmbUserKind.API,
|
||||
'has-image': this.hasImgUrls,
|
||||
};
|
||||
|
||||
return html`<uui-avatar
|
||||
.name=${this.name || 'Unknown'}
|
||||
img-src=${ifDefined(this._imgSrc ? this._imgSrc : undefined)}
|
||||
class=${classMap(classes)}></uui-avatar>`;
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
css`
|
||||
uui-avatar {
|
||||
background-color: transparent;
|
||||
border: 1.5px solid var(--uui-color-border);
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
uui-avatar.has-image {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
uui-avatar.api {
|
||||
border-radius: 9%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
[elementName]: UmbUserAvatarElement;
|
||||
}
|
||||
}
|
||||
@@ -157,7 +157,11 @@ export class UmbUserInputElement extends UUIFormControlMixin(UmbLitElement, '')
|
||||
if (!item.unique) return nothing;
|
||||
return html`
|
||||
<uui-ref-node-user name=${item.name} id=${item.unique}>
|
||||
<uui-avatar slot="icon" name=${item.name}></uui-avatar>
|
||||
<umb-user-avatar
|
||||
slot="icon"
|
||||
.name=${item.name}
|
||||
.kind=${item.kind}
|
||||
.imgUrls=${item.avatarUrls}></umb-user-avatar>
|
||||
<uui-action-bar slot="actions">
|
||||
<uui-button label=${this.localize.term('general_remove')} @click=${() => this.#removeItem(item)}></uui-button>
|
||||
</uui-action-bar>
|
||||
@@ -171,7 +175,7 @@ export class UmbUserInputElement extends UUIFormControlMixin(UmbLitElement, '')
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
uui-avatar {
|
||||
umb-user-avatar {
|
||||
font-size: var(--uui-size-4);
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
|
||||
|
||||
export const manifests: Array<ManifestTypes> = [
|
||||
{
|
||||
type: 'condition',
|
||||
name: 'User Allow Change Password Condition',
|
||||
alias: 'Umb.Condition.User.AllowChangePassword',
|
||||
api: () => import('./user-allow-change-password-action.condition.js'),
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,15 @@
|
||||
import { UmbUserKind } from '../../utils/index.js';
|
||||
import { UmbUserActionConditionBase } from '../user-allow-action-base.condition.js';
|
||||
|
||||
export class UmbUserAllowChangePasswordActionCondition extends UmbUserActionConditionBase {
|
||||
async _onUserDataChange() {
|
||||
// don't allow the current user to delete themselves
|
||||
if (this.userKind === UmbUserKind.DEFAULT) {
|
||||
this.permitted = true;
|
||||
} else {
|
||||
this.permitted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { UmbUserAllowChangePasswordActionCondition as api };
|
||||
@@ -1,3 +1,4 @@
|
||||
import { manifests as userAllowChangePasswordActionManifests } from './allow-change-password/manifests.js';
|
||||
import { manifests as userAllowDeleteActionManifests } from './allow-delete/manifests.js';
|
||||
import { manifests as userAllowDisableActionManifests } from './allow-disable/manifests.js';
|
||||
import { manifests as userAllowEnableActionManifests } from './allow-enable/manifests.js';
|
||||
@@ -7,10 +8,11 @@ import { manifests as userAllowUnlockActionManifests } from './allow-unlock/mani
|
||||
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
|
||||
|
||||
export const manifests: Array<ManifestTypes> = [
|
||||
...userAllowChangePasswordActionManifests,
|
||||
...userAllowDeleteActionManifests,
|
||||
...userAllowDisableActionManifests,
|
||||
...userAllowEnableActionManifests,
|
||||
...userAllowUnlockActionManifests,
|
||||
...userAllowExternalLoginActionManifests,
|
||||
...userAllowMfaActionManifests,
|
||||
...userAllowDeleteActionManifests,
|
||||
...userAllowUnlockActionManifests,
|
||||
];
|
||||
|
||||
@@ -16,23 +16,44 @@ export abstract class UmbUserActionConditionBase
|
||||
{
|
||||
/**
|
||||
* The unique identifier of the user being edited
|
||||
* @protected
|
||||
* @type {string}
|
||||
* @memberof UmbUserActionConditionBase
|
||||
*/
|
||||
protected userUnique?: string;
|
||||
|
||||
/**
|
||||
* The state of the user being edited
|
||||
* @protected
|
||||
* @type {(UmbUserStateEnum | null)}
|
||||
* @memberof UmbUserActionConditionBase
|
||||
*/
|
||||
protected userState?: UmbUserStateEnum | null;
|
||||
|
||||
/**
|
||||
* The kind of user being edited
|
||||
* @protected
|
||||
* @type {string}
|
||||
* @memberof UmbUserActionConditionBase
|
||||
*/
|
||||
protected userKind?: string;
|
||||
|
||||
/**
|
||||
* Creates an instance of UmbUserActionConditionBase.
|
||||
* @param {UmbControllerHost} host The host controller
|
||||
* @param {UmbConditionControllerArguments<UmbConditionConfigBase>} args The condition arguments
|
||||
* @memberof UmbUserActionConditionBase
|
||||
*/
|
||||
constructor(host: UmbControllerHost, args: UmbConditionControllerArguments<UmbConditionConfigBase>) {
|
||||
super(host, args);
|
||||
|
||||
this.consumeContext(UMB_USER_WORKSPACE_CONTEXT, (context) => {
|
||||
this.observe(
|
||||
observeMultiple([context.unique, context.state]),
|
||||
([unique, state]) => {
|
||||
observeMultiple([context.unique, context.state, context.kind]),
|
||||
([unique, state, kind]) => {
|
||||
this.userUnique = unique;
|
||||
this.userState = state;
|
||||
this.userKind = kind;
|
||||
this._onUserDataChange();
|
||||
},
|
||||
'_umbActiveUser',
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
import { manifests as clientCredentialManifests } from './client-credential/manifests.js';
|
||||
import { manifests as collectionManifests } from './collection/manifests.js';
|
||||
import { manifests as conditionsManifests } from './conditions/manifests.js';
|
||||
import { manifests as entityActionsManifests } from './entity-actions/manifests.js';
|
||||
import { manifests as entityBulkActionManifests } from './entity-bulk-actions/manifests.js';
|
||||
import { manifests as inviteManifests } from './invite/manifests.js';
|
||||
import { manifests as modalManifests } from './modals/manifests.js';
|
||||
import { manifests as propertyEditorManifests } from './property-editor/manifests.js';
|
||||
import { manifests as repositoryManifests } from './repository/manifests.js';
|
||||
import { manifests as sectionViewManifests } from './section-view/manifests.js';
|
||||
import { manifests as propertyEditorManifests } from './property-editor/manifests.js';
|
||||
import { manifests as workspaceManifests } from './workspace/manifests.js';
|
||||
import { manifests as menuItemManifests } from './menu-item/manifests.js';
|
||||
|
||||
import type { ManifestTypes, UmbBackofficeManifestKind } from '@umbraco-cms/backoffice/extension-registry';
|
||||
|
||||
export const manifests: Array<ManifestTypes | UmbBackofficeManifestKind> = [
|
||||
...clientCredentialManifests,
|
||||
...collectionManifests,
|
||||
...conditionsManifests,
|
||||
...entityActionsManifests,
|
||||
...entityBulkActionManifests,
|
||||
...inviteManifests,
|
||||
...modalManifests,
|
||||
...propertyEditorManifests,
|
||||
...repositoryManifests,
|
||||
...sectionViewManifests,
|
||||
...propertyEditorManifests,
|
||||
...workspaceManifests,
|
||||
...menuItemManifests,
|
||||
];
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { UmbUserDetailRepository } from '../../repository/index.js';
|
||||
import { UmbUserKind } from '../../utils/index.js';
|
||||
import { UMB_CREATE_USER_SUCCESS_MODAL } from './create-user-success-modal.token.js';
|
||||
import type { UmbCreateUserModalData } from './create-user-modal.token.js';
|
||||
import type { UmbUserGroupInputElement } from '@umbraco-cms/backoffice/user-group';
|
||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||
import { css, html, customElement, query } from '@umbraco-cms/backoffice/external/lit';
|
||||
@@ -7,7 +9,7 @@ import { UmbModalBaseElement, UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/bac
|
||||
import type { UmbReferenceByUnique } from '@umbraco-cms/backoffice/models';
|
||||
|
||||
@customElement('umb-create-user-modal')
|
||||
export class UmbCreateUserModalElement extends UmbModalBaseElement {
|
||||
export class UmbCreateUserModalElement extends UmbModalBaseElement<UmbCreateUserModalData> {
|
||||
#userDetailRepository = new UmbUserDetailRepository(this);
|
||||
|
||||
@query('#CreateUserForm')
|
||||
@@ -35,6 +37,7 @@ export class UmbCreateUserModalElement extends UmbModalBaseElement {
|
||||
const { data: userScaffold } = await this.#userDetailRepository.createScaffold();
|
||||
if (!userScaffold) return;
|
||||
|
||||
userScaffold.kind = this.data?.user.kind ?? UmbUserKind.DEFAULT;
|
||||
userScaffold.name = name;
|
||||
userScaffold.email = email;
|
||||
userScaffold.userName = email;
|
||||
@@ -44,7 +47,11 @@ export class UmbCreateUserModalElement extends UmbModalBaseElement {
|
||||
const { data } = await this.#userDetailRepository.create(userScaffold);
|
||||
|
||||
if (data) {
|
||||
this.#openSuccessModal(data.unique);
|
||||
if (data.kind === UmbUserKind.DEFAULT) {
|
||||
this.#openSuccessModal(data.unique);
|
||||
} else {
|
||||
this._submitModal();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,11 +80,8 @@ export class UmbCreateUserModalElement extends UmbModalBaseElement {
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`<uui-dialog-layout headline="Create user">
|
||||
<p>
|
||||
Create new users to give them access to Umbraco. When a user is created a password will be generated that you
|
||||
can share with the user.
|
||||
</p>
|
||||
return html`<uui-dialog-layout headline=${this.localize.term('user_createUserHeadline', this.data?.user.kind)}>
|
||||
<p>${this.localize.term('user_createUserDescription', this.data?.user.kind)}</p>
|
||||
|
||||
${this.#renderForm()}
|
||||
<uui-button @click=${this._rejectModal} slot="actions" label="Cancel" look="secondary"></uui-button>
|
||||
@@ -115,7 +119,8 @@ export class UmbCreateUserModalElement extends UmbModalBaseElement {
|
||||
UmbTextStyles,
|
||||
css`
|
||||
uui-input,
|
||||
uui-input-password {
|
||||
uui-input-password,
|
||||
uui-combobox {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import type { UmbUserKindType } from '../../utils/index.js';
|
||||
import { UMB_CREATE_USER_MODAL_ALIAS } from './constants.js';
|
||||
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
|
||||
|
||||
export const UMB_CREATE_USER_MODAL = new UmbModalToken(UMB_CREATE_USER_MODAL_ALIAS, {
|
||||
export interface UmbCreateUserModalData {
|
||||
user: {
|
||||
kind?: UmbUserKindType;
|
||||
};
|
||||
}
|
||||
|
||||
export const UMB_CREATE_USER_MODAL = new UmbModalToken<UmbCreateUserModalData>(UMB_CREATE_USER_MODAL_ALIAS, {
|
||||
modal: {
|
||||
type: 'dialog',
|
||||
size: 'small',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { UmbUserItemRepository } from '../../repository/item/index.js';
|
||||
import { UmbNewUserPasswordRepository } from '../../repository/new-password/index.js';
|
||||
import type { UmbUserItemModel } from '../../repository/item/types.js';
|
||||
import { UMB_USER_WORKSPACE_PATH } from '../../paths.js';
|
||||
import type {
|
||||
UmbCreateUserSuccessModalData,
|
||||
UmbCreateUserSuccessModalValue,
|
||||
@@ -69,10 +70,8 @@ export class UmbCreateUserSuccessModalElement extends UmbModalBaseElement<
|
||||
this._rejectModal({ type: 'createAnotherUser' });
|
||||
};
|
||||
|
||||
#onGoToProfile = (event: Event) => {
|
||||
event.stopPropagation();
|
||||
#onGoToProfile = () => {
|
||||
this._submitModal();
|
||||
history.pushState(null, '', 'section/user-management/view/users/user/edit/' + this.data?.user.unique); //TODO: URL Should be dynamic
|
||||
};
|
||||
|
||||
override render() {
|
||||
@@ -98,7 +97,8 @@ export class UmbCreateUserSuccessModalElement extends UmbModalBaseElement<
|
||||
slot="actions"
|
||||
label="Go to profile"
|
||||
look="primary"
|
||||
color="positive"></uui-button>
|
||||
color="positive"
|
||||
href=${UMB_USER_WORKSPACE_PATH + '/edit/' + this.data?.user.unique}></uui-button>
|
||||
</uui-dialog-layout>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,11 @@ export class UmbUserPickerModalElement extends UmbModalBaseElement<UmbUserPicker
|
||||
@selected=${() => this.#selectionManager.select(user.unique)}
|
||||
@deselected=${() => this.#selectionManager.deselect(user.unique)}
|
||||
?selected=${this.#selectionManager.isSelected(user.unique)}>
|
||||
<uui-avatar slot="icon" name=${ifDefined(user.name)}></uui-avatar>
|
||||
<umb-user-avatar
|
||||
slot="icon"
|
||||
.name=${user.name}
|
||||
.kind=${user.kind}
|
||||
.imgUrls=${user.avatarUrls}></umb-user-avatar>
|
||||
</uui-menu-item>
|
||||
`,
|
||||
)}
|
||||
@@ -74,8 +78,7 @@ export class UmbUserPickerModalElement extends UmbModalBaseElement<UmbUserPicker
|
||||
static override styles = [
|
||||
UmbTextStyles,
|
||||
css`
|
||||
uui-avatar {
|
||||
border: 2px solid var(--uui-color-surface);
|
||||
umb-user-avatar {
|
||||
font-size: 12px;
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { UMB_USER_SECTION_PATHNAME } from '../user-section/paths.js';
|
||||
import { UMB_USER_ENTITY_TYPE } from './entity.js';
|
||||
import { UMB_WORKSPACE_PATH_PATTERN } from '@umbraco-cms/backoffice/workspace';
|
||||
|
||||
export const UMB_USER_WORKSPACE_PATH = UMB_WORKSPACE_PATH_PATTERN.generateAbsolute({
|
||||
sectionName: UMB_USER_SECTION_PATHNAME,
|
||||
entityType: UMB_USER_ENTITY_TYPE,
|
||||
});
|
||||
@@ -2,10 +2,15 @@ import type { UmbUserDetailModel, UmbUserStartNodesModel } from '../../types.js'
|
||||
import { UMB_USER_ENTITY_TYPE } from '../../entity.js';
|
||||
import { UmbId } from '@umbraco-cms/backoffice/id';
|
||||
import type { UmbDetailDataSource } from '@umbraco-cms/backoffice/repository';
|
||||
import type { CreateUserRequestModel, UpdateUserRequestModel } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
import type {
|
||||
CreateUserRequestModel,
|
||||
UpdateUserRequestModel,
|
||||
UserKindModel,
|
||||
} from '@umbraco-cms/backoffice/external/backend-api';
|
||||
import { UserService } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
|
||||
import { UmbUserKind } from '../../utils/index.js';
|
||||
|
||||
/**
|
||||
* A data source for the User that fetches data from the server
|
||||
@@ -34,17 +39,18 @@ export class UmbUserServerDataSource implements UmbDetailDataSource<UmbUserDetai
|
||||
const data: UmbUserDetailModel = {
|
||||
avatarUrls: [],
|
||||
createDate: null,
|
||||
hasDocumentRootAccess: false,
|
||||
documentStartNodeUniques: [],
|
||||
email: '',
|
||||
entityType: UMB_USER_ENTITY_TYPE,
|
||||
failedLoginAttempts: 0,
|
||||
hasDocumentRootAccess: false,
|
||||
hasMediaRootAccess: false,
|
||||
isAdmin: false,
|
||||
kind: UmbUserKind.DEFAULT,
|
||||
languageIsoCode: '',
|
||||
lastLockoutDate: null,
|
||||
lastLoginDate: null,
|
||||
lastPasswordChangeDate: null,
|
||||
hasMediaRootAccess: false,
|
||||
mediaStartNodeUniques: [],
|
||||
name: '',
|
||||
state: null,
|
||||
@@ -86,6 +92,7 @@ export class UmbUserServerDataSource implements UmbDetailDataSource<UmbUserDetai
|
||||
entityType: UMB_USER_ENTITY_TYPE,
|
||||
failedLoginAttempts: data.failedLoginAttempts,
|
||||
isAdmin: data.isAdmin,
|
||||
kind: data.kind,
|
||||
languageIsoCode: data.languageIsoCode || null,
|
||||
lastLockoutDate: data.lastLockoutDate || null,
|
||||
lastLoginDate: data.lastLoginDate || null,
|
||||
@@ -130,6 +137,7 @@ export class UmbUserServerDataSource implements UmbDetailDataSource<UmbUserDetai
|
||||
};
|
||||
}),
|
||||
userName: model.userName,
|
||||
kind: model.kind as UserKindModel,
|
||||
};
|
||||
|
||||
const { data, error } = await tryExecuteAndNotify(
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import type { UmbUserEntityType } from '../../entity.js';
|
||||
import type { UmbUserKindType } from '../../utils/index.js';
|
||||
|
||||
export interface UmbUserItemModel {
|
||||
avatarUrls: Array<string>;
|
||||
entityType: UmbUserEntityType;
|
||||
kind: UmbUserKindType;
|
||||
name: string;
|
||||
unique: string;
|
||||
}
|
||||
|
||||
@@ -33,5 +33,6 @@ const mapper = (item: UserItemResponseModel): UmbUserItemModel => {
|
||||
entityType: UMB_USER_ENTITY_TYPE,
|
||||
name: item.name,
|
||||
unique: item.id,
|
||||
kind: item.kind,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { UmbUserEntityType } from './entity.js';
|
||||
import type { UmbUserKindType } from './utils/index.js';
|
||||
import type { UmbReferenceByUnique } from '@umbraco-cms/backoffice/models';
|
||||
import {
|
||||
type UserConfigurationResponseModel,
|
||||
@@ -26,6 +27,7 @@ export interface UmbUserDetailModel extends UmbUserStartNodesModel {
|
||||
updateDate: string | null;
|
||||
userGroupUniques: Array<UmbReferenceByUnique>;
|
||||
userName: string;
|
||||
kind: UmbUserKindType;
|
||||
}
|
||||
|
||||
export interface UmbUserStartNodesModel {
|
||||
|
||||
@@ -1 +1,8 @@
|
||||
export * from './is-user.function.js';
|
||||
|
||||
export type UmbUserKindType = 'Default' | 'Api';
|
||||
|
||||
export const UmbUserKind = Object.freeze({
|
||||
DEFAULT: 'Default',
|
||||
API: 'Api',
|
||||
});
|
||||
|
||||
@@ -28,7 +28,7 @@ export class UmbUserWorkspaceAccessElement extends UmbLitElement {
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html` <uui-box id="access" headline=${this.localize.term('user_access')}>
|
||||
return html` <uui-box headline=${this.localize.term('user_access')}>
|
||||
<div slot="header" class="faded-text">
|
||||
<umb-localize key="user_accessHelp"
|
||||
>Based on the assigned groups and start nodes, the user has access to the following nodes</umb-localize
|
||||
@@ -60,8 +60,8 @@ export class UmbUserWorkspaceAccessElement extends UmbLitElement {
|
||||
static override styles = [
|
||||
UmbTextStyles,
|
||||
css`
|
||||
#access {
|
||||
margin-top: var(--uui-size-space-4);
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
hr {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { UMB_USER_WORKSPACE_CONTEXT } from '../../user-workspace.context-token.js';
|
||||
import type { UmbUserDetailModel } from '../../../types.js';
|
||||
import { html, customElement, state, nothing } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { html, customElement, state, nothing, css } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||
import type { UmbInputDocumentElement } from '@umbraco-cms/backoffice/document';
|
||||
@@ -185,7 +185,14 @@ export class UmbUserWorkspaceAssignAccessElement extends UmbLitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
static override styles = [UmbTextStyles];
|
||||
static override styles = [
|
||||
UmbTextStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { UmbUserDetailModel } from '../../../types.js';
|
||||
import { UMB_USER_WORKSPACE_CONTEXT } from '../../user-workspace.context-token.js';
|
||||
import { css, html, customElement, query, nothing, ifDefined, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { css, html, customElement, query, nothing, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
|
||||
@customElement('umb-user-workspace-avatar')
|
||||
@@ -8,9 +8,6 @@ export class UmbUserAvatarElement extends UmbLitElement {
|
||||
@state()
|
||||
private _user?: UmbUserDetailModel;
|
||||
|
||||
@state()
|
||||
private _userAvatarUrls: Array<{ url: string; scale: string }> = [];
|
||||
|
||||
@query('#AvatarFileField')
|
||||
_avatarFileField?: HTMLInputElement;
|
||||
|
||||
@@ -38,30 +35,11 @@ export class UmbUserAvatarElement extends UmbLitElement {
|
||||
this.#userWorkspaceContext!.data,
|
||||
async (user) => {
|
||||
this._user = user;
|
||||
this.#setUserAvatarUrls(user);
|
||||
},
|
||||
'umbUserObserver',
|
||||
);
|
||||
};
|
||||
|
||||
#setUserAvatarUrls = async (user: UmbUserDetailModel | undefined) => {
|
||||
if (!user || !user.avatarUrls || user.avatarUrls.length === 0) {
|
||||
this._userAvatarUrls = [];
|
||||
return;
|
||||
}
|
||||
|
||||
this._userAvatarUrls = [
|
||||
{
|
||||
scale: '1x',
|
||||
url: user.avatarUrls?.[3],
|
||||
},
|
||||
{
|
||||
scale: '2x',
|
||||
url: user.avatarUrls?.[4],
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
#uploadAvatar = async () => {
|
||||
try {
|
||||
const selectedFile = await this.#selectAvatar();
|
||||
@@ -95,35 +73,24 @@ export class UmbUserAvatarElement extends UmbLitElement {
|
||||
|
||||
#deleteAvatar = async () => {
|
||||
if (!this.#userWorkspaceContext) return;
|
||||
const { error } = await this.#userWorkspaceContext.deleteAvatar();
|
||||
|
||||
if (!error) {
|
||||
this._userAvatarUrls = [];
|
||||
}
|
||||
this.#userWorkspaceContext.deleteAvatar();
|
||||
};
|
||||
|
||||
#getAvatarSrcset() {
|
||||
let string = '';
|
||||
|
||||
this._userAvatarUrls?.forEach((url) => {
|
||||
string += `${url.url} ${url.scale},`;
|
||||
});
|
||||
return string;
|
||||
}
|
||||
|
||||
#hasAvatar() {
|
||||
return this._userAvatarUrls.length > 0;
|
||||
if (!this._user) return false;
|
||||
return this._user.avatarUrls.length > 0;
|
||||
}
|
||||
|
||||
override render() {
|
||||
if (!this._user) return nothing;
|
||||
return html`
|
||||
<uui-box>
|
||||
<form id="AvatarUploadForm" novalidate>
|
||||
<uui-avatar
|
||||
<umb-user-avatar
|
||||
id="Avatar"
|
||||
.name=${this._user?.name || ''}
|
||||
img-src=${ifDefined(this.#hasAvatar() ? this._userAvatarUrls[0].url : undefined)}
|
||||
img-srcset=${ifDefined(this.#hasAvatar() ? this.#getAvatarSrcset() : undefined)}></uui-avatar>
|
||||
.name=${this._user.name}
|
||||
.kind=${this._user.kind}
|
||||
.imgUrls=${this._user.avatarUrls ?? []}></umb-user-avatar>
|
||||
<input id="AvatarFileField" type="file" name="avatarFile" required hidden />
|
||||
<uui-button label="${this.localize.term('user_changePhoto')}" @click=${this.#uploadAvatar}></uui-button>
|
||||
${this.#hasAvatar()
|
||||
@@ -143,7 +110,6 @@ export class UmbUserAvatarElement extends UmbLitElement {
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
margin-bottom: var(--uui-size-space-4);
|
||||
}
|
||||
|
||||
#Avatar {
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
import { UMB_USER_WORKSPACE_CONTEXT } from '../../user-workspace.context-token.js';
|
||||
import type {
|
||||
UmbDeleteUserClientCredentialRequestArgs,
|
||||
UmbUserClientCredentialModel,
|
||||
} from '../../../client-credential/index.js';
|
||||
import { UmbUserClientCredentialRepository } from '../../../client-credential/index.js';
|
||||
import { UMB_CREATE_USER_CLIENT_CREDENTIAL_MODAL } from '../../../client-credential/create/modal/create-user-client-credential-modal.token.js';
|
||||
import { UmbUserKind } from '../../../utils/index.js';
|
||||
import { html, customElement, state, css, nothing } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||
import { UMB_MODAL_MANAGER_CONTEXT, umbConfirmModal } from '@umbraco-cms/backoffice/modal';
|
||||
|
||||
const elementName = 'umb-user-workspace-client-credentials';
|
||||
@customElement(elementName)
|
||||
export class UmbUserWorkspaceClientCredentialsElement extends UmbLitElement {
|
||||
@state()
|
||||
private _userUnique?: string;
|
||||
|
||||
@state()
|
||||
private _userKind?: string;
|
||||
|
||||
@state()
|
||||
private _clientCredentials: UmbUserClientCredentialModel[] = [];
|
||||
|
||||
#userWorkspaceContext?: typeof UMB_USER_WORKSPACE_CONTEXT.TYPE;
|
||||
#modalManagerContext? = UMB_MODAL_MANAGER_CONTEXT.TYPE;
|
||||
#userClientCredentialRepository = new UmbUserClientCredentialRepository(this);
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.consumeContext(UMB_USER_WORKSPACE_CONTEXT, (instance) => {
|
||||
this.#userWorkspaceContext = instance;
|
||||
|
||||
this.observe(this.#userWorkspaceContext.kind, (kind) => (this._userKind = kind), 'umbUserKindObserver');
|
||||
|
||||
this.observe(
|
||||
this.#userWorkspaceContext.unique,
|
||||
async (unique) => this.#onUserUniqueChange(unique),
|
||||
'umbUserUniqueObserver',
|
||||
);
|
||||
});
|
||||
|
||||
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (instance) => {
|
||||
this.#modalManagerContext = instance;
|
||||
});
|
||||
}
|
||||
|
||||
#onUserUniqueChange(unique: string | undefined) {
|
||||
if (unique && this._userUnique !== unique) {
|
||||
this._userUnique = unique;
|
||||
this.#loadClientCredentials();
|
||||
}
|
||||
|
||||
if (!unique) {
|
||||
this._userUnique = undefined;
|
||||
this._clientCredentials = [];
|
||||
}
|
||||
}
|
||||
|
||||
async #loadClientCredentials() {
|
||||
if (!this._userUnique) throw new Error('User unique not available');
|
||||
|
||||
const { data } = await this.#userClientCredentialRepository.requestClientCredentials({
|
||||
user: { unique: this._userUnique },
|
||||
});
|
||||
|
||||
this._clientCredentials = data ?? [];
|
||||
}
|
||||
|
||||
#onAdd(event: Event) {
|
||||
event.stopPropagation();
|
||||
if (!this.#modalManagerContext) throw new Error('Modal Manager Context not available');
|
||||
if (!this._userUnique) throw new Error('User unique not available');
|
||||
|
||||
const modalContext = this.#modalManagerContext.open(this, UMB_CREATE_USER_CLIENT_CREDENTIAL_MODAL, {
|
||||
data: {
|
||||
user: {
|
||||
unique: this._userUnique,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
modalContext.onSubmit().then(() => this.#loadClientCredentials());
|
||||
}
|
||||
|
||||
async #onDelete(event: Event, client: UmbUserClientCredentialModel) {
|
||||
event.stopPropagation();
|
||||
if (!this._userUnique) throw new Error('User unique not available');
|
||||
|
||||
await umbConfirmModal(this, {
|
||||
headline: `Delete ${client.unique}`,
|
||||
content: `Are you sure you want to delete ${client.unique}?`,
|
||||
confirmLabel: 'Delete',
|
||||
color: 'danger',
|
||||
});
|
||||
|
||||
const payload: UmbDeleteUserClientCredentialRequestArgs = {
|
||||
user: { unique: this._userUnique },
|
||||
client: { unique: client.unique },
|
||||
};
|
||||
|
||||
const { error } = await this.#userClientCredentialRepository.requestDelete(payload);
|
||||
|
||||
if (!error) {
|
||||
this.#loadClientCredentials();
|
||||
}
|
||||
}
|
||||
|
||||
override render() {
|
||||
if (this._userKind !== UmbUserKind.API) return nothing;
|
||||
|
||||
return html`<uui-box>
|
||||
<div slot="headline">Client Credentials</div>
|
||||
<uui-ref-list>${this._clientCredentials.map((client) => html` ${this.#renderItem(client)} `)}</uui-ref-list>
|
||||
<uui-button
|
||||
id="add-button"
|
||||
look="placeholder"
|
||||
label=${this.localize.term('general_add')}
|
||||
@click=${this.#onAdd}></uui-button>
|
||||
</uui-box>`;
|
||||
}
|
||||
|
||||
#renderItem(client: UmbUserClientCredentialModel) {
|
||||
return html`
|
||||
<uui-ref-node name=${client.unique} readonly>
|
||||
<uui-icon slot="icon" name="icon-key"></uui-icon>
|
||||
<uui-button
|
||||
slot="actions"
|
||||
@click=${(event: Event) => this.#onDelete(event, client)}
|
||||
label="Delete ${client.unique}"
|
||||
compact
|
||||
><uui-icon name="icon-trash" look="danger"></uui-icon
|
||||
></uui-button>
|
||||
</uui-ref-node>
|
||||
`;
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
UmbTextStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
uui-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#add-button {
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
[elementName]: UmbUserWorkspaceClientCredentialsElement;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import type { UmbUserDisplayStatus } from '../../../utils.js';
|
||||
import { TimeFormatOptions, getDisplayStateFromUserStatus } from '../../../utils.js';
|
||||
import { UMB_USER_WORKSPACE_CONTEXT } from '../../user-workspace.context-token.js';
|
||||
import type { UmbUserDetailModel } from '../../../types.js';
|
||||
import { UmbUserKind } from '../../../utils/index.js';
|
||||
import { html, customElement, state, css, repeat, ifDefined, nothing } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||
@@ -42,6 +43,13 @@ export class UmbUserWorkspaceInfoElement extends UmbLitElement {
|
||||
}
|
||||
|
||||
this._userInfo = [
|
||||
{
|
||||
labelKey: 'user_kind',
|
||||
value:
|
||||
user.kind === UmbUserKind.API
|
||||
? this.localize.term('user_userKindApi')
|
||||
: this.localize.term('user_userKindDefault'),
|
||||
},
|
||||
{
|
||||
labelKey: 'user_lastLogin',
|
||||
value: user.lastLoginDate
|
||||
@@ -65,6 +73,11 @@ export class UmbUserWorkspaceInfoElement extends UmbLitElement {
|
||||
{ labelKey: 'user_updateDate', value: this.localize.date(user.updateDate!, TimeFormatOptions) },
|
||||
{ labelKey: 'general_id', value: user.unique },
|
||||
];
|
||||
|
||||
if (user.kind === UmbUserKind.API) {
|
||||
const include = ['user_kind', 'user_createDate', 'user_updateDate', 'general_id'];
|
||||
this._userInfo = this._userInfo.filter((item) => include.includes(item.labelKey));
|
||||
}
|
||||
};
|
||||
|
||||
override render() {
|
||||
@@ -104,12 +117,12 @@ export class UmbUserWorkspaceInfoElement extends UmbLitElement {
|
||||
static override styles = [
|
||||
UmbTextStyles,
|
||||
css`
|
||||
uui-tag {
|
||||
width: fit-content;
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#user-info {
|
||||
margin-bottom: var(--uui-size-space-4);
|
||||
uui-tag {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
#state {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||
import type { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
|
||||
import type { UmbUiCultureInputElement } from '@umbraco-cms/backoffice/localization';
|
||||
import { UmbUserKind } from '../../../utils/index.js';
|
||||
|
||||
@customElement('umb-user-workspace-profile-settings')
|
||||
export class UmbUserWorkspaceProfileSettingsElement extends UmbLitElement {
|
||||
@@ -84,9 +85,7 @@ export class UmbUserWorkspaceProfileSettingsElement extends UmbLitElement {
|
||||
}
|
||||
|
||||
#renderUsernameProperty() {
|
||||
if (this._usernameIsEmail) {
|
||||
return nothing;
|
||||
}
|
||||
if (this._usernameIsEmail) return nothing;
|
||||
|
||||
return html`
|
||||
<umb-property-layout
|
||||
@@ -106,6 +105,7 @@ export class UmbUserWorkspaceProfileSettingsElement extends UmbLitElement {
|
||||
}
|
||||
|
||||
#renderUILanguageProperty() {
|
||||
if (this._user?.kind === UmbUserKind.API) return nothing;
|
||||
return html`
|
||||
<umb-property-layout
|
||||
label="${this.localize.term('user_language')}"
|
||||
@@ -123,6 +123,10 @@ export class UmbUserWorkspaceProfileSettingsElement extends UmbLitElement {
|
||||
static override styles = [
|
||||
UmbTextStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
uui-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { UmbUserDetailModel } from '../index.js';
|
||||
import { UMB_USER_ENTITY_TYPE } from '../entity.js';
|
||||
import type { UmbUserWorkspaceContext } from './user-workspace.context.js';
|
||||
import { UMB_USER_WORKSPACE_CONTEXT } from './user-workspace.context-token.js';
|
||||
import type { UUIInputElement } from '@umbraco-cms/backoffice/external/uui';
|
||||
@@ -13,6 +12,7 @@ import './components/user-workspace-profile-settings/user-workspace-profile-sett
|
||||
import './components/user-workspace-access/user-workspace-access.element.js';
|
||||
import './components/user-workspace-info/user-workspace-info.element.js';
|
||||
import './components/user-workspace-avatar/user-workspace-avatar.element.js';
|
||||
import './components/user-workspace-client-credentials/user-workspace-client-credentials.element.js';
|
||||
|
||||
@customElement('umb-user-workspace-editor')
|
||||
export class UmbUserWorkspaceEditorElement extends UmbLitElement {
|
||||
@@ -65,6 +65,7 @@ export class UmbUserWorkspaceEditorElement extends UmbLitElement {
|
||||
<div id="header" slot="header">
|
||||
<uui-input id="name" .value=${this._user?.name ?? ''} @input="${this.#onNameChange}" ${umbFocus()}></uui-input>
|
||||
</div>
|
||||
<umb-workspace-entity-action-menu slot="action-menu"></umb-workspace-entity-action-menu>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -72,9 +73,11 @@ export class UmbUserWorkspaceEditorElement extends UmbLitElement {
|
||||
if (!this._user) return nothing;
|
||||
|
||||
return html`
|
||||
<umb-user-workspace-profile-settings></umb-user-workspace-profile-settings>
|
||||
<umb-user-workspace-assign-access></umb-user-workspace-assign-access>
|
||||
<umb-user-workspace-access></umb-user-workspace-access>
|
||||
<umb-stack>
|
||||
<umb-user-workspace-profile-settings></umb-user-workspace-profile-settings>
|
||||
<umb-user-workspace-assign-access></umb-user-workspace-assign-access>
|
||||
<umb-user-workspace-access></umb-user-workspace-access>
|
||||
</umb-stack>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -82,14 +85,11 @@ export class UmbUserWorkspaceEditorElement extends UmbLitElement {
|
||||
if (!this._user) return nothing;
|
||||
|
||||
return html`
|
||||
<umb-user-workspace-avatar></umb-user-workspace-avatar>
|
||||
<umb-user-workspace-info></umb-user-workspace-info>
|
||||
|
||||
<uui-box>
|
||||
<umb-entity-action-list
|
||||
.entityType=${UMB_USER_ENTITY_TYPE}
|
||||
.unique=${this._user.unique}></umb-entity-action-list>
|
||||
</uui-box>
|
||||
<umb-stack look="compact">
|
||||
<umb-user-workspace-avatar></umb-user-workspace-avatar>
|
||||
<umb-user-workspace-info></umb-user-workspace-info>
|
||||
<umb-user-workspace-client-credentials></umb-user-workspace-client-credentials>
|
||||
</umb-stack>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ export class UmbUserWorkspaceContext
|
||||
readonly data = this.#currentData.asObservable();
|
||||
readonly state = this.#currentData.asObservablePart((x) => x?.state);
|
||||
readonly unique = this.#currentData.asObservablePart((x) => x?.unique);
|
||||
readonly kind = this.#currentData.asObservablePart((x) => x?.kind);
|
||||
readonly userGroupUniques = this.#currentData.asObservablePart((x) => x?.userGroupUniques || []);
|
||||
readonly documentStartNodeUniques = this.#currentData.asObservablePart(
|
||||
(data) => data?.documentStartNodeUniques || [],
|
||||
|
||||
@@ -109,6 +109,7 @@ DON'T EDIT THIS FILE DIRECTLY. It is generated by /devops/tsconfig/index.js
|
||||
"@umbraco-cms/backoffice/store": ["./src/packages/core/store/index.ts"],
|
||||
"@umbraco-cms/backoffice/style": ["./src/packages/core/style/index.ts"],
|
||||
"@umbraco-cms/backoffice/stylesheet": ["./src/packages/templating/stylesheets/index.ts"],
|
||||
"@umbraco-cms/backoffice/sysinfo": ["./src/packages/sysinfo/index.ts"],
|
||||
"@umbraco-cms/backoffice/tags": ["./src/packages/tags/index.ts"],
|
||||
"@umbraco-cms/backoffice/template": ["./src/packages/templating/templates/index.ts"],
|
||||
"@umbraco-cms/backoffice/temporary-file": ["./src/packages/core/temporary-file/index.ts"],
|
||||
|
||||
Reference in New Issue
Block a user