Skip to content

Commit 2a723ad

Browse files
iOvergaardclaude
andcommitted
Additional pickers: Show error states for missing items in user, language, media-type, member-type, member-group, and user-group pickers
Apply the same error state handling pattern to six additional picker types: - user-input: Users - input-language: Languages - input-media-type: Media types - input-member-type: Member types - input-member-group: Member groups - user-group-input: User groups All pickers now: - Observe statuses from UmbRepositoryItemsManager - Show error state with GUID when referenced item is missing/deleted - Use umb-entity-item-ref for error display - Use specialized components (uui-ref-node, umb-user-group-ref, etc.) for successful items - Allow removal with proper confirmation dialog showing GUID Maintains code reusability by using the base class requestRemoveItem method with getItemDisplayName() for consistent error handling across all pickers. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 56cec08 commit 2a723ad

File tree

6 files changed

+231
-90
lines changed

6 files changed

+231
-90
lines changed

src/Umbraco.Web.UI.Client/src/packages/language/components/input-language/input-language.element.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
66
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
77
import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
88
import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui';
9+
import type { UmbRepositoryItemsStatus } from '@umbraco-cms/backoffice/repository';
910

1011
@customElement('umb-input-language')
1112
export class UmbInputLanguageElement extends UUIFormControlMixin(UmbLitElement, '') {
@@ -115,6 +116,9 @@ export class UmbInputLanguageElement extends UUIFormControlMixin(UmbLitElement,
115116
@state()
116117
private _items: Array<UmbLanguageItemModel> = [];
117118

119+
@state()
120+
private _statuses?: Array<UmbRepositoryItemsStatus>;
121+
118122
#pickerContext = new UmbLanguagePickerInputContext(this);
119123

120124
constructor() {
@@ -134,6 +138,7 @@ export class UmbInputLanguageElement extends UUIFormControlMixin(UmbLitElement,
134138

135139
this.observe(this.#pickerContext.selection, (selection) => (this.value = selection.join(',')), '_observeSelection');
136140
this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems), '_observerItems');
141+
this.observe(this.#pickerContext.statuses, (statuses) => (this._statuses = statuses), '_observeStatuses');
137142
}
138143

139144
protected override getFormElement() {
@@ -147,8 +152,8 @@ export class UmbInputLanguageElement extends UUIFormControlMixin(UmbLitElement,
147152
});
148153
}
149154

150-
#onRemove(item: UmbLanguageItemModel) {
151-
this.#pickerContext.requestRemoveItem(item.unique);
155+
#onRemove(unique: string) {
156+
this.#pickerContext.requestRemoveItem(unique);
152157
}
153158

154159
override render() {
@@ -167,16 +172,22 @@ export class UmbInputLanguageElement extends UUIFormControlMixin(UmbLitElement,
167172
}
168173

169174
#renderItems() {
170-
if (!this._items) return;
175+
if (!this._statuses) return;
171176
return html`
172177
<uui-ref-list>
173178
${repeat(
174-
this._items,
175-
(item) => item.unique,
176-
(item) =>
177-
html`<umb-entity-item-ref
178-
id=${item.unique}
179+
this._statuses,
180+
(status) => status.unique,
181+
(status) => {
182+
const unique = status.unique;
183+
const item = this._items?.find((x) => x.unique === unique);
184+
const isError = status.state.type === 'error';
185+
return html`<umb-entity-item-ref
186+
id=${unique}
179187
.item=${item}
188+
?error=${isError}
189+
.errorMessage=${status.state.error}
190+
.errorDetail=${isError ? unique : undefined}
180191
?readonly=${this.readonly}
181192
?standalone=${this.max === 1}>
182193
${when(
@@ -185,11 +196,12 @@ export class UmbInputLanguageElement extends UUIFormControlMixin(UmbLitElement,
185196
<uui-action-bar slot="actions">
186197
<uui-button
187198
label=${this.localize.term('general_remove')}
188-
@click=${() => this.#onRemove(item)}></uui-button>
199+
@click=${() => this.#onRemove(unique)}></uui-button>
189200
</uui-action-bar>
190201
`,
191202
)}
192-
</umb-entity-item-ref>`,
203+
</umb-entity-item-ref>`;
204+
},
193205
)}
194206
</uui-ref-list>
195207
`;

src/Umbraco.Web.UI.Client/src/packages/media/media-types/components/input-media-type/input-media-type.element.ts

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import type { UmbMediaTypeItemModel } from '../../types.js';
22
import { UmbMediaTypePickerInputContext } from './input-media-type.context.js';
3-
import { css, html, customElement, property, state, repeat, nothing } from '@umbraco-cms/backoffice/external/lit';
3+
import { css, html, customElement, property, state, repeat, nothing, when } from '@umbraco-cms/backoffice/external/lit';
44
import { splitStringToArray } from '@umbraco-cms/backoffice/utils';
55
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
66
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
77
import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/workspace';
88
import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router';
99
import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
1010
import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation';
11+
import type { UmbRepositoryItemsStatus } from '@umbraco-cms/backoffice/repository';
12+
import '@umbraco-cms/backoffice/entity-item';
1113

1214
@customElement('umb-input-media-type')
1315
export class UmbInputMediaTypeElement extends UmbFormControlMixin<string | undefined, typeof UmbLitElement>(
@@ -95,6 +97,9 @@ export class UmbInputMediaTypeElement extends UmbFormControlMixin<string | undef
9597
@state()
9698
private _items?: Array<UmbMediaTypeItemModel>;
9799

100+
@state()
101+
private _statuses?: Array<UmbRepositoryItemsStatus>;
102+
98103
@state()
99104
private _editPath = '';
100105

@@ -126,6 +131,7 @@ export class UmbInputMediaTypeElement extends UmbFormControlMixin<string | undef
126131

127132
this.observe(this.#pickerContext.selection, (selection) => (this.value = selection.join(',')), '_observeSelection');
128133
this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems), '_observerItems');
134+
this.observe(this.#pickerContext.statuses, (statuses) => (this._statuses = statuses), '_observeStatuses');
129135
}
130136

131137
protected override getFormElement() {
@@ -138,8 +144,8 @@ export class UmbInputMediaTypeElement extends UmbFormControlMixin<string | undef
138144
});
139145
}
140146

141-
#removeItem(item: UmbMediaTypeItemModel) {
142-
this.#pickerContext.requestRemoveItem(item.unique);
147+
#removeItem(unique: string) {
148+
this.#pickerContext.requestRemoveItem(unique);
143149
}
144150

145151
override render() {
@@ -158,32 +164,52 @@ export class UmbInputMediaTypeElement extends UmbFormControlMixin<string | undef
158164
}
159165

160166
#renderItems() {
161-
if (!this._items) return nothing;
167+
if (!this._statuses) return nothing;
162168
return html`
163169
<uui-ref-list>
164170
${repeat(
165-
this._items,
166-
(item) => item.unique,
167-
(item) => this.#renderItem(item),
171+
this._statuses,
172+
(status) => status.unique,
173+
(status) => {
174+
const unique = status.unique;
175+
const item = this._items?.find((x) => x.unique === unique);
176+
const isError = status.state.type === 'error';
177+
178+
// For error state, use umb-entity-item-ref
179+
if (isError) {
180+
return html`<umb-entity-item-ref
181+
id=${unique}
182+
.item=${item}
183+
?error=${true}
184+
.errorMessage=${status.state.error}
185+
.errorDetail=${unique}
186+
?standalone=${this.max === 1}>
187+
<uui-action-bar slot="actions">
188+
<uui-button
189+
label=${this.localize.term('general_remove')}
190+
@click=${() => this.#removeItem(unique)}></uui-button>
191+
</uui-action-bar>
192+
</umb-entity-item-ref>`;
193+
}
194+
195+
// For successful items, use the media type specific component
196+
if (!item) return nothing;
197+
const href = `${this._editPath}edit/${unique}`;
198+
return html`
199+
<uui-ref-node-document-type name=${this.localize.string(item.name)} id=${unique}>
200+
${this.#renderIcon(item)}
201+
<uui-action-bar slot="actions">
202+
<uui-button href=${href} label=${this.localize.term('general_open')}></uui-button>
203+
<uui-button @click=${() => this.#removeItem(unique)} label=${this.localize.term('general_remove')}></uui-button>
204+
</uui-action-bar>
205+
</uui-ref-node-document-type>
206+
`;
207+
},
168208
)}
169209
</uui-ref-list>
170210
`;
171211
}
172212

173-
#renderItem(item: UmbMediaTypeItemModel) {
174-
if (!item.unique) return;
175-
const href = `${this._editPath}edit/${item.unique}`;
176-
return html`
177-
<uui-ref-node-document-type name=${this.localize.string(item.name)} id=${item.unique}>
178-
${this.#renderIcon(item)}
179-
<uui-action-bar slot="actions">
180-
<uui-button href=${href} label=${this.localize.term('general_open')}></uui-button>
181-
<uui-button @click=${() => this.#removeItem(item)} label=${this.localize.term('general_remove')}></uui-button>
182-
</uui-action-bar>
183-
</uui-ref-node-document-type>
184-
`;
185-
}
186-
187213
#renderIcon(item: UmbMediaTypeItemModel) {
188214
if (!item.icon) return;
189215
return html`<umb-icon slot="icon" name=${item.icon}></umb-icon>`;

src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/input-member-group/input-member-group.element.ts

Lines changed: 59 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import type { UmbMemberGroupItemModel } from '../../types.js';
22
import { UmbMemberGroupPickerInputContext } from './input-member-group.context.js';
3-
import { css, html, customElement, property, state, repeat, nothing } from '@umbraco-cms/backoffice/external/lit';
3+
import { css, html, customElement, property, state, repeat, nothing, when } from '@umbraco-cms/backoffice/external/lit';
44
import { splitStringToArray } from '@umbraco-cms/backoffice/utils';
55
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
66
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
77
import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
88
import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/workspace';
99
import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router';
1010
import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation';
11+
import type { UmbRepositoryItemsStatus } from '@umbraco-cms/backoffice/repository';
12+
import '@umbraco-cms/backoffice/entity-item';
1113

1214
@customElement('umb-input-member-group')
1315
export class UmbInputMemberGroupElement extends UmbFormControlMixin<string | undefined, typeof UmbLitElement>(
@@ -124,6 +126,9 @@ export class UmbInputMemberGroupElement extends UmbFormControlMixin<string | und
124126
@state()
125127
private _items?: Array<UmbMemberGroupItemModel>;
126128

129+
@state()
130+
private _statuses?: Array<UmbRepositoryItemsStatus>;
131+
127132
#pickerContext = new UmbMemberGroupPickerInputContext(this);
128133

129134
constructor() {
@@ -152,6 +157,7 @@ export class UmbInputMemberGroupElement extends UmbFormControlMixin<string | und
152157

153158
this.observe(this.#pickerContext.selection, (selection) => (this.value = selection.join(',')), '_observeSelection');
154159
this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems), '_observeItems');
160+
this.observe(this.#pickerContext.statuses, (statuses) => (this._statuses = statuses), '_observeStatuses');
155161
}
156162

157163
protected override getFormElement() {
@@ -164,22 +170,68 @@ export class UmbInputMemberGroupElement extends UmbFormControlMixin<string | und
164170
});
165171
}
166172

167-
#removeItem(item: UmbMemberGroupItemModel) {
168-
this.#pickerContext.requestRemoveItem(item.unique);
173+
#removeItem(unique: string) {
174+
this.#pickerContext.requestRemoveItem(unique);
169175
}
170176

171177
override render() {
172178
return html`${this.#renderItems()} ${this.#renderAddButton()}`;
173179
}
174180

175181
#renderItems() {
176-
if (!this._items) return;
182+
if (!this._statuses) return;
177183
return html`
178184
<uui-ref-list>
179185
${repeat(
180-
this._items,
181-
(item) => item.unique,
182-
(item) => this.#renderItem(item),
186+
this._statuses,
187+
(status) => status.unique,
188+
(status) => {
189+
const unique = status.unique;
190+
const item = this._items?.find((x) => x.unique === unique);
191+
const isError = status.state.type === 'error';
192+
193+
// For error state, use umb-entity-item-ref
194+
if (isError) {
195+
return html`<umb-entity-item-ref
196+
id=${unique}
197+
?error=${true}
198+
.errorMessage=${status.state.error}
199+
.errorDetail=${unique}
200+
?readonly=${this.readonly}
201+
?standalone=${this.max === 1}>
202+
${when(
203+
!this.readonly,
204+
() => html`
205+
<uui-action-bar slot="actions">
206+
<uui-button
207+
label=${this.localize.term('general_remove')}
208+
@click=${() => this.#removeItem(unique)}></uui-button>
209+
</uui-action-bar>
210+
`,
211+
)}
212+
</umb-entity-item-ref>`;
213+
}
214+
215+
// For successful items, use uui-ref-node
216+
if (!item) return nothing;
217+
return html`
218+
<uui-ref-node
219+
name=${item.name}
220+
id=${unique}
221+
href="${this._editMemberGroupPath}edit/${unique}"
222+
?readonly=${this.readonly}>
223+
<uui-action-bar slot="actions">
224+
${when(
225+
!this.readonly,
226+
() => html`<uui-button
227+
@click=${() => this.#removeItem(unique)}
228+
label=${this.localize.term('general_remove')}></uui-button>`,
229+
)}
230+
</uui-action-bar>
231+
<umb-icon slot="icon" name="icon-users"></umb-icon>
232+
</uui-ref-node>
233+
`;
234+
},
183235
)}
184236
</uui-ref-list>
185237
`;
@@ -199,27 +251,6 @@ export class UmbInputMemberGroupElement extends UmbFormControlMixin<string | und
199251
}
200252
}
201253

202-
#renderItem(item: UmbMemberGroupItemModel) {
203-
if (!item.unique) return nothing;
204-
return html`
205-
<uui-ref-node
206-
name=${item.name}
207-
id=${item.unique}
208-
href="${this._editMemberGroupPath}edit/${item.unique}"
209-
?readonly=${this.readonly}>
210-
<uui-action-bar slot="actions"> ${this.#renderRemoveButton(item)} </uui-action-bar>
211-
<umb-icon slot="icon" name="icon-users"></umb-icon>
212-
</uui-ref-node>
213-
`;
214-
}
215-
216-
#renderRemoveButton(item: UmbMemberGroupItemModel) {
217-
if (this.readonly) return nothing;
218-
return html`<uui-button
219-
@click=${() => this.#removeItem(item)}
220-
label=${this.localize.term('general_remove')}></uui-button>`;
221-
}
222-
223254
static override styles = [
224255
css`
225256
#btn-add {

0 commit comments

Comments
 (0)