Merge branch 'v15/dev' into v15/feature/user-sidebar-menu

This commit is contained in:
Mads Rasmussen
2024-09-10 12:53:25 +02:00
94 changed files with 10976 additions and 1340 deletions

View File

@@ -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:

View File

@@ -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:

View File

@@ -16,10 +16,12 @@ on:
branches:
- main
- release/*
- v*/dev
pull_request:
branches:
- main
- release/*
- v*/dev
schedule:
- cron: '33 2 * * 1'

View File

@@ -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",

View File

@@ -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'),

View File

@@ -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`

View File

@@ -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',

View File

@@ -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:

View File

@@ -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',

View File

@@ -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',

View File

@@ -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

View File

@@ -72,7 +72,7 @@ const handlers = [
...userGroupsHandlers,
...userHandlers,
...documentBlueprintHandlers,
serverHandlers.serverInformationHandler,
...serverHandlers.serverInformationHandlers,
];
switch (import.meta.env.VITE_UMBRACO_INSTALL_STATUS) {

View File

@@ -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,
},
];

View File

@@ -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,
};

View File

@@ -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,
},
];

View File

@@ -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,
};
};

View File

@@ -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,

View File

@@ -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' },
],
}),
);
}),
];

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -2308,6 +2308,10 @@
"name": "icon-unlocked",
"file": "lock-open.svg"
},
{
"name": "icon-unplug",
"file": "unplug.svg"
},
{
"name": "icon-untitled",
"file": "box.svg",

View File

@@ -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"),

View File

@@ -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>
`;

View File

@@ -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}>

View File

@@ -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}>

View File

@@ -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;
}
}

View File

@@ -0,0 +1,3 @@
export * from './components/sysinfo.element.js';
export * from './modals/index.js';
export * from './repository/index.js';

View 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];

View File

@@ -0,0 +1 @@
export * from './sysinfo-modal.token.js';

View File

@@ -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',
},
});

View File

@@ -0,0 +1,8 @@
{
"name": "@umbraco-backoffice/sysinfo",
"private": true,
"type": "module",
"scripts": {
"build": "vite build"
}
}

View File

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

View File

@@ -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;
}
}

View File

@@ -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'),
},
];

View File

@@ -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 }),
});

View File

@@ -14,6 +14,11 @@ export const manifests: Array<ManifestTypes | UmbBackofficeManifestKind> = [
icon: 'icon-key',
label: '#user_changePassword',
},
conditions: [
{
alias: 'Umb.Condition.User.AllowChangePassword',
},
],
},
{
type: 'modal',

View File

@@ -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>
`;
}

View File

@@ -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);

View File

@@ -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,
});

View File

@@ -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>`;
}

View File

@@ -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,
});

View File

@@ -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%;
}

View File

@@ -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];

View File

@@ -0,0 +1 @@
export const UMB_CREATE_USER_CLIENT_CREDENTIAL_MODAL_ALIAS = 'Umb.Modal.User.ClientCredential.Create';

View File

@@ -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;
}
}

View File

@@ -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',
},
});

View File

@@ -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'),
},
];

View File

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

View File

@@ -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];

View File

@@ -0,0 +1 @@
export const UMB_USER_CLIENT_CREDENTIAL_REPOSITORY_ALIAS = 'Umb.Repository.User.ClientCredential';

View File

@@ -0,0 +1,2 @@
export * from './types.js';
export * from './user-client-credential.server.data-source.js';

View File

@@ -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>;
}

View File

@@ -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,
}),
);
}
}

View File

@@ -0,0 +1,3 @@
export * from './user-client-credential.repository.js';
export * from './constants.js';
export * from './types.js';

View File

@@ -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'),
},
];

View File

@@ -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;
}

View File

@@ -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 };

View File

@@ -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;
}
}

View File

@@ -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);
});
}
}

View File

@@ -1 +0,0 @@
export { UmbCreateUserCollectionAction } from './create-user.collection-action.js';

View File

@@ -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,

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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,
},
},
{

View File

@@ -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';

View File

@@ -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;
}
}

View File

@@ -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);
}
`,

View File

@@ -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'),
},
];

View File

@@ -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 };

View File

@@ -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,
];

View File

@@ -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',

View File

@@ -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,
];

View File

@@ -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%;
}

View File

@@ -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',

View File

@@ -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>`;
}

View File

@@ -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;
}
`,

View File

@@ -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,
});

View File

@@ -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(

View File

@@ -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;
}

View File

@@ -33,5 +33,6 @@ const mapper = (item: UserItemResponseModel): UmbUserItemModel => {
entityType: UMB_USER_ENTITY_TYPE,
name: item.name,
unique: item.id,
kind: item.kind,
};
};

View File

@@ -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 {

View File

@@ -1 +1,8 @@
export * from './is-user.function.js';
export type UmbUserKindType = 'Default' | 'Api';
export const UmbUserKind = Object.freeze({
DEFAULT: 'Default',
API: 'Api',
});

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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;
}
}

View File

@@ -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 {

View File

@@ -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%;
}

View File

@@ -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>
`;
}

View File

@@ -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 || [],

View File

@@ -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"],