Skip to content

Commit ab51aac

Browse files
iOvergaardclaudeleekelleher
authored
Backoffice Item Pickers: Show error for missing items in 10 picker types (closes #19329, #20270, #20367) (#20762)
* Add errorDetail property to umb-entity-item-ref Add optional errorDetail property to display additional context (such as file paths or IDs) in error states. This enhances the error display to show both the error message and relevant details. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Make _removeItem protected in UmbPickerInputContext Change #removeItem from private to protected to allow subclasses to reuse the removal logic while customizing the confirmation dialog. This enables better extensibility for specialized picker contexts. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix static file picker to show error state for missing files Update umb-input-static-file to observe statuses and render based on item state (loading, error, success). When a static file is missing (API returns empty array), displays error state with alert icon and file path detail using umb-entity-item-ref. Also adds standalone property support for proper single-item styling. Fixes #19329 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Show file path in static file remove confirmation dialog Override requestRemoveItem in UmbStaticFilePickerInputContext to display the file path instead of "Not found" in the confirmation dialog when removing missing static files. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Show GUID in document picker error state Display the document GUID as errorDetail when a document is not found (deleted/gone). This provides useful context for editors to identify which document was referenced. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Show GUID in document picker remove confirmation dialog Display the document GUID instead of "Not found" in the remove confirmation dialog when the document no longer exists. This provides useful context for editors. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: apply the temp model which the context uses * Refactor: Move requestRemoveItem logic to base UmbPickerInputContext Eliminated duplicate code across three picker contexts by: - Adding protected getItemDisplayName() method to base class - Moving requestRemoveItem implementation to base class - Removing duplicate implementations from document, member, and static file pickers - Static file picker overrides getItemDisplayName() to show file path Net reduction: 19 lines of code (69 removed, 50 added) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Document Type Picker: Show error state for missing items (fixes #20367) Apply the same error state handling to the document type picker that was implemented for static files, documents, and members. When a referenced document type is missing or deleted: - Show error state with the GUID as errorDetail - Allow removal with proper confirmation dialog - Use umb-entity-item-ref for error display - Use uui-ref-node-document-type for successful items 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * 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 <noreply@anthropic.com> * Lint: Remove unused 'when' imports from input-media-type and user-group-input * Refactor: Add #renderItem helper method to all pickers for consistency - Add #renderItem to user-input (extracted from inline repeat callback) - Change _renderItem to #renderItem in user-group-input for consistency - Change _renderItem to #renderItem in input-static-file for consistency All 10 pickers now use consistent #renderItem helper method pattern, improving code readability and maintainability as suggested by @nielslyngsoe * `import` sorting * Corrected (old) JSDoc typos * Markup tidy-up * exported `UmbPropertyEditorUIStaticFilePickerElement` as `element` --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: leekelleher <leekelleher@gmail.com>
1 parent afec900 commit ab51aac

File tree

15 files changed

+479
-214
lines changed

15 files changed

+479
-214
lines changed

src/Umbraco.Web.UI.Client/src/packages/core/entity-item/entity-item-ref/entity-item-ref.element.ts

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import type { ManifestEntityItemRef } from './entity-item-ref.extension.js';
2-
import { customElement, property, type PropertyValueMap, state, css, html } from '@umbraco-cms/backoffice/external/lit';
2+
import { css, customElement, html, property, state } from '@umbraco-cms/backoffice/external/lit';
33
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
44
import { UmbExtensionsElementInitializer } from '@umbraco-cms/backoffice/extension-api';
5-
import { UMB_MARK_ATTRIBUTE_NAME } from '@umbraco-cms/backoffice/const';
65
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
6+
import { UmbDeselectedEvent, UmbSelectedEvent } from '@umbraco-cms/backoffice/event';
77
import { UmbRoutePathAddendumContext } from '@umbraco-cms/backoffice/router';
8-
import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity';
8+
import { UMB_MARK_ATTRIBUTE_NAME } from '@umbraco-cms/backoffice/const';
99
import { UUIBlinkAnimationValue } from '@umbraco-cms/backoffice/external/uui';
10+
import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit';
11+
import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity';
1012

1113
import './default-item-ref.element.js';
12-
import { UmbDeselectedEvent, UmbSelectedEvent } from '@umbraco-cms/backoffice/event';
1314

1415
@customElement('umb-entity-item-ref')
1516
export class UmbEntityItemRefElement extends UmbLitElement {
@@ -20,9 +21,6 @@ export class UmbEntityItemRefElement extends UmbLitElement {
2021
private _component?: any; // TODO: Add type
2122

2223
@property({ type: Object, attribute: false })
23-
public get item(): UmbEntityModel | undefined {
24-
return this.#item;
25-
}
2624
public set item(value: UmbEntityModel | undefined) {
2725
const oldValue = this.#item;
2826
this.#item = value;
@@ -41,6 +39,9 @@ export class UmbEntityItemRefElement extends UmbLitElement {
4139
// If the component is already created, but the entity type is different, we need to destroy the component.
4240
this.#createController(value.entityType);
4341
}
42+
public get item(): UmbEntityModel | undefined {
43+
return this.#item;
44+
}
4445

4546
#readonly = false;
4647
@property({ type: Boolean, reflect: true })
@@ -124,20 +125,23 @@ export class UmbEntityItemRefElement extends UmbLitElement {
124125
error?: boolean;
125126

126127
@property({ type: String, attribute: 'error-message', reflect: false })
127-
errorMessage?: string;
128+
errorMessage?: string | null;
129+
130+
@property({ type: String, attribute: 'error-detail', reflect: false })
131+
errorDetail?: string | null;
128132

129133
#pathAddendum = new UmbRoutePathAddendumContext(this);
130134

131135
#onSelected(event: UmbSelectedEvent) {
132136
event.stopPropagation();
133-
const unique = this.#item?.unique;
137+
const unique = this.item?.unique;
134138
if (!unique) throw new Error('No unique id found for item');
135139
this.dispatchEvent(new UmbSelectedEvent(unique));
136140
}
137141

138142
#onDeselected(event: UmbDeselectedEvent) {
139143
event.stopPropagation();
140-
const unique = this.#item?.unique;
144+
const unique = this.item?.unique;
141145
if (!unique) throw new Error('No unique id found for item');
142146
this.dispatchEvent(new UmbDeselectedEvent(unique));
143147
}
@@ -163,7 +167,7 @@ export class UmbEntityItemRefElement extends UmbLitElement {
163167

164168
// TODO: I would say this code can use feature of the UmbExtensionsElementInitializer, to set properties and get a fallback element. [NL]
165169
// assign the properties to the component
166-
component.item = this.#item;
170+
component.item = this.item;
167171
component.readonly = this.readonly;
168172
component.standalone = this.standalone;
169173
component.selectOnly = this.selectOnly;
@@ -192,20 +196,25 @@ export class UmbEntityItemRefElement extends UmbLitElement {
192196
if (this._component) {
193197
return html`${this._component}`;
194198
}
199+
195200
// Error:
196201
if (this.error) {
197-
return html`<uui-ref-node
198-
style="color: var(--uui-color-danger);"
199-
.name=${this.localize.string(this.errorMessage ?? '#general_notFound')}
200-
.readonly=${this.readonly}
201-
.standalone=${this.standalone}
202-
.selectOnly=${this.selectOnly}
203-
.selected=${this.selected}
204-
.disabled=${this.disabled}>
205-
<uui-icon slot="icon" name="icon-alert" style="color: var(--uui-color-danger);"></uui-icon>
206-
<slot name="actions"></slot>
207-
</uui-ref-node>`;
202+
return html`
203+
<uui-ref-node
204+
style="color: var(--uui-color-danger);"
205+
.name=${this.localize.string(this.errorMessage ?? '#general_notFound')}
206+
.detail=${this.errorDetail ?? ''}
207+
.readonly=${this.readonly}
208+
.standalone=${this.standalone}
209+
.selectOnly=${this.selectOnly}
210+
.selected=${this.selected}
211+
.disabled=${this.disabled}>
212+
<uui-icon slot="icon" name="icon-alert" style="color: var(--uui-color-danger);"></uui-icon>
213+
<slot name="actions"></slot>
214+
</uui-ref-node>
215+
`;
208216
}
217+
209218
// Loading:
210219
return html`<uui-loader-bar style="margin-top:10px;"></uui-loader-bar>`;
211220
}

src/Umbraco.Web.UI.Client/src/packages/core/picker-input/picker-input.context.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ export class UmbPickerInputContext<
3131
public readonly interactionMemory = new UmbInteractionMemoryManager(this);
3232

3333
/**
34-
* Define a minimum amount of selected items in this input, for this input to be valid.
35-
* @returns {number} The minimum number of items required.
34+
* Define a maximum amount of selected items in this input, for this input to be valid.
35+
* @returns {number} The maximum number of items required.
3636
*/
3737
public get max() {
3838
return this._max;
@@ -43,7 +43,7 @@ export class UmbPickerInputContext<
4343
private _max = Infinity;
4444

4545
/**
46-
* Define a maximum amount of selected items in this input, for this input to be valid.
46+
* Define a minimum amount of selected items in this input, for this input to be valid.
4747
* @returns {number} The minimum number of items required.
4848
*/
4949
public get min() {
@@ -111,21 +111,32 @@ export class UmbPickerInputContext<
111111
this.getHostElement().dispatchEvent(new UmbChangeEvent());
112112
}
113113

114+
/**
115+
* Get the display name for an item to show in the remove confirmation dialog.
116+
* Subclasses can override this to provide custom formatting for missing items.
117+
* @param item - The item to get the display name for, or undefined if not found
118+
* @param unique - The unique identifier of the item
119+
* @returns The display name to show in the dialog
120+
*/
121+
protected getItemDisplayName(item: PickedItemType | undefined, unique: string): string {
122+
return item?.name ?? unique;
123+
}
124+
114125
async requestRemoveItem(unique: string) {
115126
const item = this.#itemManager.getItems().find((item) => item.unique === unique);
127+
const name = this.getItemDisplayName(item, unique);
116128

117-
const name = item?.name ?? '#general_notFound';
118129
await umbConfirmModal(this, {
119130
color: 'danger',
120-
headline: `#actions_remove ${name}?`,
131+
headline: `#actions_remove?`,
121132
content: `#defaultdialogs_confirmremove ${name}?`,
122133
confirmLabel: '#actions_remove',
123134
});
124135

125-
this.#removeItem(unique);
136+
this._removeItem(unique);
126137
}
127138

128-
#removeItem(unique: string) {
139+
protected _removeItem(unique: string) {
129140
const newSelection = this.getSelection().filter((value) => value !== unique);
130141
this.setSelection(newSelection);
131142
this.getHostElement().dispatchEvent(new UmbChangeEvent());

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

Lines changed: 65 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@ import type { UmbDocumentTypeItemModel, UmbDocumentTypeTreeItemModel } from '../
22
import { UMB_DOCUMENT_TYPE_WORKSPACE_MODAL } from '../../constants.js';
33
import { UMB_EDIT_DOCUMENT_TYPE_WORKSPACE_PATH_PATTERN } from '../../paths.js';
44
import { UmbDocumentTypePickerInputContext } from './input-document-type.context.js';
5-
import { css, html, customElement, property, state, repeat, nothing, when } from '@umbraco-cms/backoffice/external/lit';
5+
import { css, customElement, html, nothing, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit';
66
import { splitStringToArray } from '@umbraco-cms/backoffice/utils';
77
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
8+
import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation';
89
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
910
import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router';
1011
import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
11-
import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation';
12+
import type { UmbRepositoryItemsStatus } from '@umbraco-cms/backoffice/repository';
13+
14+
import '@umbraco-cms/backoffice/entity-item';
1215

1316
@customElement('umb-input-document-type')
1417
export class UmbInputDocumentTypeElement extends UmbFormControlMixin<string | undefined, typeof UmbLitElement>(
@@ -112,6 +115,9 @@ export class UmbInputDocumentTypeElement extends UmbFormControlMixin<string | un
112115
@state()
113116
private _items?: Array<UmbDocumentTypeItemModel>;
114117

118+
@state()
119+
private _statuses?: Array<UmbRepositoryItemsStatus>;
120+
115121
@state()
116122
private _editPath = '';
117123

@@ -143,6 +149,7 @@ export class UmbInputDocumentTypeElement extends UmbFormControlMixin<string | un
143149

144150
this.observe(this.#pickerContext.selection, (selection) => (this.value = selection.join(',')), '_observeSelection');
145151
this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems), '_observerItems');
152+
this.observe(this.#pickerContext.statuses, (statuses) => (this._statuses = statuses), '_observeStatuses');
146153
}
147154

148155
protected override getFormElement() {
@@ -151,8 +158,8 @@ export class UmbInputDocumentTypeElement extends UmbFormControlMixin<string | un
151158

152159
#getPickableFilter() {
153160
if (this.documentTypesOnly) {
154-
/* TODO: We do not have the same model in the tree and during the search, so theoretically, we cannot use the same filter.
155-
The search item model does not include "isFolder," so it checks for falsy intentionally.
161+
/* TODO: We do not have the same model in the tree and during the search, so theoretically, we cannot use the same filter.
162+
The search item model does not include "isFolder," so it checks for falsy intentionally.
156163
We need to investigate getting this typed correctly. [MR] */
157164
return (x: UmbDocumentTypeTreeItemModel) => !x.isFolder && x.isElement === false;
158165
}
@@ -184,8 +191,8 @@ export class UmbInputDocumentTypeElement extends UmbFormControlMixin<string | un
184191
);
185192
}
186193

187-
#removeItem(item: UmbDocumentTypeItemModel) {
188-
this.#pickerContext.requestRemoveItem(item.unique);
194+
#removeItem(unique: string) {
195+
this.#pickerContext.requestRemoveItem(unique);
189196
}
190197

191198
override render() {
@@ -204,38 +211,66 @@ export class UmbInputDocumentTypeElement extends UmbFormControlMixin<string | un
204211
}
205212

206213
#renderItems() {
207-
if (!this._items) return nothing;
214+
if (!this._statuses) return nothing;
208215
return html`
209216
<uui-ref-list>
210217
${repeat(
211-
this._items,
212-
(item) => item.unique,
213-
(item) => this.#renderItem(item),
218+
this._statuses,
219+
(status) => status.unique,
220+
(status) => {
221+
const unique = status.unique;
222+
const item = this._items?.find((x) => x.unique === unique);
223+
const isError = status.state.type === 'error';
224+
225+
// For error state, use umb-entity-item-ref
226+
if (isError) {
227+
return html`
228+
<umb-entity-item-ref
229+
id=${unique}
230+
.item=${item}
231+
?error=${true}
232+
.errorMessage=${status.state.error}
233+
.errorDetail=${unique}
234+
?readonly=${this.readonly}
235+
?standalone=${this.max === 1}>
236+
${when(
237+
!this.readonly,
238+
() => html`
239+
<uui-action-bar slot="actions">
240+
<uui-button
241+
label=${this.localize.term('general_remove')}
242+
@click=${() => this.#removeItem(unique)}></uui-button>
243+
</uui-action-bar>
244+
`,
245+
)}
246+
</umb-entity-item-ref>
247+
`;
248+
}
249+
250+
// For successful items, use the document type specific component
251+
if (!item) return nothing;
252+
const href = this._editPath + UMB_EDIT_DOCUMENT_TYPE_WORKSPACE_PATH_PATTERN.generateLocal({ unique });
253+
return html`
254+
<uui-ref-node-document-type id=${unique} name=${this.localize.string(item.name)} href=${href}>
255+
${this.#renderIcon(item)}
256+
<uui-action-bar slot="actions">
257+
${when(
258+
!this.readonly,
259+
() => html`
260+
<uui-button
261+
label=${this.localize.term('general_remove')}
262+
@click=${() => this.#removeItem(unique)}></uui-button>
263+
`,
264+
)}
265+
</uui-action-bar>
266+
</uui-ref-node-document-type>
267+
`;
268+
},
214269
)}
215270
</uui-ref-list>
216271
`;
217272
}
218273

219-
#renderItem(item: UmbDocumentTypeItemModel) {
220-
if (!item.unique) return;
221-
const href = this._editPath + UMB_EDIT_DOCUMENT_TYPE_WORKSPACE_PATH_PATTERN.generateLocal({ unique: item.unique });
222-
return html`
223-
<uui-ref-node-document-type id=${item.unique} name=${this.localize.string(item.name)} href=${href}>
224-
${this.#renderIcon(item)}
225-
<uui-action-bar slot="actions">
226-
${when(
227-
!this.readonly,
228-
() => html`
229-
<uui-button
230-
label=${this.localize.term('general_remove')}
231-
@click=${() => this.#removeItem(item)}></uui-button>
232-
`,
233-
)}
234-
</uui-action-bar>
235-
</uui-ref-node-document-type>
236-
`;
237-
}
238-
239274
#renderIcon(item: UmbDocumentTypeItemModel) {
240275
if (!item.icon) return;
241276
return html`<umb-icon slot="icon" name=${item.icon}></umb-icon>`;

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

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -239,24 +239,28 @@ export class UmbInputDocumentElement extends UmbFormControlMixin<string | undefi
239239
(status) => {
240240
const unique = status.unique;
241241
const item = this._items?.find((x) => x.unique === unique);
242-
return html`<umb-entity-item-ref
243-
id=${unique}
244-
.item=${item}
245-
?error=${status.state.type === 'error'}
246-
.errorMessage=${status.state.error}
247-
?readonly=${this.readonly}
248-
?standalone=${this.max === 1}>
249-
${when(
250-
!this.readonly,
251-
() => html`
252-
<uui-action-bar slot="actions">
253-
<uui-button
254-
label=${this.localize.term('general_remove')}
255-
@click=${() => this.#onRemove(unique)}></uui-button>
256-
</uui-action-bar>
257-
`,
258-
)}
259-
</umb-entity-item-ref>`;
242+
const isError = status.state.type === 'error';
243+
return html`
244+
<umb-entity-item-ref
245+
id=${unique}
246+
.item=${item}
247+
?error=${isError}
248+
.errorMessage=${status.state.error}
249+
.errorDetail=${isError ? unique : undefined}
250+
?readonly=${this.readonly}
251+
?standalone=${this.max === 1}>
252+
${when(
253+
!this.readonly,
254+
() => html`
255+
<uui-action-bar slot="actions">
256+
<uui-button
257+
label=${this.localize.term('general_remove')}
258+
@click=${() => this.#onRemove(unique)}></uui-button>
259+
</uui-action-bar>
260+
`,
261+
)}
262+
</umb-entity-item-ref>
263+
`;
260264
},
261265
)}
262266
</uui-ref-list>

0 commit comments

Comments
 (0)