@elasticias/ui 1.0.40 → 1.0.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/elasticias-ui.mjs +77 -43
- package/fesm2022/elasticias-ui.mjs.map +1 -1
- package/package.json +1 -1
- package/styles/_comptoir.scss +18 -0
- package/styles/_patterns.scss +26 -1
- package/types/elasticias-ui.d.ts +17 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
2
|
import { Pipe, input, Component, output, booleanAttribute, Input, Directive, signal, computed, inject, EventEmitter, Injector, forwardRef, Output, ContentChild, HostBinding, ChangeDetectionStrategy, ElementRef, LOCALE_ID, ViewContainerRef, DestroyRef, ChangeDetectorRef, effect, HostListener, ViewChild, model, InjectionToken, TemplateRef, ContentChildren, viewChild, contentChild, ViewEncapsulation, afterNextRender, ViewChildren, untracked, PLATFORM_ID } from '@angular/core';
|
|
3
|
-
import { NumberUtils, UuidUtils
|
|
3
|
+
import { NumberUtils, UuidUtils } from '@elasticias/utils';
|
|
4
4
|
import * as i1 from 'primeng/button';
|
|
5
5
|
import { ButtonModule } from 'primeng/button';
|
|
6
6
|
import * as i1$3 from '@angular/common';
|
|
@@ -22,7 +22,7 @@ import * as i1$6 from 'primeng/dialog';
|
|
|
22
22
|
import { DialogModule } from 'primeng/dialog';
|
|
23
23
|
import { SCREEN_REF_DATA_SERVICE, ScreenStateEnum } from '@elasticias/screens';
|
|
24
24
|
import { Permissions } from '@elasticias/types';
|
|
25
|
-
import { EfShortcutService, EfViewportService, formatShortcut, EfThemeConfigService, EF_BUILD_INFO, EfPermissionService, EfActiveModuleService, EF_SESSION, EfToastService } from '@elasticias/core';
|
|
25
|
+
import { EfShortcutService, EfViewportService, EF_PREFERENCE_STORE, formatShortcut, efTablePrefsKey, EfThemeConfigService, EF_BUILD_INFO, EfPermissionService, EfActiveModuleService, EF_SESSION, EF_SIDE_COLLAPSED_KEY, EfToastService } from '@elasticias/core';
|
|
26
26
|
import * as i1$7 from 'primeng/fieldset';
|
|
27
27
|
import { FieldsetModule } from 'primeng/fieldset';
|
|
28
28
|
import * as i2$2 from 'primeng/accordion';
|
|
@@ -1338,7 +1338,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
|
|
|
1338
1338
|
* Built-in presets (computed from `today` at open time):
|
|
1339
1339
|
* `today`, `this_week`, `this_month`, `last_30_days`, `last_90_days`,
|
|
1340
1340
|
* `this_quarter`, `this_year`. Override with `[presets]="customList"`
|
|
1341
|
-
* if you need a different set.
|
|
1341
|
+
* if you need a different set. `all_time` (no bounds, emitted as null
|
|
1342
|
+
* start and end) is also recognised, and offered first in the default
|
|
1343
|
+
* list with `allowAllTime`.
|
|
1342
1344
|
*/
|
|
1343
1345
|
class EfDatepickerAdvancedComponent extends AbstractEfFormControl {
|
|
1344
1346
|
host = inject(ElementRef);
|
|
@@ -1414,6 +1416,15 @@ class EfDatepickerAdvancedComponent extends AbstractEfFormControl {
|
|
|
1414
1416
|
/* `disabled` inherited from AbstractEfFormControl (effective = isDisabled). */
|
|
1415
1417
|
/** Show a `'Personnalisé…'` row that opens the calendar view. */
|
|
1416
1418
|
allowCustomRange = input(true, { ...(ngDevMode ? { debugName: "allowCustomRange" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1419
|
+
/**
|
|
1420
|
+
* Offer an `all_time` row ("Toute la période") at the top of the
|
|
1421
|
+
* default preset list. Picking it emits `start: null, end: null`.
|
|
1422
|
+
* Off by default because a report screen needs a bounded period;
|
|
1423
|
+
* list screens whose search treats null dates as "no filter" turn
|
|
1424
|
+
* it on so the user can get back to the unfiltered list. Ignored
|
|
1425
|
+
* when `[presets]` is supplied.
|
|
1426
|
+
*/
|
|
1427
|
+
allowAllTime = input(false, { ...(ngDevMode ? { debugName: "allowAllTime" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1417
1428
|
/* ── Field mode (form control) ──────────────────────────────────
|
|
1418
1429
|
`'filter'` (default) is the existing smart-bar period selector:
|
|
1419
1430
|
preset list + custom range, output-only via `rangeChange`.
|
|
@@ -1463,7 +1474,10 @@ class EfDatepickerAdvancedComponent extends AbstractEfFormControl {
|
|
|
1463
1474
|
draftStart = signal(null, ...(ngDevMode ? [{ debugName: "draftStart" }] : /* istanbul ignore next */ []));
|
|
1464
1475
|
draftEnd = signal(null, ...(ngDevMode ? [{ debugName: "draftEnd" }] : /* istanbul ignore next */ []));
|
|
1465
1476
|
/** Effective preset list — defaults if none provided. */
|
|
1466
|
-
effectivePresets = computed(() => this.presets() ??
|
|
1477
|
+
effectivePresets = computed(() => this.presets() ??
|
|
1478
|
+
(this.allowAllTime()
|
|
1479
|
+
? [{ key: 'all_time', labelKey: 'date_preset_all_time' }, ...this.defaultPresets]
|
|
1480
|
+
: this.defaultPresets), ...(ngDevMode ? [{ debugName: "effectivePresets" }] : /* istanbul ignore next */ []));
|
|
1467
1481
|
/** Current value resolved against today's date. */
|
|
1468
1482
|
resolvedRange = computed(() => {
|
|
1469
1483
|
const key = this.activePreset() ?? 'last_30_days';
|
|
@@ -2030,8 +2044,9 @@ class EfDatepickerAdvancedComponent extends AbstractEfFormControl {
|
|
|
2030
2044
|
return;
|
|
2031
2045
|
}
|
|
2032
2046
|
const range = this.resolvedRange();
|
|
2033
|
-
|
|
2034
|
-
this.
|
|
2047
|
+
// An open range has nothing to seed; the calendar opens on today.
|
|
2048
|
+
this.draftStart.set(range.start ? this.startOfDay(range.start) : null);
|
|
2049
|
+
this.draftEnd.set(range.end ? this.startOfDay(range.end) : null);
|
|
2035
2050
|
}
|
|
2036
2051
|
/** Compute the date range for a preset key. */
|
|
2037
2052
|
computePresetRange(key) {
|
|
@@ -2045,7 +2060,7 @@ class EfDatepickerAdvancedComponent extends AbstractEfFormControl {
|
|
|
2045
2060
|
/** Assemble an EfDateRange — exposes `labelKey` (for the trigger
|
|
2046
2061
|
* to render via `| translate`) and a literal `label` fallback. */
|
|
2047
2062
|
buildRange(key, start, end) {
|
|
2048
|
-
if (key === 'custom') {
|
|
2063
|
+
if (key === 'custom' && start && end) {
|
|
2049
2064
|
const sameYear = start.getFullYear() === end.getFullYear();
|
|
2050
2065
|
const fmt = sameYear ? 'd MMM' : 'd MMM yyyy';
|
|
2051
2066
|
return {
|
|
@@ -2058,13 +2073,21 @@ class EfDatepickerAdvancedComponent extends AbstractEfFormControl {
|
|
|
2058
2073
|
return {
|
|
2059
2074
|
start, end,
|
|
2060
2075
|
presetKey: key,
|
|
2061
|
-
|
|
2076
|
+
// A built-in key keeps its label when the list on show does not
|
|
2077
|
+
// carry it (e.g. `all_time` restored on a picker without the row).
|
|
2078
|
+
labelKey: preset?.labelKey ?? (EfDatepickerAdvancedComponent.BUILT_IN_KEYS.has(key) ? `date_preset_${key}` : undefined),
|
|
2062
2079
|
label: preset?.label ?? this.builtInHint(key),
|
|
2063
2080
|
};
|
|
2064
2081
|
}
|
|
2082
|
+
static BUILT_IN_KEYS = new Set([
|
|
2083
|
+
'all_time', 'today', 'this_week', 'this_month', 'last_30_days',
|
|
2084
|
+
'last_90_days', 'this_quarter', 'this_year',
|
|
2085
|
+
]);
|
|
2065
2086
|
builtInRange(key, today) {
|
|
2066
2087
|
const t = this.startOfDay(today);
|
|
2067
2088
|
switch (key) {
|
|
2089
|
+
case 'all_time':
|
|
2090
|
+
return { start: null, end: null };
|
|
2068
2091
|
case 'today':
|
|
2069
2092
|
return { start: t, end: t };
|
|
2070
2093
|
case 'this_week': {
|
|
@@ -2093,6 +2116,8 @@ class EfDatepickerAdvancedComponent extends AbstractEfFormControl {
|
|
|
2093
2116
|
builtInHint(key) {
|
|
2094
2117
|
const today = new Date();
|
|
2095
2118
|
const r = this.builtInRange(key, today);
|
|
2119
|
+
if (!r.start || !r.end)
|
|
2120
|
+
return '';
|
|
2096
2121
|
switch (key) {
|
|
2097
2122
|
case 'today':
|
|
2098
2123
|
return formatDate(today, 'd MMM', this.locale);
|
|
@@ -2201,7 +2226,7 @@ class EfDatepickerAdvancedComponent extends AbstractEfFormControl {
|
|
|
2201
2226
|
trackByDay = (_, day) => day.date.getTime();
|
|
2202
2227
|
trackByDow = (_, label) => label;
|
|
2203
2228
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfDatepickerAdvancedComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2204
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: EfDatepickerAdvancedComponent, isStandalone: true, selector: "ef-datepicker-advanced", inputs: { triggerLabelKey: { classPropertyName: "triggerLabelKey", publicName: "triggerLabelKey", isSignal: true, isRequired: false, transformFunction: null }, triggerLabel: { classPropertyName: "triggerLabel", publicName: "triggerLabel", isSignal: true, isRequired: false, transformFunction: null }, presets: { classPropertyName: "presets", publicName: "presets", isSignal: true, isRequired: false, transformFunction: null }, activePreset: { classPropertyName: "activePreset", publicName: "activePreset", isSignal: true, isRequired: false, transformFunction: null }, start: { classPropertyName: "start", publicName: "start", isSignal: true, isRequired: false, transformFunction: null }, end: { classPropertyName: "end", publicName: "end", isSignal: true, isRequired: false, transformFunction: null }, allowCustomRange: { classPropertyName: "allowCustomRange", publicName: "allowCustomRange", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, selectionMode: { classPropertyName: "selectionMode", publicName: "selectionMode", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, showTime: { classPropertyName: "showTime", publicName: "showTime", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { rangeChange: "rangeChange", opened: "opened", closed: "closed" }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown.escape": "onEscape()" }, properties: { "class.dp-host--field": "mode() === 'field'" } }, viewQueries: [{ propertyName: "anchorEl", first: true, predicate: ["anchorEl"], descendants: true, static: true }, { propertyName: "panelTpl", first: true, predicate: ["panelTpl"], descendants: true, static: true }], usesInheritance: true, ngImport: i0, template: "<div class=\"dp\" [class.dp--field]=\"mode() === 'field'\" #anchorEl>\n @if (mode() === 'field' && variant() === 'field') {\n <!-- Field mode \u2014 labelled field-box trigger (form control) -->\n @if (labelKey() || label()) {\n <ef-label\n [labelKey]=\"labelKey()\"\n [label]=\"label()\"\n [for]=\"effectiveId\"\n [required]=\"required()\"\n />\n }\n <div class=\"ef-field-control\">\n <button\n type=\"button\"\n class=\"dp__field-trigger\"\n [id]=\"effectiveId\"\n [class.open]=\"open()\"\n [class.is-invalid]=\"isInvalid\"\n [class.is-placeholder]=\"!hasFieldValue()\"\n [disabled]=\"isDisabled()\"\n [attr.aria-haspopup]=\"'dialog'\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [attr.aria-invalid]=\"isInvalid ? 'true' : null\"\n [attr.aria-describedby]=\"(showRequired || serverErrors) ? errorsId : null\"\n [attr.aria-label]=\"(labelKey() || label()) ? null : (ariaLabelKey() ? (ariaLabelKey() | translate) : (ariaLabel() || null))\"\n (click)=\"toggle($event)\"\n >\n <i class=\"ico pi pi-calendar\" aria-hidden=\"true\"></i>\n <span class=\"val\">\n {{ hasFieldValue() ? fieldDisplay() : (placeholderKey() ? (placeholderKey() | translate) : placeholder()) }}\n </span>\n <svg class=\"caret ef-chevron\" width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><polyline points=\"6 9 12 15 18 9\" /></svg>\n </button>\n @if (canClear) {\n <!-- Overlaid sibling (not nested in the trigger <button>) so it\n clears instead of toggling the calendar. -->\n <ef-clear-button\n class=\"dp__field-clear\"\n [ariaLabel]=\"'action.clear' | translate\"\n (clear)=\"clearValue()\"\n />\n }\n @if (showRequired || serverErrors) {\n <div [id]=\"errorsId\" role=\"alert\" aria-live=\"polite\">\n @if (showRequired) {\n <small class=\"text-danger\">{{ 'validation.required' | translate }}</small>\n }\n @if (serverErrors; as errors) {\n @for (error of errors; track error) {\n <small class=\"text-danger block\">{{ error }}</small>\n }\n }\n </div>\n }\n </div>\n } @else {\n <!-- Pill trigger \u2014 filter mode, or field mode with variant=\"pill\" -->\n <button\n type=\"button\"\n class=\"dp__trigger\"\n [class.open]=\"open()\"\n [disabled]=\"isDisabled()\"\n [attr.aria-haspopup]=\"'dialog'\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-label]=\"mode() === 'field' && !(labelKey() || label()) ? (ariaLabelKey() ? (ariaLabelKey() | translate) : (ariaLabel() || null)) : null\"\n (click)=\"toggle($event)\"\n >\n <i class=\"ico pi pi-calendar\" aria-hidden=\"true\"></i>\n @if (mode() === 'field') {\n <span class=\"val\">\n {{ hasFieldValue() ? fieldDisplay() : (placeholderKey() ? (placeholderKey() | translate) : placeholder()) }}\n </span>\n } @else {\n <span class=\"lbl\">\n {{ triggerLabelKey() ? (triggerLabelKey() | translate) : triggerLabel() }}\n </span>\n <span class=\"val\">\n @if (resolvedRange().labelKey; as key) {\n {{ key | translate }}\n } @else {\n {{ resolvedRange().label }}\n }\n </span>\n }\n <svg class=\"caret ef-chevron\" width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><polyline points=\"6 9 12 15 18 9\" /></svg>\n </button>\n }\n</div>\n\n<!--\n Popover panel \u2014 instantiated via ViewContainerRef + appended to\n <body> when `open()` flips on. Anchored absolutely to the trigger\n via positionPanel(); escapes ancestor overflow / stacking contexts.\n-->\n<ng-template #panelTpl>\n <div\n class=\"dp__panel open\"\n [class.wide]=\"view() === 'custom'\"\n role=\"dialog\"\n [attr.aria-label]=\"triggerLabelKey() | translate\"\n >\n <!-- \u2500\u2500 View 1 \u2014 preset list \u2500\u2500 -->\n <div class=\"dp__view\" [class.active]=\"view() === 'presets'\">\n @for (preset of effectivePresets(); track trackByPreset($index, preset)) {\n <button\n type=\"button\"\n class=\"dp__preset\"\n [class.active]=\"isActivePreset(preset.key)\"\n (click)=\"selectPreset(preset)\"\n >\n {{ preset.labelKey ? (preset.labelKey | translate) : preset.label }}\n <span class=\"hint\">{{ presetHint(preset) }}</span>\n </button>\n }\n\n @if (allowCustomRange()) {\n <div class=\"dp__divider\" role=\"separator\"></div>\n <button\n type=\"button\"\n class=\"dp__preset muted\"\n (click)=\"openCustomView()\"\n >\n <span style=\"display:inline-flex;align-items:center;gap:8px\">\n <i class=\"ico-lead pi pi-calendar\" aria-hidden=\"true\"></i>\n {{ 'date_preset_custom' | translate }}\n </span>\n <span class=\"hint\">\u21E7</span>\n </button>\n }\n </div>\n\n <!-- \u2500\u2500 View 2 \u2014 custom range with calendar \u2500\u2500 -->\n <div class=\"dp__view\" [class.active]=\"view() === 'custom'\">\n <div class=\"dp__custom-head\">\n @if (mode() !== 'field') {\n <button\n type=\"button\"\n class=\"dp__back\"\n [attr.aria-label]=\"'common_back' | translate\"\n (click)=\"backToPresets()\"\n >\n <i class=\"pi pi-chevron-left\" aria-hidden=\"true\"></i>\n </button>\n } @else {\n <span></span>\n }\n <div class=\"dp__custom-title\">\n <span class=\"pulse-dot\"></span>\n {{ (mode() === 'field' && selectionMode() === 'single' ? 'date_select_date' : 'date_custom_range') | translate }}\n </div>\n <span></span>\n </div>\n\n <div class=\"dp__cal\">\n <div class=\"dp__cal-head\">\n <button\n type=\"button\"\n class=\"dp__cal-nav\"\n [attr.aria-label]=\"'date_prev_month' | translate\"\n (click)=\"headerPrev()\"\n >\n <i class=\"pi pi-chevron-left\" aria-hidden=\"true\"></i>\n </button>\n <div class=\"dp__cal-title\">\n @switch (calMode()) {\n @case ('years') {\n <span class=\"dp__cal-pick is-static\">{{ yearWindowLabel() }}</span>\n }\n @case ('months') {\n <button type=\"button\" class=\"dp__cal-pick\" (click)=\"openYearPicker()\">\n {{ yearLabel() }}\n </button>\n }\n @default {\n <button type=\"button\" class=\"dp__cal-pick\" (click)=\"openMonthPicker()\">\n {{ monthLabel() }}\n </button>\n <button type=\"button\" class=\"dp__cal-pick\" (click)=\"openYearPicker()\">\n {{ yearLabel() }}\n </button>\n }\n }\n </div>\n <button\n type=\"button\"\n class=\"dp__cal-nav\"\n [attr.aria-label]=\"'date_next_month' | translate\"\n (click)=\"headerNext()\"\n >\n <i class=\"pi pi-chevron-right\" aria-hidden=\"true\"></i>\n </button>\n </div>\n\n @switch (calMode()) {\n @case ('months') {\n <div class=\"dp__cal-cells\">\n @for (cell of monthCells(); track cell.month) {\n <button\n type=\"button\"\n class=\"dp__cal-cell\"\n [class.active]=\"cell.active\"\n [class.today]=\"cell.today\"\n (click)=\"selectMonth(cell.month)\"\n >\n {{ cell.label }}\n </button>\n }\n </div>\n }\n @case ('years') {\n <div class=\"dp__cal-cells\">\n @for (cell of yearCells(); track cell.year) {\n <button\n type=\"button\"\n class=\"dp__cal-cell\"\n [class.active]=\"cell.active\"\n [class.today]=\"cell.today\"\n (click)=\"selectYear(cell.year)\"\n >\n {{ cell.year }}\n </button>\n }\n </div>\n }\n @default {\n <div class=\"dp__cal-dow-row\">\n @for (label of dowLabels(); track trackByDow($index, label)) {\n <div class=\"dp__cal-dow\">{{ label }}</div>\n }\n </div>\n\n <div class=\"dp__cal-grid\">\n @for (day of calendarGrid(); track trackByDay($index, day)) {\n <button\n type=\"button\"\n class=\"dp__cal-day\"\n [class.out]=\"day.out\"\n [class.today]=\"day.today\"\n [class.in-range]=\"day.inRange\"\n [class.range-start]=\"day.rangeStart\"\n [class.range-end]=\"day.rangeEnd\"\n [attr.aria-label]=\"day.date | date:'fullDate'\"\n (click)=\"selectDay(day.date)\"\n >\n <span class=\"day-num\">{{ day.day }}</span>\n </button>\n }\n </div>\n }\n }\n </div>\n\n @if (mode() === 'field' && selectionMode() === 'single') {\n <!-- Single-date field: date (+ time when enabled). Without time,\n picking a day commits & closes; with time, Apply commits. -->\n <div class=\"dp__inputs\" [class.dp__inputs--single]=\"!showTime()\">\n <div class=\"dp__field\">\n <label [attr.for]=\"effectiveId + '-single'\">{{ 'date_select_date' | translate }}</label>\n <input\n #singleInput\n [id]=\"effectiveId + '-single'\"\n type=\"text\"\n inputmode=\"numeric\"\n maxlength=\"10\"\n [value]=\"fromText()\"\n placeholder=\"JJ/MM/AAAA\"\n (keydown)=\"onDateKeydown($event, singleInput)\"\n (change)=\"commitSingleInput(singleInput)\"\n />\n </div>\n @if (showTime()) {\n <div class=\"dp__field\">\n <label [attr.for]=\"effectiveId + '-single-time'\">{{ 'date_time' | translate }}</label>\n <input\n #singleTime\n [id]=\"effectiveId + '-single-time'\"\n type=\"text\"\n inputmode=\"numeric\"\n maxlength=\"5\"\n [value]=\"fromTimeText()\"\n placeholder=\"HH:mm\"\n (keydown)=\"onTimeKeydown($event, singleTime)\"\n (change)=\"commitTimeInput('start', singleTime)\"\n />\n </div>\n }\n </div>\n } @else {\n <div class=\"dp__inputs\">\n <div class=\"dp__field\">\n <label [attr.for]=\"effectiveId + '-from'\">{{ 'date_from' | translate }}</label>\n <input\n #fromInput\n [id]=\"effectiveId + '-from'\"\n type=\"text\"\n inputmode=\"numeric\"\n maxlength=\"10\"\n [value]=\"fromText()\"\n placeholder=\"JJ/MM/AAAA\"\n (keydown)=\"onDateKeydown($event, fromInput)\"\n (change)=\"commitDateInput('start', fromInput)\"\n />\n @if (mode() === 'field' && showTime()) {\n <input\n #fromTime\n [attr.aria-label]=\"'date_time' | translate\"\n type=\"text\"\n inputmode=\"numeric\"\n maxlength=\"5\"\n [value]=\"fromTimeText()\"\n placeholder=\"HH:mm\"\n (keydown)=\"onTimeKeydown($event, fromTime)\"\n (change)=\"commitTimeInput('start', fromTime)\"\n />\n }\n </div>\n <div class=\"dp__field\">\n <label [attr.for]=\"effectiveId + '-to'\">{{ 'date_to' | translate }}</label>\n <input\n #toInput\n [id]=\"effectiveId + '-to'\"\n type=\"text\"\n inputmode=\"numeric\"\n maxlength=\"10\"\n [value]=\"toText()\"\n placeholder=\"JJ/MM/AAAA\"\n (keydown)=\"onDateKeydown($event, toInput)\"\n (change)=\"commitDateInput('end', toInput)\"\n />\n @if (mode() === 'field' && showTime()) {\n <input\n #toTime\n [attr.aria-label]=\"'date_time' | translate\"\n type=\"text\"\n inputmode=\"numeric\"\n maxlength=\"5\"\n [value]=\"toTimeText()\"\n placeholder=\"HH:mm\"\n (keydown)=\"onTimeKeydown($event, toTime)\"\n (change)=\"commitTimeInput('end', toTime)\"\n />\n }\n </div>\n </div>\n }\n\n <!-- Footer: hidden only for single field WITHOUT time (commit on pick). -->\n @if (!(mode() === 'field' && selectionMode() === 'single' && !showTime())) {\n <div class=\"dp__actions\">\n @if (mode() === 'field' && selectionMode() === 'single') {\n <span></span>\n } @else {\n <span class=\"duration\">\n <span class=\"num\">{{ draftDurationDays() }}</span>\n {{ 'date_days' | translate }}\n </span>\n }\n <div class=\"right\">\n @if (mode() !== 'field') {\n <button\n type=\"button\"\n class=\"btn btn-ghost btn-sm\"\n (click)=\"backToPresets()\"\n >\n {{ 'common_back' | translate }}\n </button>\n }\n <button\n type=\"button\"\n class=\"btn btn-primary btn-sm\"\n [disabled]=\"!draftStart()\"\n (click)=\"applyCustomRange()\"\n >\n {{ 'common_apply' | translate }}\n </button>\n </div>\n </div>\n }\n </div>\n </div>\n</ng-template>\n", styles: ["@charset \"UTF-8\";:host{display:inline-block}:host(.dp-host--field){display:block}.hint{unicode-bidi:plaintext}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: TranslateModule }, { kind: "component", type: EfLabelComponent, selector: "ef-label", inputs: ["label", "labelKey", "for", "styleClass", "required"] }, { kind: "component", type: EfClearButtonComponent, selector: "ef-clear-button", inputs: ["ariaLabel", "disabled"], outputs: ["clear"] }, { kind: "pipe", type: i1$3.DatePipe, name: "date" }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2229
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: EfDatepickerAdvancedComponent, isStandalone: true, selector: "ef-datepicker-advanced", inputs: { triggerLabelKey: { classPropertyName: "triggerLabelKey", publicName: "triggerLabelKey", isSignal: true, isRequired: false, transformFunction: null }, triggerLabel: { classPropertyName: "triggerLabel", publicName: "triggerLabel", isSignal: true, isRequired: false, transformFunction: null }, presets: { classPropertyName: "presets", publicName: "presets", isSignal: true, isRequired: false, transformFunction: null }, activePreset: { classPropertyName: "activePreset", publicName: "activePreset", isSignal: true, isRequired: false, transformFunction: null }, start: { classPropertyName: "start", publicName: "start", isSignal: true, isRequired: false, transformFunction: null }, end: { classPropertyName: "end", publicName: "end", isSignal: true, isRequired: false, transformFunction: null }, allowCustomRange: { classPropertyName: "allowCustomRange", publicName: "allowCustomRange", isSignal: true, isRequired: false, transformFunction: null }, allowAllTime: { classPropertyName: "allowAllTime", publicName: "allowAllTime", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, selectionMode: { classPropertyName: "selectionMode", publicName: "selectionMode", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, showTime: { classPropertyName: "showTime", publicName: "showTime", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { rangeChange: "rangeChange", opened: "opened", closed: "closed" }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown.escape": "onEscape()" }, properties: { "class.dp-host--field": "mode() === 'field'" } }, viewQueries: [{ propertyName: "anchorEl", first: true, predicate: ["anchorEl"], descendants: true, static: true }, { propertyName: "panelTpl", first: true, predicate: ["panelTpl"], descendants: true, static: true }], usesInheritance: true, ngImport: i0, template: "<div class=\"dp\" [class.dp--field]=\"mode() === 'field'\" #anchorEl>\n @if (mode() === 'field' && variant() === 'field') {\n <!-- Field mode \u2014 labelled field-box trigger (form control) -->\n @if (labelKey() || label()) {\n <ef-label\n [labelKey]=\"labelKey()\"\n [label]=\"label()\"\n [for]=\"effectiveId\"\n [required]=\"required()\"\n />\n }\n <div class=\"ef-field-control\">\n <button\n type=\"button\"\n class=\"dp__field-trigger\"\n [id]=\"effectiveId\"\n [class.open]=\"open()\"\n [class.is-invalid]=\"isInvalid\"\n [class.is-placeholder]=\"!hasFieldValue()\"\n [disabled]=\"isDisabled()\"\n [attr.aria-haspopup]=\"'dialog'\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [attr.aria-invalid]=\"isInvalid ? 'true' : null\"\n [attr.aria-describedby]=\"(showRequired || serverErrors) ? errorsId : null\"\n [attr.aria-label]=\"(labelKey() || label()) ? null : (ariaLabelKey() ? (ariaLabelKey() | translate) : (ariaLabel() || null))\"\n (click)=\"toggle($event)\"\n >\n <i class=\"ico pi pi-calendar\" aria-hidden=\"true\"></i>\n <span class=\"val\">\n {{ hasFieldValue() ? fieldDisplay() : (placeholderKey() ? (placeholderKey() | translate) : placeholder()) }}\n </span>\n <svg class=\"caret ef-chevron\" width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><polyline points=\"6 9 12 15 18 9\" /></svg>\n </button>\n @if (canClear) {\n <!-- Overlaid sibling (not nested in the trigger <button>) so it\n clears instead of toggling the calendar. -->\n <ef-clear-button\n class=\"dp__field-clear\"\n [ariaLabel]=\"'action.clear' | translate\"\n (clear)=\"clearValue()\"\n />\n }\n @if (showRequired || serverErrors) {\n <div [id]=\"errorsId\" role=\"alert\" aria-live=\"polite\">\n @if (showRequired) {\n <small class=\"text-danger\">{{ 'validation.required' | translate }}</small>\n }\n @if (serverErrors; as errors) {\n @for (error of errors; track error) {\n <small class=\"text-danger block\">{{ error }}</small>\n }\n }\n </div>\n }\n </div>\n } @else {\n <!-- Pill trigger \u2014 filter mode, or field mode with variant=\"pill\" -->\n <button\n type=\"button\"\n class=\"dp__trigger\"\n [class.open]=\"open()\"\n [disabled]=\"isDisabled()\"\n [attr.aria-haspopup]=\"'dialog'\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-label]=\"mode() === 'field' && !(labelKey() || label()) ? (ariaLabelKey() ? (ariaLabelKey() | translate) : (ariaLabel() || null)) : null\"\n (click)=\"toggle($event)\"\n >\n <i class=\"ico pi pi-calendar\" aria-hidden=\"true\"></i>\n @if (mode() === 'field') {\n <span class=\"val\">\n {{ hasFieldValue() ? fieldDisplay() : (placeholderKey() ? (placeholderKey() | translate) : placeholder()) }}\n </span>\n } @else {\n <span class=\"lbl\">\n {{ triggerLabelKey() ? (triggerLabelKey() | translate) : triggerLabel() }}\n </span>\n <span class=\"val\">\n @if (resolvedRange().labelKey; as key) {\n {{ key | translate }}\n } @else {\n {{ resolvedRange().label }}\n }\n </span>\n }\n <svg class=\"caret ef-chevron\" width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><polyline points=\"6 9 12 15 18 9\" /></svg>\n </button>\n }\n</div>\n\n<!--\n Popover panel \u2014 instantiated via ViewContainerRef + appended to\n <body> when `open()` flips on. Anchored absolutely to the trigger\n via positionPanel(); escapes ancestor overflow / stacking contexts.\n-->\n<ng-template #panelTpl>\n <div\n class=\"dp__panel open\"\n [class.wide]=\"view() === 'custom'\"\n role=\"dialog\"\n [attr.aria-label]=\"triggerLabelKey() | translate\"\n >\n <!-- \u2500\u2500 View 1 \u2014 preset list \u2500\u2500 -->\n <div class=\"dp__view\" [class.active]=\"view() === 'presets'\">\n @for (preset of effectivePresets(); track trackByPreset($index, preset)) {\n <button\n type=\"button\"\n class=\"dp__preset\"\n [class.active]=\"isActivePreset(preset.key)\"\n (click)=\"selectPreset(preset)\"\n >\n {{ preset.labelKey ? (preset.labelKey | translate) : preset.label }}\n <span class=\"hint\">{{ presetHint(preset) }}</span>\n </button>\n }\n\n @if (allowCustomRange()) {\n <div class=\"dp__divider\" role=\"separator\"></div>\n <button\n type=\"button\"\n class=\"dp__preset muted\"\n (click)=\"openCustomView()\"\n >\n <span style=\"display:inline-flex;align-items:center;gap:8px\">\n <i class=\"ico-lead pi pi-calendar\" aria-hidden=\"true\"></i>\n {{ 'date_preset_custom' | translate }}\n </span>\n <span class=\"hint\">\u21E7</span>\n </button>\n }\n </div>\n\n <!-- \u2500\u2500 View 2 \u2014 custom range with calendar \u2500\u2500 -->\n <div class=\"dp__view\" [class.active]=\"view() === 'custom'\">\n <div class=\"dp__custom-head\">\n @if (mode() !== 'field') {\n <button\n type=\"button\"\n class=\"dp__back\"\n [attr.aria-label]=\"'common_back' | translate\"\n (click)=\"backToPresets()\"\n >\n <i class=\"pi pi-chevron-left\" aria-hidden=\"true\"></i>\n </button>\n } @else {\n <span></span>\n }\n <div class=\"dp__custom-title\">\n <span class=\"pulse-dot\"></span>\n {{ (mode() === 'field' && selectionMode() === 'single' ? 'date_select_date' : 'date_custom_range') | translate }}\n </div>\n <span></span>\n </div>\n\n <div class=\"dp__cal\">\n <div class=\"dp__cal-head\">\n <button\n type=\"button\"\n class=\"dp__cal-nav\"\n [attr.aria-label]=\"'date_prev_month' | translate\"\n (click)=\"headerPrev()\"\n >\n <i class=\"pi pi-chevron-left\" aria-hidden=\"true\"></i>\n </button>\n <div class=\"dp__cal-title\">\n @switch (calMode()) {\n @case ('years') {\n <span class=\"dp__cal-pick is-static\">{{ yearWindowLabel() }}</span>\n }\n @case ('months') {\n <button type=\"button\" class=\"dp__cal-pick\" (click)=\"openYearPicker()\">\n {{ yearLabel() }}\n </button>\n }\n @default {\n <button type=\"button\" class=\"dp__cal-pick\" (click)=\"openMonthPicker()\">\n {{ monthLabel() }}\n </button>\n <button type=\"button\" class=\"dp__cal-pick\" (click)=\"openYearPicker()\">\n {{ yearLabel() }}\n </button>\n }\n }\n </div>\n <button\n type=\"button\"\n class=\"dp__cal-nav\"\n [attr.aria-label]=\"'date_next_month' | translate\"\n (click)=\"headerNext()\"\n >\n <i class=\"pi pi-chevron-right\" aria-hidden=\"true\"></i>\n </button>\n </div>\n\n @switch (calMode()) {\n @case ('months') {\n <div class=\"dp__cal-cells\">\n @for (cell of monthCells(); track cell.month) {\n <button\n type=\"button\"\n class=\"dp__cal-cell\"\n [class.active]=\"cell.active\"\n [class.today]=\"cell.today\"\n (click)=\"selectMonth(cell.month)\"\n >\n {{ cell.label }}\n </button>\n }\n </div>\n }\n @case ('years') {\n <div class=\"dp__cal-cells\">\n @for (cell of yearCells(); track cell.year) {\n <button\n type=\"button\"\n class=\"dp__cal-cell\"\n [class.active]=\"cell.active\"\n [class.today]=\"cell.today\"\n (click)=\"selectYear(cell.year)\"\n >\n {{ cell.year }}\n </button>\n }\n </div>\n }\n @default {\n <div class=\"dp__cal-dow-row\">\n @for (label of dowLabels(); track trackByDow($index, label)) {\n <div class=\"dp__cal-dow\">{{ label }}</div>\n }\n </div>\n\n <div class=\"dp__cal-grid\">\n @for (day of calendarGrid(); track trackByDay($index, day)) {\n <button\n type=\"button\"\n class=\"dp__cal-day\"\n [class.out]=\"day.out\"\n [class.today]=\"day.today\"\n [class.in-range]=\"day.inRange\"\n [class.range-start]=\"day.rangeStart\"\n [class.range-end]=\"day.rangeEnd\"\n [attr.aria-label]=\"day.date | date:'fullDate'\"\n (click)=\"selectDay(day.date)\"\n >\n <span class=\"day-num\">{{ day.day }}</span>\n </button>\n }\n </div>\n }\n }\n </div>\n\n @if (mode() === 'field' && selectionMode() === 'single') {\n <!-- Single-date field: date (+ time when enabled). Without time,\n picking a day commits & closes; with time, Apply commits. -->\n <div class=\"dp__inputs\" [class.dp__inputs--single]=\"!showTime()\">\n <div class=\"dp__field\">\n <label [attr.for]=\"effectiveId + '-single'\">{{ 'date_select_date' | translate }}</label>\n <input\n #singleInput\n [id]=\"effectiveId + '-single'\"\n type=\"text\"\n inputmode=\"numeric\"\n maxlength=\"10\"\n [value]=\"fromText()\"\n placeholder=\"JJ/MM/AAAA\"\n (keydown)=\"onDateKeydown($event, singleInput)\"\n (change)=\"commitSingleInput(singleInput)\"\n />\n </div>\n @if (showTime()) {\n <div class=\"dp__field\">\n <label [attr.for]=\"effectiveId + '-single-time'\">{{ 'date_time' | translate }}</label>\n <input\n #singleTime\n [id]=\"effectiveId + '-single-time'\"\n type=\"text\"\n inputmode=\"numeric\"\n maxlength=\"5\"\n [value]=\"fromTimeText()\"\n placeholder=\"HH:mm\"\n (keydown)=\"onTimeKeydown($event, singleTime)\"\n (change)=\"commitTimeInput('start', singleTime)\"\n />\n </div>\n }\n </div>\n } @else {\n <div class=\"dp__inputs\">\n <div class=\"dp__field\">\n <label [attr.for]=\"effectiveId + '-from'\">{{ 'date_from' | translate }}</label>\n <input\n #fromInput\n [id]=\"effectiveId + '-from'\"\n type=\"text\"\n inputmode=\"numeric\"\n maxlength=\"10\"\n [value]=\"fromText()\"\n placeholder=\"JJ/MM/AAAA\"\n (keydown)=\"onDateKeydown($event, fromInput)\"\n (change)=\"commitDateInput('start', fromInput)\"\n />\n @if (mode() === 'field' && showTime()) {\n <input\n #fromTime\n [attr.aria-label]=\"'date_time' | translate\"\n type=\"text\"\n inputmode=\"numeric\"\n maxlength=\"5\"\n [value]=\"fromTimeText()\"\n placeholder=\"HH:mm\"\n (keydown)=\"onTimeKeydown($event, fromTime)\"\n (change)=\"commitTimeInput('start', fromTime)\"\n />\n }\n </div>\n <div class=\"dp__field\">\n <label [attr.for]=\"effectiveId + '-to'\">{{ 'date_to' | translate }}</label>\n <input\n #toInput\n [id]=\"effectiveId + '-to'\"\n type=\"text\"\n inputmode=\"numeric\"\n maxlength=\"10\"\n [value]=\"toText()\"\n placeholder=\"JJ/MM/AAAA\"\n (keydown)=\"onDateKeydown($event, toInput)\"\n (change)=\"commitDateInput('end', toInput)\"\n />\n @if (mode() === 'field' && showTime()) {\n <input\n #toTime\n [attr.aria-label]=\"'date_time' | translate\"\n type=\"text\"\n inputmode=\"numeric\"\n maxlength=\"5\"\n [value]=\"toTimeText()\"\n placeholder=\"HH:mm\"\n (keydown)=\"onTimeKeydown($event, toTime)\"\n (change)=\"commitTimeInput('end', toTime)\"\n />\n }\n </div>\n </div>\n }\n\n <!-- Footer: hidden only for single field WITHOUT time (commit on pick). -->\n @if (!(mode() === 'field' && selectionMode() === 'single' && !showTime())) {\n <div class=\"dp__actions\">\n @if (mode() === 'field' && selectionMode() === 'single') {\n <span></span>\n } @else {\n <span class=\"duration\">\n <span class=\"num\">{{ draftDurationDays() }}</span>\n {{ 'date_days' | translate }}\n </span>\n }\n <div class=\"right\">\n @if (mode() !== 'field') {\n <button\n type=\"button\"\n class=\"btn btn-ghost btn-sm\"\n (click)=\"backToPresets()\"\n >\n {{ 'common_back' | translate }}\n </button>\n }\n <button\n type=\"button\"\n class=\"btn btn-primary btn-sm\"\n [disabled]=\"!draftStart()\"\n (click)=\"applyCustomRange()\"\n >\n {{ 'common_apply' | translate }}\n </button>\n </div>\n </div>\n }\n </div>\n </div>\n</ng-template>\n", styles: ["@charset \"UTF-8\";:host{display:inline-block}:host(.dp-host--field){display:block}.hint{unicode-bidi:plaintext}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: TranslateModule }, { kind: "component", type: EfLabelComponent, selector: "ef-label", inputs: ["label", "labelKey", "for", "styleClass", "required"] }, { kind: "component", type: EfClearButtonComponent, selector: "ef-clear-button", inputs: ["ariaLabel", "disabled"], outputs: ["clear"] }, { kind: "pipe", type: i1$3.DatePipe, name: "date" }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2205
2230
|
}
|
|
2206
2231
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfDatepickerAdvancedComponent, decorators: [{
|
|
2207
2232
|
type: Component,
|
|
@@ -2212,7 +2237,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
|
|
|
2212
2237
|
}], panelTpl: [{
|
|
2213
2238
|
type: ViewChild,
|
|
2214
2239
|
args: ['panelTpl', { static: true }]
|
|
2215
|
-
}], triggerLabelKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "triggerLabelKey", required: false }] }], triggerLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "triggerLabel", required: false }] }], presets: [{ type: i0.Input, args: [{ isSignal: true, alias: "presets", required: false }] }], activePreset: [{ type: i0.Input, args: [{ isSignal: true, alias: "activePreset", required: false }] }], start: [{ type: i0.Input, args: [{ isSignal: true, alias: "start", required: false }] }], end: [{ type: i0.Input, args: [{ isSignal: true, alias: "end", required: false }] }], allowCustomRange: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowCustomRange", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], selectionMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectionMode", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], showTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "showTime", required: false }] }], rangeChange: [{ type: i0.Output, args: ["rangeChange"] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }], onDocumentClick: [{
|
|
2240
|
+
}], triggerLabelKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "triggerLabelKey", required: false }] }], triggerLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "triggerLabel", required: false }] }], presets: [{ type: i0.Input, args: [{ isSignal: true, alias: "presets", required: false }] }], activePreset: [{ type: i0.Input, args: [{ isSignal: true, alias: "activePreset", required: false }] }], start: [{ type: i0.Input, args: [{ isSignal: true, alias: "start", required: false }] }], end: [{ type: i0.Input, args: [{ isSignal: true, alias: "end", required: false }] }], allowCustomRange: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowCustomRange", required: false }] }], allowAllTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowAllTime", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], selectionMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectionMode", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], showTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "showTime", required: false }] }], rangeChange: [{ type: i0.Output, args: ["rangeChange"] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }], onDocumentClick: [{
|
|
2216
2241
|
type: HostListener,
|
|
2217
2242
|
args: ['document:click', ['$event']]
|
|
2218
2243
|
}], onEscape: [{
|
|
@@ -4571,6 +4596,7 @@ class EfDataCardComponent {
|
|
|
4571
4596
|
become a list instead, driven by the same column config —
|
|
4572
4597
|
each column declares (or is given) a role. */
|
|
4573
4598
|
viewport = inject(EfViewportService);
|
|
4599
|
+
prefs = inject(EF_PREFERENCE_STORE);
|
|
4574
4600
|
configuredMobileLayout = inject(EF_DATA_CARD_MOBILE_LAYOUT, { optional: true });
|
|
4575
4601
|
isMobile = this.viewport.isMobile;
|
|
4576
4602
|
/** Per-screen override of the app-wide default. */
|
|
@@ -5191,14 +5217,17 @@ class EfDataCardComponent {
|
|
|
5191
5217
|
}
|
|
5192
5218
|
storageKey() {
|
|
5193
5219
|
const key = this.tableKey();
|
|
5194
|
-
return key ?
|
|
5220
|
+
return key ? efTablePrefsKey(key) : null;
|
|
5195
5221
|
}
|
|
5196
5222
|
loadPreferences() {
|
|
5197
5223
|
const key = this.storageKey();
|
|
5198
5224
|
if (!key)
|
|
5199
5225
|
return;
|
|
5200
5226
|
try {
|
|
5201
|
-
const
|
|
5227
|
+
const raw = this.prefs.get(key);
|
|
5228
|
+
if (!raw)
|
|
5229
|
+
return;
|
|
5230
|
+
const saved = JSON.parse(raw);
|
|
5202
5231
|
if (!saved)
|
|
5203
5232
|
return;
|
|
5204
5233
|
if (saved.density)
|
|
@@ -5216,10 +5245,10 @@ class EfDataCardComponent {
|
|
|
5216
5245
|
if (!key)
|
|
5217
5246
|
return;
|
|
5218
5247
|
try {
|
|
5219
|
-
|
|
5248
|
+
this.prefs.set(key, JSON.stringify({
|
|
5220
5249
|
density: this.density(),
|
|
5221
5250
|
hiddenColumnIds: this.hiddenColumnIds(),
|
|
5222
|
-
});
|
|
5251
|
+
}));
|
|
5223
5252
|
}
|
|
5224
5253
|
catch {
|
|
5225
5254
|
// Preferences are a convenience; losing them must never break
|
|
@@ -5330,7 +5359,9 @@ class EfDataCardComponent {
|
|
|
5330
5359
|
return;
|
|
5331
5360
|
const field = col.sortField ?? col.field ?? col.id;
|
|
5332
5361
|
const current = this.sort();
|
|
5333
|
-
const direction = current?.field === field
|
|
5362
|
+
const direction = current?.field === field
|
|
5363
|
+
? current.direction === 'desc' ? 'asc' : 'desc'
|
|
5364
|
+
: col.firstSortDirection ?? 'desc';
|
|
5334
5365
|
this.sortChange.emit({ field, direction });
|
|
5335
5366
|
}
|
|
5336
5367
|
trackByRow = (_, row) => {
|
|
@@ -5339,7 +5370,7 @@ class EfDataCardComponent {
|
|
|
5339
5370
|
};
|
|
5340
5371
|
trackByColumn = (_, col) => col.id;
|
|
5341
5372
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfDataCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5342
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: EfDataCardComponent, isStandalone: true, selector: "ef-data-card", inputs: { mobileLayout: { classPropertyName: "mobileLayout", publicName: "mobileLayout", isSignal: true, isRequired: false, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: false, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null }, trackByField: { classPropertyName: "trackByField", publicName: "trackByField", isSignal: true, isRequired: false, transformFunction: null }, pageNumber: { classPropertyName: "pageNumber", publicName: "pageNumber", isSignal: true, isRequired: false, transformFunction: null }, pageSize: { classPropertyName: "pageSize", publicName: "pageSize", isSignal: true, isRequired: false, transformFunction: null }, totalCount: { classPropertyName: "totalCount", publicName: "totalCount", isSignal: true, isRequired: false, transformFunction: null }, pageSizeOptions: { classPropertyName: "pageSizeOptions", publicName: "pageSizeOptions", isSignal: true, isRequired: false, transformFunction: null }, hidePager: { classPropertyName: "hidePager", publicName: "hidePager", isSignal: true, isRequired: false, transformFunction: null }, sort: { classPropertyName: "sort", publicName: "sort", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, errorMsg: { classPropertyName: "errorMsg", publicName: "errorMsg", isSignal: true, isRequired: false, transformFunction: null }, loadingKey: { classPropertyName: "loadingKey", publicName: "loadingKey", isSignal: true, isRequired: false, transformFunction: null }, rowDoubleClickable: { classPropertyName: "rowDoubleClickable", publicName: "rowDoubleClickable", isSignal: true, isRequired: false, transformFunction: null }, hasActionsColumn: { classPropertyName: "hasActionsColumn", publicName: "hasActionsColumn", isSignal: true, isRequired: false, transformFunction: null }, rowActionsContext: { classPropertyName: "rowActionsContext", publicName: "rowActionsContext", isSignal: true, isRequired: false, transformFunction: null }, showViewAction: { classPropertyName: "showViewAction", publicName: "showViewAction", isSignal: true, isRequired: false, transformFunction: null }, showEditAction: { classPropertyName: "showEditAction", publicName: "showEditAction", isSignal: true, isRequired: false, transformFunction: null }, showDuplicateAction: { classPropertyName: "showDuplicateAction", publicName: "showDuplicateAction", isSignal: true, isRequired: false, transformFunction: null }, showDeleteAction: { classPropertyName: "showDeleteAction", publicName: "showDeleteAction", isSignal: true, isRequired: false, transformFunction: null }, showDensityControl: { classPropertyName: "showDensityControl", publicName: "showDensityControl", isSignal: true, isRequired: false, transformFunction: null }, showColumnPicker: { classPropertyName: "showColumnPicker", publicName: "showColumnPicker", isSignal: true, isRequired: false, transformFunction: null }, showExportControl: { classPropertyName: "showExportControl", publicName: "showExportControl", isSignal: true, isRequired: false, transformFunction: null }, exporting: { classPropertyName: "exporting", publicName: "exporting", isSignal: true, isRequired: false, transformFunction: null }, screenContext: { classPropertyName: "screenContext", publicName: "screenContext", isSignal: true, isRequired: false, transformFunction: null }, searchTerm: { classPropertyName: "searchTerm", publicName: "searchTerm", isSignal: true, isRequired: false, transformFunction: null }, tableKey: { classPropertyName: "tableKey", publicName: "tableKey", isSignal: true, isRequired: false, transformFunction: null }, defaultDensity: { classPropertyName: "defaultDensity", publicName: "defaultDensity", isSignal: true, isRequired: false, transformFunction: null }, reorderable: { classPropertyName: "reorderable", publicName: "reorderable", isSignal: true, isRequired: false, transformFunction: null }, fixedLayout: { classPropertyName: "fixedLayout", publicName: "fixedLayout", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pageChange: "pageChange", pageSizeChange: "pageSizeChange", sortChange: "sortChange", rowDoubleClick: "rowDoubleClick", rowAction: "rowAction", exportRequest: "exportRequest", clearSearch: "clearSearch", retry: "retry", rowReorder: "rowReorder" }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown.escape": "onEscape()", "ef-row-actions-toggle": "onRowActionsToggle($event)" } }, queries: [{ propertyName: "bodyTemplates", predicate: EfColumnTemplateDirective }, { propertyName: "headerTemplates", predicate: EfColumnHeaderTemplateDirective }], ngImport: i0, template: "<!-- One cell renderer, shared by the table, the mobile row and the mobile\n detail. A new column type is added here once, not three times. -->\n<ng-template #cellTpl let-row let-col=\"col\" let-index=\"index\">\n @if (bodyTemplate(col.id); as bodyTpl) {\n <ng-container *ngTemplateOutlet=\"bodyTpl; context: { $implicit: row, col: col, index: index }\"></ng-container>\n } @else {\n @switch (col.type) {\n @case ('number') {\n {{ $any(cellValue(row, col)) | number:numberFormat(col):col.locale }}\n }\n @case ('money') {\n {{ $any(cellValue(row, col)) | currency:(col.currencyCode || 'EUR'):(col.currencyDisplay || 'symbol'):numberFormat(col):col.locale }}\n }\n @case ('date') {\n {{ $any(cellValue(row, col)) | date:(col.dateFormat || 'dd/MM/yyyy'):undefined:col.locale }}\n }\n @case ('datetime') {\n {{ $any(cellValue(row, col)) | date:(col.dateFormat || 'dd/MM/yyyy HH:mm'):undefined:col.locale }}\n }\n @case ('boolean') {\n @if (cellValue(row, col)) {\n <span class=\"bool yes\"><span class=\"dot\"></span> \u2713</span>\n } @else {\n <span class=\"bool no\"><span class=\"dot\"></span> \u2014</span>\n }\n }\n @case ('mono') {\n <span class=\"ref\">{{ cellValue(row, col) }}</span>\n }\n @case ('chip') {\n <span [class]=\"'chip ' + (col.chipPrefix || 'chip-') + cellValue(row, col)\">\n \u25CF {{ cellValue(row, col) }}\n </span>\n }\n @case ('status') {\n <ef-status-chip\n [referenceKey]=\"col.referenceKey || ''\"\n [code]=\"$any(cellValue(row, col))\"\n />\n }\n @case ('reference') {\n {{ resolveReference(cellValue(row, col), col) }}\n }\n @default {\n {{ cellValue(row, col) }}\n }\n }\n }\n</ng-template>\n\n<div class=\"tbl-wrap\" [class.has-open-menu]=\"openMenuCount() > 0 || openTool() !== null\" [attr.data-density]=\"density()\">\n <!-- \u2500\u2500 HEAD ROW \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"tbl-head\">\n <div class=\"count small\">\n @if (loading()) {\n <span>{{ loadingKey() | translate }}</span>\n } @else if (errorMsg()) {\n <!-- A refresh that failed on top of rows already on screen keeps the\n rows, so this chip is the only sign anything went wrong \u2014 and the\n rows it sits above may now be stale. It has to say so, and say it\n in the product's words: `errorMsg` is `err.message` from the HTTP\n layer, and \"500 Internal Server Error\" tells an operator nothing\n they can act on.\n\n The raw string stays on `data-error`, where devtools can read it\n and no reader ever meets it. `title` was the obvious place and is\n the wrong one: it renders the exception in a tooltip, which is\n precisely what this change exists to stop, and it is unreachable\n on touch besides.\n\n role=\"status\", not \"alert\": polite is right for a signal beside\n rows that are still usable, and when the body is showing the\n error state it already carries the alert \u2014 one failure must not\n interrupt a screen-reader twice. -->\n <span\n role=\"status\"\n [attr.data-error]=\"errorMsg()\"\n style=\"color: var(--st-cancelled-fg); font-weight: 600;\"\n >\u26A0 {{ 'common_error_refresh_failed' | translate }}</span\n >\n } @else {\n <ng-content select=\"[tbl-head-info]\"></ng-content>\n }\n </div>\n <div class=\"tbl-tools\">\n <ng-content select=\"[tbl-head-actions]\"></ng-content>\n\n @if (canExport()) {\n <div class=\"tbl-tool\">\n <button\n class=\"btn btn-ghost btn-sm\"\n type=\"button\"\n [disabled]=\"exporting()\"\n [attr.aria-busy]=\"exporting()\"\n (click)=\"requestExport()\"\n >\n <i\n class=\"pi\"\n [class.pi-download]=\"!exporting()\"\n [class.pi-spinner]=\"exporting()\"\n [class.is-spinning]=\"exporting()\"\n aria-hidden=\"true\"\n ></i>\n {{ (exporting() ? 'common_export_running' : 'common_export') | translate }}\n </button>\n </div>\n }\n\n @if (showDensityControl()) {\n <div class=\"tbl-tool\">\n <button\n class=\"btn btn-ghost btn-sm\"\n type=\"button\"\n [class.open]=\"openTool() === 'density'\"\n [attr.aria-expanded]=\"openTool() === 'density'\"\n aria-haspopup=\"menu\"\n (click)=\"toggleTool('density')\"\n >\n <i class=\"pi pi-bars\" aria-hidden=\"true\"></i>\n {{ 'common_density' | translate }}\n </button>\n @if (openTool() === 'density') {\n <div class=\"tbl-tool__menu\" role=\"menu\">\n @for (opt of densityOptions; track opt.value) {\n <button\n type=\"button\"\n role=\"menuitemradio\"\n [attr.aria-checked]=\"density() === opt.value\"\n [class.is-active]=\"density() === opt.value\"\n (click)=\"setDensity(opt.value)\"\n >\n <i class=\"pi\" [ngClass]=\"opt.icon\" aria-hidden=\"true\"></i>\n <span class=\"tbl-tool__label\">{{ opt.labelKey | translate }}</span>\n @if (density() === opt.value) {\n <i class=\"pi pi-check tbl-tool__check\" aria-hidden=\"true\"></i>\n }\n </button>\n }\n </div>\n }\n </div>\n }\n\n @if (showColumnPicker()) {\n <div class=\"tbl-tool\">\n <button\n class=\"btn btn-ghost btn-sm\"\n type=\"button\"\n [class.open]=\"openTool() === 'columns'\"\n [attr.aria-expanded]=\"openTool() === 'columns'\"\n aria-haspopup=\"menu\"\n (click)=\"toggleTool('columns')\"\n >\n <i class=\"pi pi-th-large\" aria-hidden=\"true\"></i>\n {{ 'common_columns' | translate }}\n </button>\n @if (openTool() === 'columns') {\n <div class=\"tbl-tool__menu tbl-tool__menu--wide\" role=\"menu\">\n @for (col of hideableColumns(); track col.id) {\n <button\n type=\"button\"\n role=\"menuitemcheckbox\"\n [attr.aria-checked]=\"!isColumnHidden(col.id)\"\n [class.is-active]=\"!isColumnHidden(col.id)\"\n (click)=\"toggleColumn(col.id)\"\n >\n <i class=\"pi\" [class.pi-check]=\"!isColumnHidden(col.id)\" aria-hidden=\"true\"></i>\n <span class=\"tbl-tool__label\">{{\n col.headerKey ? (col.headerKey | translate) : (col.header || col.id)\n }}</span>\n </button>\n }\n <div class=\"tbl-tool__sep\"></div>\n <button type=\"button\" role=\"menuitem\" (click)=\"resetColumns()\">\n <i class=\"pi pi-refresh\" aria-hidden=\"true\"></i>\n <span class=\"tbl-tool__label\">{{ 'common_columns_reset' | translate }}</span>\n </button>\n </div>\n }\n </div>\n }\n </div>\n </div>\n\n <!-- \u2500\u2500 TABLE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (isMobile()) {\n <!-- \u2500\u2500 MOBILE LIST \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n A six-column table on a 390px screen either overflows or\n squeezes every column to nothing, so the rows become a list.\n Same columns, same cell renderers; each column just declares\n (or is given) a role. Both shapes expand to the same detail \u2014\n they differ only in how much they show collapsed. -->\n <ul\n class=\"mlist\"\n [attr.data-layout]=\"effectiveMobileLayout()\"\n [attr.aria-busy]=\"loading() ? 'true' : null\"\n >\n @for (row of rows(); track trackByRow($index, row); let i = $index) {\n <li class=\"mrow\" [class.is-open]=\"isExpanded(row, i)\">\n <div\n class=\"mrow__head\"\n [class.is-tappable]=\"headIsControl()\"\n [attr.role]=\"headIsControl() ? 'button' : null\"\n [attr.tabindex]=\"headIsControl() ? 0 : null\"\n [attr.aria-expanded]=\"!rowNavigable() && hasDetail() ? isExpanded(row, i) : null\"\n (click)=\"onRowHeadActivate(row, i, $event)\"\n (keydown.enter)=\"onRowHeadActivate(row, i, $event)\"\n (keydown.space)=\"onRowHeadActivate(row, i, $event)\"\n >\n <div class=\"mrow__lead\">\n @if (primaryColumn(); as pc) {\n <span class=\"mrow__primary\" efOverflowTooltip efTooltip efTooltipPosition=\"top\">\n <ng-container *ngTemplateOutlet=\"cellTpl; context: { $implicit: row, col: pc, index: i }\"></ng-container>\n </span>\n }\n @if (effectiveMobileLayout() === 'row' && secondaryColumns().length) {\n <span class=\"mrow__secondary\">\n @for (sc of secondaryColumns(); track sc.id) {\n <span class=\"mrow__sec-part\">\n <ng-container *ngTemplateOutlet=\"cellTpl; context: { $implicit: row, col: sc, index: i }\"></ng-container>\n </span>\n }\n </span>\n }\n </div>\n\n <div class=\"mrow__trail\">\n @if (statusColumn(); as st) {\n <ng-container *ngTemplateOutlet=\"cellTpl; context: { $implicit: row, col: st, index: i }\"></ng-container>\n }\n @if (hasActionsColumn()) {\n <span class=\"mrow__actions\">\n <ef-row-actions [items]=\"defaultRowActions(row)\" [context]=\"rowActionsContext()\" />\n </span>\n }\n @if (hasDetail() && effectiveMobileLayout() === 'row') {\n @if (rowNavigable()) {\n <!-- The head opens the record, so expansion needs a control\n of its own. Same glyph, promoted from decoration to a\n button that carries the state it was already drawing. -->\n <button\n type=\"button\"\n class=\"mrow__disclosure\"\n [attr.aria-expanded]=\"isExpanded(row, i)\"\n [attr.aria-label]=\"(isExpanded(row, i) ? 'common_view_less' : 'common_view_more') | translate\"\n (click)=\"onDisclosureToggle(row, i, $event)\"\n >\n <i\n class=\"pi pi-chevron-down mrow__chevron\"\n [class.is-open]=\"isExpanded(row, i)\"\n aria-hidden=\"true\"\n ></i>\n </button>\n } @else {\n <!-- Says the row opens, and which way it is now. Decorative:\n the whole head is the control, and it carries the state. -->\n <i\n class=\"pi pi-chevron-down mrow__chevron\"\n [class.is-open]=\"isExpanded(row, i)\"\n aria-hidden=\"true\"\n ></i>\n }\n }\n </div>\n </div>\n\n @if (visibleDetail(row, i).length) {\n <dl class=\"mrow__detail\">\n @for (col of visibleDetail(row, i); track col.id) {\n <div class=\"mrow__field\">\n <dt>{{ $any(col).headerKey ? ($any(col).headerKey | translate) : $any(col).header }}</dt>\n <dd [class]=\"cellAlignClass($any(col))\">\n <ng-container *ngTemplateOutlet=\"cellTpl; context: { $implicit: row, col: col, index: i }\"></ng-container>\n </dd>\n </div>\n }\n </dl>\n }\n\n @if (hasMoreThanPreview() && effectiveMobileLayout() === 'card') {\n <button class=\"mrow__more\" type=\"button\" (click)=\"toggleExpanded(row, i)\">\n <span>{{ (isExpanded(row, i) ? 'common_view_less' : 'common_view_more') | translate }}</span>\n <i class=\"pi pi-chevron-down mrow__chevron\" [class.is-open]=\"isExpanded(row, i)\" aria-hidden=\"true\"></i>\n </button>\n }\n </li>\n } @empty {\n <!-- No rows has three meanings and they must not share a screen:\n still loading is a skeleton, the request failed is the error\n state, finished-and-empty is the empty state. Saying \"nothing\n here yet\" to someone whose data is still in flight \u2014 or whose\n request just failed \u2014 is a lie the whole time it shows. -->\n @if (showSkeleton()) {\n @for (line of skeletonLines(); track line) {\n <li class=\"mrow mrow--skeleton\">\n <ef-skeleton variant=\"text\" [rows]=\"2\" />\n </li>\n }\n } @else if (showError()) {\n <li class=\"mrow mrow--empty\">\n <ng-container *ngTemplateOutlet=\"errorStateTpl\"></ng-container>\n </li>\n } @else {\n <li class=\"mrow mrow--empty\">\n <ng-content select=\"[empty-state-mobile]\">\n <ng-container *ngTemplateOutlet=\"defaultEmptyTpl\"></ng-container>\n </ng-content>\n </li>\n }\n }\n </ul>\n } @else {\n <table class=\"tbl\" [class.tbl--fixed]=\"fixedLayout()\">\n <thead>\n <tr>\n @if (reorderable()) {\n <th class=\"tbl-drag-h\" aria-hidden=\"true\"></th>\n }\n @for (col of effectiveColumns(); track trackByColumn($index, col)) {\n <!-- A sortable header is a real <button> inside the <th>, not a\n click handler on the cell. The cell carries aria-sort so the\n current order is announced; the arrow stays decorative.\n Sorting was previously mouse-only and silent to assistive\n technology, on every list screen in the product. -->\n <th\n [class]=\"headerAlignClass(col)\"\n [class.is-sortable]=\"col.sortable\"\n [style.width]=\"col.width || null\"\n [attr.data-col]=\"col.id\"\n [attr.aria-sort]=\"ariaSort(col)\"\n >\n @if (col.sortable) {\n <button type=\"button\" class=\"tbl-sort\" (click)=\"onHeaderClick(col)\">\n <ng-container *ngTemplateOutlet=\"headerLabelTpl; context: { col: col }\"></ng-container>\n @if (sortIndicator(col)) {\n <span aria-hidden=\"true\" class=\"tbl-sort__arrow\">{{ sortIndicator(col) }}</span>\n }\n </button>\n } @else {\n <ng-container *ngTemplateOutlet=\"headerLabelTpl; context: { col: col }\"></ng-container>\n }\n </th>\n }\n </tr>\n </thead>\n\n <tbody [attr.aria-busy]=\"loading() ? 'true' : null\">\n @for (row of rows(); track trackByRow($index, row); let i = $index) {\n <tr\n [class.is-row-clickable]=\"rowDoubleClickable()\"\n [class.is-dragging]=\"dragFrom() === i\"\n [class.is-drop-target]=\"dragFrom() !== null && dragFrom() !== i && dragTo() === i\"\n [attr.tabindex]=\"rowDoubleClickable() ? 0 : null\"\n [attr.role]=\"rowDoubleClickable() ? 'button' : null\"\n (click)=\"onRowClick(row, $event)\"\n (keydown.enter)=\"onRowKeydown(row, $event)\"\n >\n @if (reorderable()) {\n <td class=\"tbl-drag\">\n <!-- The row is draggable only by this handle, so a drag never\n starts from a cell the operator meant to click or select.\n Arrow keys move the row for anyone not using a pointer. -->\n <button\n type=\"button\"\n class=\"tbl-drag__grip\"\n [attr.aria-label]=\"'common_reorder_row' | translate\"\n (pointerdown)=\"onDragPointerDown(i, $event)\"\n (pointermove)=\"onDragPointerMove($event)\"\n (pointerup)=\"onDragPointerUp()\"\n (pointercancel)=\"onDragPointerUp()\"\n (keydown)=\"onDragKeydown(i, $event)\"\n >\n <i class=\"pi pi-ellipsis-v\" aria-hidden=\"true\"></i>\n </button>\n </td>\n }\n @for (col of effectiveColumns(); track trackByColumn($index, col); let colIdx = $index) {\n <td efOverflowTooltip efTooltip efTooltipPosition=\"top\" [class]=\"cellAlignClass(col)\" [attr.data-col]=\"col.id\">\n @if (col.id === 'actions' && hasActionsColumn()) {\n <!-- Auto row-actions cell: View / Edit / Duplicate / Delete,\n gated by [show*Action] flags + ScreenContext perms. -->\n <ef-row-actions\n [items]=\"defaultRowActions(row)\"\n [context]=\"rowActionsContext()\"\n />\n } @else {\n <ng-container\n *ngTemplateOutlet=\"cellTpl; context: { $implicit: row, col: col, index: i }\"\n ></ng-container>\n }\n </td>\n }\n </tr>\n } @empty {\n <!-- Placeholder rows keep the real columns, so the table that\n arrives lands where the shimmer already was instead of\n reflowing the page under the reader's eyes. -->\n @if (showSkeleton()) {\n @for (line of skeletonLines(); track line) {\n <tr>\n @if (reorderable()) {\n <td class=\"tbl-drag\"></td>\n }\n @for (col of effectiveColumns(); track trackByColumn($index, col)) {\n <td [class]=\"cellAlignClass(col)\" [attr.data-col]=\"col.id\">\n <ef-skeleton variant=\"text\" [rows]=\"1\" />\n </td>\n }\n </tr>\n }\n } @else if (showError()) {\n <!-- The failure replaces the body, so it spans the full row \u2014\n crammed into column one it reads as a value, not a state. -->\n <tr>\n <td [attr.colspan]=\"effectiveColumns().length + (reorderable() ? 1 : 0)\">\n <ng-container *ngTemplateOutlet=\"errorStateTpl\"></ng-container>\n </td>\n </tr>\n } @else {\n <tr>\n <td [attr.colspan]=\"effectiveColumns().length + (reorderable() ? 1 : 0)\">\n <ng-content select=\"[empty-state]\">\n <ng-container *ngTemplateOutlet=\"defaultEmptyTpl\"></ng-container>\n </ng-content>\n </td>\n </tr>\n }\n }\n </tbody>\n </table>\n }\n\n <!-- \u2500\u2500 PAGER \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Paging controls for an empty result set are chrome with nothing to\n page, so they stay out of the way until there is something to move\n through. -->\n @if (!hidePager() && rows().length > 0) {\n <ef-pager\n [pageNumber]=\"pageNumber()\"\n [pageSize]=\"pageSize()\"\n [totalCount]=\"totalCount()\"\n [pageSizeOptions]=\"pageSizeOptions()\"\n (pageChange)=\"pageChange.emit($event)\"\n (pageSizeChange)=\"pageSizeChange.emit($event)\"\n />\n }\n</div>\n\n<!-- \u2500\u2500 DEFAULT EMPTY STATE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n Rendered when a screen projects nothing into [empty-state] /\n [empty-state-mobile]. It words itself from `searchTerm`, because\n \"nothing matched what you typed\" and \"there is nothing here yet\"\n are different situations needing different ways out. -->\n<ng-template #defaultEmptyTpl>\n <div class=\"tbl-empty\">\n <i\n class=\"pi tbl-empty__icon\"\n [class.pi-search]=\"!!searchTerm()\"\n [class.pi-inbox]=\"!searchTerm()\"\n aria-hidden=\"true\"\n ></i>\n\n @if (searchTerm()) {\n <p class=\"tbl-empty__title\">\n {{ 'common_empty_no_results' | translate: { term: searchTerm() } }}\n </p>\n <p class=\"tbl-empty__hint\">{{ 'common_empty_no_results_hint' | translate }}</p>\n <button class=\"btn btn-sm tbl-empty__action\" type=\"button\" (click)=\"clearSearch.emit()\">\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\n {{ 'common_empty_clear_filters' | translate }}\n </button>\n } @else {\n <p class=\"tbl-empty__title\">{{ 'common_empty_none_yet' | translate }}</p>\n <p class=\"tbl-empty__hint\">{{ 'common_empty_none_yet_hint' | translate }}</p>\n }\n </div>\n</ng-template>\n\n<!-- \u2500\u2500 ERROR STATE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n A request that failed and came back with nothing is not an empty\n collection, and must never borrow its words: \"nothing here yet\" is\n a confident false statement about the operator's data, and they may\n act on it. So this replaces the empty state outright, projected\n [empty-state] included \u2014 a screen's own \"create your first client\"\n copy is just as wrong here as the default.\n\n It says nothing about *what* failed. `errorMsg` is `err.message`\n from the HTTP layer, and a status line or an exception string tells\n an operator nothing they can act on; it survives on the head chip's\n `data-error` attribute, for whoever is reading the DOM. This carries\n the two things that help: their data is intact, and the way to try\n again.\n\n role=\"alert\" goes on this div, never on the <tr> / <td> / <li> that\n hosts it \u2014 a role on a row or cell takes the table's semantics down\n with it. It fires on insertion, which is exactly when @if adds it. -->\n<ng-template #errorStateTpl>\n <div class=\"tbl-empty tbl-empty--error\" role=\"alert\">\n <i class=\"pi pi-exclamation-triangle tbl-empty__icon\" aria-hidden=\"true\"></i>\n <p class=\"tbl-empty__title\">{{ 'common_error_load_failed' | translate }}</p>\n <p class=\"tbl-empty__hint\">{{ 'common_error_load_failed_hint' | translate }}</p>\n <button class=\"btn btn-sm tbl-empty__action\" type=\"button\" (click)=\"retry.emit()\">\n <i class=\"pi pi-refresh\" aria-hidden=\"true\"></i>\n {{ 'common_retry' | translate }}\n </button>\n </div>\n</ng-template>\n\n<!-- Header label, shared by the sortable and plain header cells so the\n projection rules stay in one place. -->\n<ng-template #headerLabelTpl let-col=\"col\">\n @if (headerTemplate(col.id); as headerTpl) {\n <ng-container *ngTemplateOutlet=\"headerTpl; context: { $implicit: col, col: col }\"></ng-container>\n } @else if (col.headerKey) {\n <span>{{ col.headerKey | translate }}</span>\n } @else if (col.header) {\n <span>{{ col.header }}</span>\n }\n</ng-template>\n", styles: ["@charset \"UTF-8\";.tbl thead th.is-sortable{cursor:pointer;-webkit-user-select:none;user-select:none;transition:color var(--t-fast, .14s) var(--ease-out, ease-out)}.tbl thead th.is-sortable:hover{color:var(--text)}.tbl tbody tr.is-row-clickable{cursor:pointer}.tbl-tools{display:flex;gap:6px;align-items:center}.tbl-tool{position:relative}.tbl-tool>.btn.open{color:var(--text);background:var(--paper)}.tbl-tool__menu{position:absolute;top:calc(100% + 8px);inset-inline-end:0;z-index:40;min-width:212px;max-height:340px;overflow-y:auto;padding:6px;display:grid;gap:1px;background:var(--paper-alt);border:1px solid var(--rule);border-radius:var(--r-xl);box-shadow:var(--shadow-3);animation:row-actions-menu-in var(--t-base) var(--ease-spring)}.tbl-tool__menu--wide{min-width:248px}.tbl-tool__menu button{appearance:none;background:transparent;border:0;width:100%;text-align:start;font:inherit;font-size:13.5px;font-weight:600;color:var(--text);display:flex;align-items:center;gap:12px;padding:9px 12px;border-radius:var(--r-md);cursor:pointer;transition:background-color var(--t-fast) var(--ease-out)}.tbl-tool__menu button:hover,.tbl-tool__menu button:focus-visible{background:var(--paper);outline:0}.tbl-tool__menu button>.pi{width:16px;flex:none;font-size:14px;color:var(--muted)}.tbl-tool__menu button.is-active>.pi{color:var(--module, var(--text))}.tbl-tool__label{flex:1 1 auto;min-width:0}.tbl-tool__check{color:var(--module, var(--text))}.tbl-tool__sep{margin:5px 2px;border-top:1px solid var(--rule)}.tbl-wrap[data-density=compact] .tbl tbody td{padding-top:4px;padding-bottom:4px}.tbl-wrap[data-density=comfortable] .tbl tbody td{padding-top:16px;padding-bottom:16px}.mlist{list-style:none;margin:0;padding:0;display:grid}.mrow{border-top:1px solid var(--rule)}.mrow:first-child{border-top:0}.mrow__head{display:flex;align-items:flex-start;gap:var(--s-3);padding:12px var(--s-4);min-height:var(--hit-touch)}.mrow__head.is-tappable{cursor:pointer}.mrow__head.is-tappable:active{background:var(--paper)}.mrow__lead{flex:1;min-width:0;display:grid;gap:2px}.mrow__primary{font-weight:700;font-size:14px;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mrow__secondary{display:flex;flex-wrap:wrap;align-items:baseline;gap:0 6px;font-size:12.5px;color:var(--text-mute);min-width:0}.mrow__secondary .mrow__sec-part+.mrow__sec-part:before{content:\"\\b7\";margin-inline-end:6px;color:var(--text-soft)}.mrow__trail{display:flex;align-items:center;justify-content:flex-end;gap:var(--s-2);flex-shrink:0;min-height:20px}.mrow__actions{display:inline-flex}.mrow__detail{margin:0;padding:0 var(--s-4) 12px;display:grid;gap:1px}.mrow__field{display:flex;align-items:baseline;justify-content:space-between;gap:var(--s-3);padding:7px 0;border-top:1px solid var(--rule-soft)}.mrow__field dt{font-size:11.5px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--text-mute);flex-shrink:0}.mrow__field dd{margin:0;font-size:13.5px;color:var(--text);text-align:end;min-width:0;overflow-wrap:anywhere}.mrow__more{appearance:none;width:100%;background:transparent;border:0;border-top:1px solid var(--rule-soft);padding:10px;font:inherit;font-size:13px;font-weight:600;color:var(--tenant-500);cursor:pointer}.mlist[data-layout=card]{gap:var(--s-3);padding:var(--s-3) var(--s-3) 0}.mlist[data-layout=card] .mrow{border:1px solid var(--rule);border-radius:var(--r-lg);background:var(--paper-alt);overflow:hidden}.mlist[data-layout=card] .mrow__head{border-bottom:1px solid var(--rule)}.mrow__chevron{font-size:11px;color:var(--text-soft);transition:transform var(--t-fast) var(--ease-out);flex-shrink:0}.mrow__chevron.is-open{transform:rotate(180deg);color:var(--text-mute)}.mrow__head.is-tappable:focus-visible{outline:2px solid var(--tenant-400);outline-offset:-2px}.mrow__more{display:flex;align-items:center;justify-content:center;gap:6px}.tbl-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--s-2);padding:var(--s-9) var(--s-4);text-align:center}.tbl-empty__icon{font-size:28px;line-height:1;color:var(--ink-400);margin-block-end:var(--s-1)}.tbl-empty__title{margin:0;font-size:15px;font-weight:600;color:var(--text);max-inline-size:46ch;overflow-wrap:break-word}.tbl-empty__hint{margin:0;font-size:13px;color:var(--ink-600);max-inline-size:52ch;text-wrap:pretty}.tbl-empty__action{margin-block-start:var(--s-3);min-block-size:var(--hit-base)}.tbl-empty--error .tbl-empty__icon{color:var(--st-cancelled-fg)}.mrow--empty{padding:0}.mrow--skeleton{padding:12px var(--s-4)}@media(max-width:767px){.tbl-empty{padding:var(--s-7) var(--s-4)}.tbl-empty__action{min-block-size:var(--hit-touch)}}.pi.is-spinning{animation:ef-export-spin .9s linear infinite}@keyframes ef-export-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.pi.is-spinning{animation:none}}.tbl th.tbl-drag-h,.tbl td.tbl-drag{width:28px;padding-inline:4px 0;text-align:center}.tbl td.tbl-drag{cursor:default}.tbl .tbl-drag__grip{display:inline-flex;align-items:center;justify-content:center;width:22px;height:26px;padding:0;border:0;border-radius:6px;background:transparent;color:var(--text-soft);cursor:grab;touch-action:none;transition:background var(--t-fast, .14s) var(--ease-out, ease-out),color var(--t-fast, .14s) var(--ease-out, ease-out)}.tbl .tbl-drag__grip:hover{background:var(--paper);color:var(--text)}.tbl .tbl-drag__grip:focus-visible{outline:2px solid var(--tenant, var(--rule));outline-offset:1px}.tbl .tbl-drag__grip:active{cursor:grabbing}.tbl .tbl-drag__grip i{font-size:12px}.tbl tbody tr.is-dragging>td{background:var(--paper);opacity:.55}.tbl tbody tr.is-drop-target>td{box-shadow:inset 0 2px 0 var(--tenant, var(--rule))}.tbl.tbl--fixed{table-layout:fixed}.mrow__disclosure{appearance:none;background:transparent;border:0;padding:0;margin:0;display:inline-grid;place-items:center;color:var(--text-mute);cursor:pointer;position:relative}.mrow__disclosure:after{content:\"\";position:absolute;top:50%;left:50%;width:var(--hit-touch);height:var(--hit-touch);transform:translate(-50%,-50%)}.mrow__disclosure:focus-visible{outline:2px solid color-mix(in srgb,var(--ink-active) 40%,transparent);outline-offset:2px;border-radius:var(--r-pill)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$3.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$3.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "component", type: EfPagerComponent, selector: "ef-pager", inputs: ["pageNumber", "pageSize", "totalCount", "pageSizeOptions", "linesPerPageLabelKey", "ofLabelKey", "prevLabelKey", "nextLabelKey", "hideSize"], outputs: ["pageChange", "pageSizeChange"] }, { kind: "component", type: EfStatusChipComponent, selector: "ef-status-chip", inputs: ["referenceKey", "code", "color", "label", "labelKey", "hideDot", "showCode"] }, { kind: "component", type: EfSkeletonComponent, selector: "ef-skeleton", inputs: ["variant", "rows", "height"] }, { kind: "component", type: EfRowActionsComponent, selector: "ef-row-actions", inputs: ["items", "context", "triggerIcon", "triggerAriaKey", "disabled", "menuLeft"], outputs: ["opened", "closed"] }, { kind: "directive", type: EfOverflowTooltipDirective, selector: "[efOverflowTooltip]" }, { kind: "directive", type: EfTooltipDirective, selector: "[efTooltip]" }, { kind: "pipe", type: i1$3.DecimalPipe, name: "number" }, { kind: "pipe", type: i1$3.CurrencyPipe, name: "currency" }, { kind: "pipe", type: i1$3.DatePipe, name: "date" }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
5373
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: EfDataCardComponent, isStandalone: true, selector: "ef-data-card", inputs: { mobileLayout: { classPropertyName: "mobileLayout", publicName: "mobileLayout", isSignal: true, isRequired: false, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: false, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null }, trackByField: { classPropertyName: "trackByField", publicName: "trackByField", isSignal: true, isRequired: false, transformFunction: null }, pageNumber: { classPropertyName: "pageNumber", publicName: "pageNumber", isSignal: true, isRequired: false, transformFunction: null }, pageSize: { classPropertyName: "pageSize", publicName: "pageSize", isSignal: true, isRequired: false, transformFunction: null }, totalCount: { classPropertyName: "totalCount", publicName: "totalCount", isSignal: true, isRequired: false, transformFunction: null }, pageSizeOptions: { classPropertyName: "pageSizeOptions", publicName: "pageSizeOptions", isSignal: true, isRequired: false, transformFunction: null }, hidePager: { classPropertyName: "hidePager", publicName: "hidePager", isSignal: true, isRequired: false, transformFunction: null }, sort: { classPropertyName: "sort", publicName: "sort", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, errorMsg: { classPropertyName: "errorMsg", publicName: "errorMsg", isSignal: true, isRequired: false, transformFunction: null }, loadingKey: { classPropertyName: "loadingKey", publicName: "loadingKey", isSignal: true, isRequired: false, transformFunction: null }, rowDoubleClickable: { classPropertyName: "rowDoubleClickable", publicName: "rowDoubleClickable", isSignal: true, isRequired: false, transformFunction: null }, hasActionsColumn: { classPropertyName: "hasActionsColumn", publicName: "hasActionsColumn", isSignal: true, isRequired: false, transformFunction: null }, rowActionsContext: { classPropertyName: "rowActionsContext", publicName: "rowActionsContext", isSignal: true, isRequired: false, transformFunction: null }, showViewAction: { classPropertyName: "showViewAction", publicName: "showViewAction", isSignal: true, isRequired: false, transformFunction: null }, showEditAction: { classPropertyName: "showEditAction", publicName: "showEditAction", isSignal: true, isRequired: false, transformFunction: null }, showDuplicateAction: { classPropertyName: "showDuplicateAction", publicName: "showDuplicateAction", isSignal: true, isRequired: false, transformFunction: null }, showDeleteAction: { classPropertyName: "showDeleteAction", publicName: "showDeleteAction", isSignal: true, isRequired: false, transformFunction: null }, showDensityControl: { classPropertyName: "showDensityControl", publicName: "showDensityControl", isSignal: true, isRequired: false, transformFunction: null }, showColumnPicker: { classPropertyName: "showColumnPicker", publicName: "showColumnPicker", isSignal: true, isRequired: false, transformFunction: null }, showExportControl: { classPropertyName: "showExportControl", publicName: "showExportControl", isSignal: true, isRequired: false, transformFunction: null }, exporting: { classPropertyName: "exporting", publicName: "exporting", isSignal: true, isRequired: false, transformFunction: null }, screenContext: { classPropertyName: "screenContext", publicName: "screenContext", isSignal: true, isRequired: false, transformFunction: null }, searchTerm: { classPropertyName: "searchTerm", publicName: "searchTerm", isSignal: true, isRequired: false, transformFunction: null }, tableKey: { classPropertyName: "tableKey", publicName: "tableKey", isSignal: true, isRequired: false, transformFunction: null }, defaultDensity: { classPropertyName: "defaultDensity", publicName: "defaultDensity", isSignal: true, isRequired: false, transformFunction: null }, reorderable: { classPropertyName: "reorderable", publicName: "reorderable", isSignal: true, isRequired: false, transformFunction: null }, fixedLayout: { classPropertyName: "fixedLayout", publicName: "fixedLayout", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pageChange: "pageChange", pageSizeChange: "pageSizeChange", sortChange: "sortChange", rowDoubleClick: "rowDoubleClick", rowAction: "rowAction", exportRequest: "exportRequest", clearSearch: "clearSearch", retry: "retry", rowReorder: "rowReorder" }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown.escape": "onEscape()", "ef-row-actions-toggle": "onRowActionsToggle($event)" } }, queries: [{ propertyName: "bodyTemplates", predicate: EfColumnTemplateDirective }, { propertyName: "headerTemplates", predicate: EfColumnHeaderTemplateDirective }], ngImport: i0, template: "<!-- One cell renderer, shared by the table, the mobile row and the mobile\n detail. A new column type is added here once, not three times. -->\n<ng-template #cellTpl let-row let-col=\"col\" let-index=\"index\">\n @if (bodyTemplate(col.id); as bodyTpl) {\n <ng-container *ngTemplateOutlet=\"bodyTpl; context: { $implicit: row, col: col, index: index }\"></ng-container>\n } @else {\n @switch (col.type) {\n @case ('number') {\n {{ $any(cellValue(row, col)) | number:numberFormat(col):col.locale }}\n }\n @case ('money') {\n {{ $any(cellValue(row, col)) | currency:(col.currencyCode || 'EUR'):(col.currencyDisplay || 'symbol'):numberFormat(col):col.locale }}\n }\n @case ('date') {\n {{ $any(cellValue(row, col)) | date:(col.dateFormat || 'dd/MM/yyyy'):undefined:col.locale }}\n }\n @case ('datetime') {\n {{ $any(cellValue(row, col)) | date:(col.dateFormat || 'dd/MM/yyyy HH:mm'):undefined:col.locale }}\n }\n @case ('boolean') {\n @if (cellValue(row, col)) {\n <span class=\"bool yes\"><span class=\"dot\"></span> \u2713</span>\n } @else {\n <span class=\"bool no\"><span class=\"dot\"></span> \u2014</span>\n }\n }\n @case ('mono') {\n <span class=\"ref\">{{ cellValue(row, col) }}</span>\n }\n @case ('chip') {\n <span [class]=\"'chip ' + (col.chipPrefix || 'chip-') + cellValue(row, col)\">\n \u25CF {{ cellValue(row, col) }}\n </span>\n }\n @case ('status') {\n <ef-status-chip\n [referenceKey]=\"col.referenceKey || ''\"\n [code]=\"$any(cellValue(row, col))\"\n />\n }\n @case ('reference') {\n {{ resolveReference(cellValue(row, col), col) }}\n }\n @default {\n {{ cellValue(row, col) }}\n }\n }\n }\n</ng-template>\n\n<div class=\"tbl-wrap\" [class.has-open-menu]=\"openMenuCount() > 0 || openTool() !== null\" [attr.data-density]=\"density()\">\n <!-- \u2500\u2500 HEAD ROW \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"tbl-head\">\n <div class=\"count small\">\n @if (loading()) {\n <span>{{ loadingKey() | translate }}</span>\n } @else if (errorMsg()) {\n <!-- A refresh that failed on top of rows already on screen keeps the\n rows, so this chip is the only sign anything went wrong \u2014 and the\n rows it sits above may now be stale. It has to say so, and say it\n in the product's words: `errorMsg` is `err.message` from the HTTP\n layer, and \"500 Internal Server Error\" tells an operator nothing\n they can act on.\n\n The raw string stays on `data-error`, where devtools can read it\n and no reader ever meets it. `title` was the obvious place and is\n the wrong one: it renders the exception in a tooltip, which is\n precisely what this change exists to stop, and it is unreachable\n on touch besides.\n\n role=\"status\", not \"alert\": polite is right for a signal beside\n rows that are still usable, and when the body is showing the\n error state it already carries the alert \u2014 one failure must not\n interrupt a screen-reader twice. -->\n <span\n role=\"status\"\n [attr.data-error]=\"errorMsg()\"\n style=\"color: var(--st-cancelled-fg); font-weight: 600;\"\n >\u26A0 {{ 'common_error_refresh_failed' | translate }}</span\n >\n } @else {\n <ng-content select=\"[tbl-head-info]\"></ng-content>\n }\n </div>\n <div class=\"tbl-tools\">\n <ng-content select=\"[tbl-head-actions]\"></ng-content>\n\n @if (canExport()) {\n <div class=\"tbl-tool\">\n <button\n class=\"btn btn-ghost btn-sm\"\n type=\"button\"\n [disabled]=\"exporting()\"\n [attr.aria-busy]=\"exporting()\"\n (click)=\"requestExport()\"\n >\n <i\n class=\"pi\"\n [class.pi-download]=\"!exporting()\"\n [class.pi-spinner]=\"exporting()\"\n [class.is-spinning]=\"exporting()\"\n aria-hidden=\"true\"\n ></i>\n {{ (exporting() ? 'common_export_running' : 'common_export') | translate }}\n </button>\n </div>\n }\n\n @if (showDensityControl()) {\n <div class=\"tbl-tool\">\n <button\n class=\"btn btn-ghost btn-sm\"\n type=\"button\"\n [class.open]=\"openTool() === 'density'\"\n [attr.aria-expanded]=\"openTool() === 'density'\"\n aria-haspopup=\"menu\"\n (click)=\"toggleTool('density')\"\n >\n <i class=\"pi pi-bars\" aria-hidden=\"true\"></i>\n {{ 'common_density' | translate }}\n </button>\n @if (openTool() === 'density') {\n <div class=\"tbl-tool__menu\" role=\"menu\">\n @for (opt of densityOptions; track opt.value) {\n <button\n type=\"button\"\n role=\"menuitemradio\"\n [attr.aria-checked]=\"density() === opt.value\"\n [class.is-active]=\"density() === opt.value\"\n (click)=\"setDensity(opt.value)\"\n >\n <i class=\"pi\" [ngClass]=\"opt.icon\" aria-hidden=\"true\"></i>\n <span class=\"tbl-tool__label\">{{ opt.labelKey | translate }}</span>\n @if (density() === opt.value) {\n <i class=\"pi pi-check tbl-tool__check\" aria-hidden=\"true\"></i>\n }\n </button>\n }\n </div>\n }\n </div>\n }\n\n @if (showColumnPicker()) {\n <div class=\"tbl-tool\">\n <button\n class=\"btn btn-ghost btn-sm\"\n type=\"button\"\n [class.open]=\"openTool() === 'columns'\"\n [attr.aria-expanded]=\"openTool() === 'columns'\"\n aria-haspopup=\"menu\"\n (click)=\"toggleTool('columns')\"\n >\n <i class=\"pi pi-th-large\" aria-hidden=\"true\"></i>\n {{ 'common_columns' | translate }}\n </button>\n @if (openTool() === 'columns') {\n <div class=\"tbl-tool__menu tbl-tool__menu--wide\" role=\"menu\">\n @for (col of hideableColumns(); track col.id) {\n <button\n type=\"button\"\n role=\"menuitemcheckbox\"\n [attr.aria-checked]=\"!isColumnHidden(col.id)\"\n [class.is-active]=\"!isColumnHidden(col.id)\"\n (click)=\"toggleColumn(col.id)\"\n >\n <i class=\"pi\" [class.pi-check]=\"!isColumnHidden(col.id)\" aria-hidden=\"true\"></i>\n <span class=\"tbl-tool__label\">{{\n col.headerKey ? (col.headerKey | translate) : (col.header || col.id)\n }}</span>\n </button>\n }\n <div class=\"tbl-tool__sep\"></div>\n <button type=\"button\" role=\"menuitem\" (click)=\"resetColumns()\">\n <i class=\"pi pi-refresh\" aria-hidden=\"true\"></i>\n <span class=\"tbl-tool__label\">{{ 'common_columns_reset' | translate }}</span>\n </button>\n </div>\n }\n </div>\n }\n </div>\n </div>\n\n <!-- \u2500\u2500 TABLE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (isMobile()) {\n <!-- \u2500\u2500 MOBILE LIST \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n A six-column table on a 390px screen either overflows or\n squeezes every column to nothing, so the rows become a list.\n Same columns, same cell renderers; each column just declares\n (or is given) a role. Both shapes expand to the same detail \u2014\n they differ only in how much they show collapsed. -->\n <ul\n class=\"mlist\"\n [attr.data-layout]=\"effectiveMobileLayout()\"\n [attr.aria-busy]=\"loading() ? 'true' : null\"\n >\n @for (row of rows(); track trackByRow($index, row); let i = $index) {\n <li class=\"mrow\" [class.is-open]=\"isExpanded(row, i)\">\n <div\n class=\"mrow__head\"\n [class.is-tappable]=\"headIsControl()\"\n [attr.role]=\"headIsControl() ? 'button' : null\"\n [attr.tabindex]=\"headIsControl() ? 0 : null\"\n [attr.aria-expanded]=\"!rowNavigable() && hasDetail() ? isExpanded(row, i) : null\"\n (click)=\"onRowHeadActivate(row, i, $event)\"\n (keydown.enter)=\"onRowHeadActivate(row, i, $event)\"\n (keydown.space)=\"onRowHeadActivate(row, i, $event)\"\n >\n <div class=\"mrow__lead\">\n @if (primaryColumn(); as pc) {\n <span class=\"mrow__primary\" efOverflowTooltip efTooltip efTooltipPosition=\"top\">\n <ng-container *ngTemplateOutlet=\"cellTpl; context: { $implicit: row, col: pc, index: i }\"></ng-container>\n </span>\n }\n @if (effectiveMobileLayout() === 'row' && secondaryColumns().length) {\n <span class=\"mrow__secondary\">\n @for (sc of secondaryColumns(); track sc.id) {\n <span class=\"mrow__sec-part\">\n <ng-container *ngTemplateOutlet=\"cellTpl; context: { $implicit: row, col: sc, index: i }\"></ng-container>\n </span>\n }\n </span>\n }\n </div>\n\n <div class=\"mrow__trail\">\n @if (statusColumn(); as st) {\n <ng-container *ngTemplateOutlet=\"cellTpl; context: { $implicit: row, col: st, index: i }\"></ng-container>\n }\n @if (hasActionsColumn()) {\n <span class=\"mrow__actions\">\n <ef-row-actions [items]=\"defaultRowActions(row)\" [context]=\"rowActionsContext()\" />\n </span>\n }\n @if (hasDetail() && effectiveMobileLayout() === 'row') {\n @if (rowNavigable()) {\n <!-- The head opens the record, so expansion needs a control\n of its own. Same glyph, promoted from decoration to a\n button that carries the state it was already drawing. -->\n <button\n type=\"button\"\n class=\"mrow__disclosure\"\n [attr.aria-expanded]=\"isExpanded(row, i)\"\n [attr.aria-label]=\"(isExpanded(row, i) ? 'common_view_less' : 'common_view_more') | translate\"\n (click)=\"onDisclosureToggle(row, i, $event)\"\n >\n <i\n class=\"pi pi-chevron-down mrow__chevron\"\n [class.is-open]=\"isExpanded(row, i)\"\n aria-hidden=\"true\"\n ></i>\n </button>\n } @else {\n <!-- Says the row opens, and which way it is now. Decorative:\n the whole head is the control, and it carries the state. -->\n <i\n class=\"pi pi-chevron-down mrow__chevron\"\n [class.is-open]=\"isExpanded(row, i)\"\n aria-hidden=\"true\"\n ></i>\n }\n }\n </div>\n </div>\n\n @if (visibleDetail(row, i).length) {\n <dl class=\"mrow__detail\">\n @for (col of visibleDetail(row, i); track col.id) {\n <div class=\"mrow__field\">\n <dt>{{ $any(col).headerKey ? ($any(col).headerKey | translate) : $any(col).header }}</dt>\n <dd [class]=\"cellAlignClass($any(col))\">\n <ng-container *ngTemplateOutlet=\"cellTpl; context: { $implicit: row, col: col, index: i }\"></ng-container>\n </dd>\n </div>\n }\n </dl>\n }\n\n @if (hasMoreThanPreview() && effectiveMobileLayout() === 'card') {\n <button class=\"mrow__more\" type=\"button\" (click)=\"toggleExpanded(row, i)\">\n <span>{{ (isExpanded(row, i) ? 'common_view_less' : 'common_view_more') | translate }}</span>\n <i class=\"pi pi-chevron-down mrow__chevron\" [class.is-open]=\"isExpanded(row, i)\" aria-hidden=\"true\"></i>\n </button>\n }\n </li>\n } @empty {\n <!-- No rows has three meanings and they must not share a screen:\n still loading is a skeleton, the request failed is the error\n state, finished-and-empty is the empty state. Saying \"nothing\n here yet\" to someone whose data is still in flight \u2014 or whose\n request just failed \u2014 is a lie the whole time it shows. -->\n @if (showSkeleton()) {\n @for (line of skeletonLines(); track line) {\n <li class=\"mrow mrow--skeleton\">\n <ef-skeleton variant=\"text\" [rows]=\"2\" />\n </li>\n }\n } @else if (showError()) {\n <li class=\"mrow mrow--empty\">\n <ng-container *ngTemplateOutlet=\"errorStateTpl\"></ng-container>\n </li>\n } @else {\n <li class=\"mrow mrow--empty\">\n <ng-content select=\"[empty-state-mobile]\">\n <ng-container *ngTemplateOutlet=\"defaultEmptyTpl\"></ng-container>\n </ng-content>\n </li>\n }\n }\n </ul>\n } @else {\n <table class=\"tbl\" [class.tbl--fixed]=\"fixedLayout()\">\n <thead>\n <tr>\n @if (reorderable()) {\n <th class=\"tbl-drag-h\" aria-hidden=\"true\"></th>\n }\n @for (col of effectiveColumns(); track trackByColumn($index, col)) {\n <!-- A sortable header is a real <button> inside the <th>, not a\n click handler on the cell. The cell carries aria-sort so the\n current order is announced; the arrow stays decorative.\n Sorting was previously mouse-only and silent to assistive\n technology, on every list screen in the product. -->\n <th\n [class]=\"headerAlignClass(col)\"\n [class.is-sortable]=\"col.sortable\"\n [style.width]=\"col.width || null\"\n [attr.data-col]=\"col.id\"\n [attr.aria-sort]=\"ariaSort(col)\"\n >\n @if (col.sortable) {\n <button type=\"button\" class=\"tbl-sort\" (click)=\"onHeaderClick(col)\">\n <ng-container *ngTemplateOutlet=\"headerLabelTpl; context: { col: col }\"></ng-container>\n @if (sortIndicator(col)) {\n <span aria-hidden=\"true\" class=\"tbl-sort__arrow\">{{ sortIndicator(col) }}</span>\n }\n </button>\n } @else {\n <ng-container *ngTemplateOutlet=\"headerLabelTpl; context: { col: col }\"></ng-container>\n }\n </th>\n }\n </tr>\n </thead>\n\n <tbody [attr.aria-busy]=\"loading() ? 'true' : null\">\n @for (row of rows(); track trackByRow($index, row); let i = $index) {\n <tr\n [class.is-row-clickable]=\"rowDoubleClickable()\"\n [class.is-dragging]=\"dragFrom() === i\"\n [class.is-drop-target]=\"dragFrom() !== null && dragFrom() !== i && dragTo() === i\"\n [attr.tabindex]=\"rowDoubleClickable() ? 0 : null\"\n [attr.role]=\"rowDoubleClickable() ? 'button' : null\"\n (click)=\"onRowClick(row, $event)\"\n (keydown.enter)=\"onRowKeydown(row, $event)\"\n >\n @if (reorderable()) {\n <td class=\"tbl-drag\">\n <!-- The row is draggable only by this handle, so a drag never\n starts from a cell the operator meant to click or select.\n Arrow keys move the row for anyone not using a pointer. -->\n <button\n type=\"button\"\n class=\"tbl-drag__grip\"\n [attr.aria-label]=\"'common_reorder_row' | translate\"\n (pointerdown)=\"onDragPointerDown(i, $event)\"\n (pointermove)=\"onDragPointerMove($event)\"\n (pointerup)=\"onDragPointerUp()\"\n (pointercancel)=\"onDragPointerUp()\"\n (keydown)=\"onDragKeydown(i, $event)\"\n >\n <i class=\"pi pi-ellipsis-v\" aria-hidden=\"true\"></i>\n </button>\n </td>\n }\n @for (col of effectiveColumns(); track trackByColumn($index, col); let colIdx = $index) {\n <td efOverflowTooltip efTooltip efTooltipPosition=\"top\" [class]=\"cellAlignClass(col)\" [attr.data-col]=\"col.id\">\n @if (col.id === 'actions' && hasActionsColumn()) {\n <!-- Auto row-actions cell: View / Edit / Duplicate / Delete,\n gated by [show*Action] flags + ScreenContext perms. -->\n <ef-row-actions\n [items]=\"defaultRowActions(row)\"\n [context]=\"rowActionsContext()\"\n />\n } @else {\n <ng-container\n *ngTemplateOutlet=\"cellTpl; context: { $implicit: row, col: col, index: i }\"\n ></ng-container>\n }\n </td>\n }\n </tr>\n } @empty {\n <!-- Placeholder rows keep the real columns, so the table that\n arrives lands where the shimmer already was instead of\n reflowing the page under the reader's eyes. -->\n @if (showSkeleton()) {\n @for (line of skeletonLines(); track line) {\n <tr>\n @if (reorderable()) {\n <td class=\"tbl-drag\"></td>\n }\n @for (col of effectiveColumns(); track trackByColumn($index, col)) {\n <td [class]=\"cellAlignClass(col)\" [attr.data-col]=\"col.id\">\n <ef-skeleton variant=\"text\" [rows]=\"1\" />\n </td>\n }\n </tr>\n }\n } @else if (showError()) {\n <!-- The failure replaces the body, so it spans the full row \u2014\n crammed into column one it reads as a value, not a state. -->\n <tr>\n <td [attr.colspan]=\"effectiveColumns().length + (reorderable() ? 1 : 0)\">\n <ng-container *ngTemplateOutlet=\"errorStateTpl\"></ng-container>\n </td>\n </tr>\n } @else {\n <tr>\n <td [attr.colspan]=\"effectiveColumns().length + (reorderable() ? 1 : 0)\">\n <ng-content select=\"[empty-state]\">\n <ng-container *ngTemplateOutlet=\"defaultEmptyTpl\"></ng-container>\n </ng-content>\n </td>\n </tr>\n }\n }\n </tbody>\n </table>\n }\n\n <!-- \u2500\u2500 PAGER \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Paging controls for an empty result set are chrome with nothing to\n page, so they stay out of the way until there is something to move\n through. -->\n @if (!hidePager() && rows().length > 0) {\n <ef-pager\n [pageNumber]=\"pageNumber()\"\n [pageSize]=\"pageSize()\"\n [totalCount]=\"totalCount()\"\n [pageSizeOptions]=\"pageSizeOptions()\"\n (pageChange)=\"pageChange.emit($event)\"\n (pageSizeChange)=\"pageSizeChange.emit($event)\"\n />\n }\n</div>\n\n<!-- \u2500\u2500 DEFAULT EMPTY STATE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n Rendered when a screen projects nothing into [empty-state] /\n [empty-state-mobile]. It words itself from `searchTerm`, because\n \"nothing matched what you typed\" and \"there is nothing here yet\"\n are different situations needing different ways out. -->\n<ng-template #defaultEmptyTpl>\n <div class=\"tbl-empty\">\n <i\n class=\"pi tbl-empty__icon\"\n [class.pi-search]=\"!!searchTerm()\"\n [class.pi-inbox]=\"!searchTerm()\"\n aria-hidden=\"true\"\n ></i>\n\n @if (searchTerm()) {\n <p class=\"tbl-empty__title\">\n {{ 'common_empty_no_results' | translate: { term: searchTerm() } }}\n </p>\n <p class=\"tbl-empty__hint\">{{ 'common_empty_no_results_hint' | translate }}</p>\n <button class=\"btn btn-sm tbl-empty__action\" type=\"button\" (click)=\"clearSearch.emit()\">\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\n {{ 'common_empty_clear_filters' | translate }}\n </button>\n } @else {\n <p class=\"tbl-empty__title\">{{ 'common_empty_none_yet' | translate }}</p>\n <p class=\"tbl-empty__hint\">{{ 'common_empty_none_yet_hint' | translate }}</p>\n }\n </div>\n</ng-template>\n\n<!-- \u2500\u2500 ERROR STATE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n A request that failed and came back with nothing is not an empty\n collection, and must never borrow its words: \"nothing here yet\" is\n a confident false statement about the operator's data, and they may\n act on it. So this replaces the empty state outright, projected\n [empty-state] included \u2014 a screen's own \"create your first client\"\n copy is just as wrong here as the default.\n\n It says nothing about *what* failed. `errorMsg` is `err.message`\n from the HTTP layer, and a status line or an exception string tells\n an operator nothing they can act on; it survives on the head chip's\n `data-error` attribute, for whoever is reading the DOM. This carries\n the two things that help: their data is intact, and the way to try\n again.\n\n role=\"alert\" goes on this div, never on the <tr> / <td> / <li> that\n hosts it \u2014 a role on a row or cell takes the table's semantics down\n with it. It fires on insertion, which is exactly when @if adds it. -->\n<ng-template #errorStateTpl>\n <div class=\"tbl-empty tbl-empty--error\" role=\"alert\">\n <i class=\"pi pi-exclamation-triangle tbl-empty__icon\" aria-hidden=\"true\"></i>\n <p class=\"tbl-empty__title\">{{ 'common_error_load_failed' | translate }}</p>\n <p class=\"tbl-empty__hint\">{{ 'common_error_load_failed_hint' | translate }}</p>\n <button class=\"btn btn-sm tbl-empty__action\" type=\"button\" (click)=\"retry.emit()\">\n <i class=\"pi pi-refresh\" aria-hidden=\"true\"></i>\n {{ 'common_retry' | translate }}\n </button>\n </div>\n</ng-template>\n\n<!-- Header label, shared by the sortable and plain header cells so the\n projection rules stay in one place. -->\n<ng-template #headerLabelTpl let-col=\"col\">\n @if (headerTemplate(col.id); as headerTpl) {\n <ng-container *ngTemplateOutlet=\"headerTpl; context: { $implicit: col, col: col }\"></ng-container>\n } @else if (col.headerKey) {\n <span>{{ col.headerKey | translate }}</span>\n } @else if (col.header) {\n <span>{{ col.header }}</span>\n }\n</ng-template>\n", styles: ["@charset \"UTF-8\";.tbl thead th.is-sortable{cursor:pointer;-webkit-user-select:none;user-select:none;transition:color var(--t-fast, .14s) var(--ease-out, ease-out)}.tbl thead th.is-sortable:hover{color:var(--text)}.tbl tbody tr.is-row-clickable{cursor:pointer}.tbl-tools{display:flex;gap:6px;align-items:center}.tbl-tool{position:relative}.tbl-tool>.btn.open{color:var(--text);background:var(--paper)}.tbl-tool__menu{position:absolute;top:calc(100% + 8px);inset-inline-end:0;z-index:40;min-width:212px;max-height:340px;overflow-y:auto;padding:6px;display:grid;gap:1px;background:var(--paper-alt);border:1px solid var(--rule);border-radius:var(--r-xl);box-shadow:var(--shadow-3);animation:row-actions-menu-in var(--t-base) var(--ease-spring)}.tbl-tool__menu--wide{min-width:248px}.tbl-tool__menu button{appearance:none;background:transparent;border:0;width:100%;text-align:start;font:inherit;font-size:13.5px;font-weight:600;color:var(--text);display:flex;align-items:center;gap:12px;padding:9px 12px;border-radius:var(--r-md);cursor:pointer;transition:background-color var(--t-fast) var(--ease-out)}.tbl-tool__menu button:hover,.tbl-tool__menu button:focus-visible{background:var(--paper);outline:0}.tbl-tool__menu button>.pi{width:16px;flex:none;font-size:14px;color:var(--muted)}.tbl-tool__menu button.is-active>.pi{color:var(--module, var(--text))}.tbl-tool__label{flex:1 1 auto;min-width:0}.tbl-tool__check{color:var(--module, var(--text))}.tbl-tool__sep{margin:5px 2px;border-top:1px solid var(--rule)}.tbl-wrap[data-density=compact] .tbl tbody td{--tbl-cell-pad-block: 4px;padding-top:var(--tbl-cell-pad-block);padding-bottom:var(--tbl-cell-pad-block)}.tbl-wrap[data-density=comfortable] .tbl tbody td{--tbl-cell-pad-block: 16px;padding-top:var(--tbl-cell-pad-block);padding-bottom:var(--tbl-cell-pad-block)}.mlist{list-style:none;margin:0;padding:0;display:grid}.mrow{border-top:1px solid var(--rule)}.mrow:first-child{border-top:0}.mrow__head{display:flex;align-items:flex-start;gap:var(--s-3);padding:12px var(--s-4);min-height:var(--hit-touch)}.mrow__head.is-tappable{cursor:pointer}.mrow__head.is-tappable:active{background:var(--paper)}.mrow__lead{flex:1;min-width:0;display:grid;gap:2px}.mrow__primary{font-weight:700;font-size:14px;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mrow__secondary{display:flex;flex-wrap:wrap;align-items:baseline;gap:0 6px;font-size:12.5px;color:var(--text-mute);min-width:0}.mrow__secondary .mrow__sec-part+.mrow__sec-part:before{content:\"\\b7\";margin-inline-end:6px;color:var(--text-soft)}.mrow__trail{display:flex;align-items:center;justify-content:flex-end;gap:var(--s-2);flex-shrink:0;min-height:20px}.mrow__actions{display:inline-flex}.mrow__detail{margin:0;padding:0 var(--s-4) 12px;display:grid;gap:1px}.mrow__field{display:flex;align-items:baseline;justify-content:space-between;gap:var(--s-3);padding:7px 0;border-top:1px solid var(--rule-soft)}.mrow__field dt{font-size:11.5px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--text-mute);flex-shrink:0}.mrow__field dd{margin:0;font-size:13.5px;color:var(--text);text-align:end;min-width:0;overflow-wrap:anywhere}.mrow__more{appearance:none;width:100%;background:transparent;border:0;border-top:1px solid var(--rule-soft);padding:10px;font:inherit;font-size:13px;font-weight:600;color:var(--tenant-500);cursor:pointer}.mlist[data-layout=card]{gap:var(--s-3);padding:var(--s-3) var(--s-3) 0}.mlist[data-layout=card] .mrow{border:1px solid var(--rule);border-radius:var(--r-lg);background:var(--paper-alt);overflow:hidden}.mlist[data-layout=card] .mrow__head{border-bottom:1px solid var(--rule)}.mrow__chevron{font-size:11px;color:var(--text-soft);transition:transform var(--t-fast) var(--ease-out);flex-shrink:0}.mrow__chevron.is-open{transform:rotate(180deg);color:var(--text-mute)}.mrow__head.is-tappable:focus-visible{outline:2px solid var(--tenant-400);outline-offset:-2px}.mrow__more{display:flex;align-items:center;justify-content:center;gap:6px}.tbl-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--s-2);padding:var(--s-9) var(--s-4);text-align:center}.tbl-empty__icon{font-size:28px;line-height:1;color:var(--ink-400);margin-block-end:var(--s-1)}.tbl-empty__title{margin:0;font-size:15px;font-weight:600;color:var(--text);max-inline-size:46ch;overflow-wrap:break-word}.tbl-empty__hint{margin:0;font-size:13px;color:var(--ink-600);max-inline-size:52ch;text-wrap:pretty}.tbl-empty__action{margin-block-start:var(--s-3);min-block-size:var(--hit-base)}.tbl-empty--error .tbl-empty__icon{color:var(--st-cancelled-fg)}.mrow--empty{padding:0}.mrow--skeleton{padding:12px var(--s-4)}@media(max-width:767px){.tbl-empty{padding:var(--s-7) var(--s-4)}.tbl-empty__action{min-block-size:var(--hit-touch)}}.pi.is-spinning{animation:ef-export-spin .9s linear infinite}@keyframes ef-export-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.pi.is-spinning{animation:none}}.tbl th.tbl-drag-h,.tbl td.tbl-drag{width:28px;padding-inline:4px 0;text-align:center}.tbl td.tbl-drag{cursor:default}.tbl .tbl-drag__grip{display:inline-flex;align-items:center;justify-content:center;width:22px;height:26px;padding:0;border:0;border-radius:6px;background:transparent;color:var(--text-soft);cursor:grab;touch-action:none;transition:background var(--t-fast, .14s) var(--ease-out, ease-out),color var(--t-fast, .14s) var(--ease-out, ease-out)}.tbl .tbl-drag__grip:hover{background:var(--paper);color:var(--text)}.tbl .tbl-drag__grip:focus-visible{outline:2px solid var(--tenant, var(--rule));outline-offset:1px}.tbl .tbl-drag__grip:active{cursor:grabbing}.tbl .tbl-drag__grip i{font-size:12px}.tbl tbody tr.is-dragging>td{background:var(--paper);opacity:.55}.tbl tbody tr.is-drop-target>td{box-shadow:inset 0 2px 0 var(--tenant, var(--rule))}.tbl.tbl--fixed{table-layout:fixed}.mrow__disclosure{appearance:none;background:transparent;border:0;padding:0;margin:0;display:inline-grid;place-items:center;color:var(--text-mute);cursor:pointer;position:relative}.mrow__disclosure:after{content:\"\";position:absolute;top:50%;left:50%;width:var(--hit-touch);height:var(--hit-touch);transform:translate(-50%,-50%)}.mrow__disclosure:focus-visible{outline:2px solid color-mix(in srgb,var(--ink-active) 40%,transparent);outline-offset:2px;border-radius:var(--r-pill)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$3.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$3.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "component", type: EfPagerComponent, selector: "ef-pager", inputs: ["pageNumber", "pageSize", "totalCount", "pageSizeOptions", "linesPerPageLabelKey", "ofLabelKey", "prevLabelKey", "nextLabelKey", "hideSize"], outputs: ["pageChange", "pageSizeChange"] }, { kind: "component", type: EfStatusChipComponent, selector: "ef-status-chip", inputs: ["referenceKey", "code", "color", "label", "labelKey", "hideDot", "showCode"] }, { kind: "component", type: EfSkeletonComponent, selector: "ef-skeleton", inputs: ["variant", "rows", "height"] }, { kind: "component", type: EfRowActionsComponent, selector: "ef-row-actions", inputs: ["items", "context", "triggerIcon", "triggerAriaKey", "disabled", "menuLeft"], outputs: ["opened", "closed"] }, { kind: "directive", type: EfOverflowTooltipDirective, selector: "[efOverflowTooltip]" }, { kind: "directive", type: EfTooltipDirective, selector: "[efTooltip]" }, { kind: "pipe", type: i1$3.DecimalPipe, name: "number" }, { kind: "pipe", type: i1$3.CurrencyPipe, name: "currency" }, { kind: "pipe", type: i1$3.DatePipe, name: "date" }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
5343
5374
|
}
|
|
5344
5375
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfDataCardComponent, decorators: [{
|
|
5345
5376
|
type: Component,
|
|
@@ -5352,7 +5383,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
|
|
|
5352
5383
|
EfRowActionsComponent,
|
|
5353
5384
|
EfOverflowTooltipDirective,
|
|
5354
5385
|
EfTooltipDirective,
|
|
5355
|
-
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- One cell renderer, shared by the table, the mobile row and the mobile\n detail. A new column type is added here once, not three times. -->\n<ng-template #cellTpl let-row let-col=\"col\" let-index=\"index\">\n @if (bodyTemplate(col.id); as bodyTpl) {\n <ng-container *ngTemplateOutlet=\"bodyTpl; context: { $implicit: row, col: col, index: index }\"></ng-container>\n } @else {\n @switch (col.type) {\n @case ('number') {\n {{ $any(cellValue(row, col)) | number:numberFormat(col):col.locale }}\n }\n @case ('money') {\n {{ $any(cellValue(row, col)) | currency:(col.currencyCode || 'EUR'):(col.currencyDisplay || 'symbol'):numberFormat(col):col.locale }}\n }\n @case ('date') {\n {{ $any(cellValue(row, col)) | date:(col.dateFormat || 'dd/MM/yyyy'):undefined:col.locale }}\n }\n @case ('datetime') {\n {{ $any(cellValue(row, col)) | date:(col.dateFormat || 'dd/MM/yyyy HH:mm'):undefined:col.locale }}\n }\n @case ('boolean') {\n @if (cellValue(row, col)) {\n <span class=\"bool yes\"><span class=\"dot\"></span> \u2713</span>\n } @else {\n <span class=\"bool no\"><span class=\"dot\"></span> \u2014</span>\n }\n }\n @case ('mono') {\n <span class=\"ref\">{{ cellValue(row, col) }}</span>\n }\n @case ('chip') {\n <span [class]=\"'chip ' + (col.chipPrefix || 'chip-') + cellValue(row, col)\">\n \u25CF {{ cellValue(row, col) }}\n </span>\n }\n @case ('status') {\n <ef-status-chip\n [referenceKey]=\"col.referenceKey || ''\"\n [code]=\"$any(cellValue(row, col))\"\n />\n }\n @case ('reference') {\n {{ resolveReference(cellValue(row, col), col) }}\n }\n @default {\n {{ cellValue(row, col) }}\n }\n }\n }\n</ng-template>\n\n<div class=\"tbl-wrap\" [class.has-open-menu]=\"openMenuCount() > 0 || openTool() !== null\" [attr.data-density]=\"density()\">\n <!-- \u2500\u2500 HEAD ROW \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"tbl-head\">\n <div class=\"count small\">\n @if (loading()) {\n <span>{{ loadingKey() | translate }}</span>\n } @else if (errorMsg()) {\n <!-- A refresh that failed on top of rows already on screen keeps the\n rows, so this chip is the only sign anything went wrong \u2014 and the\n rows it sits above may now be stale. It has to say so, and say it\n in the product's words: `errorMsg` is `err.message` from the HTTP\n layer, and \"500 Internal Server Error\" tells an operator nothing\n they can act on.\n\n The raw string stays on `data-error`, where devtools can read it\n and no reader ever meets it. `title` was the obvious place and is\n the wrong one: it renders the exception in a tooltip, which is\n precisely what this change exists to stop, and it is unreachable\n on touch besides.\n\n role=\"status\", not \"alert\": polite is right for a signal beside\n rows that are still usable, and when the body is showing the\n error state it already carries the alert \u2014 one failure must not\n interrupt a screen-reader twice. -->\n <span\n role=\"status\"\n [attr.data-error]=\"errorMsg()\"\n style=\"color: var(--st-cancelled-fg); font-weight: 600;\"\n >\u26A0 {{ 'common_error_refresh_failed' | translate }}</span\n >\n } @else {\n <ng-content select=\"[tbl-head-info]\"></ng-content>\n }\n </div>\n <div class=\"tbl-tools\">\n <ng-content select=\"[tbl-head-actions]\"></ng-content>\n\n @if (canExport()) {\n <div class=\"tbl-tool\">\n <button\n class=\"btn btn-ghost btn-sm\"\n type=\"button\"\n [disabled]=\"exporting()\"\n [attr.aria-busy]=\"exporting()\"\n (click)=\"requestExport()\"\n >\n <i\n class=\"pi\"\n [class.pi-download]=\"!exporting()\"\n [class.pi-spinner]=\"exporting()\"\n [class.is-spinning]=\"exporting()\"\n aria-hidden=\"true\"\n ></i>\n {{ (exporting() ? 'common_export_running' : 'common_export') | translate }}\n </button>\n </div>\n }\n\n @if (showDensityControl()) {\n <div class=\"tbl-tool\">\n <button\n class=\"btn btn-ghost btn-sm\"\n type=\"button\"\n [class.open]=\"openTool() === 'density'\"\n [attr.aria-expanded]=\"openTool() === 'density'\"\n aria-haspopup=\"menu\"\n (click)=\"toggleTool('density')\"\n >\n <i class=\"pi pi-bars\" aria-hidden=\"true\"></i>\n {{ 'common_density' | translate }}\n </button>\n @if (openTool() === 'density') {\n <div class=\"tbl-tool__menu\" role=\"menu\">\n @for (opt of densityOptions; track opt.value) {\n <button\n type=\"button\"\n role=\"menuitemradio\"\n [attr.aria-checked]=\"density() === opt.value\"\n [class.is-active]=\"density() === opt.value\"\n (click)=\"setDensity(opt.value)\"\n >\n <i class=\"pi\" [ngClass]=\"opt.icon\" aria-hidden=\"true\"></i>\n <span class=\"tbl-tool__label\">{{ opt.labelKey | translate }}</span>\n @if (density() === opt.value) {\n <i class=\"pi pi-check tbl-tool__check\" aria-hidden=\"true\"></i>\n }\n </button>\n }\n </div>\n }\n </div>\n }\n\n @if (showColumnPicker()) {\n <div class=\"tbl-tool\">\n <button\n class=\"btn btn-ghost btn-sm\"\n type=\"button\"\n [class.open]=\"openTool() === 'columns'\"\n [attr.aria-expanded]=\"openTool() === 'columns'\"\n aria-haspopup=\"menu\"\n (click)=\"toggleTool('columns')\"\n >\n <i class=\"pi pi-th-large\" aria-hidden=\"true\"></i>\n {{ 'common_columns' | translate }}\n </button>\n @if (openTool() === 'columns') {\n <div class=\"tbl-tool__menu tbl-tool__menu--wide\" role=\"menu\">\n @for (col of hideableColumns(); track col.id) {\n <button\n type=\"button\"\n role=\"menuitemcheckbox\"\n [attr.aria-checked]=\"!isColumnHidden(col.id)\"\n [class.is-active]=\"!isColumnHidden(col.id)\"\n (click)=\"toggleColumn(col.id)\"\n >\n <i class=\"pi\" [class.pi-check]=\"!isColumnHidden(col.id)\" aria-hidden=\"true\"></i>\n <span class=\"tbl-tool__label\">{{\n col.headerKey ? (col.headerKey | translate) : (col.header || col.id)\n }}</span>\n </button>\n }\n <div class=\"tbl-tool__sep\"></div>\n <button type=\"button\" role=\"menuitem\" (click)=\"resetColumns()\">\n <i class=\"pi pi-refresh\" aria-hidden=\"true\"></i>\n <span class=\"tbl-tool__label\">{{ 'common_columns_reset' | translate }}</span>\n </button>\n </div>\n }\n </div>\n }\n </div>\n </div>\n\n <!-- \u2500\u2500 TABLE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (isMobile()) {\n <!-- \u2500\u2500 MOBILE LIST \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n A six-column table on a 390px screen either overflows or\n squeezes every column to nothing, so the rows become a list.\n Same columns, same cell renderers; each column just declares\n (or is given) a role. Both shapes expand to the same detail \u2014\n they differ only in how much they show collapsed. -->\n <ul\n class=\"mlist\"\n [attr.data-layout]=\"effectiveMobileLayout()\"\n [attr.aria-busy]=\"loading() ? 'true' : null\"\n >\n @for (row of rows(); track trackByRow($index, row); let i = $index) {\n <li class=\"mrow\" [class.is-open]=\"isExpanded(row, i)\">\n <div\n class=\"mrow__head\"\n [class.is-tappable]=\"headIsControl()\"\n [attr.role]=\"headIsControl() ? 'button' : null\"\n [attr.tabindex]=\"headIsControl() ? 0 : null\"\n [attr.aria-expanded]=\"!rowNavigable() && hasDetail() ? isExpanded(row, i) : null\"\n (click)=\"onRowHeadActivate(row, i, $event)\"\n (keydown.enter)=\"onRowHeadActivate(row, i, $event)\"\n (keydown.space)=\"onRowHeadActivate(row, i, $event)\"\n >\n <div class=\"mrow__lead\">\n @if (primaryColumn(); as pc) {\n <span class=\"mrow__primary\" efOverflowTooltip efTooltip efTooltipPosition=\"top\">\n <ng-container *ngTemplateOutlet=\"cellTpl; context: { $implicit: row, col: pc, index: i }\"></ng-container>\n </span>\n }\n @if (effectiveMobileLayout() === 'row' && secondaryColumns().length) {\n <span class=\"mrow__secondary\">\n @for (sc of secondaryColumns(); track sc.id) {\n <span class=\"mrow__sec-part\">\n <ng-container *ngTemplateOutlet=\"cellTpl; context: { $implicit: row, col: sc, index: i }\"></ng-container>\n </span>\n }\n </span>\n }\n </div>\n\n <div class=\"mrow__trail\">\n @if (statusColumn(); as st) {\n <ng-container *ngTemplateOutlet=\"cellTpl; context: { $implicit: row, col: st, index: i }\"></ng-container>\n }\n @if (hasActionsColumn()) {\n <span class=\"mrow__actions\">\n <ef-row-actions [items]=\"defaultRowActions(row)\" [context]=\"rowActionsContext()\" />\n </span>\n }\n @if (hasDetail() && effectiveMobileLayout() === 'row') {\n @if (rowNavigable()) {\n <!-- The head opens the record, so expansion needs a control\n of its own. Same glyph, promoted from decoration to a\n button that carries the state it was already drawing. -->\n <button\n type=\"button\"\n class=\"mrow__disclosure\"\n [attr.aria-expanded]=\"isExpanded(row, i)\"\n [attr.aria-label]=\"(isExpanded(row, i) ? 'common_view_less' : 'common_view_more') | translate\"\n (click)=\"onDisclosureToggle(row, i, $event)\"\n >\n <i\n class=\"pi pi-chevron-down mrow__chevron\"\n [class.is-open]=\"isExpanded(row, i)\"\n aria-hidden=\"true\"\n ></i>\n </button>\n } @else {\n <!-- Says the row opens, and which way it is now. Decorative:\n the whole head is the control, and it carries the state. -->\n <i\n class=\"pi pi-chevron-down mrow__chevron\"\n [class.is-open]=\"isExpanded(row, i)\"\n aria-hidden=\"true\"\n ></i>\n }\n }\n </div>\n </div>\n\n @if (visibleDetail(row, i).length) {\n <dl class=\"mrow__detail\">\n @for (col of visibleDetail(row, i); track col.id) {\n <div class=\"mrow__field\">\n <dt>{{ $any(col).headerKey ? ($any(col).headerKey | translate) : $any(col).header }}</dt>\n <dd [class]=\"cellAlignClass($any(col))\">\n <ng-container *ngTemplateOutlet=\"cellTpl; context: { $implicit: row, col: col, index: i }\"></ng-container>\n </dd>\n </div>\n }\n </dl>\n }\n\n @if (hasMoreThanPreview() && effectiveMobileLayout() === 'card') {\n <button class=\"mrow__more\" type=\"button\" (click)=\"toggleExpanded(row, i)\">\n <span>{{ (isExpanded(row, i) ? 'common_view_less' : 'common_view_more') | translate }}</span>\n <i class=\"pi pi-chevron-down mrow__chevron\" [class.is-open]=\"isExpanded(row, i)\" aria-hidden=\"true\"></i>\n </button>\n }\n </li>\n } @empty {\n <!-- No rows has three meanings and they must not share a screen:\n still loading is a skeleton, the request failed is the error\n state, finished-and-empty is the empty state. Saying \"nothing\n here yet\" to someone whose data is still in flight \u2014 or whose\n request just failed \u2014 is a lie the whole time it shows. -->\n @if (showSkeleton()) {\n @for (line of skeletonLines(); track line) {\n <li class=\"mrow mrow--skeleton\">\n <ef-skeleton variant=\"text\" [rows]=\"2\" />\n </li>\n }\n } @else if (showError()) {\n <li class=\"mrow mrow--empty\">\n <ng-container *ngTemplateOutlet=\"errorStateTpl\"></ng-container>\n </li>\n } @else {\n <li class=\"mrow mrow--empty\">\n <ng-content select=\"[empty-state-mobile]\">\n <ng-container *ngTemplateOutlet=\"defaultEmptyTpl\"></ng-container>\n </ng-content>\n </li>\n }\n }\n </ul>\n } @else {\n <table class=\"tbl\" [class.tbl--fixed]=\"fixedLayout()\">\n <thead>\n <tr>\n @if (reorderable()) {\n <th class=\"tbl-drag-h\" aria-hidden=\"true\"></th>\n }\n @for (col of effectiveColumns(); track trackByColumn($index, col)) {\n <!-- A sortable header is a real <button> inside the <th>, not a\n click handler on the cell. The cell carries aria-sort so the\n current order is announced; the arrow stays decorative.\n Sorting was previously mouse-only and silent to assistive\n technology, on every list screen in the product. -->\n <th\n [class]=\"headerAlignClass(col)\"\n [class.is-sortable]=\"col.sortable\"\n [style.width]=\"col.width || null\"\n [attr.data-col]=\"col.id\"\n [attr.aria-sort]=\"ariaSort(col)\"\n >\n @if (col.sortable) {\n <button type=\"button\" class=\"tbl-sort\" (click)=\"onHeaderClick(col)\">\n <ng-container *ngTemplateOutlet=\"headerLabelTpl; context: { col: col }\"></ng-container>\n @if (sortIndicator(col)) {\n <span aria-hidden=\"true\" class=\"tbl-sort__arrow\">{{ sortIndicator(col) }}</span>\n }\n </button>\n } @else {\n <ng-container *ngTemplateOutlet=\"headerLabelTpl; context: { col: col }\"></ng-container>\n }\n </th>\n }\n </tr>\n </thead>\n\n <tbody [attr.aria-busy]=\"loading() ? 'true' : null\">\n @for (row of rows(); track trackByRow($index, row); let i = $index) {\n <tr\n [class.is-row-clickable]=\"rowDoubleClickable()\"\n [class.is-dragging]=\"dragFrom() === i\"\n [class.is-drop-target]=\"dragFrom() !== null && dragFrom() !== i && dragTo() === i\"\n [attr.tabindex]=\"rowDoubleClickable() ? 0 : null\"\n [attr.role]=\"rowDoubleClickable() ? 'button' : null\"\n (click)=\"onRowClick(row, $event)\"\n (keydown.enter)=\"onRowKeydown(row, $event)\"\n >\n @if (reorderable()) {\n <td class=\"tbl-drag\">\n <!-- The row is draggable only by this handle, so a drag never\n starts from a cell the operator meant to click or select.\n Arrow keys move the row for anyone not using a pointer. -->\n <button\n type=\"button\"\n class=\"tbl-drag__grip\"\n [attr.aria-label]=\"'common_reorder_row' | translate\"\n (pointerdown)=\"onDragPointerDown(i, $event)\"\n (pointermove)=\"onDragPointerMove($event)\"\n (pointerup)=\"onDragPointerUp()\"\n (pointercancel)=\"onDragPointerUp()\"\n (keydown)=\"onDragKeydown(i, $event)\"\n >\n <i class=\"pi pi-ellipsis-v\" aria-hidden=\"true\"></i>\n </button>\n </td>\n }\n @for (col of effectiveColumns(); track trackByColumn($index, col); let colIdx = $index) {\n <td efOverflowTooltip efTooltip efTooltipPosition=\"top\" [class]=\"cellAlignClass(col)\" [attr.data-col]=\"col.id\">\n @if (col.id === 'actions' && hasActionsColumn()) {\n <!-- Auto row-actions cell: View / Edit / Duplicate / Delete,\n gated by [show*Action] flags + ScreenContext perms. -->\n <ef-row-actions\n [items]=\"defaultRowActions(row)\"\n [context]=\"rowActionsContext()\"\n />\n } @else {\n <ng-container\n *ngTemplateOutlet=\"cellTpl; context: { $implicit: row, col: col, index: i }\"\n ></ng-container>\n }\n </td>\n }\n </tr>\n } @empty {\n <!-- Placeholder rows keep the real columns, so the table that\n arrives lands where the shimmer already was instead of\n reflowing the page under the reader's eyes. -->\n @if (showSkeleton()) {\n @for (line of skeletonLines(); track line) {\n <tr>\n @if (reorderable()) {\n <td class=\"tbl-drag\"></td>\n }\n @for (col of effectiveColumns(); track trackByColumn($index, col)) {\n <td [class]=\"cellAlignClass(col)\" [attr.data-col]=\"col.id\">\n <ef-skeleton variant=\"text\" [rows]=\"1\" />\n </td>\n }\n </tr>\n }\n } @else if (showError()) {\n <!-- The failure replaces the body, so it spans the full row \u2014\n crammed into column one it reads as a value, not a state. -->\n <tr>\n <td [attr.colspan]=\"effectiveColumns().length + (reorderable() ? 1 : 0)\">\n <ng-container *ngTemplateOutlet=\"errorStateTpl\"></ng-container>\n </td>\n </tr>\n } @else {\n <tr>\n <td [attr.colspan]=\"effectiveColumns().length + (reorderable() ? 1 : 0)\">\n <ng-content select=\"[empty-state]\">\n <ng-container *ngTemplateOutlet=\"defaultEmptyTpl\"></ng-container>\n </ng-content>\n </td>\n </tr>\n }\n }\n </tbody>\n </table>\n }\n\n <!-- \u2500\u2500 PAGER \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Paging controls for an empty result set are chrome with nothing to\n page, so they stay out of the way until there is something to move\n through. -->\n @if (!hidePager() && rows().length > 0) {\n <ef-pager\n [pageNumber]=\"pageNumber()\"\n [pageSize]=\"pageSize()\"\n [totalCount]=\"totalCount()\"\n [pageSizeOptions]=\"pageSizeOptions()\"\n (pageChange)=\"pageChange.emit($event)\"\n (pageSizeChange)=\"pageSizeChange.emit($event)\"\n />\n }\n</div>\n\n<!-- \u2500\u2500 DEFAULT EMPTY STATE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n Rendered when a screen projects nothing into [empty-state] /\n [empty-state-mobile]. It words itself from `searchTerm`, because\n \"nothing matched what you typed\" and \"there is nothing here yet\"\n are different situations needing different ways out. -->\n<ng-template #defaultEmptyTpl>\n <div class=\"tbl-empty\">\n <i\n class=\"pi tbl-empty__icon\"\n [class.pi-search]=\"!!searchTerm()\"\n [class.pi-inbox]=\"!searchTerm()\"\n aria-hidden=\"true\"\n ></i>\n\n @if (searchTerm()) {\n <p class=\"tbl-empty__title\">\n {{ 'common_empty_no_results' | translate: { term: searchTerm() } }}\n </p>\n <p class=\"tbl-empty__hint\">{{ 'common_empty_no_results_hint' | translate }}</p>\n <button class=\"btn btn-sm tbl-empty__action\" type=\"button\" (click)=\"clearSearch.emit()\">\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\n {{ 'common_empty_clear_filters' | translate }}\n </button>\n } @else {\n <p class=\"tbl-empty__title\">{{ 'common_empty_none_yet' | translate }}</p>\n <p class=\"tbl-empty__hint\">{{ 'common_empty_none_yet_hint' | translate }}</p>\n }\n </div>\n</ng-template>\n\n<!-- \u2500\u2500 ERROR STATE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n A request that failed and came back with nothing is not an empty\n collection, and must never borrow its words: \"nothing here yet\" is\n a confident false statement about the operator's data, and they may\n act on it. So this replaces the empty state outright, projected\n [empty-state] included \u2014 a screen's own \"create your first client\"\n copy is just as wrong here as the default.\n\n It says nothing about *what* failed. `errorMsg` is `err.message`\n from the HTTP layer, and a status line or an exception string tells\n an operator nothing they can act on; it survives on the head chip's\n `data-error` attribute, for whoever is reading the DOM. This carries\n the two things that help: their data is intact, and the way to try\n again.\n\n role=\"alert\" goes on this div, never on the <tr> / <td> / <li> that\n hosts it \u2014 a role on a row or cell takes the table's semantics down\n with it. It fires on insertion, which is exactly when @if adds it. -->\n<ng-template #errorStateTpl>\n <div class=\"tbl-empty tbl-empty--error\" role=\"alert\">\n <i class=\"pi pi-exclamation-triangle tbl-empty__icon\" aria-hidden=\"true\"></i>\n <p class=\"tbl-empty__title\">{{ 'common_error_load_failed' | translate }}</p>\n <p class=\"tbl-empty__hint\">{{ 'common_error_load_failed_hint' | translate }}</p>\n <button class=\"btn btn-sm tbl-empty__action\" type=\"button\" (click)=\"retry.emit()\">\n <i class=\"pi pi-refresh\" aria-hidden=\"true\"></i>\n {{ 'common_retry' | translate }}\n </button>\n </div>\n</ng-template>\n\n<!-- Header label, shared by the sortable and plain header cells so the\n projection rules stay in one place. -->\n<ng-template #headerLabelTpl let-col=\"col\">\n @if (headerTemplate(col.id); as headerTpl) {\n <ng-container *ngTemplateOutlet=\"headerTpl; context: { $implicit: col, col: col }\"></ng-container>\n } @else if (col.headerKey) {\n <span>{{ col.headerKey | translate }}</span>\n } @else if (col.header) {\n <span>{{ col.header }}</span>\n }\n</ng-template>\n", styles: ["@charset \"UTF-8\";.tbl thead th.is-sortable{cursor:pointer;-webkit-user-select:none;user-select:none;transition:color var(--t-fast, .14s) var(--ease-out, ease-out)}.tbl thead th.is-sortable:hover{color:var(--text)}.tbl tbody tr.is-row-clickable{cursor:pointer}.tbl-tools{display:flex;gap:6px;align-items:center}.tbl-tool{position:relative}.tbl-tool>.btn.open{color:var(--text);background:var(--paper)}.tbl-tool__menu{position:absolute;top:calc(100% + 8px);inset-inline-end:0;z-index:40;min-width:212px;max-height:340px;overflow-y:auto;padding:6px;display:grid;gap:1px;background:var(--paper-alt);border:1px solid var(--rule);border-radius:var(--r-xl);box-shadow:var(--shadow-3);animation:row-actions-menu-in var(--t-base) var(--ease-spring)}.tbl-tool__menu--wide{min-width:248px}.tbl-tool__menu button{appearance:none;background:transparent;border:0;width:100%;text-align:start;font:inherit;font-size:13.5px;font-weight:600;color:var(--text);display:flex;align-items:center;gap:12px;padding:9px 12px;border-radius:var(--r-md);cursor:pointer;transition:background-color var(--t-fast) var(--ease-out)}.tbl-tool__menu button:hover,.tbl-tool__menu button:focus-visible{background:var(--paper);outline:0}.tbl-tool__menu button>.pi{width:16px;flex:none;font-size:14px;color:var(--muted)}.tbl-tool__menu button.is-active>.pi{color:var(--module, var(--text))}.tbl-tool__label{flex:1 1 auto;min-width:0}.tbl-tool__check{color:var(--module, var(--text))}.tbl-tool__sep{margin:5px 2px;border-top:1px solid var(--rule)}.tbl-wrap[data-density=compact] .tbl tbody td{padding-top:4px;padding-bottom:4px}.tbl-wrap[data-density=comfortable] .tbl tbody td{padding-top:16px;padding-bottom:16px}.mlist{list-style:none;margin:0;padding:0;display:grid}.mrow{border-top:1px solid var(--rule)}.mrow:first-child{border-top:0}.mrow__head{display:flex;align-items:flex-start;gap:var(--s-3);padding:12px var(--s-4);min-height:var(--hit-touch)}.mrow__head.is-tappable{cursor:pointer}.mrow__head.is-tappable:active{background:var(--paper)}.mrow__lead{flex:1;min-width:0;display:grid;gap:2px}.mrow__primary{font-weight:700;font-size:14px;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mrow__secondary{display:flex;flex-wrap:wrap;align-items:baseline;gap:0 6px;font-size:12.5px;color:var(--text-mute);min-width:0}.mrow__secondary .mrow__sec-part+.mrow__sec-part:before{content:\"\\b7\";margin-inline-end:6px;color:var(--text-soft)}.mrow__trail{display:flex;align-items:center;justify-content:flex-end;gap:var(--s-2);flex-shrink:0;min-height:20px}.mrow__actions{display:inline-flex}.mrow__detail{margin:0;padding:0 var(--s-4) 12px;display:grid;gap:1px}.mrow__field{display:flex;align-items:baseline;justify-content:space-between;gap:var(--s-3);padding:7px 0;border-top:1px solid var(--rule-soft)}.mrow__field dt{font-size:11.5px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--text-mute);flex-shrink:0}.mrow__field dd{margin:0;font-size:13.5px;color:var(--text);text-align:end;min-width:0;overflow-wrap:anywhere}.mrow__more{appearance:none;width:100%;background:transparent;border:0;border-top:1px solid var(--rule-soft);padding:10px;font:inherit;font-size:13px;font-weight:600;color:var(--tenant-500);cursor:pointer}.mlist[data-layout=card]{gap:var(--s-3);padding:var(--s-3) var(--s-3) 0}.mlist[data-layout=card] .mrow{border:1px solid var(--rule);border-radius:var(--r-lg);background:var(--paper-alt);overflow:hidden}.mlist[data-layout=card] .mrow__head{border-bottom:1px solid var(--rule)}.mrow__chevron{font-size:11px;color:var(--text-soft);transition:transform var(--t-fast) var(--ease-out);flex-shrink:0}.mrow__chevron.is-open{transform:rotate(180deg);color:var(--text-mute)}.mrow__head.is-tappable:focus-visible{outline:2px solid var(--tenant-400);outline-offset:-2px}.mrow__more{display:flex;align-items:center;justify-content:center;gap:6px}.tbl-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--s-2);padding:var(--s-9) var(--s-4);text-align:center}.tbl-empty__icon{font-size:28px;line-height:1;color:var(--ink-400);margin-block-end:var(--s-1)}.tbl-empty__title{margin:0;font-size:15px;font-weight:600;color:var(--text);max-inline-size:46ch;overflow-wrap:break-word}.tbl-empty__hint{margin:0;font-size:13px;color:var(--ink-600);max-inline-size:52ch;text-wrap:pretty}.tbl-empty__action{margin-block-start:var(--s-3);min-block-size:var(--hit-base)}.tbl-empty--error .tbl-empty__icon{color:var(--st-cancelled-fg)}.mrow--empty{padding:0}.mrow--skeleton{padding:12px var(--s-4)}@media(max-width:767px){.tbl-empty{padding:var(--s-7) var(--s-4)}.tbl-empty__action{min-block-size:var(--hit-touch)}}.pi.is-spinning{animation:ef-export-spin .9s linear infinite}@keyframes ef-export-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.pi.is-spinning{animation:none}}.tbl th.tbl-drag-h,.tbl td.tbl-drag{width:28px;padding-inline:4px 0;text-align:center}.tbl td.tbl-drag{cursor:default}.tbl .tbl-drag__grip{display:inline-flex;align-items:center;justify-content:center;width:22px;height:26px;padding:0;border:0;border-radius:6px;background:transparent;color:var(--text-soft);cursor:grab;touch-action:none;transition:background var(--t-fast, .14s) var(--ease-out, ease-out),color var(--t-fast, .14s) var(--ease-out, ease-out)}.tbl .tbl-drag__grip:hover{background:var(--paper);color:var(--text)}.tbl .tbl-drag__grip:focus-visible{outline:2px solid var(--tenant, var(--rule));outline-offset:1px}.tbl .tbl-drag__grip:active{cursor:grabbing}.tbl .tbl-drag__grip i{font-size:12px}.tbl tbody tr.is-dragging>td{background:var(--paper);opacity:.55}.tbl tbody tr.is-drop-target>td{box-shadow:inset 0 2px 0 var(--tenant, var(--rule))}.tbl.tbl--fixed{table-layout:fixed}.mrow__disclosure{appearance:none;background:transparent;border:0;padding:0;margin:0;display:inline-grid;place-items:center;color:var(--text-mute);cursor:pointer;position:relative}.mrow__disclosure:after{content:\"\";position:absolute;top:50%;left:50%;width:var(--hit-touch);height:var(--hit-touch);transform:translate(-50%,-50%)}.mrow__disclosure:focus-visible{outline:2px solid color-mix(in srgb,var(--ink-active) 40%,transparent);outline-offset:2px;border-radius:var(--r-pill)}\n"] }]
|
|
5386
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- One cell renderer, shared by the table, the mobile row and the mobile\n detail. A new column type is added here once, not three times. -->\n<ng-template #cellTpl let-row let-col=\"col\" let-index=\"index\">\n @if (bodyTemplate(col.id); as bodyTpl) {\n <ng-container *ngTemplateOutlet=\"bodyTpl; context: { $implicit: row, col: col, index: index }\"></ng-container>\n } @else {\n @switch (col.type) {\n @case ('number') {\n {{ $any(cellValue(row, col)) | number:numberFormat(col):col.locale }}\n }\n @case ('money') {\n {{ $any(cellValue(row, col)) | currency:(col.currencyCode || 'EUR'):(col.currencyDisplay || 'symbol'):numberFormat(col):col.locale }}\n }\n @case ('date') {\n {{ $any(cellValue(row, col)) | date:(col.dateFormat || 'dd/MM/yyyy'):undefined:col.locale }}\n }\n @case ('datetime') {\n {{ $any(cellValue(row, col)) | date:(col.dateFormat || 'dd/MM/yyyy HH:mm'):undefined:col.locale }}\n }\n @case ('boolean') {\n @if (cellValue(row, col)) {\n <span class=\"bool yes\"><span class=\"dot\"></span> \u2713</span>\n } @else {\n <span class=\"bool no\"><span class=\"dot\"></span> \u2014</span>\n }\n }\n @case ('mono') {\n <span class=\"ref\">{{ cellValue(row, col) }}</span>\n }\n @case ('chip') {\n <span [class]=\"'chip ' + (col.chipPrefix || 'chip-') + cellValue(row, col)\">\n \u25CF {{ cellValue(row, col) }}\n </span>\n }\n @case ('status') {\n <ef-status-chip\n [referenceKey]=\"col.referenceKey || ''\"\n [code]=\"$any(cellValue(row, col))\"\n />\n }\n @case ('reference') {\n {{ resolveReference(cellValue(row, col), col) }}\n }\n @default {\n {{ cellValue(row, col) }}\n }\n }\n }\n</ng-template>\n\n<div class=\"tbl-wrap\" [class.has-open-menu]=\"openMenuCount() > 0 || openTool() !== null\" [attr.data-density]=\"density()\">\n <!-- \u2500\u2500 HEAD ROW \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"tbl-head\">\n <div class=\"count small\">\n @if (loading()) {\n <span>{{ loadingKey() | translate }}</span>\n } @else if (errorMsg()) {\n <!-- A refresh that failed on top of rows already on screen keeps the\n rows, so this chip is the only sign anything went wrong \u2014 and the\n rows it sits above may now be stale. It has to say so, and say it\n in the product's words: `errorMsg` is `err.message` from the HTTP\n layer, and \"500 Internal Server Error\" tells an operator nothing\n they can act on.\n\n The raw string stays on `data-error`, where devtools can read it\n and no reader ever meets it. `title` was the obvious place and is\n the wrong one: it renders the exception in a tooltip, which is\n precisely what this change exists to stop, and it is unreachable\n on touch besides.\n\n role=\"status\", not \"alert\": polite is right for a signal beside\n rows that are still usable, and when the body is showing the\n error state it already carries the alert \u2014 one failure must not\n interrupt a screen-reader twice. -->\n <span\n role=\"status\"\n [attr.data-error]=\"errorMsg()\"\n style=\"color: var(--st-cancelled-fg); font-weight: 600;\"\n >\u26A0 {{ 'common_error_refresh_failed' | translate }}</span\n >\n } @else {\n <ng-content select=\"[tbl-head-info]\"></ng-content>\n }\n </div>\n <div class=\"tbl-tools\">\n <ng-content select=\"[tbl-head-actions]\"></ng-content>\n\n @if (canExport()) {\n <div class=\"tbl-tool\">\n <button\n class=\"btn btn-ghost btn-sm\"\n type=\"button\"\n [disabled]=\"exporting()\"\n [attr.aria-busy]=\"exporting()\"\n (click)=\"requestExport()\"\n >\n <i\n class=\"pi\"\n [class.pi-download]=\"!exporting()\"\n [class.pi-spinner]=\"exporting()\"\n [class.is-spinning]=\"exporting()\"\n aria-hidden=\"true\"\n ></i>\n {{ (exporting() ? 'common_export_running' : 'common_export') | translate }}\n </button>\n </div>\n }\n\n @if (showDensityControl()) {\n <div class=\"tbl-tool\">\n <button\n class=\"btn btn-ghost btn-sm\"\n type=\"button\"\n [class.open]=\"openTool() === 'density'\"\n [attr.aria-expanded]=\"openTool() === 'density'\"\n aria-haspopup=\"menu\"\n (click)=\"toggleTool('density')\"\n >\n <i class=\"pi pi-bars\" aria-hidden=\"true\"></i>\n {{ 'common_density' | translate }}\n </button>\n @if (openTool() === 'density') {\n <div class=\"tbl-tool__menu\" role=\"menu\">\n @for (opt of densityOptions; track opt.value) {\n <button\n type=\"button\"\n role=\"menuitemradio\"\n [attr.aria-checked]=\"density() === opt.value\"\n [class.is-active]=\"density() === opt.value\"\n (click)=\"setDensity(opt.value)\"\n >\n <i class=\"pi\" [ngClass]=\"opt.icon\" aria-hidden=\"true\"></i>\n <span class=\"tbl-tool__label\">{{ opt.labelKey | translate }}</span>\n @if (density() === opt.value) {\n <i class=\"pi pi-check tbl-tool__check\" aria-hidden=\"true\"></i>\n }\n </button>\n }\n </div>\n }\n </div>\n }\n\n @if (showColumnPicker()) {\n <div class=\"tbl-tool\">\n <button\n class=\"btn btn-ghost btn-sm\"\n type=\"button\"\n [class.open]=\"openTool() === 'columns'\"\n [attr.aria-expanded]=\"openTool() === 'columns'\"\n aria-haspopup=\"menu\"\n (click)=\"toggleTool('columns')\"\n >\n <i class=\"pi pi-th-large\" aria-hidden=\"true\"></i>\n {{ 'common_columns' | translate }}\n </button>\n @if (openTool() === 'columns') {\n <div class=\"tbl-tool__menu tbl-tool__menu--wide\" role=\"menu\">\n @for (col of hideableColumns(); track col.id) {\n <button\n type=\"button\"\n role=\"menuitemcheckbox\"\n [attr.aria-checked]=\"!isColumnHidden(col.id)\"\n [class.is-active]=\"!isColumnHidden(col.id)\"\n (click)=\"toggleColumn(col.id)\"\n >\n <i class=\"pi\" [class.pi-check]=\"!isColumnHidden(col.id)\" aria-hidden=\"true\"></i>\n <span class=\"tbl-tool__label\">{{\n col.headerKey ? (col.headerKey | translate) : (col.header || col.id)\n }}</span>\n </button>\n }\n <div class=\"tbl-tool__sep\"></div>\n <button type=\"button\" role=\"menuitem\" (click)=\"resetColumns()\">\n <i class=\"pi pi-refresh\" aria-hidden=\"true\"></i>\n <span class=\"tbl-tool__label\">{{ 'common_columns_reset' | translate }}</span>\n </button>\n </div>\n }\n </div>\n }\n </div>\n </div>\n\n <!-- \u2500\u2500 TABLE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (isMobile()) {\n <!-- \u2500\u2500 MOBILE LIST \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n A six-column table on a 390px screen either overflows or\n squeezes every column to nothing, so the rows become a list.\n Same columns, same cell renderers; each column just declares\n (or is given) a role. Both shapes expand to the same detail \u2014\n they differ only in how much they show collapsed. -->\n <ul\n class=\"mlist\"\n [attr.data-layout]=\"effectiveMobileLayout()\"\n [attr.aria-busy]=\"loading() ? 'true' : null\"\n >\n @for (row of rows(); track trackByRow($index, row); let i = $index) {\n <li class=\"mrow\" [class.is-open]=\"isExpanded(row, i)\">\n <div\n class=\"mrow__head\"\n [class.is-tappable]=\"headIsControl()\"\n [attr.role]=\"headIsControl() ? 'button' : null\"\n [attr.tabindex]=\"headIsControl() ? 0 : null\"\n [attr.aria-expanded]=\"!rowNavigable() && hasDetail() ? isExpanded(row, i) : null\"\n (click)=\"onRowHeadActivate(row, i, $event)\"\n (keydown.enter)=\"onRowHeadActivate(row, i, $event)\"\n (keydown.space)=\"onRowHeadActivate(row, i, $event)\"\n >\n <div class=\"mrow__lead\">\n @if (primaryColumn(); as pc) {\n <span class=\"mrow__primary\" efOverflowTooltip efTooltip efTooltipPosition=\"top\">\n <ng-container *ngTemplateOutlet=\"cellTpl; context: { $implicit: row, col: pc, index: i }\"></ng-container>\n </span>\n }\n @if (effectiveMobileLayout() === 'row' && secondaryColumns().length) {\n <span class=\"mrow__secondary\">\n @for (sc of secondaryColumns(); track sc.id) {\n <span class=\"mrow__sec-part\">\n <ng-container *ngTemplateOutlet=\"cellTpl; context: { $implicit: row, col: sc, index: i }\"></ng-container>\n </span>\n }\n </span>\n }\n </div>\n\n <div class=\"mrow__trail\">\n @if (statusColumn(); as st) {\n <ng-container *ngTemplateOutlet=\"cellTpl; context: { $implicit: row, col: st, index: i }\"></ng-container>\n }\n @if (hasActionsColumn()) {\n <span class=\"mrow__actions\">\n <ef-row-actions [items]=\"defaultRowActions(row)\" [context]=\"rowActionsContext()\" />\n </span>\n }\n @if (hasDetail() && effectiveMobileLayout() === 'row') {\n @if (rowNavigable()) {\n <!-- The head opens the record, so expansion needs a control\n of its own. Same glyph, promoted from decoration to a\n button that carries the state it was already drawing. -->\n <button\n type=\"button\"\n class=\"mrow__disclosure\"\n [attr.aria-expanded]=\"isExpanded(row, i)\"\n [attr.aria-label]=\"(isExpanded(row, i) ? 'common_view_less' : 'common_view_more') | translate\"\n (click)=\"onDisclosureToggle(row, i, $event)\"\n >\n <i\n class=\"pi pi-chevron-down mrow__chevron\"\n [class.is-open]=\"isExpanded(row, i)\"\n aria-hidden=\"true\"\n ></i>\n </button>\n } @else {\n <!-- Says the row opens, and which way it is now. Decorative:\n the whole head is the control, and it carries the state. -->\n <i\n class=\"pi pi-chevron-down mrow__chevron\"\n [class.is-open]=\"isExpanded(row, i)\"\n aria-hidden=\"true\"\n ></i>\n }\n }\n </div>\n </div>\n\n @if (visibleDetail(row, i).length) {\n <dl class=\"mrow__detail\">\n @for (col of visibleDetail(row, i); track col.id) {\n <div class=\"mrow__field\">\n <dt>{{ $any(col).headerKey ? ($any(col).headerKey | translate) : $any(col).header }}</dt>\n <dd [class]=\"cellAlignClass($any(col))\">\n <ng-container *ngTemplateOutlet=\"cellTpl; context: { $implicit: row, col: col, index: i }\"></ng-container>\n </dd>\n </div>\n }\n </dl>\n }\n\n @if (hasMoreThanPreview() && effectiveMobileLayout() === 'card') {\n <button class=\"mrow__more\" type=\"button\" (click)=\"toggleExpanded(row, i)\">\n <span>{{ (isExpanded(row, i) ? 'common_view_less' : 'common_view_more') | translate }}</span>\n <i class=\"pi pi-chevron-down mrow__chevron\" [class.is-open]=\"isExpanded(row, i)\" aria-hidden=\"true\"></i>\n </button>\n }\n </li>\n } @empty {\n <!-- No rows has three meanings and they must not share a screen:\n still loading is a skeleton, the request failed is the error\n state, finished-and-empty is the empty state. Saying \"nothing\n here yet\" to someone whose data is still in flight \u2014 or whose\n request just failed \u2014 is a lie the whole time it shows. -->\n @if (showSkeleton()) {\n @for (line of skeletonLines(); track line) {\n <li class=\"mrow mrow--skeleton\">\n <ef-skeleton variant=\"text\" [rows]=\"2\" />\n </li>\n }\n } @else if (showError()) {\n <li class=\"mrow mrow--empty\">\n <ng-container *ngTemplateOutlet=\"errorStateTpl\"></ng-container>\n </li>\n } @else {\n <li class=\"mrow mrow--empty\">\n <ng-content select=\"[empty-state-mobile]\">\n <ng-container *ngTemplateOutlet=\"defaultEmptyTpl\"></ng-container>\n </ng-content>\n </li>\n }\n }\n </ul>\n } @else {\n <table class=\"tbl\" [class.tbl--fixed]=\"fixedLayout()\">\n <thead>\n <tr>\n @if (reorderable()) {\n <th class=\"tbl-drag-h\" aria-hidden=\"true\"></th>\n }\n @for (col of effectiveColumns(); track trackByColumn($index, col)) {\n <!-- A sortable header is a real <button> inside the <th>, not a\n click handler on the cell. The cell carries aria-sort so the\n current order is announced; the arrow stays decorative.\n Sorting was previously mouse-only and silent to assistive\n technology, on every list screen in the product. -->\n <th\n [class]=\"headerAlignClass(col)\"\n [class.is-sortable]=\"col.sortable\"\n [style.width]=\"col.width || null\"\n [attr.data-col]=\"col.id\"\n [attr.aria-sort]=\"ariaSort(col)\"\n >\n @if (col.sortable) {\n <button type=\"button\" class=\"tbl-sort\" (click)=\"onHeaderClick(col)\">\n <ng-container *ngTemplateOutlet=\"headerLabelTpl; context: { col: col }\"></ng-container>\n @if (sortIndicator(col)) {\n <span aria-hidden=\"true\" class=\"tbl-sort__arrow\">{{ sortIndicator(col) }}</span>\n }\n </button>\n } @else {\n <ng-container *ngTemplateOutlet=\"headerLabelTpl; context: { col: col }\"></ng-container>\n }\n </th>\n }\n </tr>\n </thead>\n\n <tbody [attr.aria-busy]=\"loading() ? 'true' : null\">\n @for (row of rows(); track trackByRow($index, row); let i = $index) {\n <tr\n [class.is-row-clickable]=\"rowDoubleClickable()\"\n [class.is-dragging]=\"dragFrom() === i\"\n [class.is-drop-target]=\"dragFrom() !== null && dragFrom() !== i && dragTo() === i\"\n [attr.tabindex]=\"rowDoubleClickable() ? 0 : null\"\n [attr.role]=\"rowDoubleClickable() ? 'button' : null\"\n (click)=\"onRowClick(row, $event)\"\n (keydown.enter)=\"onRowKeydown(row, $event)\"\n >\n @if (reorderable()) {\n <td class=\"tbl-drag\">\n <!-- The row is draggable only by this handle, so a drag never\n starts from a cell the operator meant to click or select.\n Arrow keys move the row for anyone not using a pointer. -->\n <button\n type=\"button\"\n class=\"tbl-drag__grip\"\n [attr.aria-label]=\"'common_reorder_row' | translate\"\n (pointerdown)=\"onDragPointerDown(i, $event)\"\n (pointermove)=\"onDragPointerMove($event)\"\n (pointerup)=\"onDragPointerUp()\"\n (pointercancel)=\"onDragPointerUp()\"\n (keydown)=\"onDragKeydown(i, $event)\"\n >\n <i class=\"pi pi-ellipsis-v\" aria-hidden=\"true\"></i>\n </button>\n </td>\n }\n @for (col of effectiveColumns(); track trackByColumn($index, col); let colIdx = $index) {\n <td efOverflowTooltip efTooltip efTooltipPosition=\"top\" [class]=\"cellAlignClass(col)\" [attr.data-col]=\"col.id\">\n @if (col.id === 'actions' && hasActionsColumn()) {\n <!-- Auto row-actions cell: View / Edit / Duplicate / Delete,\n gated by [show*Action] flags + ScreenContext perms. -->\n <ef-row-actions\n [items]=\"defaultRowActions(row)\"\n [context]=\"rowActionsContext()\"\n />\n } @else {\n <ng-container\n *ngTemplateOutlet=\"cellTpl; context: { $implicit: row, col: col, index: i }\"\n ></ng-container>\n }\n </td>\n }\n </tr>\n } @empty {\n <!-- Placeholder rows keep the real columns, so the table that\n arrives lands where the shimmer already was instead of\n reflowing the page under the reader's eyes. -->\n @if (showSkeleton()) {\n @for (line of skeletonLines(); track line) {\n <tr>\n @if (reorderable()) {\n <td class=\"tbl-drag\"></td>\n }\n @for (col of effectiveColumns(); track trackByColumn($index, col)) {\n <td [class]=\"cellAlignClass(col)\" [attr.data-col]=\"col.id\">\n <ef-skeleton variant=\"text\" [rows]=\"1\" />\n </td>\n }\n </tr>\n }\n } @else if (showError()) {\n <!-- The failure replaces the body, so it spans the full row \u2014\n crammed into column one it reads as a value, not a state. -->\n <tr>\n <td [attr.colspan]=\"effectiveColumns().length + (reorderable() ? 1 : 0)\">\n <ng-container *ngTemplateOutlet=\"errorStateTpl\"></ng-container>\n </td>\n </tr>\n } @else {\n <tr>\n <td [attr.colspan]=\"effectiveColumns().length + (reorderable() ? 1 : 0)\">\n <ng-content select=\"[empty-state]\">\n <ng-container *ngTemplateOutlet=\"defaultEmptyTpl\"></ng-container>\n </ng-content>\n </td>\n </tr>\n }\n }\n </tbody>\n </table>\n }\n\n <!-- \u2500\u2500 PAGER \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Paging controls for an empty result set are chrome with nothing to\n page, so they stay out of the way until there is something to move\n through. -->\n @if (!hidePager() && rows().length > 0) {\n <ef-pager\n [pageNumber]=\"pageNumber()\"\n [pageSize]=\"pageSize()\"\n [totalCount]=\"totalCount()\"\n [pageSizeOptions]=\"pageSizeOptions()\"\n (pageChange)=\"pageChange.emit($event)\"\n (pageSizeChange)=\"pageSizeChange.emit($event)\"\n />\n }\n</div>\n\n<!-- \u2500\u2500 DEFAULT EMPTY STATE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n Rendered when a screen projects nothing into [empty-state] /\n [empty-state-mobile]. It words itself from `searchTerm`, because\n \"nothing matched what you typed\" and \"there is nothing here yet\"\n are different situations needing different ways out. -->\n<ng-template #defaultEmptyTpl>\n <div class=\"tbl-empty\">\n <i\n class=\"pi tbl-empty__icon\"\n [class.pi-search]=\"!!searchTerm()\"\n [class.pi-inbox]=\"!searchTerm()\"\n aria-hidden=\"true\"\n ></i>\n\n @if (searchTerm()) {\n <p class=\"tbl-empty__title\">\n {{ 'common_empty_no_results' | translate: { term: searchTerm() } }}\n </p>\n <p class=\"tbl-empty__hint\">{{ 'common_empty_no_results_hint' | translate }}</p>\n <button class=\"btn btn-sm tbl-empty__action\" type=\"button\" (click)=\"clearSearch.emit()\">\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\n {{ 'common_empty_clear_filters' | translate }}\n </button>\n } @else {\n <p class=\"tbl-empty__title\">{{ 'common_empty_none_yet' | translate }}</p>\n <p class=\"tbl-empty__hint\">{{ 'common_empty_none_yet_hint' | translate }}</p>\n }\n </div>\n</ng-template>\n\n<!-- \u2500\u2500 ERROR STATE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n A request that failed and came back with nothing is not an empty\n collection, and must never borrow its words: \"nothing here yet\" is\n a confident false statement about the operator's data, and they may\n act on it. So this replaces the empty state outright, projected\n [empty-state] included \u2014 a screen's own \"create your first client\"\n copy is just as wrong here as the default.\n\n It says nothing about *what* failed. `errorMsg` is `err.message`\n from the HTTP layer, and a status line or an exception string tells\n an operator nothing they can act on; it survives on the head chip's\n `data-error` attribute, for whoever is reading the DOM. This carries\n the two things that help: their data is intact, and the way to try\n again.\n\n role=\"alert\" goes on this div, never on the <tr> / <td> / <li> that\n hosts it \u2014 a role on a row or cell takes the table's semantics down\n with it. It fires on insertion, which is exactly when @if adds it. -->\n<ng-template #errorStateTpl>\n <div class=\"tbl-empty tbl-empty--error\" role=\"alert\">\n <i class=\"pi pi-exclamation-triangle tbl-empty__icon\" aria-hidden=\"true\"></i>\n <p class=\"tbl-empty__title\">{{ 'common_error_load_failed' | translate }}</p>\n <p class=\"tbl-empty__hint\">{{ 'common_error_load_failed_hint' | translate }}</p>\n <button class=\"btn btn-sm tbl-empty__action\" type=\"button\" (click)=\"retry.emit()\">\n <i class=\"pi pi-refresh\" aria-hidden=\"true\"></i>\n {{ 'common_retry' | translate }}\n </button>\n </div>\n</ng-template>\n\n<!-- Header label, shared by the sortable and plain header cells so the\n projection rules stay in one place. -->\n<ng-template #headerLabelTpl let-col=\"col\">\n @if (headerTemplate(col.id); as headerTpl) {\n <ng-container *ngTemplateOutlet=\"headerTpl; context: { $implicit: col, col: col }\"></ng-container>\n } @else if (col.headerKey) {\n <span>{{ col.headerKey | translate }}</span>\n } @else if (col.header) {\n <span>{{ col.header }}</span>\n }\n</ng-template>\n", styles: ["@charset \"UTF-8\";.tbl thead th.is-sortable{cursor:pointer;-webkit-user-select:none;user-select:none;transition:color var(--t-fast, .14s) var(--ease-out, ease-out)}.tbl thead th.is-sortable:hover{color:var(--text)}.tbl tbody tr.is-row-clickable{cursor:pointer}.tbl-tools{display:flex;gap:6px;align-items:center}.tbl-tool{position:relative}.tbl-tool>.btn.open{color:var(--text);background:var(--paper)}.tbl-tool__menu{position:absolute;top:calc(100% + 8px);inset-inline-end:0;z-index:40;min-width:212px;max-height:340px;overflow-y:auto;padding:6px;display:grid;gap:1px;background:var(--paper-alt);border:1px solid var(--rule);border-radius:var(--r-xl);box-shadow:var(--shadow-3);animation:row-actions-menu-in var(--t-base) var(--ease-spring)}.tbl-tool__menu--wide{min-width:248px}.tbl-tool__menu button{appearance:none;background:transparent;border:0;width:100%;text-align:start;font:inherit;font-size:13.5px;font-weight:600;color:var(--text);display:flex;align-items:center;gap:12px;padding:9px 12px;border-radius:var(--r-md);cursor:pointer;transition:background-color var(--t-fast) var(--ease-out)}.tbl-tool__menu button:hover,.tbl-tool__menu button:focus-visible{background:var(--paper);outline:0}.tbl-tool__menu button>.pi{width:16px;flex:none;font-size:14px;color:var(--muted)}.tbl-tool__menu button.is-active>.pi{color:var(--module, var(--text))}.tbl-tool__label{flex:1 1 auto;min-width:0}.tbl-tool__check{color:var(--module, var(--text))}.tbl-tool__sep{margin:5px 2px;border-top:1px solid var(--rule)}.tbl-wrap[data-density=compact] .tbl tbody td{--tbl-cell-pad-block: 4px;padding-top:var(--tbl-cell-pad-block);padding-bottom:var(--tbl-cell-pad-block)}.tbl-wrap[data-density=comfortable] .tbl tbody td{--tbl-cell-pad-block: 16px;padding-top:var(--tbl-cell-pad-block);padding-bottom:var(--tbl-cell-pad-block)}.mlist{list-style:none;margin:0;padding:0;display:grid}.mrow{border-top:1px solid var(--rule)}.mrow:first-child{border-top:0}.mrow__head{display:flex;align-items:flex-start;gap:var(--s-3);padding:12px var(--s-4);min-height:var(--hit-touch)}.mrow__head.is-tappable{cursor:pointer}.mrow__head.is-tappable:active{background:var(--paper)}.mrow__lead{flex:1;min-width:0;display:grid;gap:2px}.mrow__primary{font-weight:700;font-size:14px;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mrow__secondary{display:flex;flex-wrap:wrap;align-items:baseline;gap:0 6px;font-size:12.5px;color:var(--text-mute);min-width:0}.mrow__secondary .mrow__sec-part+.mrow__sec-part:before{content:\"\\b7\";margin-inline-end:6px;color:var(--text-soft)}.mrow__trail{display:flex;align-items:center;justify-content:flex-end;gap:var(--s-2);flex-shrink:0;min-height:20px}.mrow__actions{display:inline-flex}.mrow__detail{margin:0;padding:0 var(--s-4) 12px;display:grid;gap:1px}.mrow__field{display:flex;align-items:baseline;justify-content:space-between;gap:var(--s-3);padding:7px 0;border-top:1px solid var(--rule-soft)}.mrow__field dt{font-size:11.5px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--text-mute);flex-shrink:0}.mrow__field dd{margin:0;font-size:13.5px;color:var(--text);text-align:end;min-width:0;overflow-wrap:anywhere}.mrow__more{appearance:none;width:100%;background:transparent;border:0;border-top:1px solid var(--rule-soft);padding:10px;font:inherit;font-size:13px;font-weight:600;color:var(--tenant-500);cursor:pointer}.mlist[data-layout=card]{gap:var(--s-3);padding:var(--s-3) var(--s-3) 0}.mlist[data-layout=card] .mrow{border:1px solid var(--rule);border-radius:var(--r-lg);background:var(--paper-alt);overflow:hidden}.mlist[data-layout=card] .mrow__head{border-bottom:1px solid var(--rule)}.mrow__chevron{font-size:11px;color:var(--text-soft);transition:transform var(--t-fast) var(--ease-out);flex-shrink:0}.mrow__chevron.is-open{transform:rotate(180deg);color:var(--text-mute)}.mrow__head.is-tappable:focus-visible{outline:2px solid var(--tenant-400);outline-offset:-2px}.mrow__more{display:flex;align-items:center;justify-content:center;gap:6px}.tbl-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--s-2);padding:var(--s-9) var(--s-4);text-align:center}.tbl-empty__icon{font-size:28px;line-height:1;color:var(--ink-400);margin-block-end:var(--s-1)}.tbl-empty__title{margin:0;font-size:15px;font-weight:600;color:var(--text);max-inline-size:46ch;overflow-wrap:break-word}.tbl-empty__hint{margin:0;font-size:13px;color:var(--ink-600);max-inline-size:52ch;text-wrap:pretty}.tbl-empty__action{margin-block-start:var(--s-3);min-block-size:var(--hit-base)}.tbl-empty--error .tbl-empty__icon{color:var(--st-cancelled-fg)}.mrow--empty{padding:0}.mrow--skeleton{padding:12px var(--s-4)}@media(max-width:767px){.tbl-empty{padding:var(--s-7) var(--s-4)}.tbl-empty__action{min-block-size:var(--hit-touch)}}.pi.is-spinning{animation:ef-export-spin .9s linear infinite}@keyframes ef-export-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.pi.is-spinning{animation:none}}.tbl th.tbl-drag-h,.tbl td.tbl-drag{width:28px;padding-inline:4px 0;text-align:center}.tbl td.tbl-drag{cursor:default}.tbl .tbl-drag__grip{display:inline-flex;align-items:center;justify-content:center;width:22px;height:26px;padding:0;border:0;border-radius:6px;background:transparent;color:var(--text-soft);cursor:grab;touch-action:none;transition:background var(--t-fast, .14s) var(--ease-out, ease-out),color var(--t-fast, .14s) var(--ease-out, ease-out)}.tbl .tbl-drag__grip:hover{background:var(--paper);color:var(--text)}.tbl .tbl-drag__grip:focus-visible{outline:2px solid var(--tenant, var(--rule));outline-offset:1px}.tbl .tbl-drag__grip:active{cursor:grabbing}.tbl .tbl-drag__grip i{font-size:12px}.tbl tbody tr.is-dragging>td{background:var(--paper);opacity:.55}.tbl tbody tr.is-drop-target>td{box-shadow:inset 0 2px 0 var(--tenant, var(--rule))}.tbl.tbl--fixed{table-layout:fixed}.mrow__disclosure{appearance:none;background:transparent;border:0;padding:0;margin:0;display:inline-grid;place-items:center;color:var(--text-mute);cursor:pointer;position:relative}.mrow__disclosure:after{content:\"\";position:absolute;top:50%;left:50%;width:var(--hit-touch);height:var(--hit-touch);transform:translate(-50%,-50%)}.mrow__disclosure:focus-visible{outline:2px solid color-mix(in srgb,var(--ink-active) 40%,transparent);outline-offset:2px;border-radius:var(--r-pill)}\n"] }]
|
|
5356
5387
|
}], propDecorators: { mobileLayout: [{ type: i0.Input, args: [{ isSignal: true, alias: "mobileLayout", required: false }] }], columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }], trackByField: [{ type: i0.Input, args: [{ isSignal: true, alias: "trackByField", required: false }] }], pageNumber: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageNumber", required: false }] }], pageSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSize", required: false }] }], totalCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "totalCount", required: false }] }], pageSizeOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSizeOptions", required: false }] }], hidePager: [{ type: i0.Input, args: [{ isSignal: true, alias: "hidePager", required: false }] }], sort: [{ type: i0.Input, args: [{ isSignal: true, alias: "sort", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], errorMsg: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorMsg", required: false }] }], loadingKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingKey", required: false }] }], rowDoubleClickable: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowDoubleClickable", required: false }] }], hasActionsColumn: [{ type: i0.Input, args: [{ isSignal: true, alias: "hasActionsColumn", required: false }] }], rowActionsContext: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowActionsContext", required: false }] }], showViewAction: [{ type: i0.Input, args: [{ isSignal: true, alias: "showViewAction", required: false }] }], showEditAction: [{ type: i0.Input, args: [{ isSignal: true, alias: "showEditAction", required: false }] }], showDuplicateAction: [{ type: i0.Input, args: [{ isSignal: true, alias: "showDuplicateAction", required: false }] }], showDeleteAction: [{ type: i0.Input, args: [{ isSignal: true, alias: "showDeleteAction", required: false }] }], showDensityControl: [{ type: i0.Input, args: [{ isSignal: true, alias: "showDensityControl", required: false }] }], showColumnPicker: [{ type: i0.Input, args: [{ isSignal: true, alias: "showColumnPicker", required: false }] }], showExportControl: [{ type: i0.Input, args: [{ isSignal: true, alias: "showExportControl", required: false }] }], exporting: [{ type: i0.Input, args: [{ isSignal: true, alias: "exporting", required: false }] }], screenContext: [{ type: i0.Input, args: [{ isSignal: true, alias: "screenContext", required: false }] }], searchTerm: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchTerm", required: false }] }], tableKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "tableKey", required: false }] }], pageChange: [{ type: i0.Output, args: ["pageChange"] }], pageSizeChange: [{ type: i0.Output, args: ["pageSizeChange"] }], sortChange: [{ type: i0.Output, args: ["sortChange"] }], rowDoubleClick: [{ type: i0.Output, args: ["rowDoubleClick"] }], rowAction: [{ type: i0.Output, args: ["rowAction"] }], exportRequest: [{ type: i0.Output, args: ["exportRequest"] }], clearSearch: [{ type: i0.Output, args: ["clearSearch"] }], retry: [{ type: i0.Output, args: ["retry"] }], bodyTemplates: [{
|
|
5357
5388
|
type: ContentChildren,
|
|
5358
5389
|
args: [EfColumnTemplateDirective]
|
|
@@ -5853,7 +5884,7 @@ class EfOrderBuilderComponent {
|
|
|
5853
5884
|
return OrderEntityHelper.calculateTotals(this.order());
|
|
5854
5885
|
}
|
|
5855
5886
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfOrderBuilderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5856
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: EfOrderBuilderComponent, isStandalone: true, selector: "ef-order-builder", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null }, order: { classPropertyName: "order", publicName: "order", isSignal: true, isRequired: true, transformFunction: null }, products: { classPropertyName: "products", publicName: "products", isSignal: true, isRequired: true, transformFunction: null }, customers: { classPropertyName: "customers", publicName: "customers", isSignal: true, isRequired: true, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, countGroupLabels: { classPropertyName: "countGroupLabels", publicName: "countGroupLabels", isSignal: true, isRequired: false, transformFunction: null }, productKeyField: { classPropertyName: "productKeyField", publicName: "productKeyField", isSignal: true, isRequired: false, transformFunction: null }, backendErrors: { classPropertyName: "backendErrors", publicName: "backendErrors", isSignal: true, isRequired: false, transformFunction: null }, auditEntries: { classPropertyName: "auditEntries", publicName: "auditEntries", isSignal: true, isRequired: false, transformFunction: null }, focusMode: { classPropertyName: "focusMode", publicName: "focusMode", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { order: "orderChange", openCatalogue: "openCatalogue", orderChanged: "orderChanged", orderValidated: "orderValidated", productsAdded: "productsAdded", focusMode: "focusModeChange" }, viewQueries: [{ propertyName: "typeahead", first: true, predicate: EfProductTypeaheadComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"order-builder\"\n [class.ob-collapsed]=\"focusMode()\"\n [class.ob-rail-open]=\"railOpen()\">\n <div class=\"ob-grid\">\n <div class=\"ob-main\">\n <!-- Order header: customer + date + express add (one card) -->\n <ef-card\n titleKey=\"order_builder.section_order\"\n [metaKey]=\"readonly() ? '' : 'order_builder.quick_add_hint'\"\n [overflowVisible]=\"true\">\n <div class=\"ob-order-head\">\n <div class=\"form-grid\">\n <div class=\"field col-6\">\n <ef-label labelKey=\"label.customer\" for=\"ob-cust\" [required]=\"true\" />\n <ef-select\n inputId=\"ob-cust\"\n [options]=\"customers()\"\n optionLabel=\"companyName\"\n optionValue=\"customerId\"\n [ngModel]=\"order().customerId\"\n (ngModelChange)=\"updateCustomer($event)\"\n [disabled]=\"readonly()\"\n [showClear]=\"true\"\n [filter]=\"true\"\n filterBy=\"companyName\"\n filterPlaceholderKey=\"label.search_customer\"\n appendTo=\"body\"\n placeholderKey=\"label.select_customer\" />\n </div>\n <div class=\"field col-3\">\n <ef-label labelKey=\"label.order_date\" for=\"ob-date\" [required]=\"true\" />\n <ef-datepicker-advanced\n mode=\"field\"\n selectionMode=\"single\"\n inputId=\"ob-date\"\n placeholderKey=\"date_select_date\"\n [ngModel]=\"order().orderDate\"\n (ngModelChange)=\"updateOrderDate($event)\"\n [disabled]=\"readonly()\" />\n </div>\n </div>\n\n @if (!readonly()) {\n <div class=\"qa-grid\">\n <div class=\"qa-combo-wrap\">\n <ef-label labelKey=\"label.article\" />\n <ef-product-typeahead\n [products]=\"products()\"\n [productKeyField]=\"productKeyField()\"\n placeholderKey=\"label.search_product\"\n [currency]=\"currency()\"\n [locale]=\"locale()\"\n (productSelect)=\"onTypeaheadSelect($event)\"\n [showVariants]=\"false\" />\n </div>\n <div class=\"qa-price\">\n <ef-label labelKey=\"label.sale_price\" />\n <ef-inputnumber\n variant=\"comptoir\"\n [value]=\"quickAddPrice()\"\n (valueChangeEvent)=\"quickAddPrice.set($event ?? 0)\"\n [minFractionDigits]=\"2\"\n [maxFractionDigits]=\"2\"\n [locale]=\"locale()\"\n [disabled]=\"readonly()\" />\n </div>\n <div class=\"qa-qtyf\">\n <ef-label labelKey=\"label.quantity\" />\n <ef-inputnumber\n variant=\"comptoir\"\n [ngModel]=\"quickAddQuantity()\"\n (ngModelChange)=\"quickAddQuantity.set($event)\"\n [min]=\"0\"\n [maxFractionDigits]=\"4\"\n [locale]=\"locale()\"\n [disabled]=\"readonly()\"\n />\n </div>\n <div class=\"qa-go\">\n <!-- Ghost while the preconditions are unmet, tenant once the\n line is addable. A disabled control painted in a faded\n tenant fill reads as an enabled button that lost its\n colour, which is the greyed-out accent the design brief\n bans; ghost reads as \"not yet\". -->\n <ef-button\n labelKey=\"action.add\"\n icon=\"pi pi-plus\"\n variant=\"comptoir\"\n size=\"base\"\n [severity]=\"quickAddDisabled() ? 'ghost' : 'tenant'\"\n [disabled]=\"quickAddDisabled()\"\n (clickEvent)=\"addQuickProduct()\" />\n </div>\n </div>\n }\n </div>\n </ef-card>\n\n <!-- Lines.\n\n ef-data-card owns the panel here instead of sitting inside an\n ef-card: `.tbl-wrap` already draws a bordered, rounded surface, so\n nesting it in another one would be a card inside a card. The title\n and the card's actions move into the head slots it provides.\n\n The columns are declared in TS (`lineColumns`) so discount and tax\n follow the document config, and so one definition drives both the\n desktop table and the mobile cards. Only the article column is\n elastic; every other column holds a number of known magnitude and\n gets a fixed track, which keeps the decimals aligned down the\n table rather than shifting with the longest description. -->\n <ef-data-card\n [columns]=\"lineColumns()\"\n [rows]=\"orderLines()\"\n trackByField=\"id\"\n [hidePager]=\"true\"\n defaultDensity=\"compact\"\n [reorderable]=\"!readonly()\"\n [fixedLayout]=\"true\"\n (rowReorder)=\"reorderLine($event)\"\n mobileLayout=\"card\">\n\n <div tbl-head-info class=\"ob-lines-head\">\n <span class=\"strong\">{{ 'order_builder.lines' | translate }}</span>\n <span class=\"meta\">\u00B7 {{ orderLines().length }}</span>\n </div>\n\n <div tbl-head-actions class=\"head-actions\">\n @if (config().showCatalogue && !readonly()) {\n <ef-button\n labelKey=\"action.open_catalogue\"\n icon=\"pi pi-box\"\n severity=\"secondary\"\n [text]=\"true\"\n (clickEvent)=\"handleOpenCatalogue()\" />\n }\n <ef-button\n [labelKey]=\"focusMode() ? 'order_builder.show_summary' : 'order_builder.fullscreen'\"\n icon=\"pi pi-window-maximize\"\n severity=\"secondary\"\n [text]=\"true\"\n (clickEvent)=\"toggleFocus()\" />\n </div>\n\n <span empty-state>{{ 'order_builder.empty' | translate }}</span>\n <span empty-state-mobile>{{ 'order_builder.empty' | translate }}</span>\n\n <ng-template efColumnTemplate=\"product\" let-item let-i=\"index\">\n <div class=\"p\">\n <span class=\"ln\">{{ i + 1 }}</span>\n <span class=\"pp\">\n <span class=\"nm\">{{ item.productDescription }}</span>\n </span>\n </div>\n </ng-template>\n\n <ng-template efColumnTemplate=\"price\" let-item>\n @if (!readonly() && isEditingCell(item, 'price')) {\n <ef-inputnumber\n [(ngModel)]=\"item.productUnitPrice\"\n (ngModelChange)=\"onFieldChange(item)\"\n [showClear]=\"false\"\n [autofocus]=\"true\"\n (blurEvent)=\"stopEditCell()\"\n (keyDownEvent)=\"onEditCellKeydown($event)\"\n [min]=\"0\"\n [minFractionDigits]=\"2\"\n [maxFractionDigits]=\"2\"\n [locale]=\"locale()\"\n size=\"small\"\n class=\"!text-end\" />\n } @else {\n <button type=\"button\" class=\"ln-val\" [disabled]=\"readonly()\" (click)=\"startEditCell(item, 'price')\">\n {{ item.productUnitPrice | number: '1.2-2' : locale() }}\n </button>\n }\n </ng-template>\n\n <ng-template efColumnTemplate=\"qty\" let-item>\n @if (!readonly() && isEditingCell(item, 'qty')) {\n <ef-inputnumber\n [(ngModel)]=\"item.quantity\"\n (ngModelChange)=\"onFieldChange(item)\"\n [showClear]=\"false\"\n [autofocus]=\"true\"\n (blurEvent)=\"stopEditCell()\"\n (keyDownEvent)=\"onEditCellKeydown($event)\"\n [min]=\"0\"\n [maxFractionDigits]=\"4\"\n [locale]=\"locale()\"\n size=\"small\"\n class=\"!text-end\" />\n } @else {\n <button type=\"button\" class=\"ln-val\" [disabled]=\"readonly()\" (click)=\"startEditCell(item, 'qty')\">\n {{ item.quantity | number: '1.0-4' : locale() }}\n </button>\n }\n </ng-template>\n\n <ng-template efColumnTemplate=\"discount\" let-item>\n @if (!readonly() && isEditingCell(item, 'discount')) {\n <ef-inputnumber\n [(ngModel)]=\"item.discount\"\n (ngModelChange)=\"onFieldChange(item)\"\n [showClear]=\"false\"\n [autofocus]=\"true\"\n (blurEvent)=\"stopEditCell()\"\n (keyDownEvent)=\"onEditCellKeydown($event)\"\n [min]=\"0\"\n [max]=\"100\"\n [maxFractionDigits]=\"2\"\n suffix=\" %\"\n [locale]=\"locale()\"\n size=\"small\"\n class=\"!text-end\" />\n } @else {\n <button type=\"button\" class=\"ln-val\" [disabled]=\"readonly()\" (click)=\"startEditCell(item, 'discount')\">\n {{ item.discount | number: '1.0-2' : locale() }} %\n </button>\n }\n </ng-template>\n\n <ng-template efColumnTemplate=\"tax\" let-item>\n @if (!readonly() && isEditingCell(item, 'tva')) {\n <ef-inputnumber\n [(ngModel)]=\"item.taxRate\"\n (ngModelChange)=\"onFieldChange(item)\"\n [showClear]=\"false\"\n [autofocus]=\"true\"\n (blurEvent)=\"stopEditCell()\"\n (keyDownEvent)=\"onEditCellKeydown($event)\"\n [min]=\"0\"\n [max]=\"100\"\n [maxFractionDigits]=\"2\"\n suffix=\" %\"\n [locale]=\"locale()\"\n size=\"small\"\n class=\"!text-end\" />\n } @else {\n <button type=\"button\" class=\"ln-val\" [disabled]=\"readonly()\" (click)=\"startEditCell(item, 'tva')\">\n {{ (item.taxRate ?? 0) | number: '1.0-2' : locale() }} %\n </button>\n }\n </ng-template>\n\n <ng-template efColumnTemplate=\"subtotal\" let-item>\n {{ calculateSubtotal(item) | number: '1.2-2' : locale() }}\n </ng-template>\n\n <ng-template efColumnTemplate=\"remove\" let-item>\n <ef-button\n icon=\"pi pi-trash\"\n severity=\"danger\"\n [text]=\"true\"\n styleClass=\"ob-del\"\n (clickEvent)=\"removeOrderLine(item.id)\" />\n </ng-template>\n </ef-data-card>\n\n </div>\n\n <!-- CONTEXT RAIL (1/3) -->\n <aside class=\"ob-rail\">\n <!-- Totaux (top) -->\n <ef-card titleKey=\"order_builder.totals\" [meta]=\"currency()\">\n <div class=\"ob-totals\">\n <div class=\"row\">\n <span class=\"lbl\">{{ 'order_builder.gross' | translate }}</span>\n <span class=\"v\">{{ grossTotal() | number: '1.2-2' : locale() }}</span>\n </div>\n <div class=\"row discount\">\n <span class=\"lbl\">{{ 'order_builder.discount' | translate }}</span>\n <span class=\"v\">\u2212{{ discountTotal() | number: '1.2-2' : locale() }}</span>\n </div>\n <div class=\"row\">\n <span class=\"lbl\">{{ 'order_builder.tax' | translate }}</span>\n <span class=\"v\">{{ tvaTotal() | number: '1.2-2' : locale() }}</span>\n </div>\n <hr />\n <div class=\"row grand\">\n <span class=\"lbl\">{{ 'order_builder.grand_total' | translate }}</span>\n <span class=\"v\">{{ finalTotal() | number: '1.2-2' : locale() }}<span class=\"cur\">{{ currency() }}</span></span>\n </div>\n </div>\n </ef-card>\n\n <!-- R\u00E9capitulatif par format -->\n @if (productsSummary().items.length > 0) {\n <ef-card titleKey=\"order_builder.recap\" metaKey=\"order_builder.auto\">\n <div class=\"ob-recap\">\n <div class=\"cols\">\n @for (col of recapColumns(); track $index) {\n <div class=\"grp\">\n @for (seg of col; track seg.code) {\n <div class=\"seg\">\n <span class=\"fmt\">{{ seg.label }}</span>\n <span class=\"n\">{{ seg.count | number: '1.0-0' : locale() }}</span>\n </div>\n }\n </div>\n }\n </div>\n <div class=\"tot\">\n <span class=\"l\">{{ 'order_builder.total_units' | translate }}</span>\n <span class=\"n\">{{ productsSummary().totalCount | number: '1.0-0' : locale() }}</span>\n </div>\n </div>\n </ef-card>\n }\n\n <!-- Historique (optional, component-owned) -->\n @if (hasAudit()) {\n <ef-change-history [entries]=\"auditEntries()\" />\n }\n </aside>\n </div>\n\n <!-- Floating summary FAB + backdrop -->\n <button type=\"button\" class=\"ob-backdrop\" aria-hidden=\"true\" tabindex=\"-1\" (click)=\"toggleRail(false)\"></button>\n <div class=\"ob-fab\" role=\"status\">\n <div class=\"ft\">\n <span class=\"k\">{{ 'order_builder.total_units' | translate }} \u00B7 {{ productsSummary().totalCount }}</span>\n <span class=\"vv\">{{ finalTotal() | number: '1.2-2' : locale() }}<span class=\"cur\">{{ currency() }}</span></span>\n </div>\n <button type=\"button\" class=\"open\" (click)=\"toggleRail(true)\">\n {{ 'order_builder.summary' | translate }}\n </button>\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block}:host .order-builder{display:grid;gap:var(--s-3)}:host .order-builder .head-actions{display:flex;align-items:center;gap:6px}:host .order-builder .form-grid{display:grid;gap:var(--s-3);grid-template-columns:repeat(12,minmax(0,1fr))}:host .order-builder .form-grid>.col-3{grid-column:span 3}:host .order-builder .form-grid>.col-4{grid-column:span 4}:host .order-builder .form-grid>.col-5{grid-column:span 5}:host .order-builder .form-grid>.col-6{grid-column:span 6}@media(max-width:767px){:host .order-builder .form-grid>[class*=col-]{grid-column:span 12}}:host .order-builder .field{display:grid;gap:4px}:host .order-builder .field>label{font-size:11px;font-weight:700;color:var(--text-mute);text-transform:uppercase;letter-spacing:.06em}:host .order-builder .ob-grid{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--s-3);align-items:start}@media(min-width:1280px){:host .order-builder .ob-grid{grid-template-columns:minmax(0,2fr) minmax(0,1fr)}}:host .order-builder .ob-main{display:grid;gap:var(--s-3);min-width:0}:host .order-builder .ob-rail{display:grid;gap:var(--s-3);align-content:start}@media(min-width:1280px){:host .order-builder .ob-rail{position:sticky;top:44px}}:host .order-builder .ob-order-head{display:grid;gap:var(--s-4)}:host .order-builder .qa-grid{display:flex;flex-wrap:wrap;gap:10px;align-items:end;padding-top:var(--s-3);border-top:1px solid var(--rule-soft)}:host .order-builder .qa-combo-wrap{flex:1 1 260px;min-width:0}:host .order-builder .qa-price{flex:0 1 132px}:host .order-builder .qa-qtyf{flex:0 0 132px}:host .order-builder .qa-go{flex:0 0 auto}@media(max-width:560px){:host .order-builder .qa-combo-wrap,:host .order-builder .qa-price,:host .order-builder .qa-qtyf,:host .order-builder .qa-go{flex:1 1 100%}}:host .order-builder ef-data-card{font-size:13px}:host .order-builder ef-data-card .p{display:flex;align-items:baseline;gap:9px;line-height:1.25}:host .order-builder ef-data-card .p .ln{flex:0 0 auto;min-width:15px;text-align:end;font-family:var(--font-mono);font-size:10.5px;font-weight:700;color:var(--text-soft)}:host .order-builder ef-data-card .p .pp{display:grid;gap:1px;min-width:0}:host .order-builder ef-data-card .p .nm{font-weight:600;letter-spacing:-.005em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}:host .order-builder ef-data-card .p .nm em{font-style:normal;color:var(--text-mute);font-weight:600}:host .order-builder ef-data-card ef-inputnumber,:host .order-builder ef-data-card ef-inputnumber .ef-field-control,:host .order-builder ef-data-card ef-inputnumber input{width:100%;min-width:0}:host .order-builder ef-data-card .num,:host .order-builder ef-data-card .total{font-family:var(--font-mono);font-feature-settings:\"tnum\";font-variant-numeric:tabular-nums}:host .order-builder ef-data-card .ln-val{appearance:none;width:100%;margin:0;padding:5px 8px;border:1px solid transparent;border-radius:7px;background:transparent;font:inherit;font-family:var(--font-mono);font-feature-settings:\"tnum\";font-variant-numeric:tabular-nums;color:inherit;text-align:end;cursor:text;transition:background .12s ease,border-color .12s ease}:host .order-builder ef-data-card .ln-val:hover:not(:disabled){background:var(--paper);border-color:var(--rule)}:host .order-builder ef-data-card .ln-val:focus-visible{outline:none;background:var(--paper);border-color:var(--tenant, var(--rule))}:host .order-builder ef-data-card .ln-val:disabled{cursor:default;background:transparent;border-color:transparent}:host .order-builder ef-data-card .total{font-family:var(--font-display);font-variation-settings:\"opsz\" 12,\"wght\" 700,\"wdth\" 85;font-size:13.5px}:host .order-builder ef-data-card .cur{font-size:9.5px;color:var(--text-soft);margin-inline-start:3px}:host .order-builder .ob-lines-head{display:flex;align-items:baseline;gap:6px;min-width:0}:host .order-builder .ob-lines-head .strong{font-weight:700;color:var(--text)}:host .order-builder .ob-lines-head .meta{color:var(--text-soft)}:host .order-builder .ob-del{color:var(--text-soft)}:host .order-builder .ob-del:hover{color:var(--st-cancelled-fg);border-color:var(--st-cancelled-bg)}:host .order-builder .ob-totals{display:grid;gap:6px}:host .order-builder .ob-totals .row{display:flex;justify-content:space-between;align-items:baseline;gap:14px}:host .order-builder .ob-totals .row .lbl{font-size:12px;font-weight:600;color:var(--text-mute)}:host .order-builder .ob-totals .row .v{font-family:var(--font-mono);font-feature-settings:\"tnum\";font-variant-numeric:tabular-nums;font-weight:700;font-size:12.5px}:host .order-builder .ob-totals .row.discount .v{color:var(--st-pending-fg)}:host .order-builder .ob-totals hr{border:0;border-top:1px solid var(--rule);margin:5px 0}:host .order-builder .ob-totals .grand .lbl{font-family:var(--font-display);font-variation-settings:\"opsz\" 12,\"wght\" 700,\"wdth\" 85;font-size:13.5px;color:var(--text)}:host .order-builder .ob-totals .grand .v{font-family:var(--font-display);font-variation-settings:\"opsz\" 96,\"wght\" 700,\"wdth\" 80;font-size:26px;letter-spacing:-.03em;color:var(--text)}:host .order-builder .ob-totals .grand .cur{font-family:var(--font-mono);font-size:.4em;color:var(--text-mute);margin-inline-start:4px}:host .order-builder .ob-recap .cols{display:grid;grid-template-columns:1fr 1fr}:host .order-builder .ob-recap .grp{display:flex;flex-direction:column;gap:3px;min-width:0}:host .order-builder .ob-recap .grp:first-child{padding-inline-end:14px}:host .order-builder .ob-recap .grp+.grp{padding-inline-start:14px;border-inline-start:1px solid var(--rule)}:host .order-builder .ob-recap .seg{display:flex;justify-content:space-between;align-items:baseline;gap:8px}:host .order-builder .ob-recap .seg .fmt{font-size:11.5px;font-weight:700;color:var(--text);letter-spacing:-.005em;min-width:0}:host .order-builder .ob-recap .seg .fmt:after{content:\":\";color:var(--text-soft)}:host .order-builder .ob-recap .seg .n{font-family:var(--font-mono);font-weight:700;font-size:12px;color:var(--text);font-variant-numeric:tabular-nums;flex-shrink:0}:host .order-builder .ob-recap .tot{display:flex;justify-content:space-between;align-items:baseline;margin-top:12px;padding-top:10px;border-top:1px solid var(--rule)}:host .order-builder .ob-recap .tot .l{font-weight:700;font-size:12px}:host .order-builder .ob-recap .tot .n{font-family:var(--font-display);font-variation-settings:\"opsz\" 12,\"wght\" 700,\"wdth\" 85;font-size:18px;font-variant-numeric:tabular-nums}:host .order-builder .ob-fab{display:none;position:fixed;inset-inline-end:20px;bottom:calc(var(--app-bottom-bar, 0px) + env(safe-area-inset-bottom,0px) + 20px);z-index:30;align-items:center;gap:10px;--ob-fab-ground: var(--ink-active);--ob-fab-on: var(--paper);background:var(--ob-fab-ground);color:var(--ob-fab-on);border-radius:var(--r-pill);padding:8px 10px 8px 16px;box-shadow:var(--shadow-3)}:host .order-builder .ob-fab .ft{display:grid;line-height:1.1}:host .order-builder .ob-fab .ft .k{font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;opacity:.65}:host .order-builder .ob-fab .ft .vv{font-family:var(--font-display);font-variation-settings:\"opsz\" 12,\"wght\" 700,\"wdth\" 82;font-size:16px}:host .order-builder .ob-fab .ft .vv .cur{font-family:var(--font-mono);font-size:.6em;opacity:.7;margin-inline-start:3px}:host .order-builder .ob-fab .open{appearance:none;cursor:pointer;border:0;background:color-mix(in srgb,var(--ob-fab-on) 14%,transparent);color:var(--ob-fab-on);font:inherit;font-size:12px;font-weight:700;border-radius:var(--r-pill);padding:8px 14px;display:inline-flex;align-items:center;gap:6px}:host .order-builder .ob-fab .open:hover{background:color-mix(in srgb,var(--ob-fab-on) 24%,transparent)}:host .order-builder .ob-fab .open:focus-visible{outline:2px solid var(--ob-fab-on);outline-offset:2px}:host .order-builder .ob-backdrop{display:none;position:fixed;inset:0;z-index:38;background:#0f11156b;-webkit-backdrop-filter:blur(1px);backdrop-filter:blur(1px)}:host .order-builder.ob-collapsed .ob-grid{grid-template-columns:minmax(0,1fr)}:host .order-builder.ob-collapsed .ob-fab{display:flex}:host .order-builder.ob-collapsed .ob-rail{position:fixed;top:0;inset-inline-end:0;height:100dvh;width:min(360px,92vw);z-index:40;background:var(--paper);border-inline-start:1px solid var(--rule);box-shadow:var(--shadow-3);padding:14px;overflow:auto;gap:var(--s-3);transform:translate(101%);transition:transform var(--t-base) var(--ease-out)}[dir=rtl] :host .order-builder.ob-collapsed .ob-rail{transform:translate(-101%)}:host .order-builder.ob-collapsed.ob-rail-open .ob-rail{transform:none}:host .order-builder.ob-collapsed.ob-rail-open .ob-backdrop{display:block}@media(max-width:1279px){:host .order-builder .ob-grid{grid-template-columns:minmax(0,1fr)}:host .order-builder .ob-fab{display:flex}:host .order-builder .ob-rail{position:fixed;top:0;inset-inline-end:0;height:100dvh;width:min(360px,92vw);z-index:40;background:var(--paper);border-inline-start:1px solid var(--rule);box-shadow:var(--shadow-3);padding:14px;overflow:auto;gap:var(--s-3);transform:translate(101%);transition:transform var(--t-base) var(--ease-out)}[dir=rtl] :host .order-builder .ob-rail{transform:translate(-101%)}:host .order-builder.ob-rail-open .ob-rail{transform:none}:host .order-builder.ob-rail-open .ob-backdrop{display:block}}:host ef-data-card .nm,:host ef-data-card .num,:host ef-data-card .ln-val,:host .ob-totals .v{unicode-bidi:plaintext}@media(max-width:767px){:host .order-builder{padding-bottom:calc(var(--app-bottom-bar, 0px) + env(safe-area-inset-bottom,0px) + 20px + 56px + var(--s-4))}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "component", type: EfSelectComponent, selector: "ef-select", inputs: ["options", "optionLabel", "optionValue", "optionDisabled", "dataKey", "group", "optionGroupLabel", "optionGroupChildren", "size", "fluid", "inline", "appendTo", "multiple", "filter", "filterBy", "filterPlaceholder", "filterPlaceholderKey", "autofocusFilter", "autofocus", "editable", "loading", "virtualScroll", "virtualScrollItemSize", "scrollHeight", "multiDisplay", "showToggleAll", "emptyMessage", "emptyMessageKey", "emptyFilterMessage", "emptyFilterMessageKey", "styleClass", "panelStyleClass", "filterByKeys", "variant", "value"], outputs: ["selectionChangeEvent", "filterChangeEvent", "panelShowEvent", "panelHideEvent"] }, { kind: "component", type: EfDatepickerAdvancedComponent, selector: "ef-datepicker-advanced", inputs: ["triggerLabelKey", "triggerLabel", "presets", "activePreset", "start", "end", "allowCustomRange", "mode", "selectionMode", "variant", "showTime"], outputs: ["rangeChange", "opened", "closed"] }, { kind: "component", type: EfInputNumberComponent, selector: "ef-inputnumber", inputs: ["value", "invalid", "format", "showButtons", "buttonLayout", "incrementButtonClass", "decrementButtonClass", "incrementButtonIcon", "decrementButtonIcon", "prefix", "suffix", "currency", "currencyDisplay", "locale", "localeMatcher", "mode", "useGrouping", "minFractionDigits", "maxFractionDigits", "min", "max", "step", "allowEmpty", "inputSize", "size", "maxlength", "minlength", "pattern", "tabindex", "title", "ariaLabelledBy", "inputStyle", "inputStyleClass", "style", "styleClass", "primeNgVariant", "autofocus", "autocomplete", "fluid", "inline", "variant", "pt", "ptOptions", "dt", "unstyled"], outputs: ["inputEvent", "valueChangeEvent", "focusEvent", "blurEvent", "keyDownEvent", "clearEvent"] }, { kind: "component", type: EfLabelComponent, selector: "ef-label", inputs: ["label", "labelKey", "for", "styleClass", "required"] }, { kind: "component", type: EfButtonComponent, selector: "ef-button", inputs: ["label", "labelKey", "icon", "variant", "severity", "size", "disabled", "rounded", "outlined", "raised", "text", "loading", "styleClass", "type", "badge", "badgeSeverity", "tooltip", "tooltipKey", "tooltipPosition", "autofocus"], outputs: ["clickEvent"] }, { kind: "component", type: EfCardComponent, selector: "ef-card", inputs: ["titleKey", "title", "metaKey", "meta", "tone", "forceHead", "hasFoot", "collapsible", "overflowVisible", "loading", "skeleton", "skeletonRows", "skeletonHeight", "collapsed"], outputs: ["collapsedChange"] }, { kind: "component", type: EfChangeHistoryComponent, selector: "ef-change-history", inputs: ["entries", "titleKey", "emptyKey", "limit", "previewLimit", "collapsible", "startCollapsed"] }, { kind: "component", type: EfProductTypeaheadComponent, selector: "ef-product-typeahead", inputs: ["products", "productKeyField", "placeholderKey", "disabled", "currency", "locale", "showVariants"], outputs: ["productSelect"] }, { kind: "component", type: EfDataCardComponent, selector: "ef-data-card", inputs: ["mobileLayout", "columns", "rows", "trackByField", "pageNumber", "pageSize", "totalCount", "pageSizeOptions", "hidePager", "sort", "loading", "errorMsg", "loadingKey", "rowDoubleClickable", "hasActionsColumn", "rowActionsContext", "showViewAction", "showEditAction", "showDuplicateAction", "showDeleteAction", "showDensityControl", "showColumnPicker", "showExportControl", "exporting", "screenContext", "searchTerm", "tableKey", "defaultDensity", "reorderable", "fixedLayout"], outputs: ["pageChange", "pageSizeChange", "sortChange", "rowDoubleClick", "rowAction", "exportRequest", "clearSearch", "retry", "rowReorder"] }, { kind: "directive", type: EfColumnTemplateDirective, selector: "[efColumnTemplate]", inputs: ["efColumnTemplate"] }, { kind: "pipe", type: i1$3.DecimalPipe, name: "number" }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }] });
|
|
5887
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: EfOrderBuilderComponent, isStandalone: true, selector: "ef-order-builder", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null }, order: { classPropertyName: "order", publicName: "order", isSignal: true, isRequired: true, transformFunction: null }, products: { classPropertyName: "products", publicName: "products", isSignal: true, isRequired: true, transformFunction: null }, customers: { classPropertyName: "customers", publicName: "customers", isSignal: true, isRequired: true, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, countGroupLabels: { classPropertyName: "countGroupLabels", publicName: "countGroupLabels", isSignal: true, isRequired: false, transformFunction: null }, productKeyField: { classPropertyName: "productKeyField", publicName: "productKeyField", isSignal: true, isRequired: false, transformFunction: null }, backendErrors: { classPropertyName: "backendErrors", publicName: "backendErrors", isSignal: true, isRequired: false, transformFunction: null }, auditEntries: { classPropertyName: "auditEntries", publicName: "auditEntries", isSignal: true, isRequired: false, transformFunction: null }, focusMode: { classPropertyName: "focusMode", publicName: "focusMode", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { order: "orderChange", openCatalogue: "openCatalogue", orderChanged: "orderChanged", orderValidated: "orderValidated", productsAdded: "productsAdded", focusMode: "focusModeChange" }, viewQueries: [{ propertyName: "typeahead", first: true, predicate: EfProductTypeaheadComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"order-builder\"\n [class.ob-collapsed]=\"focusMode()\"\n [class.ob-rail-open]=\"railOpen()\">\n <div class=\"ob-grid\">\n <div class=\"ob-main\">\n <!-- Order header: customer + date + express add (one card) -->\n <ef-card\n titleKey=\"order_builder.section_order\"\n [metaKey]=\"readonly() ? '' : 'order_builder.quick_add_hint'\"\n [overflowVisible]=\"true\">\n <div class=\"ob-order-head\">\n <div class=\"form-grid\">\n <div class=\"field col-6\">\n <ef-label labelKey=\"label.customer\" for=\"ob-cust\" [required]=\"true\" />\n <ef-select\n inputId=\"ob-cust\"\n [options]=\"customers()\"\n optionLabel=\"companyName\"\n optionValue=\"customerId\"\n [ngModel]=\"order().customerId\"\n (ngModelChange)=\"updateCustomer($event)\"\n [disabled]=\"readonly()\"\n [showClear]=\"true\"\n [filter]=\"true\"\n filterBy=\"companyName\"\n filterPlaceholderKey=\"label.search_customer\"\n appendTo=\"body\"\n placeholderKey=\"label.select_customer\" />\n </div>\n <div class=\"field col-3\">\n <ef-label labelKey=\"label.order_date\" for=\"ob-date\" [required]=\"true\" />\n <ef-datepicker-advanced\n mode=\"field\"\n selectionMode=\"single\"\n inputId=\"ob-date\"\n placeholderKey=\"date_select_date\"\n [ngModel]=\"order().orderDate\"\n (ngModelChange)=\"updateOrderDate($event)\"\n [disabled]=\"readonly()\" />\n </div>\n </div>\n\n @if (!readonly()) {\n <div class=\"qa-grid\">\n <div class=\"qa-combo-wrap\">\n <ef-label labelKey=\"label.article\" />\n <ef-product-typeahead\n [products]=\"products()\"\n [productKeyField]=\"productKeyField()\"\n placeholderKey=\"label.search_product\"\n [currency]=\"currency()\"\n [locale]=\"locale()\"\n (productSelect)=\"onTypeaheadSelect($event)\"\n [showVariants]=\"false\" />\n </div>\n <div class=\"qa-price\">\n <ef-label labelKey=\"label.sale_price\" />\n <ef-inputnumber\n variant=\"comptoir\"\n [value]=\"quickAddPrice()\"\n (valueChangeEvent)=\"quickAddPrice.set($event ?? 0)\"\n [minFractionDigits]=\"2\"\n [maxFractionDigits]=\"2\"\n [locale]=\"locale()\"\n [disabled]=\"readonly()\" />\n </div>\n <div class=\"qa-qtyf\">\n <ef-label labelKey=\"label.quantity\" />\n <ef-inputnumber\n variant=\"comptoir\"\n [ngModel]=\"quickAddQuantity()\"\n (ngModelChange)=\"quickAddQuantity.set($event)\"\n [min]=\"0\"\n [maxFractionDigits]=\"4\"\n [locale]=\"locale()\"\n [disabled]=\"readonly()\"\n />\n </div>\n <div class=\"qa-go\">\n <!-- Ghost while the preconditions are unmet, tenant once the\n line is addable. A disabled control painted in a faded\n tenant fill reads as an enabled button that lost its\n colour, which is the greyed-out accent the design brief\n bans; ghost reads as \"not yet\". -->\n <ef-button\n labelKey=\"action.add\"\n icon=\"pi pi-plus\"\n variant=\"comptoir\"\n size=\"base\"\n [severity]=\"quickAddDisabled() ? 'ghost' : 'tenant'\"\n [disabled]=\"quickAddDisabled()\"\n (clickEvent)=\"addQuickProduct()\" />\n </div>\n </div>\n }\n </div>\n </ef-card>\n\n <!-- Lines.\n\n ef-data-card owns the panel here instead of sitting inside an\n ef-card: `.tbl-wrap` already draws a bordered, rounded surface, so\n nesting it in another one would be a card inside a card. The title\n and the card's actions move into the head slots it provides.\n\n The columns are declared in TS (`lineColumns`) so discount and tax\n follow the document config, and so one definition drives both the\n desktop table and the mobile cards. Only the article column is\n elastic; every other column holds a number of known magnitude and\n gets a fixed track, which keeps the decimals aligned down the\n table rather than shifting with the longest description. -->\n <ef-data-card\n [columns]=\"lineColumns()\"\n [rows]=\"orderLines()\"\n trackByField=\"id\"\n [hidePager]=\"true\"\n defaultDensity=\"compact\"\n [reorderable]=\"!readonly()\"\n [fixedLayout]=\"true\"\n (rowReorder)=\"reorderLine($event)\"\n mobileLayout=\"card\">\n\n <div tbl-head-info class=\"ob-lines-head\">\n <span class=\"strong\">{{ 'order_builder.lines' | translate }}</span>\n <span class=\"meta\">\u00B7 {{ orderLines().length }}</span>\n </div>\n\n <div tbl-head-actions class=\"head-actions\">\n @if (config().showCatalogue && !readonly()) {\n <ef-button\n labelKey=\"action.open_catalogue\"\n icon=\"pi pi-box\"\n severity=\"secondary\"\n [text]=\"true\"\n (clickEvent)=\"handleOpenCatalogue()\" />\n }\n <ef-button\n [labelKey]=\"focusMode() ? 'order_builder.show_summary' : 'order_builder.fullscreen'\"\n icon=\"pi pi-window-maximize\"\n severity=\"secondary\"\n [text]=\"true\"\n (clickEvent)=\"toggleFocus()\" />\n </div>\n\n <span empty-state>{{ 'order_builder.empty' | translate }}</span>\n <span empty-state-mobile>{{ 'order_builder.empty' | translate }}</span>\n\n <ng-template efColumnTemplate=\"product\" let-item let-i=\"index\">\n <div class=\"p\">\n <span class=\"ln\">{{ i + 1 }}</span>\n <span class=\"pp\">\n <span class=\"nm\">{{ item.productDescription }}</span>\n </span>\n </div>\n </ng-template>\n\n <ng-template efColumnTemplate=\"price\" let-item>\n @if (!readonly() && isEditingCell(item, 'price')) {\n <ef-inputnumber\n [(ngModel)]=\"item.productUnitPrice\"\n (ngModelChange)=\"onFieldChange(item)\"\n [showClear]=\"false\"\n [autofocus]=\"true\"\n (blurEvent)=\"stopEditCell()\"\n (keyDownEvent)=\"onEditCellKeydown($event)\"\n [min]=\"0\"\n [minFractionDigits]=\"2\"\n [maxFractionDigits]=\"2\"\n [locale]=\"locale()\"\n size=\"small\"\n class=\"!text-end\" />\n } @else {\n <button type=\"button\" class=\"ln-val\" [disabled]=\"readonly()\" (click)=\"startEditCell(item, 'price')\">\n {{ item.productUnitPrice | number: '1.2-2' : locale() }}\n </button>\n }\n </ng-template>\n\n <ng-template efColumnTemplate=\"qty\" let-item>\n @if (!readonly() && isEditingCell(item, 'qty')) {\n <ef-inputnumber\n [(ngModel)]=\"item.quantity\"\n (ngModelChange)=\"onFieldChange(item)\"\n [showClear]=\"false\"\n [autofocus]=\"true\"\n (blurEvent)=\"stopEditCell()\"\n (keyDownEvent)=\"onEditCellKeydown($event)\"\n [min]=\"0\"\n [maxFractionDigits]=\"4\"\n [locale]=\"locale()\"\n size=\"small\"\n class=\"!text-end\" />\n } @else {\n <button type=\"button\" class=\"ln-val\" [disabled]=\"readonly()\" (click)=\"startEditCell(item, 'qty')\">\n {{ item.quantity | number: '1.0-4' : locale() }}\n </button>\n }\n </ng-template>\n\n <ng-template efColumnTemplate=\"discount\" let-item>\n @if (!readonly() && isEditingCell(item, 'discount')) {\n <ef-inputnumber\n [(ngModel)]=\"item.discount\"\n (ngModelChange)=\"onFieldChange(item)\"\n [showClear]=\"false\"\n [autofocus]=\"true\"\n (blurEvent)=\"stopEditCell()\"\n (keyDownEvent)=\"onEditCellKeydown($event)\"\n [min]=\"0\"\n [max]=\"100\"\n [maxFractionDigits]=\"2\"\n suffix=\" %\"\n [locale]=\"locale()\"\n size=\"small\"\n class=\"!text-end\" />\n } @else {\n <button type=\"button\" class=\"ln-val\" [disabled]=\"readonly()\" (click)=\"startEditCell(item, 'discount')\">\n {{ item.discount | number: '1.0-2' : locale() }} %\n </button>\n }\n </ng-template>\n\n <ng-template efColumnTemplate=\"tax\" let-item>\n @if (!readonly() && isEditingCell(item, 'tva')) {\n <ef-inputnumber\n [(ngModel)]=\"item.taxRate\"\n (ngModelChange)=\"onFieldChange(item)\"\n [showClear]=\"false\"\n [autofocus]=\"true\"\n (blurEvent)=\"stopEditCell()\"\n (keyDownEvent)=\"onEditCellKeydown($event)\"\n [min]=\"0\"\n [max]=\"100\"\n [maxFractionDigits]=\"2\"\n suffix=\" %\"\n [locale]=\"locale()\"\n size=\"small\"\n class=\"!text-end\" />\n } @else {\n <button type=\"button\" class=\"ln-val\" [disabled]=\"readonly()\" (click)=\"startEditCell(item, 'tva')\">\n {{ (item.taxRate ?? 0) | number: '1.0-2' : locale() }} %\n </button>\n }\n </ng-template>\n\n <ng-template efColumnTemplate=\"subtotal\" let-item>\n {{ calculateSubtotal(item) | number: '1.2-2' : locale() }}\n </ng-template>\n\n <ng-template efColumnTemplate=\"remove\" let-item>\n <ef-button\n icon=\"pi pi-trash\"\n severity=\"danger\"\n [text]=\"true\"\n styleClass=\"ob-del\"\n (clickEvent)=\"removeOrderLine(item.id)\" />\n </ng-template>\n </ef-data-card>\n\n </div>\n\n <!-- CONTEXT RAIL (1/3) -->\n <aside class=\"ob-rail\">\n <!-- Totaux (top) -->\n <ef-card titleKey=\"order_builder.totals\" [meta]=\"currency()\">\n <div class=\"ob-totals\">\n <div class=\"row\">\n <span class=\"lbl\">{{ 'order_builder.gross' | translate }}</span>\n <span class=\"v\">{{ grossTotal() | number: '1.2-2' : locale() }}</span>\n </div>\n <div class=\"row discount\">\n <span class=\"lbl\">{{ 'order_builder.discount' | translate }}</span>\n <span class=\"v\">\u2212{{ discountTotal() | number: '1.2-2' : locale() }}</span>\n </div>\n <div class=\"row\">\n <span class=\"lbl\">{{ 'order_builder.tax' | translate }}</span>\n <span class=\"v\">{{ tvaTotal() | number: '1.2-2' : locale() }}</span>\n </div>\n <hr />\n <div class=\"row grand\">\n <span class=\"lbl\">{{ 'order_builder.grand_total' | translate }}</span>\n <span class=\"v\">{{ finalTotal() | number: '1.2-2' : locale() }}<span class=\"cur\">{{ currency() }}</span></span>\n </div>\n </div>\n </ef-card>\n\n <!-- R\u00E9capitulatif par format -->\n @if (productsSummary().items.length > 0) {\n <ef-card titleKey=\"order_builder.recap\" metaKey=\"order_builder.auto\">\n <div class=\"ob-recap\">\n <div class=\"cols\">\n @for (col of recapColumns(); track $index) {\n <div class=\"grp\">\n @for (seg of col; track seg.code) {\n <div class=\"seg\">\n <span class=\"fmt\">{{ seg.label }}</span>\n <span class=\"n\">{{ seg.count | number: '1.0-0' : locale() }}</span>\n </div>\n }\n </div>\n }\n </div>\n <div class=\"tot\">\n <span class=\"l\">{{ 'order_builder.total_units' | translate }}</span>\n <span class=\"n\">{{ productsSummary().totalCount | number: '1.0-0' : locale() }}</span>\n </div>\n </div>\n </ef-card>\n }\n\n <!-- Historique (optional, component-owned) -->\n @if (hasAudit()) {\n <ef-change-history [entries]=\"auditEntries()\" />\n }\n </aside>\n </div>\n\n <!-- Floating summary FAB + backdrop -->\n <button type=\"button\" class=\"ob-backdrop\" aria-hidden=\"true\" tabindex=\"-1\" (click)=\"toggleRail(false)\"></button>\n <div class=\"ob-fab\" role=\"status\">\n <div class=\"ft\">\n <span class=\"k\">{{ 'order_builder.total_units' | translate }} \u00B7 {{ productsSummary().totalCount }}</span>\n <span class=\"vv\">{{ finalTotal() | number: '1.2-2' : locale() }}<span class=\"cur\">{{ currency() }}</span></span>\n </div>\n <button type=\"button\" class=\"open\" (click)=\"toggleRail(true)\">\n {{ 'order_builder.summary' | translate }}\n </button>\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block}:host .order-builder{display:grid;gap:var(--s-3)}:host .order-builder .head-actions{display:flex;align-items:center;gap:6px}:host .order-builder .form-grid{display:grid;gap:var(--s-3);grid-template-columns:repeat(12,minmax(0,1fr))}:host .order-builder .form-grid>.col-3{grid-column:span 3}:host .order-builder .form-grid>.col-4{grid-column:span 4}:host .order-builder .form-grid>.col-5{grid-column:span 5}:host .order-builder .form-grid>.col-6{grid-column:span 6}@media(max-width:767px){:host .order-builder .form-grid>[class*=col-]{grid-column:span 12}}:host .order-builder .field{display:grid;gap:4px}:host .order-builder .field>label{font-size:11px;font-weight:700;color:var(--text-mute);text-transform:uppercase;letter-spacing:.06em}:host .order-builder .ob-grid{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--s-3);align-items:start}@media(min-width:1280px){:host .order-builder .ob-grid{grid-template-columns:minmax(0,2fr) minmax(0,1fr)}}:host .order-builder .ob-main{display:grid;gap:var(--s-3);min-width:0}:host .order-builder .ob-rail{display:grid;gap:var(--s-3);align-content:start}@media(min-width:1280px){:host .order-builder .ob-rail{position:sticky;top:44px}}:host .order-builder .ob-order-head{display:grid;gap:var(--s-4)}:host .order-builder .qa-grid{display:flex;flex-wrap:wrap;gap:10px;align-items:end;padding-top:var(--s-3);border-top:1px solid var(--rule-soft)}:host .order-builder .qa-combo-wrap{flex:1 1 260px;min-width:0}:host .order-builder .qa-price{flex:0 1 132px}:host .order-builder .qa-qtyf{flex:0 0 132px}:host .order-builder .qa-go{flex:0 0 auto}@media(max-width:560px){:host .order-builder .qa-combo-wrap,:host .order-builder .qa-price,:host .order-builder .qa-qtyf,:host .order-builder .qa-go{flex:1 1 100%}}:host .order-builder ef-data-card{font-size:13px}:host .order-builder ef-data-card .p{display:flex;align-items:baseline;gap:9px;line-height:1.25}:host .order-builder ef-data-card .p .ln{flex:0 0 auto;min-width:15px;text-align:end;font-family:var(--font-mono);font-size:10.5px;font-weight:700;color:var(--text-soft)}:host .order-builder ef-data-card .p .pp{display:grid;gap:1px;min-width:0}:host .order-builder ef-data-card .p .nm{font-weight:600;letter-spacing:-.005em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}:host .order-builder ef-data-card .p .nm em{font-style:normal;color:var(--text-mute);font-weight:600}:host .order-builder ef-data-card ef-inputnumber,:host .order-builder ef-data-card ef-inputnumber .ef-field-control,:host .order-builder ef-data-card ef-inputnumber input{width:100%;min-width:0}:host .order-builder ef-data-card .num,:host .order-builder ef-data-card .total{font-family:var(--font-mono);font-feature-settings:\"tnum\";font-variant-numeric:tabular-nums}:host .order-builder ef-data-card .ln-val{appearance:none;width:100%;margin:0;padding:5px 8px;border:1px solid transparent;border-radius:7px;background:transparent;font:inherit;font-family:var(--font-mono);font-feature-settings:\"tnum\";font-variant-numeric:tabular-nums;color:inherit;text-align:end;cursor:text;transition:background .12s ease,border-color .12s ease}:host .order-builder ef-data-card .ln-val:hover:not(:disabled){background:var(--paper);border-color:var(--rule)}:host .order-builder ef-data-card .ln-val:focus-visible{outline:none;background:var(--paper);border-color:var(--tenant, var(--rule))}:host .order-builder ef-data-card .ln-val:disabled{cursor:default;background:transparent;border-color:transparent}:host .order-builder ef-data-card .total{font-family:var(--font-display);font-variation-settings:\"opsz\" 12,\"wght\" 700,\"wdth\" 85;font-size:13.5px}:host .order-builder ef-data-card .cur{font-size:9.5px;color:var(--text-soft);margin-inline-start:3px}:host .order-builder .ob-lines-head{display:flex;align-items:baseline;gap:6px;min-width:0}:host .order-builder .ob-lines-head .strong{font-weight:700;color:var(--text)}:host .order-builder .ob-lines-head .meta{color:var(--text-soft)}:host .order-builder .ob-del{color:var(--text-soft)}:host .order-builder .ob-del:hover{color:var(--st-cancelled-fg);border-color:var(--st-cancelled-bg)}:host .order-builder .ob-totals{display:grid;gap:6px}:host .order-builder .ob-totals .row{display:flex;justify-content:space-between;align-items:baseline;gap:14px}:host .order-builder .ob-totals .row .lbl{font-size:12px;font-weight:600;color:var(--text-mute)}:host .order-builder .ob-totals .row .v{font-family:var(--font-mono);font-feature-settings:\"tnum\";font-variant-numeric:tabular-nums;font-weight:700;font-size:12.5px}:host .order-builder .ob-totals .row.discount .v{color:var(--st-pending-fg)}:host .order-builder .ob-totals hr{border:0;border-top:1px solid var(--rule);margin:5px 0}:host .order-builder .ob-totals .grand .lbl{font-family:var(--font-display);font-variation-settings:\"opsz\" 12,\"wght\" 700,\"wdth\" 85;font-size:13.5px;color:var(--text)}:host .order-builder .ob-totals .grand .v{font-family:var(--font-display);font-variation-settings:\"opsz\" 96,\"wght\" 700,\"wdth\" 80;font-size:26px;letter-spacing:-.03em;color:var(--text)}:host .order-builder .ob-totals .grand .cur{font-family:var(--font-mono);font-size:.4em;color:var(--text-mute);margin-inline-start:4px}:host .order-builder .ob-recap .cols{display:grid;grid-template-columns:1fr 1fr}:host .order-builder .ob-recap .grp{display:flex;flex-direction:column;gap:3px;min-width:0}:host .order-builder .ob-recap .grp:first-child{padding-inline-end:14px}:host .order-builder .ob-recap .grp+.grp{padding-inline-start:14px;border-inline-start:1px solid var(--rule)}:host .order-builder .ob-recap .seg{display:flex;justify-content:space-between;align-items:baseline;gap:8px}:host .order-builder .ob-recap .seg .fmt{font-size:11.5px;font-weight:700;color:var(--text);letter-spacing:-.005em;min-width:0}:host .order-builder .ob-recap .seg .fmt:after{content:\":\";color:var(--text-soft)}:host .order-builder .ob-recap .seg .n{font-family:var(--font-mono);font-weight:700;font-size:12px;color:var(--text);font-variant-numeric:tabular-nums;flex-shrink:0}:host .order-builder .ob-recap .tot{display:flex;justify-content:space-between;align-items:baseline;margin-top:12px;padding-top:10px;border-top:1px solid var(--rule)}:host .order-builder .ob-recap .tot .l{font-weight:700;font-size:12px}:host .order-builder .ob-recap .tot .n{font-family:var(--font-display);font-variation-settings:\"opsz\" 12,\"wght\" 700,\"wdth\" 85;font-size:18px;font-variant-numeric:tabular-nums}:host .order-builder .ob-fab{display:none;position:fixed;inset-inline-end:20px;bottom:calc(var(--app-bottom-bar, 0px) + env(safe-area-inset-bottom,0px) + 20px);z-index:30;align-items:center;gap:10px;--ob-fab-ground: var(--ink-active);--ob-fab-on: var(--paper);background:var(--ob-fab-ground);color:var(--ob-fab-on);border-radius:var(--r-pill);padding:8px 10px 8px 16px;box-shadow:var(--shadow-3)}:host .order-builder .ob-fab .ft{display:grid;line-height:1.1}:host .order-builder .ob-fab .ft .k{font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;opacity:.65}:host .order-builder .ob-fab .ft .vv{font-family:var(--font-display);font-variation-settings:\"opsz\" 12,\"wght\" 700,\"wdth\" 82;font-size:16px}:host .order-builder .ob-fab .ft .vv .cur{font-family:var(--font-mono);font-size:.6em;opacity:.7;margin-inline-start:3px}:host .order-builder .ob-fab .open{appearance:none;cursor:pointer;border:0;background:color-mix(in srgb,var(--ob-fab-on) 14%,transparent);color:var(--ob-fab-on);font:inherit;font-size:12px;font-weight:700;border-radius:var(--r-pill);padding:8px 14px;display:inline-flex;align-items:center;gap:6px}:host .order-builder .ob-fab .open:hover{background:color-mix(in srgb,var(--ob-fab-on) 24%,transparent)}:host .order-builder .ob-fab .open:focus-visible{outline:2px solid var(--ob-fab-on);outline-offset:2px}:host .order-builder .ob-backdrop{display:none;position:fixed;inset:0;z-index:38;background:#0f11156b;-webkit-backdrop-filter:blur(1px);backdrop-filter:blur(1px)}:host .order-builder.ob-collapsed .ob-grid{grid-template-columns:minmax(0,1fr)}:host .order-builder.ob-collapsed .ob-fab{display:flex}:host .order-builder.ob-collapsed .ob-rail{position:fixed;top:0;inset-inline-end:0;height:100dvh;width:min(360px,92vw);z-index:40;background:var(--paper);border-inline-start:1px solid var(--rule);box-shadow:var(--shadow-3);padding:14px;overflow:auto;gap:var(--s-3);transform:translate(101%);transition:transform var(--t-base) var(--ease-out)}[dir=rtl] :host .order-builder.ob-collapsed .ob-rail{transform:translate(-101%)}:host .order-builder.ob-collapsed.ob-rail-open .ob-rail{transform:none}:host .order-builder.ob-collapsed.ob-rail-open .ob-backdrop{display:block}@media(max-width:1279px){:host .order-builder .ob-grid{grid-template-columns:minmax(0,1fr)}:host .order-builder .ob-fab{display:flex}:host .order-builder .ob-rail{position:fixed;top:0;inset-inline-end:0;height:100dvh;width:min(360px,92vw);z-index:40;background:var(--paper);border-inline-start:1px solid var(--rule);box-shadow:var(--shadow-3);padding:14px;overflow:auto;gap:var(--s-3);transform:translate(101%);transition:transform var(--t-base) var(--ease-out)}[dir=rtl] :host .order-builder .ob-rail{transform:translate(-101%)}:host .order-builder.ob-rail-open .ob-rail{transform:none}:host .order-builder.ob-rail-open .ob-backdrop{display:block}}:host ef-data-card .nm,:host ef-data-card .num,:host ef-data-card .ln-val,:host .ob-totals .v{unicode-bidi:plaintext}@media(max-width:767px){:host .order-builder{padding-bottom:calc(var(--app-bottom-bar, 0px) + env(safe-area-inset-bottom,0px) + 20px + 56px + var(--s-4))}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "component", type: EfSelectComponent, selector: "ef-select", inputs: ["options", "optionLabel", "optionValue", "optionDisabled", "dataKey", "group", "optionGroupLabel", "optionGroupChildren", "size", "fluid", "inline", "appendTo", "multiple", "filter", "filterBy", "filterPlaceholder", "filterPlaceholderKey", "autofocusFilter", "autofocus", "editable", "loading", "virtualScroll", "virtualScrollItemSize", "scrollHeight", "multiDisplay", "showToggleAll", "emptyMessage", "emptyMessageKey", "emptyFilterMessage", "emptyFilterMessageKey", "styleClass", "panelStyleClass", "filterByKeys", "variant", "value"], outputs: ["selectionChangeEvent", "filterChangeEvent", "panelShowEvent", "panelHideEvent"] }, { kind: "component", type: EfDatepickerAdvancedComponent, selector: "ef-datepicker-advanced", inputs: ["triggerLabelKey", "triggerLabel", "presets", "activePreset", "start", "end", "allowCustomRange", "allowAllTime", "mode", "selectionMode", "variant", "showTime"], outputs: ["rangeChange", "opened", "closed"] }, { kind: "component", type: EfInputNumberComponent, selector: "ef-inputnumber", inputs: ["value", "invalid", "format", "showButtons", "buttonLayout", "incrementButtonClass", "decrementButtonClass", "incrementButtonIcon", "decrementButtonIcon", "prefix", "suffix", "currency", "currencyDisplay", "locale", "localeMatcher", "mode", "useGrouping", "minFractionDigits", "maxFractionDigits", "min", "max", "step", "allowEmpty", "inputSize", "size", "maxlength", "minlength", "pattern", "tabindex", "title", "ariaLabelledBy", "inputStyle", "inputStyleClass", "style", "styleClass", "primeNgVariant", "autofocus", "autocomplete", "fluid", "inline", "variant", "pt", "ptOptions", "dt", "unstyled"], outputs: ["inputEvent", "valueChangeEvent", "focusEvent", "blurEvent", "keyDownEvent", "clearEvent"] }, { kind: "component", type: EfLabelComponent, selector: "ef-label", inputs: ["label", "labelKey", "for", "styleClass", "required"] }, { kind: "component", type: EfButtonComponent, selector: "ef-button", inputs: ["label", "labelKey", "icon", "variant", "severity", "size", "disabled", "rounded", "outlined", "raised", "text", "loading", "styleClass", "type", "badge", "badgeSeverity", "tooltip", "tooltipKey", "tooltipPosition", "autofocus"], outputs: ["clickEvent"] }, { kind: "component", type: EfCardComponent, selector: "ef-card", inputs: ["titleKey", "title", "metaKey", "meta", "tone", "forceHead", "hasFoot", "collapsible", "overflowVisible", "loading", "skeleton", "skeletonRows", "skeletonHeight", "collapsed"], outputs: ["collapsedChange"] }, { kind: "component", type: EfChangeHistoryComponent, selector: "ef-change-history", inputs: ["entries", "titleKey", "emptyKey", "limit", "previewLimit", "collapsible", "startCollapsed"] }, { kind: "component", type: EfProductTypeaheadComponent, selector: "ef-product-typeahead", inputs: ["products", "productKeyField", "placeholderKey", "disabled", "currency", "locale", "showVariants"], outputs: ["productSelect"] }, { kind: "component", type: EfDataCardComponent, selector: "ef-data-card", inputs: ["mobileLayout", "columns", "rows", "trackByField", "pageNumber", "pageSize", "totalCount", "pageSizeOptions", "hidePager", "sort", "loading", "errorMsg", "loadingKey", "rowDoubleClickable", "hasActionsColumn", "rowActionsContext", "showViewAction", "showEditAction", "showDuplicateAction", "showDeleteAction", "showDensityControl", "showColumnPicker", "showExportControl", "exporting", "screenContext", "searchTerm", "tableKey", "defaultDensity", "reorderable", "fixedLayout"], outputs: ["pageChange", "pageSizeChange", "sortChange", "rowDoubleClick", "rowAction", "exportRequest", "clearSearch", "retry", "rowReorder"] }, { kind: "directive", type: EfColumnTemplateDirective, selector: "[efColumnTemplate]", inputs: ["efColumnTemplate"] }, { kind: "pipe", type: i1$3.DecimalPipe, name: "number" }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }] });
|
|
5857
5888
|
}
|
|
5858
5889
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfOrderBuilderComponent, decorators: [{
|
|
5859
5890
|
type: Component,
|
|
@@ -7472,15 +7503,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
|
|
|
7472
7503
|
args: ['document:keydown.escape']
|
|
7473
7504
|
}] } });
|
|
7474
7505
|
|
|
7475
|
-
const SIDE_COLLAPSED_KEY = 'ef.shell.side-collapsed';
|
|
7476
|
-
function readSideCollapsed() {
|
|
7477
|
-
try {
|
|
7478
|
-
return localStorage.getItem(SIDE_COLLAPSED_KEY) === '1';
|
|
7479
|
-
}
|
|
7480
|
-
catch {
|
|
7481
|
-
return false;
|
|
7482
|
-
}
|
|
7483
|
-
}
|
|
7484
7506
|
/**
|
|
7485
7507
|
* Top-level Comptoir shell.
|
|
7486
7508
|
*
|
|
@@ -7528,20 +7550,22 @@ class EfAppShellComponent {
|
|
|
7528
7550
|
inside the side, so it is still there to bring the side back
|
|
7529
7551
|
once it is gone, and in the same spot. Remembered per browser:
|
|
7530
7552
|
it is a reading preference, not something worth a round trip. */
|
|
7531
|
-
|
|
7553
|
+
prefs = inject(EF_PREFERENCE_STORE);
|
|
7554
|
+
sideCollapsed = signal(this.prefs.get(EF_SIDE_COLLAPSED_KEY) === '1', ...(ngDevMode ? [{ debugName: "sideCollapsed" }] : /* istanbul ignore next */ []));
|
|
7532
7555
|
/** Shown in the toggle's tooltip: the button is far from the content, the shortcut is not. */
|
|
7533
7556
|
shortcutHint = formatShortcut('mod+b');
|
|
7534
7557
|
toggleSide() {
|
|
7535
7558
|
const collapsed = !this.sideCollapsed();
|
|
7536
7559
|
this.sideCollapsed.set(collapsed);
|
|
7537
|
-
|
|
7538
|
-
localStorage.setItem(SIDE_COLLAPSED_KEY, collapsed ? '1' : '0');
|
|
7539
|
-
}
|
|
7540
|
-
catch {
|
|
7541
|
-
/* private window or blocked storage: the toggle still works for this visit */
|
|
7542
|
-
}
|
|
7560
|
+
this.prefs.set(EF_SIDE_COLLAPSED_KEY, collapsed ? '1' : '0');
|
|
7543
7561
|
}
|
|
7544
7562
|
constructor() {
|
|
7563
|
+
// A store that hydrates after sign in announces the key it filled.
|
|
7564
|
+
this.prefs.changes?.pipe(takeUntilDestroyed()).subscribe((key) => {
|
|
7565
|
+
if (key === EF_SIDE_COLLAPSED_KEY) {
|
|
7566
|
+
this.sideCollapsed.set(this.prefs.get(EF_SIDE_COLLAPSED_KEY) === '1');
|
|
7567
|
+
}
|
|
7568
|
+
});
|
|
7545
7569
|
inject(EfShortcutService).register({
|
|
7546
7570
|
id: 'shell.toggle-side',
|
|
7547
7571
|
keys: 'mod+b',
|
|
@@ -10888,14 +10912,22 @@ class EfSmartBarComponent {
|
|
|
10888
10912
|
only text inside this label is the shortcut hint, so
|
|
10889
10913
|
without it a screen reader announced the primary
|
|
10890
10914
|
search field of every list screen as "Cmd K". -->
|
|
10915
|
+
<!-- type="search" fires a native, bubbling 'search' Event
|
|
10916
|
+
on Enter and on the browser's clear button (Chromium,
|
|
10917
|
+
WebKit). A host (search) binding hears that DOM event
|
|
10918
|
+
as well as the output below, so it would receive the
|
|
10919
|
+
Event as the search text. Stopped here; the output is
|
|
10920
|
+
the only 'search' that leaves this component. -->
|
|
10891
10921
|
<input
|
|
10892
10922
|
#searchInput
|
|
10893
10923
|
type="search"
|
|
10924
|
+
enterkeyhint="search"
|
|
10894
10925
|
[attr.aria-label]="accessibleLabel()"
|
|
10895
10926
|
[placeholder]="(placeholderKey() ? (placeholderKey() | translate) : placeholder())"
|
|
10896
10927
|
[ngModel]="searchText()"
|
|
10897
10928
|
(ngModelChange)="searchText.set($event)"
|
|
10898
10929
|
(keydown.enter)="onSubmit()"
|
|
10930
|
+
(search)="$event.stopPropagation()"
|
|
10899
10931
|
/>
|
|
10900
10932
|
@if (showKbdHint() && kbdLabel()) {
|
|
10901
10933
|
<!-- Decorative, and hidden from the accessibility tree:
|
|
@@ -10935,15 +10967,11 @@ class EfSmartBarComponent {
|
|
|
10935
10967
|
|
|
10936
10968
|
<ng-content select="[drawer]"></ng-content>
|
|
10937
10969
|
</div>
|
|
10938
|
-
`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
10970
|
+
`, isInline: true, styles: ["input[type=search]::-webkit-search-cancel-button{-webkit-appearance:none;appearance:none;display:none}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
10939
10971
|
}
|
|
10940
10972
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfSmartBarComponent, decorators: [{
|
|
10941
10973
|
type: Component,
|
|
10942
|
-
args: [{
|
|
10943
|
-
selector: 'ef-smart-bar',
|
|
10944
|
-
standalone: true,
|
|
10945
|
-
imports: [CommonModule, FormsModule, TranslateModule],
|
|
10946
|
-
template: `
|
|
10974
|
+
args: [{ selector: 'ef-smart-bar', standalone: true, imports: [CommonModule, FormsModule, TranslateModule], template: `
|
|
10947
10975
|
<div class="screen-search">
|
|
10948
10976
|
<div class="smart-bar">
|
|
10949
10977
|
<label class="search">
|
|
@@ -10951,14 +10979,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
|
|
|
10951
10979
|
only text inside this label is the shortcut hint, so
|
|
10952
10980
|
without it a screen reader announced the primary
|
|
10953
10981
|
search field of every list screen as "Cmd K". -->
|
|
10982
|
+
<!-- type="search" fires a native, bubbling 'search' Event
|
|
10983
|
+
on Enter and on the browser's clear button (Chromium,
|
|
10984
|
+
WebKit). A host (search) binding hears that DOM event
|
|
10985
|
+
as well as the output below, so it would receive the
|
|
10986
|
+
Event as the search text. Stopped here; the output is
|
|
10987
|
+
the only 'search' that leaves this component. -->
|
|
10954
10988
|
<input
|
|
10955
10989
|
#searchInput
|
|
10956
10990
|
type="search"
|
|
10991
|
+
enterkeyhint="search"
|
|
10957
10992
|
[attr.aria-label]="accessibleLabel()"
|
|
10958
10993
|
[placeholder]="(placeholderKey() ? (placeholderKey() | translate) : placeholder())"
|
|
10959
10994
|
[ngModel]="searchText()"
|
|
10960
10995
|
(ngModelChange)="searchText.set($event)"
|
|
10961
10996
|
(keydown.enter)="onSubmit()"
|
|
10997
|
+
(search)="$event.stopPropagation()"
|
|
10962
10998
|
/>
|
|
10963
10999
|
@if (showKbdHint() && kbdLabel()) {
|
|
10964
11000
|
<!-- Decorative, and hidden from the accessibility tree:
|
|
@@ -10998,9 +11034,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
|
|
|
10998
11034
|
|
|
10999
11035
|
<ng-content select="[drawer]"></ng-content>
|
|
11000
11036
|
</div>
|
|
11001
|
-
`,
|
|
11002
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
11003
|
-
}]
|
|
11037
|
+
`, changeDetection: ChangeDetectionStrategy.OnPush, styles: ["input[type=search]::-webkit-search-cancel-button{-webkit-appearance:none;appearance:none;display:none}\n"] }]
|
|
11004
11038
|
}], ctorParameters: () => [], propDecorators: { searchInput: [{ type: i0.ViewChild, args: ['searchInput', { isSignal: true }] }], searchText: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchText", required: false }] }, { type: i0.Output, args: ["searchTextChange"] }], placeholderKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholderKey", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], clearLabelKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearLabelKey", required: false }] }], clearTitleKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearTitleKey", required: false }] }], searchLabelKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchLabelKey", required: false }] }], showKbdHint: [{ type: i0.Input, args: [{ isSignal: true, alias: "showKbdHint", required: false }] }], hideClear: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideClear", required: false }] }], hideSearch: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideSearch", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], shortcut: [{ type: i0.Input, args: [{ isSignal: true, alias: "shortcut", required: false }] }], search: [{ type: i0.Output, args: ["search"] }], clear: [{ type: i0.Output, args: ["clear"] }] } });
|
|
11005
11039
|
|
|
11006
11040
|
/**
|