Skip to content

Commit 42f49eb

Browse files
OskarKrugerDESKTOP-16T01G5\Umbracoleekelleher
authored
Visual update to user group modal (#17934)
* Initial commit of changes * final touches on styling * Refactored to observe and resolve the document/media node names --------- Co-authored-by: DESKTOP-16T01G5\Umbraco <[email protected]> Co-authored-by: leekelleher <[email protected]>
1 parent 44c3080 commit 42f49eb

File tree

2 files changed

+230
-50
lines changed

2 files changed

+230
-50
lines changed

src/Umbraco.Web.UI.Client/src/packages/user/user-group/components/user-group-ref/user-group-ref.element.ts

Lines changed: 186 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { UUIRefNodeElement } from '@umbraco-cms/backoffice/external/uui';
2-
import { customElement, html, property } from '@umbraco-cms/backoffice/external/lit';
2+
import { css, customElement, html, property, state } from '@umbraco-cms/backoffice/external/lit';
33
import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api';
44
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
5-
import { map } from '@umbraco-cms/backoffice/external/rxjs';
6-
import type { ManifestEntityUserPermission } from '@umbraco-cms/backoffice/user-permission';
5+
import { UmbDocumentItemRepository } from '@umbraco-cms/backoffice/document';
6+
import { UmbMediaItemRepository } from '@umbraco-cms/backoffice/media';
77

88
/**
99
* @element umb-user-group-ref
@@ -12,6 +12,39 @@ import type { ManifestEntityUserPermission } from '@umbraco-cms/backoffice/user-
1212
*/
1313
@customElement('umb-user-group-ref')
1414
export class UmbUserGroupRefElement extends UmbElementMixin(UUIRefNodeElement) {
15+
#documentItemRepository?: UmbDocumentItemRepository;
16+
#mediaItemRepository?: UmbMediaItemRepository;
17+
18+
@property({ type: Boolean })
19+
documentRootAccess: boolean = false;
20+
21+
@property()
22+
public get documentStartNode(): string | null | undefined {
23+
return '';
24+
}
25+
public set documentStartNode(value: string | null | undefined) {
26+
this.#observeDocumentStartNode(value);
27+
}
28+
29+
@property({ type: Boolean })
30+
mediaRootAccess: boolean = false;
31+
32+
@property()
33+
public get mediaStartNode(): string | null | undefined {
34+
return '';
35+
}
36+
public set mediaStartNode(value: string | null | undefined) {
37+
this.#observeMediaStartNode(value);
38+
}
39+
40+
@property({ type: Array })
41+
public get sections(): Array<string> {
42+
return [];
43+
}
44+
public set sections(value: Array<string>) {
45+
this.#observeSections(value);
46+
}
47+
1548
@property({ type: Array, attribute: 'user-permission-aliases' })
1649
public get userPermissionAliases(): Array<string> {
1750
return [];
@@ -20,45 +53,171 @@ export class UmbUserGroupRefElement extends UmbElementMixin(UUIRefNodeElement) {
2053
this.#observeUserPermissions(value);
2154
}
2255

23-
#userPermissionLabels: Array<string> = [];
56+
@state()
57+
private _documentLabel: string = '';
58+
59+
@state()
60+
private _mediaLabel: string = '';
61+
62+
@state()
63+
private _sectionLabels: Array<string> = [];
64+
65+
@state()
66+
private _userPermissionLabels: Array<string> = [];
67+
68+
async #observeDocumentStartNode(unique: string | null | undefined) {
69+
if (this.documentRootAccess) return;
70+
if (!unique) return;
71+
72+
if (!this.#documentItemRepository) {
73+
this.#documentItemRepository = new UmbDocumentItemRepository(this);
74+
}
75+
76+
const { error, asObservable } = await this.#documentItemRepository.requestItems([unique]);
77+
if (error) return;
78+
79+
this.observe(
80+
asObservable(),
81+
(data) => (this._documentLabel = data[0].variants?.[0].name ?? unique),
82+
'_observeDocumentStartNode',
83+
);
84+
}
85+
86+
async #observeMediaStartNode(unique: string | null | undefined) {
87+
if (this.mediaRootAccess) return;
88+
if (!unique) return;
2489

25-
async #observeUserPermissions(value: Array<string>) {
26-
if (value) {
90+
if (!this.#mediaItemRepository) {
91+
this.#mediaItemRepository = new UmbMediaItemRepository(this);
92+
}
93+
94+
const { error, asObservable } = await this.#mediaItemRepository.requestItems([unique]);
95+
if (error) return;
96+
97+
this.observe(
98+
asObservable(),
99+
(data) => (this._mediaLabel = data[0].variants?.[0].name ?? unique),
100+
'_observeMediaStartNode',
101+
);
102+
}
103+
104+
async #observeSections(aliases: Array<string>) {
105+
if (aliases?.length) {
27106
this.observe(
28-
umbExtensionsRegistry.byType('entityUserPermission').pipe(
29-
map((manifests) => {
30-
return manifests.filter((manifest) => manifest.alias && value.includes(manifest.alias));
31-
}),
32-
),
33-
(userPermissionManifests) => this.#setUserPermissionLabels(userPermissionManifests),
34-
'userPermissionLabels',
107+
umbExtensionsRegistry.byTypeAndAliases('section', aliases),
108+
(manifests) => {
109+
this._sectionLabels = manifests.map((manifest) =>
110+
manifest.meta.label ? this.localize.string(manifest.meta.label) : manifest.name,
111+
);
112+
},
113+
'_observeSections',
35114
);
36115
} else {
37-
this.removeUmbControllerByAlias('userPermissionLabels');
116+
this.removeUmbControllerByAlias('_observeSections');
38117
}
39118
}
40119

41-
#setUserPermissionLabels(manifests: Array<ManifestEntityUserPermission>) {
42-
this.#userPermissionLabels = manifests.map((manifest) =>
43-
manifest.meta.label ? this.localize.string(manifest.meta.label) : manifest.name,
44-
);
120+
async #observeUserPermissions(aliases: Array<string>) {
121+
if (aliases?.length) {
122+
this.observe(
123+
umbExtensionsRegistry.byTypeAndAliases('entityUserPermission', aliases),
124+
(manifests) => {
125+
this._userPermissionLabels = manifests.map((manifest) =>
126+
manifest.meta.label ? this.localize.string(manifest.meta.label) : manifest.name,
127+
);
128+
},
129+
'_observeUserPermission',
130+
);
131+
} else {
132+
this.removeUmbControllerByAlias('_observeUserPermission');
133+
}
45134
}
46135

47136
protected override renderDetail() {
48-
const details: string[] = [];
137+
return html`
138+
<small id="detail">${this.detail}</small>
139+
${this.#renderDetails()}
140+
<slot name="detail"></slot>
141+
`;
142+
}
49143

50-
if (this.#userPermissionLabels.length > 0) {
51-
details.push(this.#userPermissionLabels.join(', '));
52-
}
144+
#renderDetails() {
145+
const hasSections = this._sectionLabels.length;
146+
const hasDocument = !!this._documentLabel || this.documentRootAccess;
147+
const hasMedia = !!this._mediaLabel || this.mediaRootAccess;
148+
const hasUserPermissions = this._userPermissionLabels.length;
53149

54-
if (this.detail !== '') {
55-
details.push(this.detail);
56-
}
150+
if (!hasSections && !hasDocument && !hasMedia && !hasUserPermissions) return;
151+
152+
return html`
153+
<div id="details">
154+
${this.#renderSections()} ${this.#renderDocumentStartNode()} ${this.#renderMediaStartNode()}
155+
${this.#renderUserPermissions()}
156+
</div>
157+
`;
158+
}
159+
160+
#renderSections() {
161+
if (!this._sectionLabels.length) return;
162+
return html`
163+
<div>
164+
<small>
165+
<strong><umb-localize key="main_sections">Sections</umb-localize>:</strong>
166+
${this._sectionLabels.join(', ')}
167+
</small>
168+
</div>
169+
`;
170+
}
57171

58-
return html`<small id="detail">${details.join(' | ')}<slot name="detail"></slot></small>`;
172+
#renderDocumentStartNode() {
173+
if (!this._documentLabel && !this.documentRootAccess) return;
174+
return html`
175+
<div>
176+
<small>
177+
<strong><umb-localize key="user_startnode">Document Start Node</umb-localize>:</strong>
178+
${this.documentRootAccess ? this.localize.term('contentTypeEditor_allDocuments') : this._documentLabel}
179+
</small>
180+
</div>
181+
`;
59182
}
60183

61-
static override styles = [...UUIRefNodeElement.styles];
184+
#renderMediaStartNode() {
185+
if (!this._mediaLabel && !this.mediaRootAccess) return;
186+
return html`
187+
<div>
188+
<small>
189+
<strong><umb-localize key="user_mediastartnode">Media Start Node</umb-localize>:</strong>
190+
${this.mediaRootAccess ? this.localize.term('contentTypeEditor_allMediaItems') : this._mediaLabel}
191+
</small>
192+
</div>
193+
`;
194+
}
195+
196+
#renderUserPermissions() {
197+
if (!this._userPermissionLabels.length) return;
198+
return html`
199+
<div>
200+
<small>
201+
<strong><umb-localize key="user_userPermissions">User permissions</umb-localize>:</strong>
202+
${this._userPermissionLabels.join(', ')}
203+
</small>
204+
</div>
205+
`;
206+
}
207+
208+
static override styles = [
209+
...UUIRefNodeElement.styles,
210+
css`
211+
#details {
212+
color: var(--uui-color-text-alt);
213+
margin-top: var(--uui-size-space-1);
214+
}
215+
216+
#details > div {
217+
margin-bottom: var(--uui-size-space-1);
218+
}
219+
`,
220+
];
62221
}
63222

64223
declare global {

src/Umbraco.Web.UI.Client/src/packages/user/user-group/modals/user-group-picker/user-group-picker-modal.element.ts

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,64 @@
11
import { UmbUserGroupCollectionRepository } from '../../collection/repository/index.js';
22
import type { UmbUserGroupDetailModel } from '../../types.js';
3-
import { html, customElement, state, ifDefined } from '@umbraco-cms/backoffice/external/lit';
3+
import { customElement, html, repeat, state, when } from '@umbraco-cms/backoffice/external/lit';
4+
import { UmbDeselectedEvent, UmbSelectedEvent } from '@umbraco-cms/backoffice/event';
45
import { UmbSelectionManager } from '@umbraco-cms/backoffice/utils';
5-
import type { UMB_USER_GROUP_PICKER_MODAL } from '@umbraco-cms/backoffice/user-group';
66
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
7+
import type { UMB_USER_GROUP_PICKER_MODAL } from '@umbraco-cms/backoffice/user-group';
78
import type { UUIMenuItemEvent } from '@umbraco-cms/backoffice/external/uui';
8-
import { UmbSelectedEvent, UmbDeselectedEvent } from '@umbraco-cms/backoffice/event';
9+
10+
import '../../components/user-group-ref/user-group-ref.element.js';
911

1012
@customElement('umb-user-group-picker-modal')
1113
export class UmbUserGroupPickerModalElement extends UmbModalBaseElement<
12-
(typeof UMB_USER_GROUP_PICKER_MODAL)['DATA'],
13-
(typeof UMB_USER_GROUP_PICKER_MODAL)['VALUE']
14+
typeof UMB_USER_GROUP_PICKER_MODAL.DATA,
15+
typeof UMB_USER_GROUP_PICKER_MODAL.VALUE
1416
> {
1517
@state()
1618
private _userGroups: Array<UmbUserGroupDetailModel> = [];
1719

1820
#selectionManager = new UmbSelectionManager(this);
1921
#userGroupCollectionRepository = new UmbUserGroupCollectionRepository(this);
2022

21-
override connectedCallback(): void {
23+
constructor() {
24+
super();
25+
26+
this.#observeUserGroups();
27+
}
28+
29+
override connectedCallback() {
2230
super.connectedCallback();
2331

24-
// TODO: in theory this config could change during the lifetime of the modal, so we could observe it
2532
this.#selectionManager.setSelectable(true);
2633
this.#selectionManager.setMultiple(this.data?.multiple ?? false);
2734
this.#selectionManager.setSelection(this.value?.selection ?? []);
28-
this.observe(this.#selectionManager.selection, (selection) => this.updateValue({ selection }), 'selectionObserver');
29-
}
3035

31-
protected override firstUpdated(): void {
32-
this.#observeUserGroups();
36+
this.observe(this.#selectionManager.selection, (selection) => this.updateValue({ selection }), 'selectionObserver');
3337
}
3438

3539
async #observeUserGroups() {
3640
const { error, asObservable } = await this.#userGroupCollectionRepository.requestCollection();
3741
if (error) return;
42+
3843
this.observe(asObservable(), (items) => (this._userGroups = items), 'umbUserGroupsObserver');
3944
}
4045

4146
#onSelected(event: UUIMenuItemEvent, item: UmbUserGroupDetailModel) {
4247
if (!item.unique) throw new Error('User group unique is required');
4348
event.stopPropagation();
49+
4450
this.#selectionManager.select(item.unique);
51+
4552
this.requestUpdate();
4653
this.modalContext?.dispatchEvent(new UmbSelectedEvent(item.unique));
4754
}
4855

4956
#onDeselected(event: UUIMenuItemEvent, item: UmbUserGroupDetailModel) {
5057
if (!item.unique) throw new Error('User group unique is required');
5158
event.stopPropagation();
59+
5260
this.#selectionManager.deselect(item.unique);
61+
5362
this.requestUpdate();
5463
this.modalContext?.dispatchEvent(new UmbDeselectedEvent(item.unique));
5564
}
@@ -61,24 +70,36 @@ export class UmbUserGroupPickerModalElement extends UmbModalBaseElement<
6170

6271
override render() {
6372
return html`
64-
<umb-body-layout headline=${this.localize.term('user_selectUserGroup', false)}>
73+
<umb-body-layout headline=${this.localize.term('user_selectUserGroup', true)}>
6574
<uui-box>
66-
${this._userGroups.map(
67-
(item) => html`
68-
<uui-menu-item
69-
label=${ifDefined(item.name)}
75+
${repeat(
76+
this._userGroups,
77+
(userGroup) => userGroup.alias,
78+
(userGroup) => html`
79+
<umb-user-group-ref
80+
.name=${userGroup.name}
81+
select-only
7082
selectable
71-
@selected=${(event: UUIMenuItemEvent) => this.#onSelected(event, item)}
72-
@deselected=${(event: UUIMenuItemEvent) => this.#onDeselected(event, item)}
73-
?selected=${this.#selectionManager.isSelected(item.unique)}>
74-
<umb-icon .name=${item.icon || undefined} slot="icon"></umb-icon>
75-
</uui-menu-item>
83+
?selected=${this.#selectionManager.isSelected(userGroup.unique)}
84+
?documentRootAccess=${userGroup.documentRootAccess}
85+
.documentStartNode=${!userGroup.documentRootAccess ? userGroup.documentStartNode?.unique : null}
86+
?mediaRootAccess=${userGroup.mediaRootAccess}
87+
.mediaStartNode=${!userGroup.mediaRootAccess ? userGroup.mediaStartNode?.unique : null}
88+
.sections=${userGroup.sections}
89+
@selected=${(event: UUIMenuItemEvent) => this.#onSelected(event, userGroup)}
90+
@deselected=${(event: UUIMenuItemEvent) => this.#onDeselected(event, userGroup)}>
91+
${when(userGroup.icon, () => html`<umb-icon name=${userGroup.icon!} slot="icon"></umb-icon>`)}
92+
</umb-user-group-ref>
7693
`,
7794
)}
7895
</uui-box>
7996
<div slot="actions">
80-
<uui-button label="Close" @click=${this._rejectModal}></uui-button>
81-
<uui-button label="Submit" look="primary" color="positive" @click=${this.#onSubmit}></uui-button>
97+
<uui-button label=${this.localize.term('general_cancel')} @click=${this._rejectModal}></uui-button>
98+
<uui-button
99+
label=${this.localize.term('buttons_select')}
100+
look="primary"
101+
color="positive"
102+
@click=${this.#onSubmit}></uui-button>
82103
</div>
83104
</umb-body-layout>
84105
`;

0 commit comments

Comments
 (0)