@yuuvis/client-framework 2.18.0 → 2.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/breadcrumb/index.d.ts +2 -0
  2. package/breadcrumb/lib/breadcrumb/breadcrumb.component.d.ts +94 -0
  3. package/breadcrumb/lib/models/breadcrumb-item.model.d.ts +14 -0
  4. package/breadcrumb/lib/models/index.d.ts +1 -0
  5. package/common/lib/components/confirm/confirm.component.d.ts +1 -0
  6. package/common/lib/components/confirm/confirm.interface.d.ts +2 -0
  7. package/fesm2022/yuuvis-client-framework-breadcrumb.mjs +117 -0
  8. package/fesm2022/yuuvis-client-framework-breadcrumb.mjs.map +1 -0
  9. package/fesm2022/yuuvis-client-framework-common.mjs +24 -10
  10. package/fesm2022/yuuvis-client-framework-common.mjs.map +1 -1
  11. package/fesm2022/yuuvis-client-framework-forms.mjs +2 -2
  12. package/fesm2022/yuuvis-client-framework-forms.mjs.map +1 -1
  13. package/fesm2022/yuuvis-client-framework-list.mjs +365 -121
  14. package/fesm2022/yuuvis-client-framework-list.mjs.map +1 -1
  15. package/fesm2022/yuuvis-client-framework-object-details.mjs +28 -26
  16. package/fesm2022/yuuvis-client-framework-object-details.mjs.map +1 -1
  17. package/fesm2022/yuuvis-client-framework-object-form.mjs.map +1 -1
  18. package/fesm2022/yuuvis-client-framework-object-relationship.mjs +6 -5
  19. package/fesm2022/yuuvis-client-framework-object-relationship.mjs.map +1 -1
  20. package/fesm2022/yuuvis-client-framework-object-versions.mjs +1 -1
  21. package/fesm2022/yuuvis-client-framework-object-versions.mjs.map +1 -1
  22. package/fesm2022/yuuvis-client-framework-query-list.mjs +462 -127
  23. package/fesm2022/yuuvis-client-framework-query-list.mjs.map +1 -1
  24. package/fesm2022/yuuvis-client-framework-renderer.mjs +14 -16
  25. package/fesm2022/yuuvis-client-framework-renderer.mjs.map +1 -1
  26. package/fesm2022/yuuvis-client-framework-sort.mjs +26 -15
  27. package/fesm2022/yuuvis-client-framework-sort.mjs.map +1 -1
  28. package/fesm2022/yuuvis-client-framework-tile-list.mjs +709 -182
  29. package/fesm2022/yuuvis-client-framework-tile-list.mjs.map +1 -1
  30. package/lib/assets/i18n/ar.json +217 -0
  31. package/lib/assets/i18n/de.json +7 -3
  32. package/lib/assets/i18n/en.json +7 -3
  33. package/list/lib/list.component.d.ts +256 -44
  34. package/object-details/lib/object-details-header/object-details-header.component.d.ts +5 -2
  35. package/object-details/lib/object-details-shell/object-details-shell.component.d.ts +5 -2
  36. package/object-details/lib/object-details.component.d.ts +3 -1
  37. package/object-relationship/lib/object-relationship.component.d.ts +5 -2
  38. package/package.json +8 -4
  39. package/query-list/lib/query-list.component.d.ts +381 -86
  40. package/tile-list/lib/tile-list/tile-list.component.d.ts +527 -72
@@ -1,7 +1,7 @@
1
1
  import * as i0 from '@angular/core';
2
- import { input, output, viewChild, ChangeDetectionStrategy, Component, Injectable, inject, ViewContainerRef, effect, Directive, DestroyRef, ElementRef, contentChild, computed, viewChildren, signal, linkedSignal, untracked } from '@angular/core';
2
+ import { input, output, viewChild, ChangeDetectionStrategy, Component, Injectable, inject, ViewContainerRef, effect, Directive, DestroyRef, ElementRef, contentChild, viewChildren, computed, signal, linkedSignal, untracked } from '@angular/core';
3
3
  import * as i1 from '@yuuvis/client-core';
4
- import { ObjectConfigService, DmsService, DmsObject, SearchService, BaseObjectTypeField, TranslateModule, SystemService, ContentStreamField, Utils, Sort } from '@yuuvis/client-core';
4
+ import { ObjectConfigService, DmsService, SearchService, DmsObject, BaseObjectTypeField, TranslateModule, SystemService, ContentStreamField, Utils, Sort } from '@yuuvis/client-core';
5
5
  import { coerceBooleanProperty } from '@angular/cdk/coercion';
6
6
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
7
7
  import * as i1$2 from '@angular/forms';
@@ -43,10 +43,15 @@ class TileActionsMenuComponent {
43
43
  this.itemSelect.emit(action);
44
44
  }
45
45
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: TileActionsMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
46
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.20", type: TileActionsMenuComponent, isStandalone: true, selector: "yuv-tile-actions-menu", inputs: { actions: { classPropertyName: "actions", publicName: "actions", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { itemSelect: "itemSelect" }, providers: [{ provide: MAT_TOOLTIP_DEFAULT_OPTIONS, useValue: { position: 'below' } }], viewQueries: [{ propertyName: "matMenu", first: true, predicate: ["menuRef"], descendants: true, isSignal: true }], ngImport: i0, template: `
46
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.20", type: TileActionsMenuComponent, isStandalone: true, selector: "yuv-tile-actions-menu", inputs: { actions: { classPropertyName: "actions", publicName: "actions", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { itemSelect: "itemSelect" }, providers: [{ provide: MAT_TOOLTIP_DEFAULT_OPTIONS, useValue: { position: 'right' } }], viewQueries: [{ propertyName: "matMenu", first: true, predicate: ["menuRef"], descendants: true, isSignal: true }], ngImport: i0, template: `
47
47
  <mat-menu #menuRef>
48
48
  @for (action of actions(); track action.id) {
49
- <button mat-menu-item (click)="itemClicked($event, action)" [matTooltip]="action.description" [matTooltipDisabled]="!action.description">
49
+ <button
50
+ mat-menu-item
51
+ (click)="itemClicked($event, action)"
52
+ [matTooltip]="action.description"
53
+ [matTooltipDisabled]="!action.description"
54
+ >
50
55
  @if (action.icon) {
51
56
  <mat-icon>{{ action.icon }}</mat-icon>
52
57
  }
@@ -61,7 +66,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
61
66
  args: [{ selector: 'yuv-tile-actions-menu', imports: [MatMenu, MatIcon, MatMenuItem, MatTooltip], template: `
62
67
  <mat-menu #menuRef>
63
68
  @for (action of actions(); track action.id) {
64
- <button mat-menu-item (click)="itemClicked($event, action)" [matTooltip]="action.description" [matTooltipDisabled]="!action.description">
69
+ <button
70
+ mat-menu-item
71
+ (click)="itemClicked($event, action)"
72
+ [matTooltip]="action.description"
73
+ [matTooltipDisabled]="!action.description"
74
+ >
65
75
  @if (action.icon) {
66
76
  <mat-icon>{{ action.icon }}</mat-icon>
67
77
  }
@@ -69,7 +79,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
69
79
  </button>
70
80
  }
71
81
  </mat-menu>
72
- `, providers: [{ provide: MAT_TOOLTIP_DEFAULT_OPTIONS, useValue: { position: 'below' } }], changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block}\n"] }]
82
+ `, providers: [{ provide: MAT_TOOLTIP_DEFAULT_OPTIONS, useValue: { position: 'right' } }], changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block}\n"] }]
73
83
  }] });
74
84
 
75
85
  class TileExtensionService {
@@ -139,151 +149,413 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
139
149
  }]
140
150
  }] });
141
151
 
152
+ const MATERIAL_IMPORTS = [MatIconModule, MatPaginatorModule, MatTooltipModule, MatMenuTrigger];
142
153
  /**
143
- * List that renders the result of a search query as object config based tiles. It also contains a component to
144
- * set up that configuration.
154
+ * Query-driven tile list that renders `DmsObject` search results as rich, configurable
155
+ * tiles based on `ObjectConfig` definitions.
156
+ *
157
+ * The component wraps `yuv-query-list` and adds DMS-specific concerns: it resolves the
158
+ * `ObjectConfigRecord` for each result item (via `ObjectConfigService`), maps raw
159
+ * `SearchResultItem` data to the `TileData` view model, and supports per-object-type
160
+ * flavors, inline actions, context menus, and keyboard shortcuts for copy/cut.
161
+ *
162
+ * **Key Features:**
163
+ * - Automatic object config resolution (title, description, icon, meta, aside, actions)
164
+ * - Per-tile action buttons rendered from the resolved `ObjectConfigRecord`
165
+ * - Optional `ObjectFlavor` overlay to switch the visual representation of specific SOTs
166
+ * - Server-side pagination forwarded from the inner `yuv-query-list`
167
+ * - Multi-selection with Shift/Ctrl modifier keys and mouse drag-to-select
168
+ * - Programmatic pre-selection by object ID (`preselect` input, `selectById()` method)
169
+ * - Per-item CSS highlight styles via the `highlights` input
170
+ * - Optimistic list updates without a full re-fetch (`updateListItems`, `updateTileList`, `dropItems`)
171
+ * - Keyboard shortcuts: **Ctrl+C** emits `tileCopy`, **Ctrl+X** emits `tileCut`
172
+ * - Optional custom context menu via projected `TileActionsMenuComponent`
173
+ * - Isolated tile config bucket so multiple instances can have different column layouts
174
+ *
175
+ * **Content Projection Slots:**
176
+ * - `TileActionsMenuComponent` — optional; project a `<yuv-tile-actions-menu>` to attach
177
+ * a context menu to every tile's action trigger.
178
+ * - `#empty` — optional template reference; shown when the query returns no results.
179
+ *
180
+ * **Basic usage:**
181
+ * ```html
182
+ * <yuv-tile-list [query]="query" (itemSelect)="onSelect($event)" />
183
+ * ```
145
184
  *
185
+ * **Multi-select with custom bucket and highlights:**
186
+ * ```html
187
+ * <yuv-tile-list
188
+ * bucket="my-feature"
189
+ * [query]="query"
190
+ * [multiselect]="true"
191
+ * [highlights]="highlights"
192
+ * (selectionChange)="onSelectionChange($event)"
193
+ * />
194
+ * ```
195
+ *
196
+ * **With context menu:**
197
+ * ```html
198
+ * <yuv-tile-list [query]="query" (ctxMenu)="onCtxMenu($event)">
199
+ * <yuv-tile-actions-menu>
200
+ * <button mat-menu-item (click)="openDetails()">Open</button>
201
+ * </yuv-tile-actions-menu>
202
+ * </yuv-tile-list>
203
+ * ```
146
204
  */
147
- const MATERIAL_IMPORTS = [MatIconModule, MatPaginatorModule, MatTooltipModule, MatMenuTrigger];
148
205
  class TileListComponent {
206
+ //#region Dependencies
149
207
  #objectConfigService;
150
208
  #destroyRef;
151
209
  #elRef;
152
210
  #actionService;
153
211
  #dmsService;
154
- onCopy(event) {
155
- event.preventDefault();
156
- if (this._selection.length)
157
- this.tileCopy.emit(this._selectionToTileData(this._selection));
158
- }
159
- onCut(event) {
160
- event.preventDefault();
161
- if (this._selection.length)
162
- this.tileCut.emit(this._selectionToTileData(this._selection));
163
- }
164
212
  #busy;
165
213
  #preselect;
166
214
  #rawResultItems;
215
+ //#endregion
167
216
  constructor() {
217
+ //#region Dependencies
168
218
  this.#objectConfigService = inject(ObjectConfigService);
169
219
  this.#destroyRef = inject(DestroyRef);
170
220
  this.#elRef = inject(ElementRef);
171
221
  this.#actionService = inject(ActionsService);
172
222
  this.#dmsService = inject(DmsService);
223
+ //#endregion
224
+ //#region Angular stuff
225
+ /**
226
+ * Optional projected `TileActionsMenuComponent` instance.
227
+ *
228
+ * When present, its `matMenu()` is wired as the context menu for every tile's
229
+ * action trigger button. The menu closes automatically whenever the user selects
230
+ * a menu item (managed by `#closeMenuEffect`).
231
+ *
232
+ * Project it into the component:
233
+ * ```html
234
+ * <yuv-tile-list ...>
235
+ * <yuv-tile-actions-menu>
236
+ * <button mat-menu-item>Open</button>
237
+ * </yuv-tile-actions-menu>
238
+ * </yuv-tile-list>
239
+ * ```
240
+ */
173
241
  this.menuComponent = contentChild(TileActionsMenuComponent);
174
- this.menu = computed(() => {
175
- const comp = this.menuComponent();
176
- return comp?.matMenu() ?? null;
177
- });
242
+ /**
243
+ * Optional projected element shown when the query returns an empty result set.
244
+ *
245
+ * Reference the element with the `#empty` template variable:
246
+ * ```html
247
+ * <yuv-tile-list ...>
248
+ * <div #empty>No items found.</div>
249
+ * </yuv-tile-list>
250
+ * ```
251
+ */
178
252
  this.emptyContent = contentChild('empty');
253
+ /**
254
+ * Reference to the inner `QueryListComponent` instance.
255
+ *
256
+ * Used internally to delegate imperative operations (select, multiSelect, refresh, …).
257
+ * Prefer the public API methods on this component over accessing `list()` directly from
258
+ * the parent, as the inner list's API may change independently.
259
+ */
179
260
  this.list = viewChild.required('list');
180
- this.menuTriggers = viewChildren(MatMenuTrigger);
181
- this.transformer = (res) => {
182
- this.#rawResultItems = res;
183
- const mappedItems = this.#mapToTileData(res.map((i) => new DmsObject(i)));
184
- const items = mappedItems.map((item) => ({
185
- ...item,
186
- actions: (item.actions || [])
187
- .map((a) => this.#actionService.getActionById(a.id, this.options()?.actionContext))
188
- .filter((a) => a !== undefined)
189
- }));
190
- // untracked(() => this.items.set(items));
191
- return items;
192
- };
193
- this.#busy = computed(() => this.list().busy());
194
- this._selection = [];
195
- this.selectedTile = signal([]);
196
261
  /**
197
- * The ID of the selected list item
262
+ * All `MatMenuTrigger` instances rendered inside the tile list.
263
+ *
264
+ * Used by `#closeMenuEffect` to close every open context/action menu when the user
265
+ * selects a menu item, ensuring only one menu is open at a time.
198
266
  */
199
- this.selection = signal([]);
267
+ this.menuTriggers = viewChildren(MatMenuTrigger);
200
268
  /**
201
- * Tile configurations are stored globally for all apps. If you want a
202
- * separate config for your app/component you can specify a bucket. A bucket
203
- * is basically an ID where your custom tile config will be stored and
204
- * retrieved. Buckets should be unique so be sure to use a unique namespace.
269
+ * Namespace key for storing and retrieving this instance's tile configuration.
270
+ *
271
+ * Tile column/field layout is persisted globally via `ObjectConfigService`. Providing
272
+ * a bucket isolates this instance's configuration from the global default, so two
273
+ * tile lists with different purposes (e.g. inbox vs. archive) can each remember their
274
+ * own preferred layout independently.
275
+ *
276
+ * Use a unique, stable string — e.g. `"my-app.inbox-list"`. If omitted, the global
277
+ * default configuration is used.
205
278
  */
206
279
  this.bucket = input();
207
280
  /**
208
- * The number of items to display per page.
281
+ * Number of result items to request per page from the search service.
282
+ *
283
+ * Forwarded to the inner `QueryListComponent`. When the total result count exceeds
284
+ * this value, pagination controls appear. Reducing this number improves initial load
285
+ * time; increasing it reduces the need for pagination.
286
+ *
287
+ * @default SearchService.DEFAULT_QUERY_SIZE
209
288
  */
210
289
  this.pageSize = input(SearchService.DEFAULT_QUERY_SIZE);
211
290
  /**
212
- * Sets up the ability to select multiple tiles
291
+ * Enables multi-selection mode.
292
+ *
293
+ * When `true`, the user can hold **Shift** to range-select or **Ctrl** to toggle
294
+ * individual tiles, and mouse drag-to-select becomes available. `selectionChange`
295
+ * then emits the full selection array; `itemSelect` still emits only the most
296
+ * recently added single tile.
297
+ *
213
298
  * @default false
214
299
  */
215
300
  this.multiselect = input(false);
216
301
  /**
217
- * If `true`, the tiles will be rendered in a more compact, denser style.
302
+ * Renders tiles in a compact, reduced-height style.
303
+ *
304
+ * Drives the `[class.dense]` host binding. Use this when vertical space is limited
305
+ * or when many items need to be visible at once without scrolling.
306
+ *
218
307
  * @default false
219
308
  */
220
309
  this.dense = input(false);
221
310
  /**
222
- * Configuration options for the tile list component.
311
+ * Extended configuration options for the tile list.
312
+ *
313
+ * See `TileListConfigOptions` for the full set of options, including:
314
+ * - `actionContext` — passed to `ActionsService.getActionById()` when resolving inline
315
+ * tile actions, so actions can behave differently per context.
316
+ * - `configTypes` — virtual config type overrides that let you map specific
317
+ * object-type / SOT combinations to a different `ObjectConfigRecord` entry.
223
318
  */
224
319
  this.options = input(undefined);
225
320
  /**
226
- * The object flavor to be applied to the tiles.
321
+ * Object flavor to overlay on matching tiles.
322
+ *
323
+ * An `ObjectFlavor` defines an alternative visual representation for objects that carry
324
+ * a specific SOT. When set, tiles whose `DmsObject.sots` array includes
325
+ * `flavor.sot` are rendered using the config entry identified by `flavor.id` instead
326
+ * of their own object-type config.
327
+ *
328
+ * Changes are applied reactively via `#flavorEffect` and re-evaluated against the
329
+ * last raw result set via `applyFlavor()`.
227
330
  */
228
331
  this.flavor = input();
229
332
  /**
230
- * The search query to be executed. This may be a SearchQuery object or a CMIS query statement.
231
- * Ensure that the query includes the object type ID field to allow proper tile rendering.
333
+ * The search query to execute.
334
+ *
335
+ * Accepts a structured `SearchQuery` object or a raw CMIS query string. The query is
336
+ * re-executed reactively every time this input changes. The result set must include the
337
+ * `system:objectTypeId` field — tiles without a resolved object type will throw at
338
+ * mapping time.
232
339
  */
233
340
  this.query = input();
341
+ /**
342
+ * Object IDs to select as soon as the list finishes loading.
343
+ *
344
+ * If the list is still busy when this input is set, the IDs are stored internally and
345
+ * applied once the query completes (via `#preselectEffect`). Items whose IDs are not
346
+ * found in the current result set are silently ignored.
347
+ *
348
+ * For programmatic selection after the list is loaded, prefer `selectById()`.
349
+ *
350
+ * @default []
351
+ */
234
352
  this.preselect = input([]);
235
- this.#preselect = linkedSignal(this.preselect);
353
+ /**
354
+ * Per-item CSS style overrides rendered as inline styles on matching tiles.
355
+ *
356
+ * Each `TileListHighlight` entry specifies an array of object IDs and a
357
+ * `cssStyles` record. All styles for the same ID are merged, with later entries in
358
+ * the array taking precedence. Used to visually call out specific items (e.g. newly
359
+ * created, recently modified, flagged).
360
+ *
361
+ * @default []
362
+ */
236
363
  this.highlights = input([]);
237
- this.highlightStyles = computed(() => {
238
- const x = {};
239
- (this.highlights() || []).forEach((highlight) => {
240
- highlight.ids.forEach((id) => {
241
- if (!x[id])
242
- x[id] = {};
243
- x[id] = { ...x[id], ...highlight.cssStyles };
244
- });
245
- });
246
- return x;
247
- });
248
364
  /**
249
- * Prevent selection changes while the provided function returns false.
365
+ * Guard function that temporarily blocks all selection changes.
366
+ *
367
+ * Forwarded to the inner `ListComponent`. As long as the returned predicate evaluates
368
+ * to `true`, any attempt to change the selection is silently ignored. Useful when the
369
+ * parent has unsaved changes tied to the current selection.
370
+ *
371
+ * @default () => false (never prevents)
250
372
  */
251
373
  this.preventChangeUntil = input(() => false);
252
374
  /**
253
- * If `true`, the list will select an item automatically on initialization.
254
- * First, list will search for an item item that has the "selected"-attribute
255
- * and is not disabled. If no such item exists, the first item will be selected.
375
+ * Automatically selects an item when the list is first rendered.
376
+ *
377
+ * Follows the same priority as `ListComponent.autoSelect`: first non-disabled item
378
+ * with the `selected` attribute, then index 0. Accepts any truthy string so it can
379
+ * be set as a plain HTML attribute: `<yuv-tile-list autoSelect>`.
380
+ *
381
+ * @default false
382
+ */
383
+ this.autoSelect = input(false, {
384
+ transform: (value) => coerceBooleanProperty(value)
385
+ });
386
+ /**
387
+ * Suppresses the component's built-in context menu handling.
388
+ *
389
+ * When `true`, right-clicking a tile does not call `event.preventDefault()`, does not
390
+ * auto-select the right-clicked tile, and does not emit `ctxMenu`. Use this when the
391
+ * parent wants to handle `contextmenu` events itself.
392
+ *
256
393
  * @default false
257
394
  */
258
- this.autoSelect = input(false, { transform: (value) => coerceBooleanProperty(value) });
259
395
  this.disableCustomContextMenu = input(false);
260
396
  /**
261
- * Emitted when a list item has been selected
397
+ * Emits the `TileData` of the most recently selected tile.
398
+ *
399
+ * In single-select mode this fires for every selection change. In multi-select mode
400
+ * it fires only when exactly one tile ends up selected (i.e. a plain click with no
401
+ * modifier keys). For the full multi-selection result, listen to `selectionChange`.
262
402
  */
263
403
  this.itemSelect = output();
404
+ /**
405
+ * Emits the currently selected tiles when the user presses **Ctrl+C**.
406
+ *
407
+ * The parent is responsible for handling the actual copy operation (e.g. writing to
408
+ * the clipboard or storing the objects for a subsequent paste action). The default
409
+ * browser copy behavior is suppressed.
410
+ */
264
411
  this.tileCopy = output();
412
+ /**
413
+ * Emits the currently selected tiles when the user presses **Ctrl+X**.
414
+ *
415
+ * The parent is responsible for handling the cut operation. The default browser cut
416
+ * behavior is suppressed. Typically used together with `dropItems()` on a target list
417
+ * to implement a move-via-clipboard interaction.
418
+ */
265
419
  this.tileCut = output();
420
+ /**
421
+ * Mirrors the inner `QueryListComponent.busy` signal as an output event.
422
+ *
423
+ * Emits `true` when a query request starts and `false` when it completes (or errors).
424
+ * Useful when the parent needs to react to loading state changes without holding a
425
+ * `@ViewChild` reference to this component.
426
+ */
266
427
  this.busy = output();
428
+ /**
429
+ * Emits once per query execution when the search result arrives.
430
+ *
431
+ * Provides the server-side total item count and the raw `SearchResultItem[]` for the
432
+ * current page. Forwarded directly from the inner `QueryListComponent`.
433
+ */
267
434
  this.queryResult = output();
268
435
  /**
269
- * Emitted when selected items changed. If 'multiselect' input is set to true, this will
270
- * emit the whole selection, while 'itemSelect' will only emit the item that currently
271
- * has bee added to the selection.
436
+ * Emits the full current selection as `TileData[]` on every selection change.
437
+ *
438
+ * Unlike `itemSelect` which emits a single tile — this output always reflects the
439
+ * complete selection. In single-select mode the array contains at most one element.
440
+ * Emits an empty array when the selection is cleared.
272
441
  */
273
442
  this.selectionChange = output();
274
443
  /**
275
- * Emitted when a list item has been double-clicked
444
+ * Emits the `TileData` of a tile when it is double-clicked.
445
+ *
446
+ * The primary single-click selection is handled separately via `itemSelect`. Use this
447
+ * output to trigger a secondary action such as opening a detail view or navigating
448
+ * to a route.
276
449
  */
277
450
  this.itemDblClick = output();
451
+ /**
452
+ * Emits when the user right-clicks a tile and `disableCustomContextMenu` is `false`.
453
+ *
454
+ * Provides the originating mouse event (for positioning a custom menu overlay) and
455
+ * the current selection as an array of object IDs. The right-clicked tile is
456
+ * auto-selected before the event fires if it was not already part of the selection.
457
+ */
458
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
278
459
  this.ctxMenu = output();
279
- // the items rendered in the list
460
+ //#endregion
461
+ //#region Properties
462
+ /**
463
+ * The `MatMenu` instance resolved from the projected `TileActionsMenuComponent`.
464
+ *
465
+ * `null` when no `TileActionsMenuComponent` is projected. Used in the template to
466
+ * conditionally bind `[matMenuTriggerFor]` on tile action buttons.
467
+ */
468
+ this.menu = computed(() => {
469
+ const comp = this.menuComponent();
470
+ return comp?.matMenu() ?? null;
471
+ });
472
+ /**
473
+ * The full `TileData` objects for all currently selected tiles.
474
+ *
475
+ * Updated on every selection change alongside `selection`. Prefer this signal when
476
+ * the parent needs rich tile data (fields, DMS object reference, actions) rather than
477
+ * just IDs.
478
+ */
479
+ this.selectedTile = signal([]);
480
+ /**
481
+ * Object IDs of the currently selected tiles.
482
+ *
483
+ * A flat `string[]` signal — convenient for ID comparisons, permission checks, or
484
+ * passing to services that work with IDs. For the full tile data, use `selectedTile`.
485
+ */
486
+ this.selection = signal([]);
487
+ this.#busy = computed(() => this.list().busy());
488
+ this.#preselect = linkedSignal(this.preselect);
489
+ /**
490
+ * Computed map of per-object-ID inline CSS styles derived from the `highlights` input.
491
+ *
492
+ * Merges all `TileListHighlight` entries that reference the same ID so the template
493
+ * only needs a single lookup: `highlightStyles()[item.id]`. Returns an empty object
494
+ * for IDs with no highlight rules.
495
+ */
496
+ this.highlightStyles = computed(() => {
497
+ const x = {};
498
+ (this.highlights() || []).forEach((highlight) => {
499
+ highlight.ids.forEach((id) => {
500
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
501
+ if (!x[id])
502
+ x[id] = {};
503
+ x[id] = { ...x[id], ...highlight.cssStyles };
504
+ });
505
+ });
506
+ return x;
507
+ });
508
+ /**
509
+ * The `TileData` view models currently rendered in the list.
510
+ *
511
+ * Populated by `onQueryResult()` after each successful search response and also
512
+ * prepended by `dropItems()`. The template iterates over this signal directly.
513
+ * Read this from the parent to access displayed tile data without an extra query.
514
+ */
280
515
  this.items = signal([]);
516
+ /**
517
+ * Indicates whether at least one search has been executed successfully.
518
+ *
519
+ * Remains `false` until the first `onQueryResult()` call. Use this in the template
520
+ * to distinguish between "initial loading" (show a spinner) and "empty result"
521
+ * (show the empty-state slot) without relying on `items().length` alone.
522
+ */
281
523
  this.searchExecuted = signal(false);
524
+ this._selection = [];
525
+ //#endregion
526
+ //#region Public methods
527
+ /**
528
+ * Transformer function passed to the inner `QueryListComponent`.
529
+ *
530
+ * Maps raw `SearchResultItem[]` to the `InnerTileData` view model by:
531
+ * 1. Storing the raw items in `#rawResultItems` so `applyFlavor()` can re-map them
532
+ * without a new server request.
533
+ * 2. Calling `#mapToTileData()` to resolve `ObjectConfig` fields (title, icon, …).
534
+ * 3. Resolving each tile's action IDs to full `Action` objects via `ActionsService`,
535
+ * filtering out any IDs that are not registered.
536
+ *
537
+ * Re-runs automatically whenever the query result or the `ObjectConfigRecord` changes
538
+ * (the latter is triggered by calling `list().runTransformerAgain()` from
539
+ * `#getObjectConfig()`).
540
+ */
541
+ this.transformer = (res) => {
542
+ this.#rawResultItems = res;
543
+ const mappedItems = this.#mapToTileData(res.map((i) => new DmsObject(i)));
544
+ const items = mappedItems.map((item) => ({
545
+ ...item,
546
+ actions: (item.actions || [])
547
+ .map((action) => this.#actionService.getActionById(action.id, this.options()?.actionContext))
548
+ .filter((action) => action !== undefined)
549
+ }));
550
+ // untracked(() => this.items.set(items));
551
+ return items;
552
+ };
553
+ //#endregion
282
554
  // #region Effect Methods
283
555
  this.#flavorEffect = () => {
284
- const f = this.flavor();
285
- if (f)
286
- this.applyFlavor(f);
556
+ const flavor = this.flavor();
557
+ if (flavor)
558
+ this.applyFlavor(flavor);
287
559
  };
288
560
  this.#preselectEffect = () => {
289
561
  const preselect = this.#preselect();
@@ -309,12 +581,152 @@ class TileListComponent {
309
581
  effect(this.#preselectEffect);
310
582
  effect(this.#closeMenuEffect);
311
583
  }
584
+ //#region Lifecycle hooks
585
+ ngOnInit() {
586
+ this.#getObjectConfig();
587
+ }
588
+ //#endregion
589
+ //#region UI Methods
590
+ /**
591
+ * Host **Ctrl+C** handler — emits the current selection via `tileCopy`.
592
+ *
593
+ * Suppresses the default browser copy behavior. Fires only when at least one tile
594
+ * is selected. Bound via the `host` metadata as `(keydown.control.c)`.
595
+ *
596
+ * @param event The keyboard event (used to call `preventDefault`).
597
+ */
598
+ onCopy(event) {
599
+ event.preventDefault();
600
+ if (this._selection.length)
601
+ this.tileCopy.emit(this._selectionToTileData(this._selection));
602
+ }
603
+ /**
604
+ * Host **Ctrl+X** handler — emits the current selection via `tileCut`.
605
+ *
606
+ * Suppresses the default browser cut behavior. Fires only when at least one tile
607
+ * is selected. Bound via the `host` metadata as `(keydown.control.x)`.
608
+ *
609
+ * @param event The keyboard event (used to call `preventDefault`).
610
+ */
611
+ onCut(event) {
612
+ event.preventDefault();
613
+ if (this._selection.length)
614
+ this.tileCut.emit(this._selectionToTileData(this._selection));
615
+ }
616
+ /**
617
+ * Handles a double-click event forwarded from the inner `QueryListComponent`.
618
+ *
619
+ * If the double-clicked tile is not already the sole selected item, it is selected
620
+ * first (without modifier keys) so that `itemDblClick` always fires in the context
621
+ * of a known selection state. Then `itemDblClick` is emitted with the tile's data.
622
+ *
623
+ * Called from the template via `(itemDoubleClick)`. Not intended for external callers.
624
+ *
625
+ * @param index Zero-based index of the double-clicked tile in the current `items` array.
626
+ */
312
627
  onItemDoubleClick(index) {
313
628
  const selectionIsEqual = this._selection.length === 1 && this._selection[0] === index;
314
629
  if (!selectionIsEqual)
315
630
  this.#internalSelectByIndex(index);
316
631
  this.itemDblClick.emit(this.items()[index]);
317
632
  }
633
+ /**
634
+ * Handles the query result emitted by the inner `QueryListComponent`.
635
+ *
636
+ * Marks the component as having executed at least one search (`searchExecuted`),
637
+ * updates the `items` signal with the freshly mapped `TileData` view models, and
638
+ * re-emits the raw result via `queryResult` so the parent can update counters or
639
+ * analytics without a separate query.
640
+ *
641
+ * Called from the template via `(queryResult)`. Not intended for external callers.
642
+ *
643
+ * @param result The search result object containing `totalCount` and `items`.
644
+ */
645
+ onQueryResult(result) {
646
+ this.searchExecuted.set(true);
647
+ const newItems = this.#mapToTileData(result.items.map((i) => new DmsObject(i)));
648
+ // Preserve drop-in items at the top so that `items` indices stay in sync with
649
+ // the rendered `resultItems` from QueryListComponent (which prepends drop-ins).
650
+ const dropInCount = this.list().dropInSize();
651
+ const currentItems = this.items();
652
+ const dropInItems = currentItems.slice(0, dropInCount);
653
+ let merged;
654
+ if (dropInItems.length > 0) {
655
+ const dropInIds = new Set(dropInItems.map((item) => item.id));
656
+ const filtered = newItems.filter((item) => !dropInIds.has(item.id));
657
+ merged = [...dropInItems, ...filtered];
658
+ }
659
+ else {
660
+ merged = newItems;
661
+ }
662
+ // If any selected items are absent from the new result set (e.g. after deletion),
663
+ // clear the internal selection indices. Without this, #isSelectionEqual would return
664
+ // true when the inner list auto-selects the same index (now pointing to a different
665
+ // item), causing selectionChange to be silently skipped and the right panel to stay
666
+ // stale with the deleted item's content.
667
+ if (this._selection.length > 0) {
668
+ const mergedIds = new Set(merged.map((i) => i.id));
669
+ const selectionOutdated = this._selection.some((idx) => !mergedIds.has(this.items()[idx]?.id));
670
+ if (selectionOutdated) {
671
+ this._selection = [];
672
+ this._lastSelection = undefined;
673
+ }
674
+ }
675
+ this.items.set(merged);
676
+ this.queryResult.emit(result);
677
+ }
678
+ /**
679
+ * Handles the committed selection emitted by the inner list.
680
+ *
681
+ * For a single-item selection the full `#select()` path is taken so that `itemSelect`
682
+ * is also emitted. For multi-item selections only `#updateSelectionState()` runs to
683
+ * keep `selectionChange` and the internal state in sync without the single-item emit.
684
+ *
685
+ * Called from the template via `(itemSelect)` on `yuv-query-list`.
686
+ * Not intended for external callers.
687
+ *
688
+ * @param sel Array of zero-based indices representing the new selection.
689
+ */
690
+ onListItemsSelect(sel) {
691
+ if (sel.length === 1) {
692
+ this.#internalSelectByIndex(sel[0]);
693
+ return;
694
+ }
695
+ this.#updateSelectionState(sel);
696
+ }
697
+ /**
698
+ * Handles live drag-selection changes forwarded from the inner `QueryListComponent`.
699
+ *
700
+ * Fires continuously while the user drags across tiles, updating the selection state
701
+ * in real time so the visual highlight tracks the gesture. Does not emit `itemSelect`
702
+ * (that fires only on the final committed selection via `onListItemsSelect`).
703
+ *
704
+ * Called from the template via `(dragSelectChange)`. Not intended for external callers.
705
+ *
706
+ * @param sel Current array of zero-based indices covered by the drag gesture.
707
+ */
708
+ onDragSelectChange(sel) {
709
+ this.#updateSelectionState(sel);
710
+ }
711
+ /**
712
+ * Handles a right-click (contextmenu) event on a tile.
713
+ *
714
+ * Suppresses the browser's native context menu. If the clicked tile is not already
715
+ * part of the selection, it is selected first. After a short delay (to allow the
716
+ * selection state to propagate), `ctxMenu` is emitted with the originating event and
717
+ * the current selection so the parent can position and populate a custom menu.
718
+ *
719
+ * The delay is intentional: it gives Angular change detection a tick to apply the
720
+ * selection update before the parent reads `selection()` in the `ctxMenu` handler.
721
+ *
722
+ * No-ops when `disableCustomContextMenu` is `true`.
723
+ *
724
+ * Called from the template via `(contextmenu)` on each tile. Not intended for
725
+ * external callers.
726
+ *
727
+ * @param event The originating mouse event (used to suppress default and for positioning).
728
+ * @param index Zero-based index of the right-clicked tile.
729
+ */
318
730
  contextMenuHandler(event, index) {
319
731
  if (this.disableCustomContextMenu())
320
732
  return;
@@ -325,18 +737,45 @@ class TileListComponent {
325
737
  }
326
738
  setTimeout(() => {
327
739
  this.ctxMenu.emit({ event, selection: this.selection() });
740
+ // eslint-disable-next-line @typescript-eslint/no-magic-numbers
328
741
  }, 200);
329
742
  }
330
- executeAction(t, a, evt) {
331
- evt.preventDefault();
332
- evt.stopPropagation();
743
+ /**
744
+ * Executes a tile inline action against the appropriate set of DMS objects.
745
+ *
746
+ * Resolves the target objects from the DMS service: if the tile whose action button
747
+ * was clicked is part of the current selection, the action runs against all selected
748
+ * objects (batch action); otherwise it runs against only that single tile's object.
749
+ *
750
+ * Suppresses the click event to prevent the tile's own selection handler from firing.
751
+ * Called from the template via the tile action buttons. Not intended for external callers.
752
+ *
753
+ * @param tileData The tile whose action button was clicked.
754
+ * @param action The resolved `Action` to execute.
755
+ * @param event The originating click event (stopped to prevent tile selection).
756
+ */
757
+ executeAction(tileData, action, event) {
758
+ event.preventDefault();
759
+ event.stopPropagation();
333
760
  const selectedIds = this.selection();
334
- const ids = selectedIds.includes(t.id) ? selectedIds : [t.id];
761
+ const ids = selectedIds.includes(tileData.id) ? selectedIds : [tileData.id];
335
762
  this.#dmsService
336
763
  .getDmsObjects(ids)
337
- .pipe(switchMap((objects) => a.run(objects)))
764
+ .pipe(switchMap((objects) => action.run(objects)))
338
765
  .subscribe();
339
766
  }
767
+ /**
768
+ * Selects tiles by their object IDs.
769
+ *
770
+ * Looks up each ID in the current `items` array and selects the matching indices
771
+ * in the inner list. If the list is still loading when this is called, the IDs are
772
+ * stored and applied automatically once the query completes (via `#preselectEffect`).
773
+ *
774
+ * IDs not found in the current result set are silently ignored. The first found item
775
+ * also receives keyboard focus via `setActiveItem()`.
776
+ *
777
+ * @param ids Array of object IDs (`system:objectId`) to select.
778
+ */
340
779
  selectById(ids) {
341
780
  if (this.#busy()) {
342
781
  this.#preselect.set(ids);
@@ -345,98 +784,115 @@ class TileListComponent {
345
784
  this.#executeSelectById(ids);
346
785
  }
347
786
  }
348
- #executeSelectById(ids) {
349
- const indices = ids.map((id) => this.items().findIndex((i) => i.id === id)).filter((i) => i !== -1);
350
- this.list().multiSelect(indices);
351
- if (indices.length > 0) {
352
- this.list().setActiveItem(indices[0]);
353
- }
354
- this.#elRef.nativeElement.focus();
355
- this.#updateSelectionState(indices);
356
- }
787
+ /**
788
+ * Programmatically replaces the entire selection with the given indices.
789
+ *
790
+ * Only effective when `multiselect` is `true`. Delegates to the inner
791
+ * `QueryListComponent`. Out-of-range indices are silently discarded.
792
+ *
793
+ * @param index Array of zero-based item indices to select.
794
+ */
357
795
  multiSelect(index) {
358
796
  this.list().multiSelect(index);
359
797
  }
798
+ /**
799
+ * Selects the tile at the given zero-based index.
800
+ *
801
+ * Delegates to the inner `QueryListComponent`. Clamps the index to the valid range.
802
+ * Use `selectById()` when you have object IDs rather than positional indices.
803
+ *
804
+ * @param index Zero-based index of the tile to select.
805
+ */
360
806
  select(index) {
361
807
  this.list().select(index);
362
808
  }
363
- #internalSelectByIndex(idx, evt) {
364
- this.#select(idx, evt?.shiftKey, evt?.ctrlKey);
365
- }
366
- onQueryResult(e) {
367
- this.searchExecuted.set(true);
368
- this.items.set(this.#mapToTileData(e.items.map((i) => new DmsObject(i))));
369
- this.queryResult.emit(e);
370
- }
371
- onListItemsSelect(sel) {
372
- if (sel.length === 1) {
373
- this.#internalSelectByIndex(sel[0]);
374
- return;
375
- }
376
- this.#updateSelectionState(sel);
377
- }
378
- onDragSelectChange(sel) {
379
- this.#updateSelectionState(sel);
380
- }
381
809
  /**
382
- * Shared method to update all selection state and emit outputs.
383
- * Used by `onListItemsSelect`, `onDragSelectChange`, and `selectById`.
810
+ * Re-executes the current query without changing the page or query parameters.
811
+ *
812
+ * If pagination is active, re-fetches the same page. Use this to reflect server-side
813
+ * changes (e.g. after a create/delete operation) without navigating away.
384
814
  */
385
- #updateSelectionState(sel) {
386
- const sorted = [...sel].sort();
387
- // skip if selection hasn't changed
388
- if (this.#isSelectionEqual(sorted))
389
- return;
390
- this._selection = sorted;
391
- this._lastSelection = this._selection.length ? this._selection[this._selection.length - 1] : undefined;
392
- const tiles = this._selectionToTileData(this._selection);
393
- this.selection.set(tiles.map((t) => t.id));
394
- this.selectionChange.emit(tiles);
395
- this.selectedTile.set(tiles);
396
- }
397
815
  refresh() {
398
816
  this.list().refresh();
399
817
  }
818
+ /**
819
+ * Prepends `DmsObject` instances to the top of the tile list as temporary drop-in items.
820
+ *
821
+ * The objects are mapped through the same `ObjectConfig` resolution pipeline as regular
822
+ * query results, including action resolution. The existing selection is shifted so that
823
+ * currently selected tiles remain selected after the prepend.
824
+ *
825
+ * Use this for optimistic UI: show newly created or pasted objects immediately before
826
+ * the server index reflects them in the query results.
827
+ *
828
+ * Drop-in items are cleared automatically when the user navigates to a different page.
829
+ *
830
+ * @param objects `DmsObject` instances to prepend to the visible list.
831
+ */
400
832
  dropItems(objects) {
401
833
  const mappedItems = this.#mapToTileData(objects);
402
834
  const items = mappedItems.map((item) => ({
403
835
  ...item,
404
836
  actions: (item.actions || [])
405
- .map((a) => this.#actionService.getActionById(a.id, this.options()?.actionContext))
406
- .filter((a) => a !== undefined)
837
+ .map((objectConfigAction) => this.#actionService.getActionById(objectConfigAction.id, this.options()?.actionContext))
838
+ .filter((action) => action !== undefined)
407
839
  }));
408
840
  this.items.set([...items, ...this.items()]);
409
841
  this.list().dropItems(items);
410
842
  }
843
+ /**
844
+ * Toggles an `ObjectFlavor` on or off and re-maps the current result set.
845
+ *
846
+ * If the given flavor is already the active `appliedFlavor`, it is cleared (toggle
847
+ * off). Otherwise the flavor is applied and all tiles whose `DmsObject.sots` includes
848
+ * `flavor.sot` are re-rendered using the config entry for `flavor.id`.
849
+ *
850
+ * Re-mapping runs against the last raw `SearchResultItem[]` (`#rawResultItems`) so
851
+ * no server round-trip is needed. Called automatically by `#flavorEffect` when the
852
+ * `flavor` input changes.
853
+ *
854
+ * @param flavor The `ObjectFlavor` to apply or remove.
855
+ */
411
856
  applyFlavor(flavor) {
412
857
  this.appliedFlavor = this.appliedFlavor?.id === flavor.id ? undefined : flavor;
413
858
  if (this.#rawResultItems)
414
859
  this.items.set(this.#mapToTileData(this.#rawResultItems.map((i) => new DmsObject(i))));
415
860
  }
416
861
  /**
417
- * Updates an item in the list at the specified index with the provided value.
862
+ * Applies per-index optimistic overrides to the displayed tile data.
418
863
  *
419
- * Use this method for optimistic updates of list items. The updates be removed
420
- * when the user navigates to another search result page.
864
+ * Delegates to the inner `QueryListComponent.updateListItems()`. Each entry patches
865
+ * the tile at the given `index` with a new `TileData` value. Overrides accumulate
866
+ * until the user navigates to a different page, at which point the fresh server
867
+ * response replaces all local data.
421
868
  *
422
- * @param index Index of the item to update.
423
- * @param value The new value for the item.
869
+ * For DMS-object-based updates, prefer `updateTileList()` which resolves indices
870
+ * from object IDs automatically.
871
+ *
872
+ * @param updates Array of `{ index, value }` pairs describing which tiles to patch.
424
873
  */
425
874
  updateListItems(updates) {
426
875
  this.list().updateListItems(updates);
427
876
  }
428
877
  /**
429
- * Updates the tile list with the provided DmsObjects. Only tiles that are
430
- * already present in the list will be updated. The update is based on
431
- * the object ID and will be gone when the user navigates to another search
432
- * result page. Use this method for optimistic updates.
433
- * @param listItems The DmsObjects to update the tiles with.
878
+ * Optimistically updates tiles in the list that correspond to the given `DmsObject` instances.
879
+ *
880
+ * For each provided object, finds its matching tile by `id` in the current `items`
881
+ * array, re-maps it through `#mapToTileData()` to get a fresh `TileData` view model,
882
+ * and applies it via `updateListItems()`. Tiles not currently visible in the list are
883
+ * silently skipped.
884
+ *
885
+ * This is the preferred way to reflect server-confirmed changes (e.g. rename, metadata
886
+ * edit) without issuing a full query refresh. Updates are scoped to the current page
887
+ * and are discarded on the next page navigation.
888
+ *
889
+ * @param listItems Updated `DmsObject` instances to apply to the list.
434
890
  */
435
891
  updateTileList(listItems) {
436
892
  const updates = [];
437
893
  listItems.forEach((item) => {
438
894
  const idx = this.items().findIndex((listItem) => listItem.id === item.id);
439
- if (idx >= 0 && this.oc) {
895
+ if (idx >= 0 && this.objectConfig) {
440
896
  const mappedTileData = this.#mapToTileData([item]);
441
897
  updates.push({ index: idx, value: mappedTileData[0] });
442
898
  }
@@ -444,8 +900,15 @@ class TileListComponent {
444
900
  this.updateListItems(updates);
445
901
  }
446
902
  /**
447
- * Clears the current selection.
448
- * @param silent If true, the selectionChange event will not be emitted.
903
+ * Clears the current selection and resets all internal selection state.
904
+ *
905
+ * Resets `_selection`, `_lastSelection`, the `selection` signal, and `selectedTile`
906
+ * to empty. Also delegates to the inner list's `clear()` to sync the visual state.
907
+ * If the selection is already empty, the method is a no-op.
908
+ *
909
+ * @param silent When `true`, skips emitting `selectionChange`. Use this when the
910
+ * parent needs to reset state programmatically without triggering
911
+ * downstream reactions.
449
912
  */
450
913
  clearSelection(silent = false) {
451
914
  if (this._selection.length) {
@@ -459,8 +922,14 @@ class TileListComponent {
459
922
  }
460
923
  }
461
924
  /**
462
- * Selects the next item in the list. If the last item is already selected,
463
- * it wraps around to the first item. Does nothing if no item is currently selected.
925
+ * Advances the selection to the next tile in the list.
926
+ *
927
+ * Moves from the last selected index forward by one. Wraps around to index 0
928
+ * when the last tile is currently selected. Does nothing if no tile is selected
929
+ * (`_lastSelection` is `undefined`).
930
+ *
931
+ * Useful for implementing keyboard-driven "next item" navigation from a parent
932
+ * detail panel without giving focus back to the list.
464
933
  */
465
934
  selectNext() {
466
935
  if (this._lastSelection !== undefined) {
@@ -471,8 +940,14 @@ class TileListComponent {
471
940
  }
472
941
  }
473
942
  /**
474
- * Selects the previous item in the list. If the first item is already selected,
475
- * it wraps around to the last item. Does nothing if no item is currently selected.
943
+ * Moves the selection to the previous tile in the list.
944
+ *
945
+ * Moves from the last selected index backward by one. Wraps around to the last tile
946
+ * when index 0 is currently selected. Does nothing if no tile is selected
947
+ * (`_lastSelection` is `undefined`).
948
+ *
949
+ * Useful for implementing keyboard-driven "previous item" navigation from a parent
950
+ * detail panel without giving focus back to the list.
476
951
  */
477
952
  selectPrev() {
478
953
  if (this._lastSelection !== undefined) {
@@ -482,14 +957,69 @@ class TileListComponent {
482
957
  this.#select(i);
483
958
  }
484
959
  }
485
- menuItemClicked(t, event) {
486
- if (this.selection().includes(t.id)) {
960
+ /**
961
+ * Handles a click on a projected `TileActionsMenuComponent` menu item.
962
+ *
963
+ * If the tile that owns the clicked menu item is part of the current selection,
964
+ * the click event is stopped from propagating so the tile's own `(click)` handler
965
+ * does not deselect other tiles. If the tile is not selected, the event is allowed
966
+ * to propagate normally so the tile gets selected as a side effect.
967
+ *
968
+ * Called from the template via the `TileActionsMenuComponent` item click binding.
969
+ * Not intended for external callers.
970
+ *
971
+ * @param tileData The tile that owns the clicked menu item.
972
+ * @param event The originating click event.
973
+ */
974
+ menuItemClicked(tileData, event) {
975
+ if (this.selection().includes(tileData.id)) {
487
976
  // only prevent event propagation if the click came from an element that
488
977
  // is part of the current selection
489
978
  event.stopPropagation();
490
979
  event.preventDefault();
491
980
  }
492
981
  }
982
+ //#endregion
983
+ //#region Utilities
984
+ #getObjectConfig() {
985
+ this.#objectConfigService
986
+ .getObjectConfigs$(this.bucket() || '', true)
987
+ .pipe(takeUntilDestroyed(this.#destroyRef))
988
+ .subscribe({
989
+ next: (res) => {
990
+ this.objectConfig = res;
991
+ this.list().runTransformerAgain();
992
+ }
993
+ });
994
+ }
995
+ #internalSelectByIndex(idx, evt) {
996
+ this.#select(idx, evt?.shiftKey, evt?.ctrlKey);
997
+ }
998
+ #executeSelectById(ids) {
999
+ const indices = ids.map((id) => this.items().findIndex((i) => i.id === id)).filter((i) => i !== -1);
1000
+ this.list().multiSelect(indices);
1001
+ if (indices.length > 0) {
1002
+ this.list().setActiveItem(indices[0]);
1003
+ }
1004
+ this.#elRef.nativeElement.focus();
1005
+ this.#updateSelectionState(indices);
1006
+ }
1007
+ /**
1008
+ * Shared method to update all selection state and emit outputs.
1009
+ * Used by `onListItemsSelect`, `onDragSelectChange`, and `selectById`.
1010
+ */
1011
+ #updateSelectionState(sel) {
1012
+ const sorted = [...sel].sort();
1013
+ // skip if selection hasn't changed
1014
+ if (this.#isSelectionEqual(sorted))
1015
+ return;
1016
+ this._selection = sorted;
1017
+ this._lastSelection = this._selection.length ? this._selection[this._selection.length - 1] : undefined;
1018
+ const tiles = this._selectionToTileData(this._selection);
1019
+ this.selection.set(tiles.map((tile) => tile.id));
1020
+ this.selectionChange.emit(tiles);
1021
+ this.selectedTile.set(tiles);
1022
+ }
493
1023
  #select(index, shiftKey = false, ctrlKey = false) {
494
1024
  this.#elRef.nativeElement.focus();
495
1025
  let newSelection;
@@ -532,69 +1062,76 @@ class TileListComponent {
532
1062
  this._selection = newSelection;
533
1063
  this._lastSelection = this._selection.length === 0 ? undefined : index;
534
1064
  const tiles = this._selectionToTileData(this._selection);
535
- this.selection.set(tiles.map((t) => t.id));
1065
+ this.selection.set(tiles.map((tile) => tile.id));
536
1066
  this.selectionChange.emit(tiles);
1067
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
537
1068
  tiles.length === 1 && this.itemSelect.emit(tiles[0]);
538
1069
  this.selectedTile.set(tiles);
539
1070
  }
540
1071
  #isSelectionEqual(newSelection) {
541
1072
  if (newSelection.length !== this._selection.length)
542
1073
  return false;
543
- return newSelection.every((v, i) => v === this._selection[i]);
1074
+ return newSelection.every((value, index) => value === this._selection[index]);
544
1075
  }
545
1076
  _selectionToTileData(selection) {
546
1077
  return selection.map((idx) => this.items()[idx]);
547
1078
  }
548
1079
  #mapToTileData(objects) {
549
1080
  return objects.map((dmsObject) => {
1081
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
550
1082
  const sots = dmsObject.sots || [];
551
1083
  if (!dmsObject.objectTypeId) {
552
- throw new Error(`DmsObject with id '${dmsObject.id}' is missing objectTypeId. Please make sure that your query includes the object type ID field.`);
1084
+ throw new Error(
1085
+ // eslint-disable-next-line max-len
1086
+ `DmsObject with id '${dmsObject.id}' is missing objectTypeId. Please make sure that your query includes the object type ID field.`);
553
1087
  }
554
- let oc = this.oc[dmsObject.objectTypeId];
555
- // check if result oitem matches virtual config type
1088
+ let objectConfig = this.objectConfig[dmsObject.objectTypeId];
1089
+ // check if result item matches virtual config type
556
1090
  const cfgTypes = this.options()?.configTypes || [];
557
1091
  cfgTypes.forEach((cft) => {
558
1092
  const matchesType = !cft.objectType || cft.objectType === dmsObject.objectTypeId;
559
- const matchesSOTs = !cft.sots || !cft.sots.length || cft.sots.every((sot) => sots.includes(sot));
1093
+ const matchesSOTs = !cft.sots?.length || cft.sots.every((sot) => sots.includes(sot));
560
1094
  if (matchesType && matchesSOTs) {
561
- oc = this.oc[cft.id];
1095
+ objectConfig = this.objectConfig[cft.id];
562
1096
  }
563
1097
  });
564
1098
  // only apply a flavor if the object has that SOT
565
- const ownAppliedFlavor = this.appliedFlavor && sots.includes(this.appliedFlavor.sot) && this.oc[this.appliedFlavor.sot];
1099
+ const ownAppliedFlavor = this.appliedFlavor && sots.includes(this.appliedFlavor.sot) && this.objectConfig[this.appliedFlavor.sot];
566
1100
  if (ownAppliedFlavor) {
567
- oc = this.oc[this.appliedFlavor.id];
1101
+ objectConfig = this.objectConfig[this.appliedFlavor.id];
568
1102
  }
569
- if (!oc)
570
- oc = this.#objectConfigService.getDefaultConfig(dmsObject.objectTypeId);
1103
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1104
+ if (!objectConfig)
1105
+ objectConfig = this.#objectConfigService.getDefaultConfig(dmsObject.objectTypeId);
571
1106
  const tli = {
572
1107
  objectTypeId: dmsObject.objectTypeId,
573
1108
  id: dmsObject.id,
574
- actions: oc.actions,
575
- badges: oc.badges,
1109
+ actions: objectConfig.actions,
1110
+ badges: objectConfig.badges,
576
1111
  instanceData: dmsObject.data,
577
1112
  dmsObject,
578
- title: oc.title ? this.#getResolvedObjectConfigItem(oc.title.propertyName, dmsObject.data) : { propertyName: '', value: '' }
1113
+ title: objectConfig.title
1114
+ ? this.#getResolvedObjectConfigItem(objectConfig.title.propertyName, dmsObject.data)
1115
+ : { propertyName: '', value: '' }
579
1116
  };
580
- tli.icon = oc.icon
1117
+ tli.icon = objectConfig.icon
581
1118
  ? {
582
1119
  rendererType: 'icon',
583
1120
  propertyName: 'custom',
584
- value: oc.icon.svg,
585
- meta: { isFontIcon: YmtMatIconRegistryService.isFontIcon(oc.icon.svg), objectTypeId: '' }
1121
+ value: objectConfig.icon.svg,
1122
+ meta: { isFontIcon: YmtMatIconRegistryService.isFontIcon(objectConfig.icon.svg), objectTypeId: '' }
586
1123
  }
587
1124
  : {
588
1125
  rendererType: 'icon',
589
1126
  propertyName: BaseObjectTypeField.OBJECT_TYPE_ID,
590
1127
  value: ownAppliedFlavor && this.appliedFlavor ? this.appliedFlavor.sot : dmsObject.objectTypeId
591
1128
  };
592
- if (oc.description)
593
- tli.description = this.#getResolvedObjectConfigItem(oc.description.propertyName, dmsObject.data);
594
- if (oc.meta)
595
- tli.meta = this.#getResolvedObjectConfigItem(oc.meta.propertyName, dmsObject.data);
596
- if (oc.aside)
597
- tli.aside = this.#getResolvedObjectConfigItem(oc.aside.propertyName, dmsObject.data);
1129
+ if (objectConfig.description)
1130
+ tli.description = this.#getResolvedObjectConfigItem(objectConfig.description.propertyName, dmsObject.data);
1131
+ if (objectConfig.meta)
1132
+ tli.meta = this.#getResolvedObjectConfigItem(objectConfig.meta.propertyName, dmsObject.data);
1133
+ if (objectConfig.aside)
1134
+ tli.aside = this.#getResolvedObjectConfigItem(objectConfig.aside.propertyName, dmsObject.data);
598
1135
  return tli;
599
1136
  });
600
1137
  }
@@ -618,23 +1155,13 @@ class TileListComponent {
618
1155
  }
619
1156
  return item;
620
1157
  }
621
- ngOnInit() {
622
- this.#objectConfigService
623
- .getObjectConfigs$(this.bucket() || '', true)
624
- .pipe(takeUntilDestroyed(this.#destroyRef))
625
- .subscribe({
626
- next: (res) => {
627
- this.oc = res;
628
- this.list().runTransformerAgain();
629
- }
630
- });
631
- }
1158
+ //#endregion
632
1159
  // #region Effect Methods
633
1160
  #flavorEffect;
634
1161
  #preselectEffect;
635
1162
  #closeMenuEffect;
636
1163
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: TileListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
637
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.20", type: TileListComponent, isStandalone: true, selector: "yuv-tile-list", inputs: { bucket: { classPropertyName: "bucket", publicName: "bucket", isSignal: true, isRequired: false, transformFunction: null }, pageSize: { classPropertyName: "pageSize", publicName: "pageSize", isSignal: true, isRequired: false, transformFunction: null }, multiselect: { classPropertyName: "multiselect", publicName: "multiselect", isSignal: true, isRequired: false, transformFunction: null }, dense: { classPropertyName: "dense", publicName: "dense", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, flavor: { classPropertyName: "flavor", publicName: "flavor", isSignal: true, isRequired: false, transformFunction: null }, query: { classPropertyName: "query", publicName: "query", isSignal: true, isRequired: false, transformFunction: null }, preselect: { classPropertyName: "preselect", publicName: "preselect", isSignal: true, isRequired: false, transformFunction: null }, highlights: { classPropertyName: "highlights", publicName: "highlights", isSignal: true, isRequired: false, transformFunction: null }, preventChangeUntil: { classPropertyName: "preventChangeUntil", publicName: "preventChangeUntil", isSignal: true, isRequired: false, transformFunction: null }, autoSelect: { classPropertyName: "autoSelect", publicName: "autoSelect", isSignal: true, isRequired: false, transformFunction: null }, disableCustomContextMenu: { classPropertyName: "disableCustomContextMenu", publicName: "disableCustomContextMenu", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemSelect: "itemSelect", tileCopy: "tileCopy", tileCut: "tileCut", busy: "busy", queryResult: "queryResult", selectionChange: "selectionChange", itemDblClick: "itemDblClick", ctxMenu: "ctxMenu" }, host: { listeners: { "keydown.control.c": "onCopy($event)", "keydown.control.x": "onCut($event)" }, properties: { "class.dense": "dense()" } }, providers: [], queries: [{ propertyName: "menuComponent", first: true, predicate: TileActionsMenuComponent, descendants: true, isSignal: true }, { propertyName: "emptyContent", first: true, predicate: ["empty"], descendants: true, isSignal: true }], viewQueries: [{ propertyName: "list", first: true, predicate: ["list"], descendants: true, isSignal: true }, { propertyName: "menuTriggers", predicate: MatMenuTrigger, descendants: true, isSignal: true }], ngImport: i0, template: "<yuv-query-list\n #list\n [query]=\"query()\"\n [transformer]=\"transformer\"\n [preventChangeUntil]=\"preventChangeUntil()\"\n [autoSelect]=\"autoSelect()\"\n [pageSize]=\"pageSize()\"\n [multiselect]=\"multiselect()\"\n (itemDoubleClick)=\"onItemDoubleClick($event)\"\n (itemSelect)=\"onListItemsSelect($event)\"\n (queryResult)=\"onQueryResult($event)\"\n (dragSelectChange)=\"onDragSelectChange($event)\"\n>\n <ng-template #yuvQueryListItem let-item let-index=\"index\">\n <yuv-list-tile [class.dense]=\"dense()\" (contextmenu)=\"contextMenuHandler($event, index)\">\n <ng-template #iconSlot><ng-container *yuvRenderer=\"item.icon\"></ng-container></ng-template>\n <ng-template #titleSlot><ng-container *yuvRenderer=\"item.title\"></ng-container></ng-template>\n <ng-template #descriptionSlot><ng-container *yuvRenderer=\"item.description\"></ng-container></ng-template>\n <ng-template #metaSlot><ng-container *yuvRenderer=\"item.meta\"></ng-container></ng-template>\n <ng-template #asideSlot><ng-container *yuvRenderer=\"item.aside\"></ng-container></ng-template>\n <ng-template #actionsSlot>\n @for (a of item.actions; track a.id) {\n <button ymt-icon-button [icon-button-size]=\"'small'\" [matTooltip]=\"a.label\" (click)=\"executeAction(item, a, $event)\">\n <mat-icon inert=\"true\">{{ a.icon }}</mat-icon>\n </button>\n }\n\n @if (menu()) {\n <button\n ymt-icon-button\n [icon-button-size]=\"'small'\"\n (click)=\"menuItemClicked(item, $event)\"\n [matTooltip]=\"'yuv.tile-list.item.actions-menu.button.tooltip' | translate\"\n [matMenuTriggerFor]=\"menu()\"\n >\n <mat-icon inert=\"true\">more_vert</mat-icon>\n </button>\n }\n <ng-content select=\"yuv-tile-actions-menu, [yuv-tile-actions-menu]\"></ng-content>\n </ng-template>\n <ng-template #extensionSlot> <ng-container *yuvTileExtension=\"{ typeId: item.objectTypeId, data: item.instanceData }\"></ng-container> </ng-template>\n <ng-template #badgesSlot>{{ item.badges }}</ng-template>\n </yuv-list-tile>\n </ng-template>\n\n <ng-template #yuvQueryListEmpty>\n <div class=\"empyt-list\">\n @let searchExe = searchExecuted();\n @if (searchExe && emptyContent()) {\n <ng-content></ng-content>\n }\n </div>\n </ng-template>\n <div class=\"offset\" (click)=\"clearSelection()\"></div>\n</yuv-query-list>\n", styles: [":host{--paging-background: var(--ymt-surface);display:flex;flex-direction:column}:host yuv-query-list{flex:1;overflow-y:auto;display:flex;flex-flow:column;height:100%}:host yuv-query-list .offset{flex:1 1 auto}:host .empyt-list{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%}\n"], dependencies: [{ kind: "ngmodule", type: TranslateModule }, { kind: "pipe", type: i1.TranslatePipe, name: "translate" }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "ngmodule", type: YuvListModule }, { kind: "component", type: i2.ListTileComponent, selector: "yuv-list-tile" }, { kind: "ngmodule", type: YuvQueryListModule }, { kind: "component", type: i3.QueryListComponent, selector: "yuv-query-list", inputs: ["query", "idProperty", "transformer", "preventChangeUntil", "autoSelect", "pageSize", "enableDragSelect", "multiselect", "selfHandleSelection"], outputs: ["itemSelect", "dragSelectChange", "itemDoubleClick", "queryResult"] }, { kind: "directive", type: RendererDirective, selector: "[yuvRenderer]", inputs: ["yuvRenderer"] }, { kind: "directive", type: TileExtensionDirective, selector: "[yuvTileExtension]", inputs: ["yuvTileExtension"] }, { kind: "directive", type: YmtIconButtonDirective, selector: "button[ymtIconButton],button[ymt-icon-button],a[ymtIconButton],a[ymt-icon-button]", inputs: ["disabled", "disableRipple", "aria-disabled", "disabledInteractive", "icon-button-size"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatPaginatorModule }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i2$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }] }); }
1164
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.20", type: TileListComponent, isStandalone: true, selector: "yuv-tile-list", inputs: { bucket: { classPropertyName: "bucket", publicName: "bucket", isSignal: true, isRequired: false, transformFunction: null }, pageSize: { classPropertyName: "pageSize", publicName: "pageSize", isSignal: true, isRequired: false, transformFunction: null }, multiselect: { classPropertyName: "multiselect", publicName: "multiselect", isSignal: true, isRequired: false, transformFunction: null }, dense: { classPropertyName: "dense", publicName: "dense", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, flavor: { classPropertyName: "flavor", publicName: "flavor", isSignal: true, isRequired: false, transformFunction: null }, query: { classPropertyName: "query", publicName: "query", isSignal: true, isRequired: false, transformFunction: null }, preselect: { classPropertyName: "preselect", publicName: "preselect", isSignal: true, isRequired: false, transformFunction: null }, highlights: { classPropertyName: "highlights", publicName: "highlights", isSignal: true, isRequired: false, transformFunction: null }, preventChangeUntil: { classPropertyName: "preventChangeUntil", publicName: "preventChangeUntil", isSignal: true, isRequired: false, transformFunction: null }, autoSelect: { classPropertyName: "autoSelect", publicName: "autoSelect", isSignal: true, isRequired: false, transformFunction: null }, disableCustomContextMenu: { classPropertyName: "disableCustomContextMenu", publicName: "disableCustomContextMenu", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemSelect: "itemSelect", tileCopy: "tileCopy", tileCut: "tileCut", busy: "busy", queryResult: "queryResult", selectionChange: "selectionChange", itemDblClick: "itemDblClick", ctxMenu: "ctxMenu" }, host: { listeners: { "keydown.control.c": "onCopy($event)", "keydown.control.x": "onCut($event)" }, properties: { "class.dense": "dense()" } }, providers: [], queries: [{ propertyName: "menuComponent", first: true, predicate: TileActionsMenuComponent, descendants: true, isSignal: true }, { propertyName: "emptyContent", first: true, predicate: ["empty"], descendants: true, isSignal: true }], viewQueries: [{ propertyName: "list", first: true, predicate: ["list"], descendants: true, isSignal: true }, { propertyName: "menuTriggers", predicate: MatMenuTrigger, descendants: true, isSignal: true }], ngImport: i0, template: "<yuv-query-list\n #list\n [query]=\"query()\"\n [transformer]=\"transformer\"\n idProperty=\"id\"\n [preventChangeUntil]=\"preventChangeUntil()\"\n [autoSelect]=\"autoSelect()\"\n [pageSize]=\"pageSize()\"\n [multiselect]=\"multiselect()\"\n (itemDoubleClick)=\"onItemDoubleClick($event)\"\n (itemSelect)=\"onListItemsSelect($event)\"\n (queryResult)=\"onQueryResult($event)\"\n (dragSelectChange)=\"onDragSelectChange($event)\"\n>\n <ng-template #yuvQueryListItem let-item let-index=\"index\">\n <yuv-list-tile [class.dense]=\"dense()\" [style]=\"highlightStyles()[item.id]\" (contextmenu)=\"contextMenuHandler($event, index)\">\n <ng-template #iconSlot><ng-container *yuvRenderer=\"item.icon\" /></ng-template>\n <ng-template #titleSlot><ng-container *yuvRenderer=\"item.title\" /></ng-template>\n <ng-template #descriptionSlot><ng-container *yuvRenderer=\"item.description\" /></ng-template>\n <ng-template #metaSlot><ng-container *yuvRenderer=\"item.meta\" /></ng-template>\n <ng-template #asideSlot><ng-container *yuvRenderer=\"item.aside\" /></ng-template>\n <ng-template #actionsSlot>\n @for (a of item.actions; track a.id) {\n <button\n ymt-icon-button\n [matTooltip]=\"a.label\"\n icon-button-size=\"small\"\n (click)=\"executeAction(item, a, $event)\"\n >\n <mat-icon inert=\"true\">{{ a.icon }}</mat-icon>\n </button>\n }\n\n @if (menu()) {\n <button\n ymt-icon-button\n icon-button-size=\"small\"\n (click)=\"menuItemClicked(item, $event)\"\n [matTooltip]=\"'yuv.tile-list.item.actions-menu.button.tooltip' | translate\"\n [matMenuTriggerFor]=\"menu()\"\n >\n <mat-icon inert=\"true\">more_vert</mat-icon>\n </button>\n }\n <ng-content select=\"yuv-tile-actions-menu, [yuv-tile-actions-menu]\" />\n </ng-template>\n <ng-template #extensionSlot>\n <ng-container *yuvTileExtension=\"{ typeId: item.objectTypeId, data: item.instanceData }\" />\n </ng-template>\n <ng-template #badgesSlot>{{ item.badges }}</ng-template>\n </yuv-list-tile>\n </ng-template>\n\n <ng-template #yuvQueryListEmpty>\n <div class=\"empty-list\">\n @let searchExe = searchExecuted();\n @if (searchExe && emptyContent()) {\n <ng-content />\n }\n </div>\n </ng-template>\n <div class=\"offset\" (click)=\"clearSelection()\"></div>\n</yuv-query-list>\n", styles: [":host{--paging-background: var(--ymt-surface);display:flex;flex-direction:column}:host yuv-query-list{flex:1;overflow-y:auto;display:flex;flex-flow:column;height:100%}:host yuv-query-list .offset{flex:1 1 auto}:host .empty-list{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%}\n"], dependencies: [{ kind: "ngmodule", type: TranslateModule }, { kind: "pipe", type: i1.TranslatePipe, name: "translate" }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "ngmodule", type: YuvListModule }, { kind: "component", type: i2.ListTileComponent, selector: "yuv-list-tile" }, { kind: "ngmodule", type: YuvQueryListModule }, { kind: "component", type: i3.QueryListComponent, selector: "yuv-query-list", inputs: ["query", "idProperty", "transformer", "preventChangeUntil", "autoSelect", "pageSize", "enableDragSelect", "multiselect", "selfHandleSelection", "includePermissions"], outputs: ["itemSelect", "dragSelectChange", "itemDoubleClick", "queryResult"] }, { kind: "directive", type: RendererDirective, selector: "[yuvRenderer]", inputs: ["yuvRenderer"] }, { kind: "directive", type: TileExtensionDirective, selector: "[yuvTileExtension]", inputs: ["yuvTileExtension"] }, { kind: "directive", type: YmtIconButtonDirective, selector: "button[ymtIconButton],button[ymt-icon-button],a[ymtIconButton],a[ymt-icon-button]", inputs: ["disabled", "disableRipple", "aria-disabled", "disabledInteractive", "icon-button-size"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatPaginatorModule }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i2$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
638
1165
  }
639
1166
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: TileListComponent, decorators: [{
640
1167
  type: Component,
@@ -648,11 +1175,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
648
1175
  TileExtensionDirective,
649
1176
  YmtIconButtonDirective,
650
1177
  ...MATERIAL_IMPORTS
651
- ], host: {
1178
+ ], changeDetection: ChangeDetectionStrategy.OnPush, host: {
652
1179
  '[class.dense]': 'dense()',
653
1180
  '(keydown.control.c)': 'onCopy($event)',
654
1181
  '(keydown.control.x)': 'onCut($event)'
655
- }, template: "<yuv-query-list\n #list\n [query]=\"query()\"\n [transformer]=\"transformer\"\n [preventChangeUntil]=\"preventChangeUntil()\"\n [autoSelect]=\"autoSelect()\"\n [pageSize]=\"pageSize()\"\n [multiselect]=\"multiselect()\"\n (itemDoubleClick)=\"onItemDoubleClick($event)\"\n (itemSelect)=\"onListItemsSelect($event)\"\n (queryResult)=\"onQueryResult($event)\"\n (dragSelectChange)=\"onDragSelectChange($event)\"\n>\n <ng-template #yuvQueryListItem let-item let-index=\"index\">\n <yuv-list-tile [class.dense]=\"dense()\" (contextmenu)=\"contextMenuHandler($event, index)\">\n <ng-template #iconSlot><ng-container *yuvRenderer=\"item.icon\"></ng-container></ng-template>\n <ng-template #titleSlot><ng-container *yuvRenderer=\"item.title\"></ng-container></ng-template>\n <ng-template #descriptionSlot><ng-container *yuvRenderer=\"item.description\"></ng-container></ng-template>\n <ng-template #metaSlot><ng-container *yuvRenderer=\"item.meta\"></ng-container></ng-template>\n <ng-template #asideSlot><ng-container *yuvRenderer=\"item.aside\"></ng-container></ng-template>\n <ng-template #actionsSlot>\n @for (a of item.actions; track a.id) {\n <button ymt-icon-button [icon-button-size]=\"'small'\" [matTooltip]=\"a.label\" (click)=\"executeAction(item, a, $event)\">\n <mat-icon inert=\"true\">{{ a.icon }}</mat-icon>\n </button>\n }\n\n @if (menu()) {\n <button\n ymt-icon-button\n [icon-button-size]=\"'small'\"\n (click)=\"menuItemClicked(item, $event)\"\n [matTooltip]=\"'yuv.tile-list.item.actions-menu.button.tooltip' | translate\"\n [matMenuTriggerFor]=\"menu()\"\n >\n <mat-icon inert=\"true\">more_vert</mat-icon>\n </button>\n }\n <ng-content select=\"yuv-tile-actions-menu, [yuv-tile-actions-menu]\"></ng-content>\n </ng-template>\n <ng-template #extensionSlot> <ng-container *yuvTileExtension=\"{ typeId: item.objectTypeId, data: item.instanceData }\"></ng-container> </ng-template>\n <ng-template #badgesSlot>{{ item.badges }}</ng-template>\n </yuv-list-tile>\n </ng-template>\n\n <ng-template #yuvQueryListEmpty>\n <div class=\"empyt-list\">\n @let searchExe = searchExecuted();\n @if (searchExe && emptyContent()) {\n <ng-content></ng-content>\n }\n </div>\n </ng-template>\n <div class=\"offset\" (click)=\"clearSelection()\"></div>\n</yuv-query-list>\n", styles: [":host{--paging-background: var(--ymt-surface);display:flex;flex-direction:column}:host yuv-query-list{flex:1;overflow-y:auto;display:flex;flex-flow:column;height:100%}:host yuv-query-list .offset{flex:1 1 auto}:host .empyt-list{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%}\n"] }]
1182
+ }, template: "<yuv-query-list\n #list\n [query]=\"query()\"\n [transformer]=\"transformer\"\n idProperty=\"id\"\n [preventChangeUntil]=\"preventChangeUntil()\"\n [autoSelect]=\"autoSelect()\"\n [pageSize]=\"pageSize()\"\n [multiselect]=\"multiselect()\"\n (itemDoubleClick)=\"onItemDoubleClick($event)\"\n (itemSelect)=\"onListItemsSelect($event)\"\n (queryResult)=\"onQueryResult($event)\"\n (dragSelectChange)=\"onDragSelectChange($event)\"\n>\n <ng-template #yuvQueryListItem let-item let-index=\"index\">\n <yuv-list-tile [class.dense]=\"dense()\" [style]=\"highlightStyles()[item.id]\" (contextmenu)=\"contextMenuHandler($event, index)\">\n <ng-template #iconSlot><ng-container *yuvRenderer=\"item.icon\" /></ng-template>\n <ng-template #titleSlot><ng-container *yuvRenderer=\"item.title\" /></ng-template>\n <ng-template #descriptionSlot><ng-container *yuvRenderer=\"item.description\" /></ng-template>\n <ng-template #metaSlot><ng-container *yuvRenderer=\"item.meta\" /></ng-template>\n <ng-template #asideSlot><ng-container *yuvRenderer=\"item.aside\" /></ng-template>\n <ng-template #actionsSlot>\n @for (a of item.actions; track a.id) {\n <button\n ymt-icon-button\n [matTooltip]=\"a.label\"\n icon-button-size=\"small\"\n (click)=\"executeAction(item, a, $event)\"\n >\n <mat-icon inert=\"true\">{{ a.icon }}</mat-icon>\n </button>\n }\n\n @if (menu()) {\n <button\n ymt-icon-button\n icon-button-size=\"small\"\n (click)=\"menuItemClicked(item, $event)\"\n [matTooltip]=\"'yuv.tile-list.item.actions-menu.button.tooltip' | translate\"\n [matMenuTriggerFor]=\"menu()\"\n >\n <mat-icon inert=\"true\">more_vert</mat-icon>\n </button>\n }\n <ng-content select=\"yuv-tile-actions-menu, [yuv-tile-actions-menu]\" />\n </ng-template>\n <ng-template #extensionSlot>\n <ng-container *yuvTileExtension=\"{ typeId: item.objectTypeId, data: item.instanceData }\" />\n </ng-template>\n <ng-template #badgesSlot>{{ item.badges }}</ng-template>\n </yuv-list-tile>\n </ng-template>\n\n <ng-template #yuvQueryListEmpty>\n <div class=\"empty-list\">\n @let searchExe = searchExecuted();\n @if (searchExe && emptyContent()) {\n <ng-content />\n }\n </div>\n </ng-template>\n <div class=\"offset\" (click)=\"clearSelection()\"></div>\n</yuv-query-list>\n", styles: [":host{--paging-background: var(--ymt-surface);display:flex;flex-direction:column}:host yuv-query-list{flex:1;overflow-y:auto;display:flex;flex-flow:column;height:100%}:host yuv-query-list .offset{flex:1 1 auto}:host .empty-list{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%}\n"] }]
656
1183
  }], ctorParameters: () => [] });
657
1184
 
658
1185
  class ActionSelectComponent {