@masterteam/client-components 0.0.86 → 0.0.88
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.
|
@@ -559,8 +559,6 @@ class ClientListStateService {
|
|
|
559
559
|
existing.areaType !== item.areaType) {
|
|
560
560
|
return item;
|
|
561
561
|
}
|
|
562
|
-
const keepPaging = existing.config.isPaginated === item.config.isPaginated &&
|
|
563
|
-
existing.take === item.take;
|
|
564
562
|
return {
|
|
565
563
|
...item,
|
|
566
564
|
loading: existing.loading,
|
|
@@ -571,7 +569,12 @@ class ClientListStateService {
|
|
|
571
569
|
columns: existing.columns,
|
|
572
570
|
rows: existing.rows,
|
|
573
571
|
cards: existing.cards,
|
|
574
|
-
|
|
572
|
+
// Paging is settled by the caller, which compared the incoming config
|
|
573
|
+
// against the previous one. Re-deciding it here — with only the two
|
|
574
|
+
// states and no view of what changed in the config — is what used to
|
|
575
|
+
// drop the user's page whenever their rows-per-page differed from the
|
|
576
|
+
// configured default.
|
|
577
|
+
skip: item.skip,
|
|
575
578
|
take: item.take,
|
|
576
579
|
expanded: existing.expanded,
|
|
577
580
|
dashboardData: existing.dashboardData,
|
|
@@ -1799,7 +1802,7 @@ class ClientListCardsView {
|
|
|
1799
1802
|
});
|
|
1800
1803
|
}
|
|
1801
1804
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: ClientListCardsView, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1802
|
-
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: 1.25rem;--mt-cards-min: 380px;--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,
|
|
1805
|
+
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: 1.25rem;--mt-cards-min: 355px;--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,
|
|
1803
1806
|
EntitiesPreview]] });
|
|
1804
1807
|
}
|
|
1805
1808
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: ClientListCardsView, decorators: [{
|
|
@@ -1816,7 +1819,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
1816
1819
|
PopoverModule,
|
|
1817
1820
|
TableCaption,
|
|
1818
1821
|
EmptyState,
|
|
1819
|
-
], 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: 1.25rem;--mt-cards-min: 380px;--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"] }]
|
|
1822
|
+
], 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: 1.25rem;--mt-cards-min: 355px;--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"] }]
|
|
1820
1823
|
}], 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 }] }] } });
|
|
1821
1824
|
|
|
1822
1825
|
function toDashboardExtraFilters(filters = []) {
|
|
@@ -1941,6 +1944,40 @@ function toScalar(value) {
|
|
|
1941
1944
|
item['displayName']);
|
|
1942
1945
|
}
|
|
1943
1946
|
|
|
1947
|
+
/**
|
|
1948
|
+
* Whether the offset a list is currently sitting on still describes the same
|
|
1949
|
+
* result set, i.e. this reconfigure changed nothing about what gets fetched.
|
|
1950
|
+
*
|
|
1951
|
+
* Hosts rebuild the `configurations` array from a `computed()`, so *any*
|
|
1952
|
+
* unrelated state change — a permission refresh after a write, a language
|
|
1953
|
+
* switch, a re-rendered header template — hands the list a brand new array
|
|
1954
|
+
* with identical content. That must not move the user off their page, and
|
|
1955
|
+
* must not cost a request.
|
|
1956
|
+
*
|
|
1957
|
+
* The page size is compared **as configured**, never as currently applied. The
|
|
1958
|
+
* table owns its rows-per-page: the user picks it from the paginator and
|
|
1959
|
+
* `mt-table` restores a persisted value, and either of those legitimately
|
|
1960
|
+
* leaves the runtime `take` different from the configured one. Reading that
|
|
1961
|
+
* difference as drift to correct is what used to reset the paginator and
|
|
1962
|
+
* refetch page 1 on every reconfigure.
|
|
1963
|
+
*/
|
|
1964
|
+
function preservesPaging(existing, areaType, next) {
|
|
1965
|
+
if (!existing ||
|
|
1966
|
+
existing.type !== 'form' ||
|
|
1967
|
+
existing.areaType !== areaType ||
|
|
1968
|
+
next.type !== 'form') {
|
|
1969
|
+
return false;
|
|
1970
|
+
}
|
|
1971
|
+
const current = existing.config;
|
|
1972
|
+
return (current.isPaginated === next.isPaginated &&
|
|
1973
|
+
current.take === next.take &&
|
|
1974
|
+
current.contextKey === next.contextKey &&
|
|
1975
|
+
serializeFilters(current.filters) === serializeFilters(next.filters));
|
|
1976
|
+
}
|
|
1977
|
+
function serializeFilters(filters) {
|
|
1978
|
+
return JSON.stringify(filters ?? []);
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1944
1981
|
const DEFAULT_AREA_TYPE = 'table';
|
|
1945
1982
|
const DEFAULT_MODE = 'auto';
|
|
1946
1983
|
const DEFAULT_TYPE = 'form';
|
|
@@ -2546,6 +2583,10 @@ class ClientList {
|
|
|
2546
2583
|
const key = config.key ?? this.createItemKey(config, index);
|
|
2547
2584
|
const existing = previousItems[key];
|
|
2548
2585
|
const normalizedConfig = this.toNormalizedConfig(config, type, areaType, mode, isPaginated, take, showHeader, collapseEnabled, layout);
|
|
2586
|
+
// Paging is runtime state, not config state: it survives a reconfigure
|
|
2587
|
+
// that changed nothing about the fetch, and the *applied* page size
|
|
2588
|
+
// (the user's rows-per-page choice) is carried forward with it.
|
|
2589
|
+
const keepsPaging = preservesPaging(existing, areaType, normalizedConfig);
|
|
2549
2590
|
if (isInformative) {
|
|
2550
2591
|
return {
|
|
2551
2592
|
key,
|
|
@@ -2581,12 +2622,13 @@ class ClientList {
|
|
|
2581
2622
|
columns: [],
|
|
2582
2623
|
rows: [],
|
|
2583
2624
|
cards: [],
|
|
2584
|
-
//
|
|
2585
|
-
//
|
|
2586
|
-
// the
|
|
2587
|
-
//
|
|
2588
|
-
|
|
2589
|
-
|
|
2625
|
+
// For infinite scroll `skip` is the next offset into the loaded
|
|
2626
|
+
// set, so it travels with that set: kept while the fetch is
|
|
2627
|
+
// unchanged (the cards themselves are kept too, by
|
|
2628
|
+
// `mergeItemState`), and restarted at 0 the moment the fetch
|
|
2629
|
+
// changes and the loaded set stops meaning anything.
|
|
2630
|
+
skip: keepsPaging ? existing.skip : 0,
|
|
2631
|
+
take: keepsPaging ? existing.take : take,
|
|
2590
2632
|
expanded: existing?.expanded ?? config.collapse?.expandedByDefault ?? true,
|
|
2591
2633
|
dashboardData: null,
|
|
2592
2634
|
rawData: existing?.rawData ?? null,
|
|
@@ -2605,12 +2647,8 @@ class ClientList {
|
|
|
2605
2647
|
columns: [],
|
|
2606
2648
|
rows: [],
|
|
2607
2649
|
cards: [],
|
|
2608
|
-
skip: existing
|
|
2609
|
-
|
|
2610
|
-
existing.take === take
|
|
2611
|
-
? existing.skip
|
|
2612
|
-
: 0,
|
|
2613
|
-
take,
|
|
2650
|
+
skip: keepsPaging ? existing.skip : 0,
|
|
2651
|
+
take: keepsPaging ? existing.take : take,
|
|
2614
2652
|
expanded: existing?.expanded ?? config.collapse?.expandedByDefault ?? true,
|
|
2615
2653
|
dashboardData: null,
|
|
2616
2654
|
rawData: existing?.rawData ?? null,
|