@masterteam/client-components 0.0.95 → 0.0.97
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.
- package/fesm2022/masterteam-client-components-client-instance-preview.mjs +27 -38
- package/fesm2022/masterteam-client-components-client-instance-preview.mjs.map +1 -1
- package/fesm2022/masterteam-client-components-client-list.mjs +273 -78
- package/fesm2022/masterteam-client-components-client-list.mjs.map +1 -1
- package/package.json +5 -5
- package/types/masterteam-client-components-client-instance-preview.d.ts +40 -22
- package/types/masterteam-client-components-client-list.d.ts +109 -16
|
@@ -7,8 +7,8 @@ import { TranslocoService, TranslocoModule, TranslocoPipe } from '@jsverse/trans
|
|
|
7
7
|
import { Button } from '@masterteam/components/button';
|
|
8
8
|
import { RuntimeActionRunner, resolveActionLabel } from '@masterteam/components/runtime-action';
|
|
9
9
|
import { HttpClient } from '@angular/common/http';
|
|
10
|
-
import { switchMap, map, of, Observable, share, take } from 'rxjs';
|
|
11
|
-
import { resolveLocalizedLabel, readEntityValueColor, serializeEntityRawValue,
|
|
10
|
+
import { switchMap, map, forkJoin, catchError, of, shareReplay, Observable, share, take } from 'rxjs';
|
|
11
|
+
import { isSectionKey, resolveLocalizedLabel, readEntityValueColor, serializeEntityRawValue, EntitiesPreview } from '@masterteam/components/entities';
|
|
12
12
|
import { Table, TableExportService, TableGroupingController, TableValueResolver, TableCaption } from '@masterteam/components/table';
|
|
13
13
|
import * as i2 from 'primeng/skeleton';
|
|
14
14
|
import { SkeletonModule } from 'primeng/skeleton';
|
|
@@ -45,6 +45,8 @@ class ClientListApiService {
|
|
|
45
45
|
http = inject(HttpClient);
|
|
46
46
|
runtimeFetchBaseUrl = 'fetch/query';
|
|
47
47
|
informativeBaseUrl = 'LevelData';
|
|
48
|
+
/** Card layouts already read back, keyed by context. See {@link getCardLayout}. */
|
|
49
|
+
cardLayouts = new Map();
|
|
48
50
|
/**
|
|
49
51
|
* Fetches a table page. Resolves to the response **and** the `propertyKeys`
|
|
50
52
|
* the request carried, because a write that wants its record projected the
|
|
@@ -97,21 +99,132 @@ class ClientListApiService {
|
|
|
97
99
|
: {}),
|
|
98
100
|
});
|
|
99
101
|
}
|
|
100
|
-
|
|
102
|
+
/**
|
|
103
|
+
* Fetches a board of cards.
|
|
104
|
+
*
|
|
105
|
+
* Without a `hierarchy` scope this is a plain `Card` projection. With one it
|
|
106
|
+
* cannot be: the backend accepts descendant scope on the **`Table`
|
|
107
|
+
* projection only** and answers any other projection with a 400 that takes
|
|
108
|
+
* the whole list down. So a descendant board is fetched as a hierarchy table
|
|
109
|
+
* and re-dressed as cards — the rows carry the same records, their owning
|
|
110
|
+
* record and the pre-pagination totals, and {@link getCardLayout} supplies
|
|
111
|
+
* the card layout the board would otherwise lose.
|
|
112
|
+
*/
|
|
113
|
+
getCards(contextKey, filters = [], skip, take, hierarchy) {
|
|
101
114
|
// `page` and `pageSize` must be sent together or not at all (fetch/query
|
|
102
115
|
// request constraints), so a missing or non-positive `take` means the
|
|
103
116
|
// caller wants the whole set in one call.
|
|
104
117
|
const pageSize = typeof take === 'number' && take > 0 ? take : null;
|
|
105
|
-
|
|
118
|
+
const paging = pageSize === null
|
|
119
|
+
? {}
|
|
120
|
+
: { page: this.toPage(skip ?? 0, pageSize), pageSize };
|
|
121
|
+
if (!hierarchy) {
|
|
122
|
+
return this.queryRuntime({
|
|
123
|
+
contextKey,
|
|
124
|
+
projection: 'Card',
|
|
125
|
+
includeState: [...DEFAULT_INCLUDE_STATE],
|
|
126
|
+
filters,
|
|
127
|
+
display: { areas: ['card'] },
|
|
128
|
+
...paging,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return forkJoin({
|
|
132
|
+
layout: this.getCardLayout(contextKey),
|
|
133
|
+
// No `propertyKeys`: a descendant request rejects any key it cannot
|
|
134
|
+
// resolve in one of the child catalogs, and a card-only property (one
|
|
135
|
+
// the module's table does not show) is exactly such a key — asking for
|
|
136
|
+
// it would 400 the board instead of costing it one field. So the
|
|
137
|
+
// hierarchy request takes the module's table selection and the card
|
|
138
|
+
// layout picks from what came back.
|
|
139
|
+
rows: this.queryRuntime({
|
|
140
|
+
contextKey,
|
|
141
|
+
projection: 'Table',
|
|
142
|
+
surfaceKey: 'table',
|
|
143
|
+
includeState: [...DEFAULT_INCLUDE_STATE],
|
|
144
|
+
filters,
|
|
145
|
+
...paging,
|
|
146
|
+
hierarchy: this.toHierarchyScope(hierarchy),
|
|
147
|
+
}),
|
|
148
|
+
}).pipe(map(({ layout, rows }) => this.withCardLayout(rows, layout)));
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* The card layout authored for a context, with every property key rewritten
|
|
152
|
+
* to its normalized form.
|
|
153
|
+
*
|
|
154
|
+
* A `Card` response names its properties by the owning context's own key
|
|
155
|
+
* (`leveldatafinancial_title`), while a hierarchy response normalizes every
|
|
156
|
+
* row, column and catalog entry across the child levels down to the shared
|
|
157
|
+
* key (`title`). Sent through unchanged the layout would resolve against
|
|
158
|
+
* nothing and every card would render empty.
|
|
159
|
+
*
|
|
160
|
+
* Cached per context for the life of the app: it is a second request on a
|
|
161
|
+
* path that already makes one, it is pure configuration, and it is re-read
|
|
162
|
+
* on the next full load anyway. A card layout edited in the control panel
|
|
163
|
+
* therefore needs a reload of the app to show up on a descendant board —
|
|
164
|
+
* the same page-load boundary the rest of the display configuration has.
|
|
165
|
+
*/
|
|
166
|
+
getCardLayout(contextKey) {
|
|
167
|
+
const cached = this.cardLayouts.get(contextKey);
|
|
168
|
+
if (cached)
|
|
169
|
+
return cached;
|
|
170
|
+
const layout = this.queryRuntime({
|
|
106
171
|
contextKey,
|
|
107
172
|
projection: 'Card',
|
|
108
173
|
includeState: [...DEFAULT_INCLUDE_STATE],
|
|
109
|
-
filters,
|
|
174
|
+
filters: [],
|
|
110
175
|
display: { areas: ['card'] },
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
176
|
+
// The layout lives in `projectionMeta`, so one record is enough to carry
|
|
177
|
+
// it back — the board's own records come from the hierarchy request.
|
|
178
|
+
page: 1,
|
|
179
|
+
pageSize: 1,
|
|
180
|
+
}).pipe(map((response) => this.toNormalizedDisplayOrder(response.data)),
|
|
181
|
+
// A board that renders its rows with the table's columns still shows the
|
|
182
|
+
// right records; one that fails outright shows nothing. The layout is
|
|
183
|
+
// the optional half of the pair.
|
|
184
|
+
catchError(() => of([])), shareReplay({ bufferSize: 1, refCount: false }));
|
|
185
|
+
this.cardLayouts.set(contextKey, layout);
|
|
186
|
+
return layout;
|
|
187
|
+
}
|
|
188
|
+
toNormalizedDisplayOrder(data) {
|
|
189
|
+
const normalizedByKey = new Map((data?.catalog?.properties ?? []).map((property) => [
|
|
190
|
+
property.key,
|
|
191
|
+
property.normalizedKey || property.key,
|
|
192
|
+
]));
|
|
193
|
+
return (data?.projectionMeta?.displayOrder ?? []).map((item) => ({
|
|
194
|
+
...item,
|
|
195
|
+
propertyKey: normalizedByKey.get(item.propertyKey) ?? item.propertyKey,
|
|
196
|
+
}));
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Dresses a hierarchy table response as the card payload the board reads:
|
|
200
|
+
* the same records, plus the authored card layout narrowed to the properties
|
|
201
|
+
* this response actually carries. A layout entry with no property behind it
|
|
202
|
+
* would render as a nameless blank slot, so it is dropped — section markers
|
|
203
|
+
* excepted, since they are layout rather than data and have no property to
|
|
204
|
+
* begin with.
|
|
205
|
+
*
|
|
206
|
+
* Leaves the response untouched when there is no usable layout: the card
|
|
207
|
+
* mapper then falls back to the response's own table columns, which is a
|
|
208
|
+
* plainer board but a complete one.
|
|
209
|
+
*/
|
|
210
|
+
withCardLayout(response, displayOrder) {
|
|
211
|
+
const data = response.data;
|
|
212
|
+
if (!data || displayOrder.length === 0)
|
|
213
|
+
return response;
|
|
214
|
+
const available = new Set((data.catalog?.properties ?? []).flatMap((property) => [property.key, property.normalizedKey].filter((key) => !!key)));
|
|
215
|
+
const usable = displayOrder.filter((item) => isSectionKey(item.propertyKey) || available.has(item.propertyKey));
|
|
216
|
+
if (usable.every((item) => isSectionKey(item.propertyKey)))
|
|
217
|
+
return response;
|
|
218
|
+
return {
|
|
219
|
+
...response,
|
|
220
|
+
data: {
|
|
221
|
+
...data,
|
|
222
|
+
projectionMeta: {
|
|
223
|
+
...(data.projectionMeta ?? { kind: 'Card' }),
|
|
224
|
+
displayOrder: usable,
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
};
|
|
115
228
|
}
|
|
116
229
|
getInformativeDashboard(levelId, moduleId) {
|
|
117
230
|
return this.http.get(`${this.informativeBaseUrl}/levels/${levelId}/modules/${moduleId}/dashboard`);
|
|
@@ -292,6 +405,67 @@ function toDashboardExtraFilters(filters = []) {
|
|
|
292
405
|
return extraFilters;
|
|
293
406
|
}
|
|
294
407
|
|
|
408
|
+
/**
|
|
409
|
+
* A card in the row shape `mt-table` and the table toolbar work with:
|
|
410
|
+
* `{ Id, [entityKey]: EntityData, ... }`. Grouping, filtering, search, export
|
|
411
|
+
* and row actions all run over this, never over the card itself, so a board
|
|
412
|
+
* and a table of the same list read the same fields.
|
|
413
|
+
*
|
|
414
|
+
* `displayProperties` are spread in first: they hold row-only cells a host
|
|
415
|
+
* derived for the list (see `ClientListStateService.setCardsResult`) — values
|
|
416
|
+
* a board can group or filter on without ever rendering them on the card.
|
|
417
|
+
*/
|
|
418
|
+
function toClientListCardRow(card) {
|
|
419
|
+
const row = {
|
|
420
|
+
Id: card.id,
|
|
421
|
+
...(card.displayProperties ?? {}),
|
|
422
|
+
[CLIENT_LIST_RECORD_STATE_KEY]: card.state ?? null,
|
|
423
|
+
// Only present on a descendant board. Row actions read the owning record
|
|
424
|
+
// off the row, so a card's actions address its own owner rather than the
|
|
425
|
+
// aggregation context — the same contract a hierarchy table row carries.
|
|
426
|
+
[CLIENT_LIST_RECORD_SOURCE_KEY]: card.source ?? null,
|
|
427
|
+
};
|
|
428
|
+
for (const entity of card.entities ?? []) {
|
|
429
|
+
if (entity.viewType === 'Section')
|
|
430
|
+
continue;
|
|
431
|
+
const e = entity;
|
|
432
|
+
const key = (typeof e['key'] === 'string' && e['key']) ||
|
|
433
|
+
(typeof e['normalizedKey'] === 'string' && e['normalizedKey']) ||
|
|
434
|
+
entity.name;
|
|
435
|
+
const normalizedKey = typeof e['normalizedKey'] === 'string' ? e['normalizedKey'] : null;
|
|
436
|
+
const keys = new Set([key, normalizedKey].filter((entry) => typeof entry === 'string' && entry.length > 0));
|
|
437
|
+
for (const entityKey of keys) {
|
|
438
|
+
row[entityKey] = entity;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return row;
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* The columns a board's toolbar filters, groups and exports by, read off one
|
|
445
|
+
* card's entities — each carries its `key`, `name` (label) and `viewType`,
|
|
446
|
+
* which is everything `TableValueResolver.getColumnFilterType` needs.
|
|
447
|
+
*/
|
|
448
|
+
function toClientListCardColumns(card) {
|
|
449
|
+
// Section markers are card-layout only — never table columns.
|
|
450
|
+
return (card?.entities ?? [])
|
|
451
|
+
.filter((entity) => entity.viewType !== 'Section')
|
|
452
|
+
.map((entity) => {
|
|
453
|
+
const raw = entity;
|
|
454
|
+
const key = (typeof raw['key'] === 'string' && raw['key']) ||
|
|
455
|
+
(typeof raw['normalizedKey'] === 'string' && raw['normalizedKey']) ||
|
|
456
|
+
entity.name;
|
|
457
|
+
return {
|
|
458
|
+
key: String(key),
|
|
459
|
+
normalizedKey: typeof raw['normalizedKey'] === 'string'
|
|
460
|
+
? raw['normalizedKey']
|
|
461
|
+
: String(key),
|
|
462
|
+
label: entity.name ?? String(key),
|
|
463
|
+
type: 'entity',
|
|
464
|
+
viewType: (entity.viewType ?? 'Text'),
|
|
465
|
+
};
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
295
469
|
const UNSORTABLE_ENTITY_VIEW_TYPES = new Set([
|
|
296
470
|
'Attachment',
|
|
297
471
|
'Checkbox',
|
|
@@ -451,6 +625,7 @@ class ClientListStateService {
|
|
|
451
625
|
const cards = isAppend
|
|
452
626
|
? this.concatDistinctCards(target.cards, newCards)
|
|
453
627
|
: newCards;
|
|
628
|
+
const shaped = this.shapeCards(cards, config, response);
|
|
454
629
|
const schema = this.resolvePrimarySchema(response);
|
|
455
630
|
const firstCardModule = newCards[0]?.module;
|
|
456
631
|
const moduleKey = schema?.key ?? firstCardModule?.key ?? target.moduleKey;
|
|
@@ -481,9 +656,13 @@ class ClientListStateService {
|
|
|
481
656
|
...target,
|
|
482
657
|
title,
|
|
483
658
|
moduleKey,
|
|
484
|
-
columns:
|
|
659
|
+
columns: shaped.columns,
|
|
485
660
|
rows: [],
|
|
486
|
-
cards,
|
|
661
|
+
cards: shaped.cards,
|
|
662
|
+
// Pre-pagination aggregates, kept verbatim off the response, exactly
|
|
663
|
+
// as a hierarchy table keeps them. Absent on an ordinary board.
|
|
664
|
+
totals: response.projectionMeta?.totals,
|
|
665
|
+
sourceGroups: response.projectionMeta?.sourceGroups,
|
|
487
666
|
// `skip` is the offset of the *next* page to fetch, always a multiple
|
|
488
667
|
// of the effective page size so `toPage()` stays exact.
|
|
489
668
|
skip: config.isPaginated
|
|
@@ -611,9 +790,11 @@ class ClientListStateService {
|
|
|
611
790
|
records[recordIndex] = merged;
|
|
612
791
|
else
|
|
613
792
|
records.unshift(merged);
|
|
793
|
+
const shaped = this.shapeCards(cards, target.config, raw);
|
|
614
794
|
this.patchItem(key, {
|
|
615
795
|
...target,
|
|
616
|
-
cards,
|
|
796
|
+
cards: shaped.cards,
|
|
797
|
+
columns: shaped.columns,
|
|
617
798
|
totalCount: isInsert ? target.totalCount + 1 : target.totalCount,
|
|
618
799
|
error: null,
|
|
619
800
|
rawData: this.withRecords(raw, records, isInsert ? 1 : 0),
|
|
@@ -742,6 +923,46 @@ class ClientListStateService {
|
|
|
742
923
|
rawData: existing.rawData,
|
|
743
924
|
};
|
|
744
925
|
}
|
|
926
|
+
/**
|
|
927
|
+
* Runs the host's `transformResult` over a board, so it offers the same
|
|
928
|
+
* columns the table of the same list does — the ones a host derives
|
|
929
|
+
* included (a descendant list's level and owning record, say). That is what
|
|
930
|
+
* lets a board group, filter and export by them, and what lets a
|
|
931
|
+
* `table.groupBy` default naming a derived column group a board at all.
|
|
932
|
+
*
|
|
933
|
+
* The card face is left alone: the card keeps rendering its own layout, and
|
|
934
|
+
* the derived cells ride on `displayProperties`, which only the card's row
|
|
935
|
+
* representation reads. Cells the transform rewrote rather than added are
|
|
936
|
+
* not carried over — the card already shows those fields its own way.
|
|
937
|
+
*
|
|
938
|
+
* Without a transform the board keeps its old behaviour: no `columns`, so
|
|
939
|
+
* the view derives them from the first card's entities.
|
|
940
|
+
*/
|
|
941
|
+
shapeCards(cards, config, response) {
|
|
942
|
+
if (!config.transformResult || cards.length === 0) {
|
|
943
|
+
return { cards, columns: [] };
|
|
944
|
+
}
|
|
945
|
+
// Built from the card's own fields only, never from cells an earlier
|
|
946
|
+
// shaping added, so reshaping an accumulated (appended) board is
|
|
947
|
+
// idempotent.
|
|
948
|
+
const baseRows = cards.map((card) => toClientListCardRow({ ...card, displayProperties: undefined }));
|
|
949
|
+
const shaped = config.transformResult({ columns: toClientListCardColumns(cards[0]), rows: baseRows }, response);
|
|
950
|
+
const shapedById = new Map(shaped.rows.map((row) => [row['Id'], row]));
|
|
951
|
+
return {
|
|
952
|
+
columns: shaped.columns,
|
|
953
|
+
cards: cards.map((card, index) => {
|
|
954
|
+
const row = shapedById.get(card.id);
|
|
955
|
+
if (!row)
|
|
956
|
+
return card;
|
|
957
|
+
const base = baseRows[index];
|
|
958
|
+
const derived = Object.fromEntries(Object.entries(row).filter(([rowKey]) => !(rowKey in base)));
|
|
959
|
+
return {
|
|
960
|
+
...card,
|
|
961
|
+
displayProperties: Object.keys(derived).length ? derived : undefined,
|
|
962
|
+
};
|
|
963
|
+
}),
|
|
964
|
+
};
|
|
965
|
+
}
|
|
745
966
|
toCards(data) {
|
|
746
967
|
const records = this.orderRecords(data?.records ?? [], data?.projectionMeta?.groups ?? []);
|
|
747
968
|
const catalogProperties = data?.catalog?.properties ?? [];
|
|
@@ -774,6 +995,10 @@ class ClientListStateService {
|
|
|
774
995
|
state: record.state ?? null,
|
|
775
996
|
properties: normalizedEntities,
|
|
776
997
|
entities: normalizedEntities,
|
|
998
|
+
// Only present on a descendant board, where cards come from several
|
|
999
|
+
// owners at once and the list's own context no longer identifies the
|
|
1000
|
+
// record a card belongs to.
|
|
1001
|
+
source: record.source ?? null,
|
|
777
1002
|
};
|
|
778
1003
|
});
|
|
779
1004
|
}
|
|
@@ -1275,11 +1500,11 @@ class ClientListTableView {
|
|
|
1275
1500
|
};
|
|
1276
1501
|
}
|
|
1277
1502
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: ClientListTableView, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1278
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: ClientListTableView, isStandalone: true, selector: "mt-client-list-table-view", inputs: { state: { classPropertyName: "state", publicName: "state", isSignal: true, isRequired: true, transformFunction: null }, rowActions: { classPropertyName: "rowActions", publicName: "rowActions", isSignal: true, isRequired: false, transformFunction: null }, actionShape: { classPropertyName: "actionShape", publicName: "actionShape", isSignal: true, isRequired: false, transformFunction: null }, rowActionsLoadingFn: { classPropertyName: "rowActionsLoadingFn", publicName: "rowActionsLoadingFn", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { lazyLoad: "lazyLoad", rowClick: "rowClick", rowActionsRequested: "rowActionsRequested" }, ngImport: i0, template: "<div class=\"grid gap-4\" [style.gridTemplateColumns]=\"gridTemplateColumns\">\
|
|
1503
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: ClientListTableView, isStandalone: true, selector: "mt-client-list-table-view", inputs: { state: { classPropertyName: "state", publicName: "state", isSignal: true, isRequired: true, transformFunction: null }, rowActions: { classPropertyName: "rowActions", publicName: "rowActions", isSignal: true, isRequired: false, transformFunction: null }, actionShape: { classPropertyName: "actionShape", publicName: "actionShape", isSignal: true, isRequired: false, transformFunction: null }, rowActionsLoadingFn: { classPropertyName: "rowActionsLoadingFn", publicName: "rowActionsLoadingFn", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { lazyLoad: "lazyLoad", rowClick: "rowClick", rowActionsRequested: "rowActionsRequested" }, ngImport: i0, template: "<div class=\"grid gap-4\" [style.gridTemplateColumns]=\"gridTemplateColumns\">\n @if (table().config.contentStart) {\n <div [style.gridColumn]=\"slotGridSpan(table().config.layout.startSpan)\">\n <ng-container\n [ngTemplateOutlet]=\"table().config.contentStart\"\n [ngTemplateOutletContext]=\"templateContext(table())\"\n />\n </div>\n }\n\n <div [style.gridColumn]=\"slotGridSpan(table().config.layout.tableSpan)\">\n @if (table().loading && table().rows.length === 0) {\n <div class=\"flex flex-col gap-3 py-3\">\n <p-skeleton height=\"3rem\" />\n <p-skeleton height=\"3rem\" />\n <p-skeleton height=\"3rem\" />\n </div>\n } @else if (table().error) {\n <div\n class=\"rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-700\"\n >\n {{ table().error }}\n </div>\n } @else {\n <mt-table\n [noCard]=\"true\"\n [data]=\"table().rows\"\n [columns]=\"table().columns\"\n dataKey=\"Id\"\n [storageKey]=\"tableStorageKey()\"\n [persistStateExclude]=\"table().config.table.persistStateExclude\"\n [captionStartTemplate]=\"table().config.toolbarStart ?? null\"\n [captionEndTemplate]=\"table().config.toolbarEnd ?? null\"\n [rowActions]=\"rowActions()\"\n [actionShape]=\"actionShape()\"\n [rowActionsLoadingFn]=\"rowActionsLoadingFn()\"\n (rowActionsRequested)=\"rowActionsRequested.emit($event)\"\n [generalSearch]=\"table().config.table.searchable\"\n [showFilters]=\"true\"\n filterMode=\"column\"\n [printable]=\"true\"\n [groupable]=\"true\"\n [cellClickFilter]=\"true\"\n [freezeActions]=\"true\"\n [exportable]=\"table().config.table.exportable\"\n [loading]=\"table().loading\"\n [clickableRows]=\"true\"\n [lazy]=\"table().config.isPaginated\"\n [lazyTotalRecords]=\"table().totalCount\"\n [virtualScroll]=\"!table().config.isPaginated\"\n [virtualScrollItemSize]=\"56\"\n scrollHeight=\"520px\"\n [pageSize]=\"pageSize()\"\n [filters]=\"bucket().filters()\"\n (filtersChange)=\"bucket().filters.set($event)\"\n [filterTerm]=\"bucket().filterTerm()\"\n (filterTermChange)=\"bucket().filterTerm.set($event)\"\n [groupBy]=\"bucket().groupBy()\"\n (groupByChange)=\"bucket().groupBy.set($event)\"\n [sortField]=\"bucket().sortField()\"\n (sortFieldChange)=\"bucket().sortField.set($event)\"\n [sortDirection]=\"bucket().sortDirection()\"\n (sortDirectionChange)=\"bucket().sortDirection.set($event)\"\n [activeTab]=\"bucket().activeTab()\"\n (activeTabChange)=\"bucket().activeTab.set($event)\"\n (lazyLoad)=\"lazyLoad.emit($event)\"\n (rowClick)=\"rowClick.emit($event)\"\n />\n }\n </div>\n\n @if (table().config.contentEnd) {\n <div [style.gridColumn]=\"slotGridSpan(table().config.layout.endSpan)\">\n <ng-container\n [ngTemplateOutlet]=\"table().config.contentEnd\"\n [ngTemplateOutletContext]=\"templateContext(table())\"\n />\n </div>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: Table, selector: "mt-table", inputs: ["filters", "data", "columns", "rowActions", "size", "showGridlines", "stripedRows", "selectableRows", "clickableRows", "generalSearch", "lazyLocalSearch", "showFilters", "filterMode", "loading", "updating", "lazy", "lazyLocalSort", "lazyTotalRecords", "reorderableColumns", "reorderableRows", "dataKey", "storageKey", "storageMode", "persistStateExclude", "exportable", "printable", "groupable", "groupCountMap", "cellClickFilter", "freezeActions", "virtualScroll", "virtualScrollItemSize", "scrollHeight", "printTitle", "exportFilename", "actionShape", "rowActionsLoadingFn", "tableLayout", "noCard", "tabs", "tabsOptionLabel", "tabsOptionValue", "activeTab", "actions", "captionStartTemplate", "captionEndTemplate", "emptyTitle", "emptyDescription", "emptyActionLabel", "emptyActionIcon", "paginatorPosition", "alwaysShowPaginator", "rowsPerPageOptions", "pageSize", "currentPage", "first", "filterTerm", "groupBy", "sortField", "sortDirection"], outputs: ["selectionChange", "cellChange", "lazyLoad", "columnReorder", "rowReorder", "rowClick", "emptyAction", "rowActionsRequested", "filtersChange", "activeTabChange", "onTabChange", "pageSizeChange", "currentPageChange", "firstChange", "filterTermChange", "groupByChange", "sortFieldChange", "sortDirectionChange"] }, { kind: "ngmodule", type: SkeletonModule }, { kind: "component", type: i2.Skeleton, selector: "p-skeleton", inputs: ["styleClass", "shape", "animation", "borderRadius", "size", "width", "height"] }] });
|
|
1279
1504
|
}
|
|
1280
1505
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: ClientListTableView, decorators: [{
|
|
1281
1506
|
type: Component,
|
|
1282
|
-
args: [{ selector: 'mt-client-list-table-view', standalone: true, imports: [CommonModule, Table, SkeletonModule], template: "<div class=\"grid gap-4\" [style.gridTemplateColumns]=\"gridTemplateColumns\">\
|
|
1507
|
+
args: [{ selector: 'mt-client-list-table-view', standalone: true, imports: [CommonModule, Table, SkeletonModule], template: "<div class=\"grid gap-4\" [style.gridTemplateColumns]=\"gridTemplateColumns\">\n @if (table().config.contentStart) {\n <div [style.gridColumn]=\"slotGridSpan(table().config.layout.startSpan)\">\n <ng-container\n [ngTemplateOutlet]=\"table().config.contentStart\"\n [ngTemplateOutletContext]=\"templateContext(table())\"\n />\n </div>\n }\n\n <div [style.gridColumn]=\"slotGridSpan(table().config.layout.tableSpan)\">\n @if (table().loading && table().rows.length === 0) {\n <div class=\"flex flex-col gap-3 py-3\">\n <p-skeleton height=\"3rem\" />\n <p-skeleton height=\"3rem\" />\n <p-skeleton height=\"3rem\" />\n </div>\n } @else if (table().error) {\n <div\n class=\"rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-700\"\n >\n {{ table().error }}\n </div>\n } @else {\n <mt-table\n [noCard]=\"true\"\n [data]=\"table().rows\"\n [columns]=\"table().columns\"\n dataKey=\"Id\"\n [storageKey]=\"tableStorageKey()\"\n [persistStateExclude]=\"table().config.table.persistStateExclude\"\n [captionStartTemplate]=\"table().config.toolbarStart ?? null\"\n [captionEndTemplate]=\"table().config.toolbarEnd ?? null\"\n [rowActions]=\"rowActions()\"\n [actionShape]=\"actionShape()\"\n [rowActionsLoadingFn]=\"rowActionsLoadingFn()\"\n (rowActionsRequested)=\"rowActionsRequested.emit($event)\"\n [generalSearch]=\"table().config.table.searchable\"\n [showFilters]=\"true\"\n filterMode=\"column\"\n [printable]=\"true\"\n [groupable]=\"true\"\n [cellClickFilter]=\"true\"\n [freezeActions]=\"true\"\n [exportable]=\"table().config.table.exportable\"\n [loading]=\"table().loading\"\n [clickableRows]=\"true\"\n [lazy]=\"table().config.isPaginated\"\n [lazyTotalRecords]=\"table().totalCount\"\n [virtualScroll]=\"!table().config.isPaginated\"\n [virtualScrollItemSize]=\"56\"\n scrollHeight=\"520px\"\n [pageSize]=\"pageSize()\"\n [filters]=\"bucket().filters()\"\n (filtersChange)=\"bucket().filters.set($event)\"\n [filterTerm]=\"bucket().filterTerm()\"\n (filterTermChange)=\"bucket().filterTerm.set($event)\"\n [groupBy]=\"bucket().groupBy()\"\n (groupByChange)=\"bucket().groupBy.set($event)\"\n [sortField]=\"bucket().sortField()\"\n (sortFieldChange)=\"bucket().sortField.set($event)\"\n [sortDirection]=\"bucket().sortDirection()\"\n (sortDirectionChange)=\"bucket().sortDirection.set($event)\"\n [activeTab]=\"bucket().activeTab()\"\n (activeTabChange)=\"bucket().activeTab.set($event)\"\n (lazyLoad)=\"lazyLoad.emit($event)\"\n (rowClick)=\"rowClick.emit($event)\"\n />\n }\n </div>\n\n @if (table().config.contentEnd) {\n <div [style.gridColumn]=\"slotGridSpan(table().config.layout.endSpan)\">\n <ng-container\n [ngTemplateOutlet]=\"table().config.contentEnd\"\n [ngTemplateOutletContext]=\"templateContext(table())\"\n />\n </div>\n }\n</div>\n" }]
|
|
1283
1508
|
}], propDecorators: { state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: true }] }], rowActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowActions", required: false }] }], actionShape: [{ type: i0.Input, args: [{ isSignal: true, alias: "actionShape", required: false }] }], rowActionsLoadingFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowActionsLoadingFn", required: false }] }], lazyLoad: [{ type: i0.Output, args: ["lazyLoad"] }], rowClick: [{ type: i0.Output, args: ["rowClick"] }], rowActionsRequested: [{ type: i0.Output, args: ["rowActionsRequested"] }] } });
|
|
1284
1509
|
|
|
1285
1510
|
const EMPTY_KEY_SET = new Set();
|
|
@@ -1335,8 +1560,15 @@ class ClientListCardsView {
|
|
|
1335
1560
|
* observed so scrolling it into view triggers `loadMore`. */
|
|
1336
1561
|
loadMoreSentinel = viewChild('loadMoreSentinel', ...(ngDevMode ? [{ debugName: "loadMoreSentinel" }] : /* istanbul ignore next */ []));
|
|
1337
1562
|
loadMoreObserver = null;
|
|
1338
|
-
/**
|
|
1339
|
-
|
|
1563
|
+
/**
|
|
1564
|
+
* Shared toolbar bucket — same instance the table view uses. Passes the same
|
|
1565
|
+
* `table.groupBy` default, so a board opened first starts grouped exactly as
|
|
1566
|
+
* the table would. The service seeds it once per list, whichever view asks
|
|
1567
|
+
* first, so the user's own regrouping still sticks.
|
|
1568
|
+
*/
|
|
1569
|
+
bucket = computed(() => this.toolbar.forList(this.state().key, {
|
|
1570
|
+
groupBy: this.state().config.table.groupBy,
|
|
1571
|
+
}), ...(ngDevMode ? [{ debugName: "bucket" }] : /* istanbul ignore next */ []));
|
|
1340
1572
|
/** True while a column filter or search term is active — drives the
|
|
1341
1573
|
* reset button's visibility in the caption toolbar. */
|
|
1342
1574
|
hasActiveFilter = computed(() => Object.keys(this.bucket().filters()).length > 0 ||
|
|
@@ -1386,38 +1618,16 @@ class ClientListCardsView {
|
|
|
1386
1618
|
this.bucket().filterTerm.set('');
|
|
1387
1619
|
}
|
|
1388
1620
|
/**
|
|
1389
|
-
* Columns for filter/group/export.
|
|
1390
|
-
* (
|
|
1391
|
-
*
|
|
1392
|
-
*
|
|
1393
|
-
* everything `TableValueResolver.getColumnFilterType` needs to classify
|
|
1394
|
-
* filter behaviour (select / boolean / date / user / text).
|
|
1621
|
+
* Columns for filter/group/export. A board whose host reshapes the list
|
|
1622
|
+
* (`transformResult`) carries the reshaped columns on `state.columns` — the
|
|
1623
|
+
* same ones the table shows, host-derived ones included — and otherwise
|
|
1624
|
+
* they are rebuilt from the first card's entity metadata.
|
|
1395
1625
|
*/
|
|
1396
1626
|
columns = computed(() => {
|
|
1397
1627
|
const fromState = this.state().columns;
|
|
1398
1628
|
if (fromState && fromState.length > 0)
|
|
1399
1629
|
return fromState;
|
|
1400
|
-
|
|
1401
|
-
if (!sampleCard?.entities?.length)
|
|
1402
|
-
return [];
|
|
1403
|
-
// Section markers are card-layout only — never table columns.
|
|
1404
|
-
return sampleCard.entities
|
|
1405
|
-
.filter((entity) => entity.viewType !== 'Section')
|
|
1406
|
-
.map((entity) => {
|
|
1407
|
-
const raw = entity;
|
|
1408
|
-
const key = (typeof raw['key'] === 'string' && raw['key']) ||
|
|
1409
|
-
(typeof raw['normalizedKey'] === 'string' && raw['normalizedKey']) ||
|
|
1410
|
-
entity.name;
|
|
1411
|
-
return {
|
|
1412
|
-
key: String(key),
|
|
1413
|
-
normalizedKey: typeof raw['normalizedKey'] === 'string'
|
|
1414
|
-
? raw['normalizedKey']
|
|
1415
|
-
: String(key),
|
|
1416
|
-
label: entity.name ?? String(key),
|
|
1417
|
-
type: 'entity',
|
|
1418
|
-
viewType: (entity.viewType ?? 'Text'),
|
|
1419
|
-
};
|
|
1420
|
-
});
|
|
1630
|
+
return toClientListCardColumns(this.cardsState().cards[0]);
|
|
1421
1631
|
}, ...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
|
|
1422
1632
|
/**
|
|
1423
1633
|
* Row view of each card. Cards carry their data as `entities: EntityData[]`,
|
|
@@ -1428,28 +1638,7 @@ class ClientListCardsView {
|
|
|
1428
1638
|
*/
|
|
1429
1639
|
rowRepresentations = computed(() => this.cardsState().cards.map((card) => this.cardToRow(card)), ...(ngDevMode ? [{ debugName: "rowRepresentations" }] : /* istanbul ignore next */ []));
|
|
1430
1640
|
cardToRow(card) {
|
|
1431
|
-
|
|
1432
|
-
const record = fromDisplay && Object.keys(fromDisplay).length > 0
|
|
1433
|
-
? {
|
|
1434
|
-
Id: card.id,
|
|
1435
|
-
...fromDisplay,
|
|
1436
|
-
[CLIENT_LIST_RECORD_STATE_KEY]: card.state ?? null,
|
|
1437
|
-
}
|
|
1438
|
-
: { Id: card.id, [CLIENT_LIST_RECORD_STATE_KEY]: card.state ?? null };
|
|
1439
|
-
for (const entity of card.entities ?? []) {
|
|
1440
|
-
if (entity.viewType === 'Section')
|
|
1441
|
-
continue;
|
|
1442
|
-
const e = entity;
|
|
1443
|
-
const key = (typeof e['key'] === 'string' && e['key']) ||
|
|
1444
|
-
(typeof e['normalizedKey'] === 'string' && e['normalizedKey']) ||
|
|
1445
|
-
entity.name;
|
|
1446
|
-
const normalizedKey = typeof e['normalizedKey'] === 'string' ? e['normalizedKey'] : null;
|
|
1447
|
-
const keys = new Set([key, normalizedKey].filter((entry) => typeof entry === 'string' && entry.length > 0));
|
|
1448
|
-
for (const entityKey of keys) {
|
|
1449
|
-
record[entityKey] = entity;
|
|
1450
|
-
}
|
|
1451
|
-
}
|
|
1452
|
-
return record;
|
|
1641
|
+
return toClientListCardRow(card);
|
|
1453
1642
|
}
|
|
1454
1643
|
/**
|
|
1455
1644
|
* Stable card → row lookup. Row actions (and their `hidden` / `action`
|
|
@@ -2001,7 +2190,7 @@ class ClientListCardsView {
|
|
|
2001
2190
|
});
|
|
2002
2191
|
}
|
|
2003
2192
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: ClientListCardsView, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2004
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: ClientListCardsView, isStandalone: true, selector: "mt-client-list-cards-view", inputs: { state: { classPropertyName: "state", publicName: "state", isSignal: true, isRequired: true, transformFunction: null }, rowActions: { classPropertyName: "rowActions", publicName: "rowActions", isSignal: true, isRequired: false, transformFunction: null }, rowActionsLoadingFn: { classPropertyName: "rowActionsLoadingFn", publicName: "rowActionsLoadingFn", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { lazyLoad: "lazyLoad", loadMore: "loadMore", cardClick: "cardClick", rowActionsRequested: "rowActionsRequested" }, providers: [MTDateFormatPipe], viewQueries: [{ propertyName: "loadMoreSentinel", first: true, predicate: ["loadMoreSentinel"], descendants: true, isSignal: true }], ngImport: i0, template: "<mt-table-caption\n [generalSearch]=\"true\"\n [showFilters]=\"true\"\n filterMode=\"popover\"\n [printable]=\"true\"\n [groupable]=\"true\"\n [exportable]=\"true\"\n [columns]=\"columns()\"\n [data]=\"rowRepresentations()\"\n [groupColumns]=\"grouping.groupableColumns()\"\n [actions]=\"toolbarActions()\"\n [captionStart]=\"cardsState().config.toolbarStart ?? null\"\n [captionEnd]=\"cardsState().config.toolbarEnd ?? null\"\n [filters]=\"bucket().filters()\"\n (filtersChange)=\"bucket().filters.set($event)\"\n [filterTerm]=\"bucket().filterTerm()\"\n (filterTermChange)=\"bucket().filterTerm.set($event)\"\n [groupBy]=\"bucket().groupBy()\"\n (groupByChange)=\"bucket().groupBy.set($event)\"\n [activeTab]=\"bucket().activeTab()\"\n (activeTabChange)=\"bucket().activeTab.set($event)\"\n (exportRequested)=\"exportExcel()\"\n (printRequested)=\"printPdf()\"\n/>\n\n@if (badgeLegend().length > 0) {\n <!--\n Decodes the coloured count pills on the cards below. Rendered once for the\n whole board rather than per card, so it costs one line of vertical space\n instead of repeating a key inside every tile (finding M-04).\n -->\n <div\n class=\"mb-3 flex flex-wrap items-center gap-x-4 gap-y-2 px-1\"\n role=\"list\"\n [attr.aria-label]=\"\n 'components.clientComponents.list.badgeLegend' | transloco\n \"\n >\n <span class=\"text-xs font-medium text-gray-500 dark:text-gray-400\">\n {{ \"components.clientComponents.list.badgeLegend\" | transloco }}\n </span>\n @for (entry of badgeLegend(); track entry.key) {\n <span class=\"flex items-center gap-1.5\" role=\"listitem\">\n <span\n class=\"inline-flex h-4 min-w-4 items-center justify-center rounded text-[10px] font-bold\"\n [style.background-color]=\"entry.color\"\n [style.color]=\"entry.textColor\"\n aria-hidden=\"true\"\n >\n #\n </span>\n <span class=\"text-xs text-gray-600 dark:text-gray-300\">{{\n entry.label\n }}</span>\n </span>\n }\n </div>\n}\n\n@if (cardsState().loading && cardsState().cards.length === 0) {\n <div class=\"mt-cards-grid\">\n @for (i of [1, 2, 3, 4, 5, 6]; track i) {\n <p-skeleton height=\"20rem\" class=\"rounded-lg\" />\n }\n </div>\n} @else if (cardsState().error) {\n <div class=\"rounded-lg border border-red-200 bg-red-50 p-6 text-red-600\">\n {{ cardsState().error }}\n </div>\n} @else if (visibleCards().length === 0) {\n <!--\n The same empty state the tables use, so a board and a list stop looking like\n two products (finding H-12). A board narrowed by a search or a filter says\n so and offers to clear it, instead of the bare grey \"No cards found.\"\n -->\n <mt-empty-state\n [variant]=\"hasActiveCardFilters() ? 'filtered' : 'empty'\"\n [title]=\"\n hasActiveCardFilters()\n ? ''\n : ('components.clientComponents.list.noCardsFound' | transloco)\n \"\n [actionLabel]=\"\n hasActiveCardFilters()\n ? ('components.emptyState.clearFilters' | transloco)\n : ''\n \"\n actionIcon=\"general.x-close\"\n (actionClick)=\"clearCardFilters()\"\n />\n} @else if (grouping.groupingActive()) {\n <div class=\"flex flex-col gap-6\">\n @for (group of groupedCards(); track group.key) {\n <section class=\"flex flex-col gap-3\">\n <header\n class=\"flex items-center gap-3 px-2 py-2 bg-surface-50 dark:bg-surface-900 border-y border-surface-200 dark:border-surface-700 rounded\"\n [style.--mt-group-accent]=\"group.accent ?? 'var(--p-primary-color)'\"\n >\n <span\n class=\"inline-block w-1 h-5 rounded-full\"\n [style.background]=\"'var(--mt-group-accent)'\"\n ></span>\n <span\n class=\"text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400\"\n >\n {{ group.columnLabel }}\n </span>\n <span class=\"text-sm font-semibold text-gray-900 dark:text-gray-100\">\n {{ group.label }}\n </span>\n <span\n class=\"ml-auto inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-primary-50 text-primary-700 dark:bg-primary-950 dark:text-primary-300\"\n >\n {{ group.count }}\n </span>\n </header>\n <div class=\"mt-cards-grid\">\n @for (card of group.cards; track card.id) {\n <mt-card\n class=\"relative cursor-pointer p-5 shadow-sm transition-shadow hover:shadow-md\"\n (click)=\"cardClick.emit(card)\"\n >\n <ng-template #headless>\n <ng-container\n [ngTemplateOutlet]=\"cardActionsMenu\"\n [ngTemplateOutletContext]=\"{ $implicit: card }\"\n />\n <mt-entities-preview\n [entities]=\"card.entities\"\n attachmentShape=\"compact\"\n [stackOnNarrow]=\"false\"\n [clickableKeys]=\"clickableEntityKeys()\"\n [activeKeys]=\"activeKeysForCard(card)\"\n [clickableTooltip]=\"filterByThisLabel()\"\n [activeTooltip]=\"clearFilterLabel()\"\n (entityClick)=\"onEntityFilterClick(card, $event)\"\n />\n </ng-template>\n </mt-card>\n }\n </div>\n </section>\n }\n </div>\n} @else {\n <div class=\"mt-cards-grid\">\n @for (card of visibleCards(); track card.id) {\n @defer (on viewport) {\n <mt-card\n class=\"relative cursor-pointer p-5 shadow-sm transition-shadow hover:shadow-md\"\n (click)=\"cardClick.emit(card)\"\n >\n <ng-template #headless>\n <ng-container\n [ngTemplateOutlet]=\"cardActionsMenu\"\n [ngTemplateOutletContext]=\"{ $implicit: card }\"\n />\n <mt-entities-preview\n [entities]=\"card.entities\"\n attachmentShape=\"compact\"\n [stackOnNarrow]=\"false\"\n [clickableKeys]=\"clickableEntityKeys()\"\n [activeKeys]=\"activeKeysForCard(card)\"\n [clickableTooltip]=\"filterByThisLabel()\"\n [activeTooltip]=\"clearFilterLabel()\"\n (entityClick)=\"onEntityFilterClick(card, $event)\"\n />\n </ng-template>\n </mt-card>\n } @placeholder {\n <div class=\"min-h-[20rem]\">\n <p-skeleton height=\"20rem\" class=\"rounded-lg\" />\n </div>\n }\n }\n </div>\n}\n\n<!--\n Infinite-scroll trigger. Deliberately outside the @if/@else chain above:\n inside it the element would be torn down and rebuilt every time the view\n swapped branch (error, empty filter result, group-by), and the observer\n would be left watching a detached node. Kept in the DOM and hidden via CSS\n when there is nothing left to load, so appending a page never recreates it.\n It also has to sit outside so infinite scroll keeps working while a\n group-by is active \u2014 grouping is a pure FE view over whatever has been\n fetched, not a reason to stop fetching.\n-->\n<div\n #loadMoreSentinel\n class=\"mt-cards-grid py-4\"\n [class.hidden]=\"!hasMoreCards()\"\n>\n @for (i of [1, 2, 3]; track i) {\n <p-skeleton height=\"20rem\" class=\"rounded-lg\" />\n }\n</div>\n\n<!--\n Per-card escalation marker and 3-dots actions menu. The compact corner cover\n only indicates active escalation state; its preview action lives in the menu\n so the marker never displaces or competes with the menu trigger.\n-->\n<ng-template #cardActionsMenu let-card>\n @let escalationAction = escalationMarkerAction(card);\n @if (escalationAction) {\n <span\n class=\"mt-escalation-marker absolute end-0 top-0 z-10 h-[18px] w-[20px]\"\n role=\"img\"\n tabindex=\"0\"\n [attr.aria-label]=\"escalationLabel()\"\n [mtTooltip]=\"escalationLabel()\"\n tooltipPosition=\"top\"\n ></span>\n }\n @if (rowActions().length > 0) {\n <div\n class=\"absolute end-2 top-2 z-20 flex items-center gap-1\"\n (click)=\"$event.stopPropagation()\"\n >\n @for (action of inlineActionsForCard(card); track $index) {\n <mt-button\n [icon]=\"action.icon\"\n [severity]=\"action.color\"\n [variant]=\"action.variant\"\n [size]=\"actionSize(action) || 'small'\"\n [tooltip]=\"action.tooltip\"\n [label]=\"action.label\"\n [loading]=\"resolveActionLoading(action, card)\"\n (onClick)=\"runCardAction($event, action, card)\"\n />\n }\n @if (showCardActionsMenu(card)) {\n <button\n type=\"button\"\n class=\"flex h-7 w-7 cursor-pointer items-center justify-center rounded-md border-0 bg-transparent p-0 text-base text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 dark:text-gray-300 dark:hover:bg-surface-700 dark:hover:text-gray-100\"\n [attr.aria-label]=\"actionsLabel()\"\n [mtTooltip]=\"actionsLabel()\"\n tooltipPosition=\"top\"\n (click)=\"onCardActionsToggle($event, cardPopover, card)\"\n >\n <mt-icon icon=\"general.dots-vertical\" class=\"text-base\" />\n </button>\n <p-popover #cardPopover appendTo=\"body\">\n <div class=\"flex min-w-44 flex-col gap-1 p-1\">\n @if (isCardActionsLoading(card)) {\n <div class=\"flex flex-col gap-2 p-1\">\n <p-skeleton height=\"1.75rem\" />\n <p-skeleton height=\"1.75rem\" />\n <p-skeleton height=\"1.75rem\" />\n </div>\n } @else {\n @for (action of menuActionsForCard(card); track $index) {\n @if (isFirstDestructiveCardAction(card, $index)) {\n <hr\n class=\"my-1 border-0 border-t border-surface-200 dark:border-surface-700\"\n />\n }\n <button\n type=\"button\"\n class=\"flex cursor-pointer items-center gap-2 rounded px-2 py-2 text-start text-sm transition-colors\"\n [class]=\"\n action.color === 'danger'\n ? 'text-red-600 hover:bg-red-50 dark:hover:bg-red-950'\n : action.color === 'warn'\n ? 'text-orange-600 hover:bg-orange-50 dark:text-orange-400 dark:hover:bg-orange-950'\n : 'text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-surface-700'\n \"\n (click)=\"\n runCardAction($event, action, card); cardPopover.hide()\n \"\n >\n @if (action.icon) {\n <mt-icon [icon]=\"action.icon\" class=\"text-base\" />\n }\n <span>{{ action.label || action.tooltip }}</span>\n </button>\n }\n }\n </div>\n </p-popover>\n }\n </div>\n }\n</ng-template>\n", styles: [".mt-cards-grid{--mt-cards-gap: 1rem;--mt-cards-min: 335px;--mt-cards-track: max( var(--mt-cards-min), (100% - 3 * var(--mt-cards-gap)) / 4 );display:grid;gap:var(--mt-cards-gap);grid-template-columns:repeat(auto-fill,minmax(min(100%,var(--mt-cards-track)),1fr))}.mt-cards-grid.hidden,.mt-cards-grid[hidden]{display:none}.mt-escalation-marker{border-start-end-radius:.5rem;background:var(--p-button-text-warn-color, var(--p-orange-500, #f59e0b));clip-path:polygon(0 0,100% 0,100% 100%);transition:filter .15s ease}.mt-escalation-marker:dir(rtl){clip-path:polygon(0 0,100% 0,0 100%)}.mt-escalation-marker:hover{filter:brightness(.92)}.mt-escalation-marker:focus-visible{filter:brightness(.92) drop-shadow(0 0 2px var(--p-button-text-warn-color, #f59e0b));outline:none}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: TranslocoModule }, { kind: "component", type: Card, selector: "mt-card", inputs: ["class", "title", "paddingless"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs", "ariaLabel"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "directive", type: Tooltip, selector: "[mtTooltip]" }, { kind: "component", type: Icon, selector: "mt-icon", inputs: ["icon"] }, { kind: "component", type: EntitiesPreview, selector: "mt-entities-preview", inputs: ["entities", "attachmentShape", "entityListContext", "stackOnNarrow", "clickableKeys", "activeKeys", "clickableTooltip", "activeTooltip"], outputs: ["entityClick"] }, { kind: "ngmodule", type: SkeletonModule }, { kind: "component", type: i2.Skeleton, selector: "p-skeleton", inputs: ["styleClass", "shape", "animation", "borderRadius", "size", "width", "height"] }, { kind: "ngmodule", type: PopoverModule }, { kind: "component", type: i3.Popover, selector: "p-popover", inputs: ["ariaLabel", "ariaLabelledBy", "dismissable", "style", "styleClass", "appendTo", "autoZIndex", "ariaCloseLabel", "baseZIndex", "focusOnShow", "showTransitionOptions", "hideTransitionOptions", "motionOptions"], outputs: ["onShow", "onHide"] }, { kind: "component", type: TableCaption, selector: "mt-table-caption", inputs: ["generalSearch", "showFilters", "filterMode", "exportable", "printable", "groupable", "columns", "data", "groupColumns", "tabs", "tabsOptionLabel", "tabsOptionValue", "actions", "captionStart", "captionEnd", "activeTab", "filters", "filterTerm", "groupBy"], outputs: ["activeTabChange", "filtersChange", "filterTermChange", "groupByChange", "exportRequested", "printRequested", "onTabChange", "searchChange", "filterApplied", "filterReset"] }, { kind: "component", type: EmptyState, selector: "mt-empty-state", inputs: ["variant", "illustration", "title", "description", "actionLabel", "actionIcon"], outputs: ["actionClick"] }, { kind: "pipe", type: i4.TranslocoPipe, name: "transloco" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, deferBlockDependencies: [() => [i1.NgTemplateOutlet, Card,
|
|
2193
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: ClientListCardsView, isStandalone: true, selector: "mt-client-list-cards-view", inputs: { state: { classPropertyName: "state", publicName: "state", isSignal: true, isRequired: true, transformFunction: null }, rowActions: { classPropertyName: "rowActions", publicName: "rowActions", isSignal: true, isRequired: false, transformFunction: null }, rowActionsLoadingFn: { classPropertyName: "rowActionsLoadingFn", publicName: "rowActionsLoadingFn", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { lazyLoad: "lazyLoad", loadMore: "loadMore", cardClick: "cardClick", rowActionsRequested: "rowActionsRequested" }, providers: [MTDateFormatPipe], viewQueries: [{ propertyName: "loadMoreSentinel", first: true, predicate: ["loadMoreSentinel"], descendants: true, isSignal: true }], ngImport: i0, template: "<mt-table-caption\r\n [generalSearch]=\"true\"\r\n [showFilters]=\"true\"\r\n filterMode=\"popover\"\r\n [printable]=\"true\"\r\n [groupable]=\"true\"\r\n [exportable]=\"true\"\r\n [columns]=\"columns()\"\r\n [data]=\"rowRepresentations()\"\r\n [groupColumns]=\"grouping.groupableColumns()\"\r\n [actions]=\"toolbarActions()\"\r\n [captionStart]=\"cardsState().config.toolbarStart ?? null\"\r\n [captionEnd]=\"cardsState().config.toolbarEnd ?? null\"\r\n [filters]=\"bucket().filters()\"\r\n (filtersChange)=\"bucket().filters.set($event)\"\r\n [filterTerm]=\"bucket().filterTerm()\"\r\n (filterTermChange)=\"bucket().filterTerm.set($event)\"\r\n [groupBy]=\"bucket().groupBy()\"\r\n (groupByChange)=\"bucket().groupBy.set($event)\"\r\n [activeTab]=\"bucket().activeTab()\"\r\n (activeTabChange)=\"bucket().activeTab.set($event)\"\r\n (exportRequested)=\"exportExcel()\"\r\n (printRequested)=\"printPdf()\"\r\n/>\r\n\r\n@if (badgeLegend().length > 0) {\r\n <!--\r\n Decodes the coloured count pills on the cards below. Rendered once for the\r\n whole board rather than per card, so it costs one line of vertical space\r\n instead of repeating a key inside every tile (finding M-04).\r\n -->\r\n <div\r\n class=\"mb-3 flex flex-wrap items-center gap-x-4 gap-y-2 px-1\"\r\n role=\"list\"\r\n [attr.aria-label]=\"\r\n 'components.clientComponents.list.badgeLegend' | transloco\r\n \"\r\n >\r\n <span class=\"text-xs font-medium text-gray-500 dark:text-gray-400\">\r\n {{ \"components.clientComponents.list.badgeLegend\" | transloco }}\r\n </span>\r\n @for (entry of badgeLegend(); track entry.key) {\r\n <span class=\"flex items-center gap-1.5\" role=\"listitem\">\r\n <span\r\n class=\"inline-flex h-4 min-w-4 items-center justify-center rounded text-[10px] font-bold\"\r\n [style.background-color]=\"entry.color\"\r\n [style.color]=\"entry.textColor\"\r\n aria-hidden=\"true\"\r\n >\r\n #\r\n </span>\r\n <span class=\"text-xs text-gray-600 dark:text-gray-300\">{{\r\n entry.label\r\n }}</span>\r\n </span>\r\n }\r\n </div>\r\n}\r\n\r\n@if (cardsState().loading && cardsState().cards.length === 0) {\r\n <div class=\"mt-cards-grid\">\r\n @for (i of [1, 2, 3, 4, 5, 6]; track i) {\r\n <p-skeleton height=\"20rem\" class=\"rounded-lg\" />\r\n }\r\n </div>\r\n} @else if (cardsState().error) {\r\n <div class=\"rounded-lg border border-red-200 bg-red-50 p-6 text-red-600\">\r\n {{ cardsState().error }}\r\n </div>\r\n} @else if (visibleCards().length === 0) {\r\n <!--\r\n The same empty state the tables use, so a board and a list stop looking like\r\n two products (finding H-12). A board narrowed by a search or a filter says\r\n so and offers to clear it, instead of the bare grey \"No cards found.\"\r\n -->\r\n <mt-empty-state\r\n [variant]=\"hasActiveCardFilters() ? 'filtered' : 'empty'\"\r\n [title]=\"\r\n hasActiveCardFilters()\r\n ? ''\r\n : ('components.clientComponents.list.noCardsFound' | transloco)\r\n \"\r\n [actionLabel]=\"\r\n hasActiveCardFilters()\r\n ? ('components.emptyState.clearFilters' | transloco)\r\n : ''\r\n \"\r\n actionIcon=\"general.x-close\"\r\n (actionClick)=\"clearCardFilters()\"\r\n />\r\n} @else if (grouping.groupingActive()) {\r\n <div class=\"flex flex-col gap-6\">\r\n @for (group of groupedCards(); track group.key) {\r\n <section class=\"flex flex-col gap-3\">\r\n <header\r\n class=\"flex items-center gap-3 px-2 py-2 bg-surface-50 dark:bg-surface-900 border-y border-surface-200 dark:border-surface-700 rounded\"\r\n [style.--mt-group-accent]=\"group.accent ?? 'var(--p-primary-color)'\"\r\n >\r\n <span\r\n class=\"inline-block w-1 h-5 rounded-full\"\r\n [style.background]=\"'var(--mt-group-accent)'\"\r\n ></span>\r\n <span\r\n class=\"text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400\"\r\n >\r\n {{ group.columnLabel }}\r\n </span>\r\n <span class=\"text-sm font-semibold text-gray-900 dark:text-gray-100\">\r\n {{ group.label }}\r\n </span>\r\n <span\r\n class=\"ml-auto inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-primary-50 text-primary-700 dark:bg-primary-950 dark:text-primary-300\"\r\n >\r\n {{ group.count }}\r\n </span>\r\n </header>\r\n <div class=\"mt-cards-grid\">\r\n @for (card of group.cards; track card.id) {\r\n <mt-card\r\n class=\"relative cursor-pointer p-5 shadow-sm transition-shadow hover:shadow-md\"\r\n (click)=\"cardClick.emit(card)\"\r\n >\r\n <ng-template #headless>\r\n <ng-container\r\n [ngTemplateOutlet]=\"cardActionsMenu\"\r\n [ngTemplateOutletContext]=\"{ $implicit: card }\"\r\n />\r\n <mt-entities-preview\r\n [entities]=\"card.entities\"\r\n attachmentShape=\"compact\"\r\n [stackOnNarrow]=\"false\"\r\n [clickableKeys]=\"clickableEntityKeys()\"\r\n [activeKeys]=\"activeKeysForCard(card)\"\r\n [clickableTooltip]=\"filterByThisLabel()\"\r\n [activeTooltip]=\"clearFilterLabel()\"\r\n (entityClick)=\"onEntityFilterClick(card, $event)\"\r\n />\r\n </ng-template>\r\n </mt-card>\r\n }\r\n </div>\r\n </section>\r\n }\r\n </div>\r\n} @else {\r\n <div class=\"mt-cards-grid\">\r\n @for (card of visibleCards(); track card.id) {\r\n @defer (on viewport) {\r\n <mt-card\r\n class=\"relative cursor-pointer p-5 shadow-sm transition-shadow hover:shadow-md\"\r\n (click)=\"cardClick.emit(card)\"\r\n >\r\n <ng-template #headless>\r\n <ng-container\r\n [ngTemplateOutlet]=\"cardActionsMenu\"\r\n [ngTemplateOutletContext]=\"{ $implicit: card }\"\r\n />\r\n <mt-entities-preview\r\n [entities]=\"card.entities\"\r\n attachmentShape=\"compact\"\r\n [stackOnNarrow]=\"false\"\r\n [clickableKeys]=\"clickableEntityKeys()\"\r\n [activeKeys]=\"activeKeysForCard(card)\"\r\n [clickableTooltip]=\"filterByThisLabel()\"\r\n [activeTooltip]=\"clearFilterLabel()\"\r\n (entityClick)=\"onEntityFilterClick(card, $event)\"\r\n />\r\n </ng-template>\r\n </mt-card>\r\n } @placeholder {\r\n <div class=\"min-h-[20rem]\">\r\n <p-skeleton height=\"20rem\" class=\"rounded-lg\" />\r\n </div>\r\n }\r\n }\r\n </div>\r\n}\r\n\r\n<!--\r\n Infinite-scroll trigger. Deliberately outside the @if/@else chain above:\r\n inside it the element would be torn down and rebuilt every time the view\r\n swapped branch (error, empty filter result, group-by), and the observer\r\n would be left watching a detached node. Kept in the DOM and hidden via CSS\r\n when there is nothing left to load, so appending a page never recreates it.\r\n It also has to sit outside so infinite scroll keeps working while a\r\n group-by is active \u2014 grouping is a pure FE view over whatever has been\r\n fetched, not a reason to stop fetching.\r\n-->\r\n<div\r\n #loadMoreSentinel\r\n class=\"mt-cards-grid py-4\"\r\n [class.hidden]=\"!hasMoreCards()\"\r\n>\r\n @for (i of [1, 2, 3]; track i) {\r\n <p-skeleton height=\"20rem\" class=\"rounded-lg\" />\r\n }\r\n</div>\r\n\r\n<!--\r\n Per-card escalation marker and 3-dots actions menu. The compact corner cover\r\n only indicates active escalation state; its preview action lives in the menu\r\n so the marker never displaces or competes with the menu trigger.\r\n-->\r\n<ng-template #cardActionsMenu let-card>\r\n @let escalationAction = escalationMarkerAction(card);\r\n @if (escalationAction) {\r\n <span\r\n class=\"mt-escalation-marker absolute end-0 top-0 z-10 h-[18px] w-[20px]\"\r\n role=\"img\"\r\n tabindex=\"0\"\r\n [attr.aria-label]=\"escalationLabel()\"\r\n [mtTooltip]=\"escalationLabel()\"\r\n tooltipPosition=\"top\"\r\n ></span>\r\n }\r\n @if (rowActions().length > 0) {\r\n <div\r\n class=\"absolute end-2 top-2 z-20 flex items-center gap-1\"\r\n (click)=\"$event.stopPropagation()\"\r\n >\r\n @for (action of inlineActionsForCard(card); track $index) {\r\n <mt-button\r\n [icon]=\"action.icon\"\r\n [severity]=\"action.color\"\r\n [variant]=\"action.variant\"\r\n [size]=\"actionSize(action) || 'small'\"\r\n [tooltip]=\"action.tooltip\"\r\n [label]=\"action.label\"\r\n [loading]=\"resolveActionLoading(action, card)\"\r\n (onClick)=\"runCardAction($event, action, card)\"\r\n />\r\n }\r\n @if (showCardActionsMenu(card)) {\r\n <button\r\n type=\"button\"\r\n class=\"flex h-7 w-7 cursor-pointer items-center justify-center rounded-md border-0 bg-transparent p-0 text-base text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 dark:text-gray-300 dark:hover:bg-surface-700 dark:hover:text-gray-100\"\r\n [attr.aria-label]=\"actionsLabel()\"\r\n [mtTooltip]=\"actionsLabel()\"\r\n tooltipPosition=\"top\"\r\n (click)=\"onCardActionsToggle($event, cardPopover, card)\"\r\n >\r\n <mt-icon icon=\"general.dots-vertical\" class=\"text-base\" />\r\n </button>\r\n <p-popover #cardPopover appendTo=\"body\">\r\n <div class=\"flex min-w-44 flex-col gap-1 p-1\">\r\n @if (isCardActionsLoading(card)) {\r\n <div class=\"flex flex-col gap-2 p-1\">\r\n <p-skeleton height=\"1.75rem\" />\r\n <p-skeleton height=\"1.75rem\" />\r\n <p-skeleton height=\"1.75rem\" />\r\n </div>\r\n } @else {\r\n @for (action of menuActionsForCard(card); track $index) {\r\n @if (isFirstDestructiveCardAction(card, $index)) {\r\n <hr\r\n class=\"my-1 border-0 border-t border-surface-200 dark:border-surface-700\"\r\n />\r\n }\r\n <button\r\n type=\"button\"\r\n class=\"flex cursor-pointer items-center gap-2 rounded px-2 py-2 text-start text-sm transition-colors\"\r\n [class]=\"\r\n action.color === 'danger'\r\n ? 'text-red-600 hover:bg-red-50 dark:hover:bg-red-950'\r\n : action.color === 'warn'\r\n ? 'text-orange-600 hover:bg-orange-50 dark:text-orange-400 dark:hover:bg-orange-950'\r\n : 'text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-surface-700'\r\n \"\r\n (click)=\"\r\n runCardAction($event, action, card); cardPopover.hide()\r\n \"\r\n >\r\n @if (action.icon) {\r\n <mt-icon [icon]=\"action.icon\" class=\"text-base\" />\r\n }\r\n <span>{{ action.label || action.tooltip }}</span>\r\n </button>\r\n }\r\n }\r\n </div>\r\n </p-popover>\r\n }\r\n </div>\r\n }\r\n</ng-template>\r\n", styles: [".mt-cards-grid{--mt-cards-gap: 1rem;--mt-cards-min: 335px;--mt-cards-track: max( var(--mt-cards-min), (100% - 3 * var(--mt-cards-gap)) / 4 );display:grid;gap:var(--mt-cards-gap);grid-template-columns:repeat(auto-fill,minmax(min(100%,var(--mt-cards-track)),1fr))}.mt-cards-grid.hidden,.mt-cards-grid[hidden]{display:none}.mt-escalation-marker{border-start-end-radius:.5rem;background:var(--p-button-text-warn-color, var(--p-orange-500, #f59e0b));clip-path:polygon(0 0,100% 0,100% 100%);transition:filter .15s ease}.mt-escalation-marker:dir(rtl){clip-path:polygon(0 0,100% 0,0 100%)}.mt-escalation-marker:hover{filter:brightness(.92)}.mt-escalation-marker:focus-visible{filter:brightness(.92) drop-shadow(0 0 2px var(--p-button-text-warn-color, #f59e0b));outline:none}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: TranslocoModule }, { kind: "component", type: Card, selector: "mt-card", inputs: ["class", "title", "paddingless"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs", "ariaLabel"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "directive", type: Tooltip, selector: "[mtTooltip]" }, { kind: "component", type: Icon, selector: "mt-icon", inputs: ["icon"] }, { kind: "component", type: EntitiesPreview, selector: "mt-entities-preview", inputs: ["entities", "attachmentShape", "stackOnNarrow", "clickableKeys", "activeKeys", "clickableTooltip", "activeTooltip"], outputs: ["entityClick"] }, { kind: "ngmodule", type: SkeletonModule }, { kind: "component", type: i2.Skeleton, selector: "p-skeleton", inputs: ["styleClass", "shape", "animation", "borderRadius", "size", "width", "height"] }, { kind: "ngmodule", type: PopoverModule }, { kind: "component", type: i3.Popover, selector: "p-popover", inputs: ["ariaLabel", "ariaLabelledBy", "dismissable", "style", "styleClass", "appendTo", "autoZIndex", "ariaCloseLabel", "baseZIndex", "focusOnShow", "showTransitionOptions", "hideTransitionOptions", "motionOptions"], outputs: ["onShow", "onHide"] }, { kind: "component", type: TableCaption, selector: "mt-table-caption", inputs: ["generalSearch", "showFilters", "filterMode", "exportable", "printable", "groupable", "columns", "data", "groupColumns", "tabs", "tabsOptionLabel", "tabsOptionValue", "actions", "captionStart", "captionEnd", "activeTab", "filters", "filterTerm", "groupBy"], outputs: ["activeTabChange", "filtersChange", "filterTermChange", "groupByChange", "exportRequested", "printRequested", "onTabChange", "searchChange", "filterApplied", "filterReset"] }, { kind: "component", type: EmptyState, selector: "mt-empty-state", inputs: ["variant", "illustration", "title", "description", "actionLabel", "actionIcon"], outputs: ["actionClick"] }, { kind: "pipe", type: i4.TranslocoPipe, name: "transloco" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, deferBlockDependencies: [() => [i1.NgTemplateOutlet, Card,
|
|
2005
2194
|
EntitiesPreview]] });
|
|
2006
2195
|
}
|
|
2007
2196
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: ClientListCardsView, decorators: [{
|
|
@@ -2018,7 +2207,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
2018
2207
|
PopoverModule,
|
|
2019
2208
|
TableCaption,
|
|
2020
2209
|
EmptyState,
|
|
2021
|
-
], providers: [MTDateFormatPipe], template: "<mt-table-caption\n [generalSearch]=\"true\"\n [showFilters]=\"true\"\n filterMode=\"popover\"\n [printable]=\"true\"\n [groupable]=\"true\"\n [exportable]=\"true\"\n [columns]=\"columns()\"\n [data]=\"rowRepresentations()\"\n [groupColumns]=\"grouping.groupableColumns()\"\n [actions]=\"toolbarActions()\"\n [captionStart]=\"cardsState().config.toolbarStart ?? null\"\n [captionEnd]=\"cardsState().config.toolbarEnd ?? null\"\n [filters]=\"bucket().filters()\"\n (filtersChange)=\"bucket().filters.set($event)\"\n [filterTerm]=\"bucket().filterTerm()\"\n (filterTermChange)=\"bucket().filterTerm.set($event)\"\n [groupBy]=\"bucket().groupBy()\"\n (groupByChange)=\"bucket().groupBy.set($event)\"\n [activeTab]=\"bucket().activeTab()\"\n (activeTabChange)=\"bucket().activeTab.set($event)\"\n (exportRequested)=\"exportExcel()\"\n (printRequested)=\"printPdf()\"\n/>\n\n@if (badgeLegend().length > 0) {\n <!--\n Decodes the coloured count pills on the cards below. Rendered once for the\n whole board rather than per card, so it costs one line of vertical space\n instead of repeating a key inside every tile (finding M-04).\n -->\n <div\n class=\"mb-3 flex flex-wrap items-center gap-x-4 gap-y-2 px-1\"\n role=\"list\"\n [attr.aria-label]=\"\n 'components.clientComponents.list.badgeLegend' | transloco\n \"\n >\n <span class=\"text-xs font-medium text-gray-500 dark:text-gray-400\">\n {{ \"components.clientComponents.list.badgeLegend\" | transloco }}\n </span>\n @for (entry of badgeLegend(); track entry.key) {\n <span class=\"flex items-center gap-1.5\" role=\"listitem\">\n <span\n class=\"inline-flex h-4 min-w-4 items-center justify-center rounded text-[10px] font-bold\"\n [style.background-color]=\"entry.color\"\n [style.color]=\"entry.textColor\"\n aria-hidden=\"true\"\n >\n #\n </span>\n <span class=\"text-xs text-gray-600 dark:text-gray-300\">{{\n entry.label\n }}</span>\n </span>\n }\n </div>\n}\n\n@if (cardsState().loading && cardsState().cards.length === 0) {\n <div class=\"mt-cards-grid\">\n @for (i of [1, 2, 3, 4, 5, 6]; track i) {\n <p-skeleton height=\"20rem\" class=\"rounded-lg\" />\n }\n </div>\n} @else if (cardsState().error) {\n <div class=\"rounded-lg border border-red-200 bg-red-50 p-6 text-red-600\">\n {{ cardsState().error }}\n </div>\n} @else if (visibleCards().length === 0) {\n <!--\n The same empty state the tables use, so a board and a list stop looking like\n two products (finding H-12). A board narrowed by a search or a filter says\n so and offers to clear it, instead of the bare grey \"No cards found.\"\n -->\n <mt-empty-state\n [variant]=\"hasActiveCardFilters() ? 'filtered' : 'empty'\"\n [title]=\"\n hasActiveCardFilters()\n ? ''\n : ('components.clientComponents.list.noCardsFound' | transloco)\n \"\n [actionLabel]=\"\n hasActiveCardFilters()\n ? ('components.emptyState.clearFilters' | transloco)\n : ''\n \"\n actionIcon=\"general.x-close\"\n (actionClick)=\"clearCardFilters()\"\n />\n} @else if (grouping.groupingActive()) {\n <div class=\"flex flex-col gap-6\">\n @for (group of groupedCards(); track group.key) {\n <section class=\"flex flex-col gap-3\">\n <header\n class=\"flex items-center gap-3 px-2 py-2 bg-surface-50 dark:bg-surface-900 border-y border-surface-200 dark:border-surface-700 rounded\"\n [style.--mt-group-accent]=\"group.accent ?? 'var(--p-primary-color)'\"\n >\n <span\n class=\"inline-block w-1 h-5 rounded-full\"\n [style.background]=\"'var(--mt-group-accent)'\"\n ></span>\n <span\n class=\"text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400\"\n >\n {{ group.columnLabel }}\n </span>\n <span class=\"text-sm font-semibold text-gray-900 dark:text-gray-100\">\n {{ group.label }}\n </span>\n <span\n class=\"ml-auto inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-primary-50 text-primary-700 dark:bg-primary-950 dark:text-primary-300\"\n >\n {{ group.count }}\n </span>\n </header>\n <div class=\"mt-cards-grid\">\n @for (card of group.cards; track card.id) {\n <mt-card\n class=\"relative cursor-pointer p-5 shadow-sm transition-shadow hover:shadow-md\"\n (click)=\"cardClick.emit(card)\"\n >\n <ng-template #headless>\n <ng-container\n [ngTemplateOutlet]=\"cardActionsMenu\"\n [ngTemplateOutletContext]=\"{ $implicit: card }\"\n />\n <mt-entities-preview\n [entities]=\"card.entities\"\n attachmentShape=\"compact\"\n [stackOnNarrow]=\"false\"\n [clickableKeys]=\"clickableEntityKeys()\"\n [activeKeys]=\"activeKeysForCard(card)\"\n [clickableTooltip]=\"filterByThisLabel()\"\n [activeTooltip]=\"clearFilterLabel()\"\n (entityClick)=\"onEntityFilterClick(card, $event)\"\n />\n </ng-template>\n </mt-card>\n }\n </div>\n </section>\n }\n </div>\n} @else {\n <div class=\"mt-cards-grid\">\n @for (card of visibleCards(); track card.id) {\n @defer (on viewport) {\n <mt-card\n class=\"relative cursor-pointer p-5 shadow-sm transition-shadow hover:shadow-md\"\n (click)=\"cardClick.emit(card)\"\n >\n <ng-template #headless>\n <ng-container\n [ngTemplateOutlet]=\"cardActionsMenu\"\n [ngTemplateOutletContext]=\"{ $implicit: card }\"\n />\n <mt-entities-preview\n [entities]=\"card.entities\"\n attachmentShape=\"compact\"\n [stackOnNarrow]=\"false\"\n [clickableKeys]=\"clickableEntityKeys()\"\n [activeKeys]=\"activeKeysForCard(card)\"\n [clickableTooltip]=\"filterByThisLabel()\"\n [activeTooltip]=\"clearFilterLabel()\"\n (entityClick)=\"onEntityFilterClick(card, $event)\"\n />\n </ng-template>\n </mt-card>\n } @placeholder {\n <div class=\"min-h-[20rem]\">\n <p-skeleton height=\"20rem\" class=\"rounded-lg\" />\n </div>\n }\n }\n </div>\n}\n\n<!--\n Infinite-scroll trigger. Deliberately outside the @if/@else chain above:\n inside it the element would be torn down and rebuilt every time the view\n swapped branch (error, empty filter result, group-by), and the observer\n would be left watching a detached node. Kept in the DOM and hidden via CSS\n when there is nothing left to load, so appending a page never recreates it.\n It also has to sit outside so infinite scroll keeps working while a\n group-by is active \u2014 grouping is a pure FE view over whatever has been\n fetched, not a reason to stop fetching.\n-->\n<div\n #loadMoreSentinel\n class=\"mt-cards-grid py-4\"\n [class.hidden]=\"!hasMoreCards()\"\n>\n @for (i of [1, 2, 3]; track i) {\n <p-skeleton height=\"20rem\" class=\"rounded-lg\" />\n }\n</div>\n\n<!--\n Per-card escalation marker and 3-dots actions menu. The compact corner cover\n only indicates active escalation state; its preview action lives in the menu\n so the marker never displaces or competes with the menu trigger.\n-->\n<ng-template #cardActionsMenu let-card>\n @let escalationAction = escalationMarkerAction(card);\n @if (escalationAction) {\n <span\n class=\"mt-escalation-marker absolute end-0 top-0 z-10 h-[18px] w-[20px]\"\n role=\"img\"\n tabindex=\"0\"\n [attr.aria-label]=\"escalationLabel()\"\n [mtTooltip]=\"escalationLabel()\"\n tooltipPosition=\"top\"\n ></span>\n }\n @if (rowActions().length > 0) {\n <div\n class=\"absolute end-2 top-2 z-20 flex items-center gap-1\"\n (click)=\"$event.stopPropagation()\"\n >\n @for (action of inlineActionsForCard(card); track $index) {\n <mt-button\n [icon]=\"action.icon\"\n [severity]=\"action.color\"\n [variant]=\"action.variant\"\n [size]=\"actionSize(action) || 'small'\"\n [tooltip]=\"action.tooltip\"\n [label]=\"action.label\"\n [loading]=\"resolveActionLoading(action, card)\"\n (onClick)=\"runCardAction($event, action, card)\"\n />\n }\n @if (showCardActionsMenu(card)) {\n <button\n type=\"button\"\n class=\"flex h-7 w-7 cursor-pointer items-center justify-center rounded-md border-0 bg-transparent p-0 text-base text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 dark:text-gray-300 dark:hover:bg-surface-700 dark:hover:text-gray-100\"\n [attr.aria-label]=\"actionsLabel()\"\n [mtTooltip]=\"actionsLabel()\"\n tooltipPosition=\"top\"\n (click)=\"onCardActionsToggle($event, cardPopover, card)\"\n >\n <mt-icon icon=\"general.dots-vertical\" class=\"text-base\" />\n </button>\n <p-popover #cardPopover appendTo=\"body\">\n <div class=\"flex min-w-44 flex-col gap-1 p-1\">\n @if (isCardActionsLoading(card)) {\n <div class=\"flex flex-col gap-2 p-1\">\n <p-skeleton height=\"1.75rem\" />\n <p-skeleton height=\"1.75rem\" />\n <p-skeleton height=\"1.75rem\" />\n </div>\n } @else {\n @for (action of menuActionsForCard(card); track $index) {\n @if (isFirstDestructiveCardAction(card, $index)) {\n <hr\n class=\"my-1 border-0 border-t border-surface-200 dark:border-surface-700\"\n />\n }\n <button\n type=\"button\"\n class=\"flex cursor-pointer items-center gap-2 rounded px-2 py-2 text-start text-sm transition-colors\"\n [class]=\"\n action.color === 'danger'\n ? 'text-red-600 hover:bg-red-50 dark:hover:bg-red-950'\n : action.color === 'warn'\n ? 'text-orange-600 hover:bg-orange-50 dark:text-orange-400 dark:hover:bg-orange-950'\n : 'text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-surface-700'\n \"\n (click)=\"\n runCardAction($event, action, card); cardPopover.hide()\n \"\n >\n @if (action.icon) {\n <mt-icon [icon]=\"action.icon\" class=\"text-base\" />\n }\n <span>{{ action.label || action.tooltip }}</span>\n </button>\n }\n }\n </div>\n </p-popover>\n }\n </div>\n }\n</ng-template>\n", styles: [".mt-cards-grid{--mt-cards-gap: 1rem;--mt-cards-min: 335px;--mt-cards-track: max( var(--mt-cards-min), (100% - 3 * var(--mt-cards-gap)) / 4 );display:grid;gap:var(--mt-cards-gap);grid-template-columns:repeat(auto-fill,minmax(min(100%,var(--mt-cards-track)),1fr))}.mt-cards-grid.hidden,.mt-cards-grid[hidden]{display:none}.mt-escalation-marker{border-start-end-radius:.5rem;background:var(--p-button-text-warn-color, var(--p-orange-500, #f59e0b));clip-path:polygon(0 0,100% 0,100% 100%);transition:filter .15s ease}.mt-escalation-marker:dir(rtl){clip-path:polygon(0 0,100% 0,0 100%)}.mt-escalation-marker:hover{filter:brightness(.92)}.mt-escalation-marker:focus-visible{filter:brightness(.92) drop-shadow(0 0 2px var(--p-button-text-warn-color, #f59e0b));outline:none}\n"] }]
|
|
2210
|
+
], providers: [MTDateFormatPipe], template: "<mt-table-caption\r\n [generalSearch]=\"true\"\r\n [showFilters]=\"true\"\r\n filterMode=\"popover\"\r\n [printable]=\"true\"\r\n [groupable]=\"true\"\r\n [exportable]=\"true\"\r\n [columns]=\"columns()\"\r\n [data]=\"rowRepresentations()\"\r\n [groupColumns]=\"grouping.groupableColumns()\"\r\n [actions]=\"toolbarActions()\"\r\n [captionStart]=\"cardsState().config.toolbarStart ?? null\"\r\n [captionEnd]=\"cardsState().config.toolbarEnd ?? null\"\r\n [filters]=\"bucket().filters()\"\r\n (filtersChange)=\"bucket().filters.set($event)\"\r\n [filterTerm]=\"bucket().filterTerm()\"\r\n (filterTermChange)=\"bucket().filterTerm.set($event)\"\r\n [groupBy]=\"bucket().groupBy()\"\r\n (groupByChange)=\"bucket().groupBy.set($event)\"\r\n [activeTab]=\"bucket().activeTab()\"\r\n (activeTabChange)=\"bucket().activeTab.set($event)\"\r\n (exportRequested)=\"exportExcel()\"\r\n (printRequested)=\"printPdf()\"\r\n/>\r\n\r\n@if (badgeLegend().length > 0) {\r\n <!--\r\n Decodes the coloured count pills on the cards below. Rendered once for the\r\n whole board rather than per card, so it costs one line of vertical space\r\n instead of repeating a key inside every tile (finding M-04).\r\n -->\r\n <div\r\n class=\"mb-3 flex flex-wrap items-center gap-x-4 gap-y-2 px-1\"\r\n role=\"list\"\r\n [attr.aria-label]=\"\r\n 'components.clientComponents.list.badgeLegend' | transloco\r\n \"\r\n >\r\n <span class=\"text-xs font-medium text-gray-500 dark:text-gray-400\">\r\n {{ \"components.clientComponents.list.badgeLegend\" | transloco }}\r\n </span>\r\n @for (entry of badgeLegend(); track entry.key) {\r\n <span class=\"flex items-center gap-1.5\" role=\"listitem\">\r\n <span\r\n class=\"inline-flex h-4 min-w-4 items-center justify-center rounded text-[10px] font-bold\"\r\n [style.background-color]=\"entry.color\"\r\n [style.color]=\"entry.textColor\"\r\n aria-hidden=\"true\"\r\n >\r\n #\r\n </span>\r\n <span class=\"text-xs text-gray-600 dark:text-gray-300\">{{\r\n entry.label\r\n }}</span>\r\n </span>\r\n }\r\n </div>\r\n}\r\n\r\n@if (cardsState().loading && cardsState().cards.length === 0) {\r\n <div class=\"mt-cards-grid\">\r\n @for (i of [1, 2, 3, 4, 5, 6]; track i) {\r\n <p-skeleton height=\"20rem\" class=\"rounded-lg\" />\r\n }\r\n </div>\r\n} @else if (cardsState().error) {\r\n <div class=\"rounded-lg border border-red-200 bg-red-50 p-6 text-red-600\">\r\n {{ cardsState().error }}\r\n </div>\r\n} @else if (visibleCards().length === 0) {\r\n <!--\r\n The same empty state the tables use, so a board and a list stop looking like\r\n two products (finding H-12). A board narrowed by a search or a filter says\r\n so and offers to clear it, instead of the bare grey \"No cards found.\"\r\n -->\r\n <mt-empty-state\r\n [variant]=\"hasActiveCardFilters() ? 'filtered' : 'empty'\"\r\n [title]=\"\r\n hasActiveCardFilters()\r\n ? ''\r\n : ('components.clientComponents.list.noCardsFound' | transloco)\r\n \"\r\n [actionLabel]=\"\r\n hasActiveCardFilters()\r\n ? ('components.emptyState.clearFilters' | transloco)\r\n : ''\r\n \"\r\n actionIcon=\"general.x-close\"\r\n (actionClick)=\"clearCardFilters()\"\r\n />\r\n} @else if (grouping.groupingActive()) {\r\n <div class=\"flex flex-col gap-6\">\r\n @for (group of groupedCards(); track group.key) {\r\n <section class=\"flex flex-col gap-3\">\r\n <header\r\n class=\"flex items-center gap-3 px-2 py-2 bg-surface-50 dark:bg-surface-900 border-y border-surface-200 dark:border-surface-700 rounded\"\r\n [style.--mt-group-accent]=\"group.accent ?? 'var(--p-primary-color)'\"\r\n >\r\n <span\r\n class=\"inline-block w-1 h-5 rounded-full\"\r\n [style.background]=\"'var(--mt-group-accent)'\"\r\n ></span>\r\n <span\r\n class=\"text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400\"\r\n >\r\n {{ group.columnLabel }}\r\n </span>\r\n <span class=\"text-sm font-semibold text-gray-900 dark:text-gray-100\">\r\n {{ group.label }}\r\n </span>\r\n <span\r\n class=\"ml-auto inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-primary-50 text-primary-700 dark:bg-primary-950 dark:text-primary-300\"\r\n >\r\n {{ group.count }}\r\n </span>\r\n </header>\r\n <div class=\"mt-cards-grid\">\r\n @for (card of group.cards; track card.id) {\r\n <mt-card\r\n class=\"relative cursor-pointer p-5 shadow-sm transition-shadow hover:shadow-md\"\r\n (click)=\"cardClick.emit(card)\"\r\n >\r\n <ng-template #headless>\r\n <ng-container\r\n [ngTemplateOutlet]=\"cardActionsMenu\"\r\n [ngTemplateOutletContext]=\"{ $implicit: card }\"\r\n />\r\n <mt-entities-preview\r\n [entities]=\"card.entities\"\r\n attachmentShape=\"compact\"\r\n [stackOnNarrow]=\"false\"\r\n [clickableKeys]=\"clickableEntityKeys()\"\r\n [activeKeys]=\"activeKeysForCard(card)\"\r\n [clickableTooltip]=\"filterByThisLabel()\"\r\n [activeTooltip]=\"clearFilterLabel()\"\r\n (entityClick)=\"onEntityFilterClick(card, $event)\"\r\n />\r\n </ng-template>\r\n </mt-card>\r\n }\r\n </div>\r\n </section>\r\n }\r\n </div>\r\n} @else {\r\n <div class=\"mt-cards-grid\">\r\n @for (card of visibleCards(); track card.id) {\r\n @defer (on viewport) {\r\n <mt-card\r\n class=\"relative cursor-pointer p-5 shadow-sm transition-shadow hover:shadow-md\"\r\n (click)=\"cardClick.emit(card)\"\r\n >\r\n <ng-template #headless>\r\n <ng-container\r\n [ngTemplateOutlet]=\"cardActionsMenu\"\r\n [ngTemplateOutletContext]=\"{ $implicit: card }\"\r\n />\r\n <mt-entities-preview\r\n [entities]=\"card.entities\"\r\n attachmentShape=\"compact\"\r\n [stackOnNarrow]=\"false\"\r\n [clickableKeys]=\"clickableEntityKeys()\"\r\n [activeKeys]=\"activeKeysForCard(card)\"\r\n [clickableTooltip]=\"filterByThisLabel()\"\r\n [activeTooltip]=\"clearFilterLabel()\"\r\n (entityClick)=\"onEntityFilterClick(card, $event)\"\r\n />\r\n </ng-template>\r\n </mt-card>\r\n } @placeholder {\r\n <div class=\"min-h-[20rem]\">\r\n <p-skeleton height=\"20rem\" class=\"rounded-lg\" />\r\n </div>\r\n }\r\n }\r\n </div>\r\n}\r\n\r\n<!--\r\n Infinite-scroll trigger. Deliberately outside the @if/@else chain above:\r\n inside it the element would be torn down and rebuilt every time the view\r\n swapped branch (error, empty filter result, group-by), and the observer\r\n would be left watching a detached node. Kept in the DOM and hidden via CSS\r\n when there is nothing left to load, so appending a page never recreates it.\r\n It also has to sit outside so infinite scroll keeps working while a\r\n group-by is active \u2014 grouping is a pure FE view over whatever has been\r\n fetched, not a reason to stop fetching.\r\n-->\r\n<div\r\n #loadMoreSentinel\r\n class=\"mt-cards-grid py-4\"\r\n [class.hidden]=\"!hasMoreCards()\"\r\n>\r\n @for (i of [1, 2, 3]; track i) {\r\n <p-skeleton height=\"20rem\" class=\"rounded-lg\" />\r\n }\r\n</div>\r\n\r\n<!--\r\n Per-card escalation marker and 3-dots actions menu. The compact corner cover\r\n only indicates active escalation state; its preview action lives in the menu\r\n so the marker never displaces or competes with the menu trigger.\r\n-->\r\n<ng-template #cardActionsMenu let-card>\r\n @let escalationAction = escalationMarkerAction(card);\r\n @if (escalationAction) {\r\n <span\r\n class=\"mt-escalation-marker absolute end-0 top-0 z-10 h-[18px] w-[20px]\"\r\n role=\"img\"\r\n tabindex=\"0\"\r\n [attr.aria-label]=\"escalationLabel()\"\r\n [mtTooltip]=\"escalationLabel()\"\r\n tooltipPosition=\"top\"\r\n ></span>\r\n }\r\n @if (rowActions().length > 0) {\r\n <div\r\n class=\"absolute end-2 top-2 z-20 flex items-center gap-1\"\r\n (click)=\"$event.stopPropagation()\"\r\n >\r\n @for (action of inlineActionsForCard(card); track $index) {\r\n <mt-button\r\n [icon]=\"action.icon\"\r\n [severity]=\"action.color\"\r\n [variant]=\"action.variant\"\r\n [size]=\"actionSize(action) || 'small'\"\r\n [tooltip]=\"action.tooltip\"\r\n [label]=\"action.label\"\r\n [loading]=\"resolveActionLoading(action, card)\"\r\n (onClick)=\"runCardAction($event, action, card)\"\r\n />\r\n }\r\n @if (showCardActionsMenu(card)) {\r\n <button\r\n type=\"button\"\r\n class=\"flex h-7 w-7 cursor-pointer items-center justify-center rounded-md border-0 bg-transparent p-0 text-base text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 dark:text-gray-300 dark:hover:bg-surface-700 dark:hover:text-gray-100\"\r\n [attr.aria-label]=\"actionsLabel()\"\r\n [mtTooltip]=\"actionsLabel()\"\r\n tooltipPosition=\"top\"\r\n (click)=\"onCardActionsToggle($event, cardPopover, card)\"\r\n >\r\n <mt-icon icon=\"general.dots-vertical\" class=\"text-base\" />\r\n </button>\r\n <p-popover #cardPopover appendTo=\"body\">\r\n <div class=\"flex min-w-44 flex-col gap-1 p-1\">\r\n @if (isCardActionsLoading(card)) {\r\n <div class=\"flex flex-col gap-2 p-1\">\r\n <p-skeleton height=\"1.75rem\" />\r\n <p-skeleton height=\"1.75rem\" />\r\n <p-skeleton height=\"1.75rem\" />\r\n </div>\r\n } @else {\r\n @for (action of menuActionsForCard(card); track $index) {\r\n @if (isFirstDestructiveCardAction(card, $index)) {\r\n <hr\r\n class=\"my-1 border-0 border-t border-surface-200 dark:border-surface-700\"\r\n />\r\n }\r\n <button\r\n type=\"button\"\r\n class=\"flex cursor-pointer items-center gap-2 rounded px-2 py-2 text-start text-sm transition-colors\"\r\n [class]=\"\r\n action.color === 'danger'\r\n ? 'text-red-600 hover:bg-red-50 dark:hover:bg-red-950'\r\n : action.color === 'warn'\r\n ? 'text-orange-600 hover:bg-orange-50 dark:text-orange-400 dark:hover:bg-orange-950'\r\n : 'text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-surface-700'\r\n \"\r\n (click)=\"\r\n runCardAction($event, action, card); cardPopover.hide()\r\n \"\r\n >\r\n @if (action.icon) {\r\n <mt-icon [icon]=\"action.icon\" class=\"text-base\" />\r\n }\r\n <span>{{ action.label || action.tooltip }}</span>\r\n </button>\r\n }\r\n }\r\n </div>\r\n </p-popover>\r\n }\r\n </div>\r\n }\r\n</ng-template>\r\n", styles: [".mt-cards-grid{--mt-cards-gap: 1rem;--mt-cards-min: 335px;--mt-cards-track: max( var(--mt-cards-min), (100% - 3 * var(--mt-cards-gap)) / 4 );display:grid;gap:var(--mt-cards-gap);grid-template-columns:repeat(auto-fill,minmax(min(100%,var(--mt-cards-track)),1fr))}.mt-cards-grid.hidden,.mt-cards-grid[hidden]{display:none}.mt-escalation-marker{border-start-end-radius:.5rem;background:var(--p-button-text-warn-color, var(--p-orange-500, #f59e0b));clip-path:polygon(0 0,100% 0,100% 100%);transition:filter .15s ease}.mt-escalation-marker:dir(rtl){clip-path:polygon(0 0,100% 0,0 100%)}.mt-escalation-marker:hover{filter:brightness(.92)}.mt-escalation-marker:focus-visible{filter:brightness(.92) drop-shadow(0 0 2px var(--p-button-text-warn-color, #f59e0b));outline:none}\n"] }]
|
|
2022
2211
|
}], ctorParameters: () => [], propDecorators: { state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: true }] }], rowActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowActions", required: false }] }], rowActionsLoadingFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowActionsLoadingFn", required: false }] }], lazyLoad: [{ type: i0.Output, args: ["lazyLoad"] }], loadMore: [{ type: i0.Output, args: ["loadMore"] }], cardClick: [{ type: i0.Output, args: ["cardClick"] }], rowActionsRequested: [{ type: i0.Output, args: ["rowActionsRequested"] }], loadMoreSentinel: [{ type: i0.ViewChild, args: ['loadMoreSentinel', { isSignal: true }] }] } });
|
|
2023
2212
|
|
|
2024
2213
|
const DEFAULT_GRID_COLUMNS$1 = 12;
|
|
@@ -2043,11 +2232,11 @@ class ClientListInformativeView {
|
|
|
2043
2232
|
};
|
|
2044
2233
|
}
|
|
2045
2234
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: ClientListInformativeView, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2046
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: ClientListInformativeView, isStandalone: true, selector: "mt-client-list-informative-view", inputs: { state: { classPropertyName: "state", publicName: "state", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: "<div class=\"grid gap-4\" [style.gridTemplateColumns]=\"gridTemplateColumns\">\
|
|
2235
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: ClientListInformativeView, isStandalone: true, selector: "mt-client-list-informative-view", inputs: { state: { classPropertyName: "state", publicName: "state", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: "<div class=\"grid gap-4\" [style.gridTemplateColumns]=\"gridTemplateColumns\">\n @if (informativeState().config.contentStart) {\n <div\n [style.gridColumn]=\"\n slotGridSpan(informativeState().config.layout.startSpan)\n \"\n >\n <ng-container\n [ngTemplateOutlet]=\"informativeState().config.contentStart\"\n [ngTemplateOutletContext]=\"templateContext(informativeState())\"\n />\n </div>\n }\n\n <div\n [style.gridColumn]=\"\n slotGridSpan(informativeState().config.layout.tableSpan)\n \"\n >\n <mt-card>\n @if (\n informativeState().loading &&\n !informativeState().dashboardData?.charts?.length\n ) {\n <div class=\"flex flex-col gap-3 py-3\">\n <p-skeleton height=\"6rem\" />\n <p-skeleton height=\"12rem\" />\n <p-skeleton height=\"12rem\" />\n </div>\n } @else if (informativeState().error) {\n <div\n class=\"rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-700\"\n >\n {{ informativeState().error }}\n </div>\n } @else if (!informativeState().dashboardData?.charts?.length) {\n <div class=\"p-6 text-center text-gray-400\">\n {{\n \"components.clientComponents.list.noInformativeDashboard\"\n | transloco\n }}\n </div>\n } @else {\n <div class=\"min-h-[420px]\">\n <mt-dashboard-viewer\n [isPage]=\"false\"\n [showFilters]=\"false\"\n [dashboardData]=\"informativeState().dashboardData\"\n [extraFilters]=\"dashboardExtraFilters()\"\n />\n </div>\n }\n </mt-card>\n </div>\n\n @if (informativeState().config.contentEnd) {\n <div\n [style.gridColumn]=\"\n slotGridSpan(informativeState().config.layout.endSpan)\n \"\n >\n <ng-container\n [ngTemplateOutlet]=\"informativeState().config.contentEnd\"\n [ngTemplateOutletContext]=\"templateContext(informativeState())\"\n />\n </div>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: Card, selector: "mt-card", inputs: ["class", "title", "paddingless"] }, { kind: "component", type: DashboardViewer, selector: "mt-dashboard-viewer", inputs: ["isPage", "pageTitle", "backButton", "pageId", "dashboardData", "chartsData", "dialogsData", "filtersData", "extraFilters", "lookups", "statuses", "levelsSchema", "levelLogsResolver", "showFilters", "showFilterTrigger", "ignoreQueryFilter"], outputs: ["pageLoaded", "onBack", "chartClick"] }, { kind: "ngmodule", type: SkeletonModule }, { kind: "component", type: i2.Skeleton, selector: "p-skeleton", inputs: ["styleClass", "shape", "animation", "borderRadius", "size", "width", "height"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }] });
|
|
2047
2236
|
}
|
|
2048
2237
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: ClientListInformativeView, decorators: [{
|
|
2049
2238
|
type: Component,
|
|
2050
|
-
args: [{ selector: 'mt-client-list-informative-view', standalone: true, imports: [CommonModule, Card, DashboardViewer, SkeletonModule, TranslocoPipe], template: "<div class=\"grid gap-4\" [style.gridTemplateColumns]=\"gridTemplateColumns\">\
|
|
2239
|
+
args: [{ selector: 'mt-client-list-informative-view', standalone: true, imports: [CommonModule, Card, DashboardViewer, SkeletonModule, TranslocoPipe], template: "<div class=\"grid gap-4\" [style.gridTemplateColumns]=\"gridTemplateColumns\">\n @if (informativeState().config.contentStart) {\n <div\n [style.gridColumn]=\"\n slotGridSpan(informativeState().config.layout.startSpan)\n \"\n >\n <ng-container\n [ngTemplateOutlet]=\"informativeState().config.contentStart\"\n [ngTemplateOutletContext]=\"templateContext(informativeState())\"\n />\n </div>\n }\n\n <div\n [style.gridColumn]=\"\n slotGridSpan(informativeState().config.layout.tableSpan)\n \"\n >\n <mt-card>\n @if (\n informativeState().loading &&\n !informativeState().dashboardData?.charts?.length\n ) {\n <div class=\"flex flex-col gap-3 py-3\">\n <p-skeleton height=\"6rem\" />\n <p-skeleton height=\"12rem\" />\n <p-skeleton height=\"12rem\" />\n </div>\n } @else if (informativeState().error) {\n <div\n class=\"rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-700\"\n >\n {{ informativeState().error }}\n </div>\n } @else if (!informativeState().dashboardData?.charts?.length) {\n <div class=\"p-6 text-center text-gray-400\">\n {{\n \"components.clientComponents.list.noInformativeDashboard\"\n | transloco\n }}\n </div>\n } @else {\n <div class=\"min-h-[420px]\">\n <mt-dashboard-viewer\n [isPage]=\"false\"\n [showFilters]=\"false\"\n [dashboardData]=\"informativeState().dashboardData\"\n [extraFilters]=\"dashboardExtraFilters()\"\n />\n </div>\n }\n </mt-card>\n </div>\n\n @if (informativeState().config.contentEnd) {\n <div\n [style.gridColumn]=\"\n slotGridSpan(informativeState().config.layout.endSpan)\n \"\n >\n <ng-container\n [ngTemplateOutlet]=\"informativeState().config.contentEnd\"\n [ngTemplateOutletContext]=\"templateContext(informativeState())\"\n />\n </div>\n }\n</div>\n" }]
|
|
2051
2240
|
}], propDecorators: { state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: true }] }] } });
|
|
2052
2241
|
|
|
2053
2242
|
function mergeRuntimeFilters(configured = [], runtime = {}, columns = []) {
|
|
@@ -2998,7 +3187,7 @@ class ClientList {
|
|
|
2998
3187
|
this.state.setLoading(key, true);
|
|
2999
3188
|
this.state.setError(key, null);
|
|
3000
3189
|
const sub = this.api
|
|
3001
|
-
.getCards(config.contextKey, filters, config.isPaginated ? skip : undefined, config.isPaginated ? take : undefined)
|
|
3190
|
+
.getCards(config.contextKey, filters, config.isPaginated ? skip : undefined, config.isPaginated ? take : undefined, config.hierarchy)
|
|
3002
3191
|
.subscribe({
|
|
3003
3192
|
next: (response) => {
|
|
3004
3193
|
let hasLoadedData = false;
|
|
@@ -3095,15 +3284,18 @@ class ClientList {
|
|
|
3095
3284
|
return (columnKeys ?? []).length > 0 ? 'override' : DEFAULT_MODE;
|
|
3096
3285
|
}
|
|
3097
3286
|
/**
|
|
3098
|
-
* Descendant scope only survives where
|
|
3099
|
-
* a usable root id.
|
|
3100
|
-
* the backend answers an unsupported
|
|
3101
|
-
*
|
|
3102
|
-
*
|
|
3287
|
+
* Descendant scope only survives where it can actually be fetched: a table
|
|
3288
|
+
* or a cards board, with a usable root id. An informative dashboard drops it
|
|
3289
|
+
* rather than sending it, because the backend answers an unsupported
|
|
3290
|
+
* combination with a 400 that takes the whole list down.
|
|
3291
|
+
*
|
|
3292
|
+
* A cards board does not send it as `Card` either — the backend takes
|
|
3293
|
+
* descendant scope on `Table` only. `ClientListApiService.getCards` fetches
|
|
3294
|
+
* the hierarchy table and re-dresses it as cards.
|
|
3103
3295
|
*/
|
|
3104
3296
|
resolveHierarchyScope(config, areaType) {
|
|
3105
3297
|
const scope = config?.hierarchy;
|
|
3106
|
-
if (!scope || areaType !== 'table')
|
|
3298
|
+
if (!scope || (areaType !== 'table' && areaType !== 'cards'))
|
|
3107
3299
|
return null;
|
|
3108
3300
|
const rootId = this.resolvePositiveInteger(scope.rootId);
|
|
3109
3301
|
return rootId == null ? null : { ...scope, rootId };
|
|
@@ -3323,6 +3515,9 @@ class ClientList {
|
|
|
3323
3515
|
config.contextKey ?? 'x',
|
|
3324
3516
|
this.serializeFilters(filters),
|
|
3325
3517
|
'Card',
|
|
3518
|
+
// Two boards on the same context and filters are still different
|
|
3519
|
+
// fetches when they are rooted at different records.
|
|
3520
|
+
this.serializeHierarchy(config.hierarchy),
|
|
3326
3521
|
config.isPaginated ? skip : 0,
|
|
3327
3522
|
config.isPaginated ? take : 0,
|
|
3328
3523
|
].join('|');
|
|
@@ -3382,7 +3577,7 @@ class ClientList {
|
|
|
3382
3577
|
ClientListStateService,
|
|
3383
3578
|
ClientListRuntimeActionsService,
|
|
3384
3579
|
ClientListToolbarService,
|
|
3385
|
-
], ngImport: i0, template: "<div class=\"flex flex-col gap-4\">\
|
|
3580
|
+
], ngImport: i0, template: "<div class=\"flex flex-col gap-4\">\n @for (item of items(); track item.key) {\n <section class=\"flex flex-col gap-4\">\n @if (item.config.showHeader) {\n <div class=\"flex w-full items-center gap-2\">\n <div class=\"flex min-w-0 flex-1 items-center gap-2\">\n @if (item.config.collapse.enabled) {\n <mt-button\n variant=\"text\"\n severity=\"secondary\"\n [icon]=\"\n item.expanded\n ? item.config.collapse.collapseIcon\n : item.config.collapse.expandIcon\n \"\n (onClick)=\"toggleExpanded(item.key)\"\n />\n }\n <h3 class=\"m-0 text-lg font-semibold\">\n {{ item.title || item.moduleKey || defaultTitle(item) }}\n </h3>\n @if (item.config.headerStart) {\n <div class=\"flex items-center gap-2\">\n <ng-container\n [ngTemplateOutlet]=\"item.config.headerStart\"\n [ngTemplateOutletContext]=\"templateContext(item)\"\n />\n </div>\n }\n </div>\n @if (item.config.headerEnd) {\n <div class=\"ml-auto flex shrink-0 items-center gap-2\">\n <ng-container\n [ngTemplateOutlet]=\"item.config.headerEnd\"\n [ngTemplateOutletContext]=\"templateContext(item)\"\n />\n </div>\n }\n </div>\n }\n\n @if (item.expanded || !item.config.collapse.enabled) {\n @if (item.config.templateContent) {\n <ng-container\n [ngTemplateOutlet]=\"item.config.templateContent\"\n [ngTemplateOutletContext]=\"templateContext(item)\"\n />\n } @else if (item.type === \"informative\") {\n <mt-client-list-informative-view [state]=\"item\" />\n } @else if (item.areaType === \"table\") {\n <mt-client-list-table-view\n [state]=\"item\"\n [rowActions]=\"rowActionsFor(item)\"\n [actionShape]=\"item.config.actionShape\"\n [rowActionsLoadingFn]=\"rowActionsLoadingFnFor(item)\"\n (rowActionsRequested)=\"onRowActionsRequested(item, $event)\"\n (lazyLoad)=\"onLazyLoad(item.key, $event)\"\n (rowClick)=\"onTableRowClick(item, $event)\"\n />\n } @else {\n <mt-client-list-cards-view\n [state]=\"item\"\n [rowActions]=\"rowActionsFor(item)\"\n [rowActionsLoadingFn]=\"rowActionsLoadingFnFor(item)\"\n (rowActionsRequested)=\"onRowActionsRequested(item, $event)\"\n (lazyLoad)=\"onLazyLoad(item.key, $event)\"\n (loadMore)=\"loadMoreCards(item.key)\"\n (cardClick)=\"onCardClick(item, $event)\"\n />\n }\n }\n </section>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs", "ariaLabel"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: ClientListTableView, selector: "mt-client-list-table-view", inputs: ["state", "rowActions", "actionShape", "rowActionsLoadingFn"], outputs: ["lazyLoad", "rowClick", "rowActionsRequested"] }, { kind: "component", type: ClientListCardsView, selector: "mt-client-list-cards-view", inputs: ["state", "rowActions", "rowActionsLoadingFn"], outputs: ["lazyLoad", "loadMore", "cardClick", "rowActionsRequested"] }, { kind: "component", type: ClientListInformativeView, selector: "mt-client-list-informative-view", inputs: ["state"] }] });
|
|
3386
3581
|
}
|
|
3387
3582
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: ClientList, decorators: [{
|
|
3388
3583
|
type: Component,
|
|
@@ -3396,7 +3591,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
3396
3591
|
ClientListStateService,
|
|
3397
3592
|
ClientListRuntimeActionsService,
|
|
3398
3593
|
ClientListToolbarService,
|
|
3399
|
-
], template: "<div class=\"flex flex-col gap-4\">\
|
|
3594
|
+
], template: "<div class=\"flex flex-col gap-4\">\n @for (item of items(); track item.key) {\n <section class=\"flex flex-col gap-4\">\n @if (item.config.showHeader) {\n <div class=\"flex w-full items-center gap-2\">\n <div class=\"flex min-w-0 flex-1 items-center gap-2\">\n @if (item.config.collapse.enabled) {\n <mt-button\n variant=\"text\"\n severity=\"secondary\"\n [icon]=\"\n item.expanded\n ? item.config.collapse.collapseIcon\n : item.config.collapse.expandIcon\n \"\n (onClick)=\"toggleExpanded(item.key)\"\n />\n }\n <h3 class=\"m-0 text-lg font-semibold\">\n {{ item.title || item.moduleKey || defaultTitle(item) }}\n </h3>\n @if (item.config.headerStart) {\n <div class=\"flex items-center gap-2\">\n <ng-container\n [ngTemplateOutlet]=\"item.config.headerStart\"\n [ngTemplateOutletContext]=\"templateContext(item)\"\n />\n </div>\n }\n </div>\n @if (item.config.headerEnd) {\n <div class=\"ml-auto flex shrink-0 items-center gap-2\">\n <ng-container\n [ngTemplateOutlet]=\"item.config.headerEnd\"\n [ngTemplateOutletContext]=\"templateContext(item)\"\n />\n </div>\n }\n </div>\n }\n\n @if (item.expanded || !item.config.collapse.enabled) {\n @if (item.config.templateContent) {\n <ng-container\n [ngTemplateOutlet]=\"item.config.templateContent\"\n [ngTemplateOutletContext]=\"templateContext(item)\"\n />\n } @else if (item.type === \"informative\") {\n <mt-client-list-informative-view [state]=\"item\" />\n } @else if (item.areaType === \"table\") {\n <mt-client-list-table-view\n [state]=\"item\"\n [rowActions]=\"rowActionsFor(item)\"\n [actionShape]=\"item.config.actionShape\"\n [rowActionsLoadingFn]=\"rowActionsLoadingFnFor(item)\"\n (rowActionsRequested)=\"onRowActionsRequested(item, $event)\"\n (lazyLoad)=\"onLazyLoad(item.key, $event)\"\n (rowClick)=\"onTableRowClick(item, $event)\"\n />\n } @else {\n <mt-client-list-cards-view\n [state]=\"item\"\n [rowActions]=\"rowActionsFor(item)\"\n [rowActionsLoadingFn]=\"rowActionsLoadingFnFor(item)\"\n (rowActionsRequested)=\"onRowActionsRequested(item, $event)\"\n (lazyLoad)=\"onLazyLoad(item.key, $event)\"\n (loadMore)=\"loadMoreCards(item.key)\"\n (cardClick)=\"onCardClick(item, $event)\"\n />\n }\n }\n </section>\n }\n</div>\n" }]
|
|
3400
3595
|
}], ctorParameters: () => [], propDecorators: { configurations: [{ type: i0.Input, args: [{ isSignal: true, alias: "configurations", required: true }] }], defaultTake: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultTake", required: false }] }], loaded: [{ type: i0.Output, args: ["loaded"] }], errored: [{ type: i0.Output, args: ["errored"] }], itemClicked: [{ type: i0.Output, args: ["itemClicked"] }] } });
|
|
3401
3596
|
|
|
3402
3597
|
/**
|