@masterteam/client-components 0.0.94 → 0.0.96
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/assets/client-components.css +1 -1
- package/fesm2022/masterteam-client-components-client-instance-preview.mjs +26 -3
- package/fesm2022/masterteam-client-components-client-instance-preview.mjs.map +1 -1
- package/fesm2022/masterteam-client-components-client-list.mjs +161 -31
- package/fesm2022/masterteam-client-components-client-list.mjs.map +1 -1
- package/package.json +4 -4
- package/types/masterteam-client-components-client-instance-preview.d.ts +39 -2
- package/types/masterteam-client-components-client-list.d.ts +83 -15
|
@@ -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`);
|
|
@@ -484,6 +597,10 @@ class ClientListStateService {
|
|
|
484
597
|
columns: [],
|
|
485
598
|
rows: [],
|
|
486
599
|
cards,
|
|
600
|
+
// Pre-pagination aggregates, kept verbatim off the response, exactly
|
|
601
|
+
// as a hierarchy table keeps them. Absent on an ordinary board.
|
|
602
|
+
totals: response.projectionMeta?.totals,
|
|
603
|
+
sourceGroups: response.projectionMeta?.sourceGroups,
|
|
487
604
|
// `skip` is the offset of the *next* page to fetch, always a multiple
|
|
488
605
|
// of the effective page size so `toPage()` stays exact.
|
|
489
606
|
skip: config.isPaginated
|
|
@@ -774,6 +891,10 @@ class ClientListStateService {
|
|
|
774
891
|
state: record.state ?? null,
|
|
775
892
|
properties: normalizedEntities,
|
|
776
893
|
entities: normalizedEntities,
|
|
894
|
+
// Only present on a descendant board, where cards come from several
|
|
895
|
+
// owners at once and the list's own context no longer identifies the
|
|
896
|
+
// record a card belongs to.
|
|
897
|
+
source: record.source ?? null,
|
|
777
898
|
};
|
|
778
899
|
});
|
|
779
900
|
}
|
|
@@ -1275,11 +1396,11 @@ class ClientListTableView {
|
|
|
1275
1396
|
};
|
|
1276
1397
|
}
|
|
1277
1398
|
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\">\
|
|
1399
|
+
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
1400
|
}
|
|
1280
1401
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: ClientListTableView, decorators: [{
|
|
1281
1402
|
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\">\
|
|
1403
|
+
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
1404
|
}], 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
1405
|
|
|
1285
1406
|
const EMPTY_KEY_SET = new Set();
|
|
@@ -1429,13 +1550,17 @@ class ClientListCardsView {
|
|
|
1429
1550
|
rowRepresentations = computed(() => this.cardsState().cards.map((card) => this.cardToRow(card)), ...(ngDevMode ? [{ debugName: "rowRepresentations" }] : /* istanbul ignore next */ []));
|
|
1430
1551
|
cardToRow(card) {
|
|
1431
1552
|
const fromDisplay = card.displayProperties;
|
|
1553
|
+
// `source` is only present on a descendant board. Row actions read the
|
|
1554
|
+
// owning record off the row, so a card's actions address its own owner
|
|
1555
|
+
// rather than the aggregation context — the same contract a hierarchy
|
|
1556
|
+
// table row carries.
|
|
1557
|
+
const metadata = {
|
|
1558
|
+
[CLIENT_LIST_RECORD_STATE_KEY]: card.state ?? null,
|
|
1559
|
+
[CLIENT_LIST_RECORD_SOURCE_KEY]: card.source ?? null,
|
|
1560
|
+
};
|
|
1432
1561
|
const record = fromDisplay && Object.keys(fromDisplay).length > 0
|
|
1433
|
-
? {
|
|
1434
|
-
|
|
1435
|
-
...fromDisplay,
|
|
1436
|
-
[CLIENT_LIST_RECORD_STATE_KEY]: card.state ?? null,
|
|
1437
|
-
}
|
|
1438
|
-
: { Id: card.id, [CLIENT_LIST_RECORD_STATE_KEY]: card.state ?? null };
|
|
1562
|
+
? { Id: card.id, ...fromDisplay, ...metadata }
|
|
1563
|
+
: { Id: card.id, ...metadata };
|
|
1439
1564
|
for (const entity of card.entities ?? []) {
|
|
1440
1565
|
if (entity.viewType === 'Section')
|
|
1441
1566
|
continue;
|
|
@@ -2001,8 +2126,7 @@ class ClientListCardsView {
|
|
|
2001
2126
|
});
|
|
2002
2127
|
}
|
|
2003
2128
|
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", "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
|
-
EntitiesPreview]] });
|
|
2129
|
+
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]=\"cardBody\"\n [ngTemplateOutletContext]=\"{ $implicit: card }\"\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]=\"cardBody\"\n [ngTemplateOutletContext]=\"{ $implicit: card }\"\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 One card's contents, shared by the grouped and ungrouped grids above so the\n two never drift apart.\n-->\n<ng-template #cardBody let-card>\n <ng-container\n [ngTemplateOutlet]=\"cardActionsMenu\"\n [ngTemplateOutletContext]=\"{ $implicit: card }\"\n />\n @if (card.source; as source) {\n <!--\n Where this card came from. Only a descendant board carries a source, and\n on one the cards come from several records at once \u2014 without this every\n card reads as belonging to the record being viewed, and the same amount\n under two different projects is indistinguishable. Padded clear of the\n actions cluster pinned to the card's top corner.\n -->\n <div\n class=\"mb-3 flex min-w-0 items-center gap-1.5 border-b border-surface-200 pb-2 pe-16 text-xs text-gray-500 dark:border-surface-700 dark:text-gray-400\"\n >\n <span class=\"shrink-0\">{{ source.levelName }}</span>\n <span aria-hidden=\"true\">\u00B7</span>\n <span class=\"truncate font-medium text-gray-700 dark:text-gray-200\">{{\n source.name\n }}</span>\n </div>\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\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", "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]] });
|
|
2006
2130
|
}
|
|
2007
2131
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: ClientListCardsView, decorators: [{
|
|
2008
2132
|
type: Component,
|
|
@@ -2018,7 +2142,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
2018
2142
|
PopoverModule,
|
|
2019
2143
|
TableCaption,
|
|
2020
2144
|
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"] }]
|
|
2145
|
+
], 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]=\"cardBody\"\n [ngTemplateOutletContext]=\"{ $implicit: card }\"\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]=\"cardBody\"\n [ngTemplateOutletContext]=\"{ $implicit: card }\"\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 One card's contents, shared by the grouped and ungrouped grids above so the\n two never drift apart.\n-->\n<ng-template #cardBody let-card>\n <ng-container\n [ngTemplateOutlet]=\"cardActionsMenu\"\n [ngTemplateOutletContext]=\"{ $implicit: card }\"\n />\n @if (card.source; as source) {\n <!--\n Where this card came from. Only a descendant board carries a source, and\n on one the cards come from several records at once \u2014 without this every\n card reads as belonging to the record being viewed, and the same amount\n under two different projects is indistinguishable. Padded clear of the\n actions cluster pinned to the card's top corner.\n -->\n <div\n class=\"mb-3 flex min-w-0 items-center gap-1.5 border-b border-surface-200 pb-2 pe-16 text-xs text-gray-500 dark:border-surface-700 dark:text-gray-400\"\n >\n <span class=\"shrink-0\">{{ source.levelName }}</span>\n <span aria-hidden=\"true\">\u00B7</span>\n <span class=\"truncate font-medium text-gray-700 dark:text-gray-200\">{{\n source.name\n }}</span>\n </div>\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\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"] }]
|
|
2022
2146
|
}], 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
2147
|
|
|
2024
2148
|
const DEFAULT_GRID_COLUMNS$1 = 12;
|
|
@@ -2043,11 +2167,11 @@ class ClientListInformativeView {
|
|
|
2043
2167
|
};
|
|
2044
2168
|
}
|
|
2045
2169
|
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\">\
|
|
2170
|
+
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
2171
|
}
|
|
2048
2172
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: ClientListInformativeView, decorators: [{
|
|
2049
2173
|
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\">\
|
|
2174
|
+
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
2175
|
}], propDecorators: { state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: true }] }] } });
|
|
2052
2176
|
|
|
2053
2177
|
function mergeRuntimeFilters(configured = [], runtime = {}, columns = []) {
|
|
@@ -2998,7 +3122,7 @@ class ClientList {
|
|
|
2998
3122
|
this.state.setLoading(key, true);
|
|
2999
3123
|
this.state.setError(key, null);
|
|
3000
3124
|
const sub = this.api
|
|
3001
|
-
.getCards(config.contextKey, filters, config.isPaginated ? skip : undefined, config.isPaginated ? take : undefined)
|
|
3125
|
+
.getCards(config.contextKey, filters, config.isPaginated ? skip : undefined, config.isPaginated ? take : undefined, config.hierarchy)
|
|
3002
3126
|
.subscribe({
|
|
3003
3127
|
next: (response) => {
|
|
3004
3128
|
let hasLoadedData = false;
|
|
@@ -3095,15 +3219,18 @@ class ClientList {
|
|
|
3095
3219
|
return (columnKeys ?? []).length > 0 ? 'override' : DEFAULT_MODE;
|
|
3096
3220
|
}
|
|
3097
3221
|
/**
|
|
3098
|
-
* Descendant scope only survives where
|
|
3099
|
-
* a usable root id.
|
|
3100
|
-
* the backend answers an unsupported
|
|
3101
|
-
*
|
|
3102
|
-
*
|
|
3222
|
+
* Descendant scope only survives where it can actually be fetched: a table
|
|
3223
|
+
* or a cards board, with a usable root id. An informative dashboard drops it
|
|
3224
|
+
* rather than sending it, because the backend answers an unsupported
|
|
3225
|
+
* combination with a 400 that takes the whole list down.
|
|
3226
|
+
*
|
|
3227
|
+
* A cards board does not send it as `Card` either — the backend takes
|
|
3228
|
+
* descendant scope on `Table` only. `ClientListApiService.getCards` fetches
|
|
3229
|
+
* the hierarchy table and re-dresses it as cards.
|
|
3103
3230
|
*/
|
|
3104
3231
|
resolveHierarchyScope(config, areaType) {
|
|
3105
3232
|
const scope = config?.hierarchy;
|
|
3106
|
-
if (!scope || areaType !== 'table')
|
|
3233
|
+
if (!scope || (areaType !== 'table' && areaType !== 'cards'))
|
|
3107
3234
|
return null;
|
|
3108
3235
|
const rootId = this.resolvePositiveInteger(scope.rootId);
|
|
3109
3236
|
return rootId == null ? null : { ...scope, rootId };
|
|
@@ -3323,6 +3450,9 @@ class ClientList {
|
|
|
3323
3450
|
config.contextKey ?? 'x',
|
|
3324
3451
|
this.serializeFilters(filters),
|
|
3325
3452
|
'Card',
|
|
3453
|
+
// Two boards on the same context and filters are still different
|
|
3454
|
+
// fetches when they are rooted at different records.
|
|
3455
|
+
this.serializeHierarchy(config.hierarchy),
|
|
3326
3456
|
config.isPaginated ? skip : 0,
|
|
3327
3457
|
config.isPaginated ? take : 0,
|
|
3328
3458
|
].join('|');
|
|
@@ -3382,7 +3512,7 @@ class ClientList {
|
|
|
3382
3512
|
ClientListStateService,
|
|
3383
3513
|
ClientListRuntimeActionsService,
|
|
3384
3514
|
ClientListToolbarService,
|
|
3385
|
-
], ngImport: i0, template: "<div class=\"flex flex-col gap-4\">\
|
|
3515
|
+
], 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
3516
|
}
|
|
3387
3517
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: ClientList, decorators: [{
|
|
3388
3518
|
type: Component,
|
|
@@ -3396,7 +3526,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
3396
3526
|
ClientListStateService,
|
|
3397
3527
|
ClientListRuntimeActionsService,
|
|
3398
3528
|
ClientListToolbarService,
|
|
3399
|
-
], template: "<div class=\"flex flex-col gap-4\">\
|
|
3529
|
+
], 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
3530
|
}], 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
3531
|
|
|
3402
3532
|
/**
|