@fuentis/phoenix-ui 0.0.9-alpha.636 → 0.0.9-alpha.638
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/fuentis-phoenix-ui.mjs +136 -21
- package/fesm2022/fuentis-phoenix-ui.mjs.map +1 -1
- package/index.d.ts +19 -1
- package/package.json +1 -1
|
@@ -2009,6 +2009,8 @@ class TableCaptionComponent {
|
|
|
2009
2009
|
/** Local storage keys */
|
|
2010
2010
|
FILTER_KEY = '';
|
|
2011
2011
|
COLUMN_KEY = '';
|
|
2012
|
+
/** Live subscription to the current filter form; torn down on each rebuild. */
|
|
2013
|
+
filtersValueSub;
|
|
2012
2014
|
/**
|
|
2013
2015
|
* When table detects that selected filter values no longer exist in data,
|
|
2014
2016
|
* it passes the remaining valid values here so table-caption can prune its form.
|
|
@@ -2023,7 +2025,8 @@ class TableCaptionComponent {
|
|
|
2023
2025
|
*/
|
|
2024
2026
|
lastSignature = '';
|
|
2025
2027
|
ngOnChanges(changes) {
|
|
2026
|
-
if (changes['filterValidValues'] &&
|
|
2028
|
+
if (changes['filterValidValues'] &&
|
|
2029
|
+
!changes['filterValidValues'].firstChange) {
|
|
2027
2030
|
const validMap = changes['filterValidValues'].currentValue;
|
|
2028
2031
|
if (validMap)
|
|
2029
2032
|
this.applyValidValuesPatch(validMap);
|
|
@@ -2031,8 +2034,17 @@ class TableCaptionComponent {
|
|
|
2031
2034
|
}
|
|
2032
2035
|
if (!this.tableConfiguration)
|
|
2033
2036
|
return;
|
|
2034
|
-
|
|
2035
|
-
|
|
2037
|
+
// Storage key: prefer an explicit `stateKey` (unique per table view; avoids
|
|
2038
|
+
// the collisions where `key` doubles as the row-id field, e.g. two tables
|
|
2039
|
+
// sharing `filters_controls`), fall back to `key`. `stateKeyPrefix` (e.g. the
|
|
2040
|
+
// current user id) scopes state per user so a shared machine does not leak
|
|
2041
|
+
// one user's filters/columns to another.
|
|
2042
|
+
const stateBase = this.tableConfiguration.stateKey ?? this.tableConfiguration.key ?? 'table';
|
|
2043
|
+
const statePrefix = this.tableConfiguration.stateKeyPrefix
|
|
2044
|
+
? `${this.tableConfiguration.stateKeyPrefix}.`
|
|
2045
|
+
: '';
|
|
2046
|
+
this.FILTER_KEY = `filters_${statePrefix}${stateBase}`;
|
|
2047
|
+
this.COLUMN_KEY = `columns_${statePrefix}${stateBase}`;
|
|
2036
2048
|
// Default visible columns: all non-hidden columns
|
|
2037
2049
|
this._selectedColumns = (this.columns ?? []).filter((c) => !c?.hidden);
|
|
2038
2050
|
// Restore columns once when config/columns arrive
|
|
@@ -2075,25 +2087,40 @@ class TableCaptionComponent {
|
|
|
2075
2087
|
? !!f.selected
|
|
2076
2088
|
: t === 'range'
|
|
2077
2089
|
? this.normalizeRangeInitial(f.selected, f)
|
|
2078
|
-
: f.selected ?? '';
|
|
2090
|
+
: (f.selected ?? '');
|
|
2079
2091
|
fg.addControl(f.key, this.fb.control(initialValue));
|
|
2080
2092
|
}
|
|
2081
2093
|
this.filtersForm = fg;
|
|
2082
2094
|
// 1) Restore persisted values (but only valid keys)
|
|
2083
2095
|
this.restoreFilterState(filtersCfg);
|
|
2084
|
-
//
|
|
2085
|
-
|
|
2096
|
+
// An empty `filtersCfg` means the filter options are not built yet (they are
|
|
2097
|
+
// derived from the loaded table data), NOT that the user cleared their
|
|
2098
|
+
// filters. Persisting here would write an empty payload and, via
|
|
2099
|
+
// persistState(), DELETE the saved state before the real config arrives on
|
|
2100
|
+
// the next build — the init-order wipe. Only persist once a real filter
|
|
2101
|
+
// config exists; a genuine "user cleared" is an empty value with a non-empty
|
|
2102
|
+
// config and still persists (and removes) correctly.
|
|
2103
|
+
const hasFilterConfig = filtersCfg.length > 0;
|
|
2104
|
+
// 2) Subscribe with debounce and emit normalized payload.
|
|
2105
|
+
// buildFiltersForm() runs again whenever the filter signature changes (e.g.
|
|
2106
|
+
// range bounds move on a data reload), so tear down the previous form's
|
|
2107
|
+
// subscription first — otherwise they accumulate for the component's whole
|
|
2108
|
+
// lifetime and emit/persist in multiples.
|
|
2109
|
+
this.filtersValueSub?.unsubscribe();
|
|
2110
|
+
this.filtersValueSub = this.filtersForm.valueChanges
|
|
2086
2111
|
.pipe(takeUntilDestroyed(this.dr), debounceTime(120), map((raw) => this.normalizeOutgoingPayload(raw, filtersCfg)))
|
|
2087
2112
|
.subscribe((payload) => {
|
|
2088
2113
|
this.applyFiltersEvent.emit(payload);
|
|
2089
|
-
|
|
2114
|
+
if (hasFilterConfig)
|
|
2115
|
+
this.persistState(payload, this.FILTER_KEY);
|
|
2090
2116
|
});
|
|
2091
2117
|
// 3) Emit initial state (normalized) once
|
|
2092
2118
|
queueMicrotask(() => {
|
|
2093
2119
|
const raw = this.filtersForm.getRawValue();
|
|
2094
2120
|
const payload = this.normalizeOutgoingPayload(raw, filtersCfg);
|
|
2095
2121
|
this.applyFiltersEvent.emit(payload);
|
|
2096
|
-
|
|
2122
|
+
if (hasFilterConfig)
|
|
2123
|
+
this.persistState(payload, this.FILTER_KEY);
|
|
2097
2124
|
});
|
|
2098
2125
|
}
|
|
2099
2126
|
/**
|
|
@@ -2263,7 +2290,9 @@ class TableCaptionComponent {
|
|
|
2263
2290
|
continue;
|
|
2264
2291
|
}
|
|
2265
2292
|
if (t === 'multiselect') {
|
|
2266
|
-
const arr = Array.isArray(v)
|
|
2293
|
+
const arr = Array.isArray(v)
|
|
2294
|
+
? v.filter((x) => x != null && x !== '')
|
|
2295
|
+
: [];
|
|
2267
2296
|
if (arr.length)
|
|
2268
2297
|
out[key] = arr;
|
|
2269
2298
|
continue;
|
|
@@ -2293,6 +2322,11 @@ class TableCaptionComponent {
|
|
|
2293
2322
|
* NOTE:
|
|
2294
2323
|
* - boolean-multiselect is restored into OPTION OBJECTS (for UI),
|
|
2295
2324
|
* even though persisted payload is boolean[].
|
|
2325
|
+
* - Since `options` are rebuilt from the currently loaded table data (see
|
|
2326
|
+
* createDynamicFilters), a value selected in one context (e.g. a different
|
|
2327
|
+
* Entity in "Add link") may no longer exist in the current options. We
|
|
2328
|
+
* revalidate multiselect/dropdown selections against the current options
|
|
2329
|
+
* on every restore, so stale values don't render as broken/empty entries.
|
|
2296
2330
|
*/
|
|
2297
2331
|
restoreFilterState(cfg) {
|
|
2298
2332
|
const stored = localStorage.getItem(this.FILTER_KEY);
|
|
@@ -2308,25 +2342,39 @@ class TableCaptionComponent {
|
|
|
2308
2342
|
if (known.has(k))
|
|
2309
2343
|
patch[k] = parsed[k];
|
|
2310
2344
|
}
|
|
2345
|
+
let changed = false;
|
|
2311
2346
|
for (const f of cfg) {
|
|
2312
2347
|
const t = f.type ?? 'text';
|
|
2348
|
+
const options = Array.isArray(f.options) ? f.options : [];
|
|
2349
|
+
const ov = f.optionValue ?? 'key';
|
|
2313
2350
|
if (t === 'boolean-multiselect') {
|
|
2314
|
-
// parsed payload is boolean[]; map -> option objects
|
|
2351
|
+
// parsed payload is boolean[]; map -> option objects (already validated against options)
|
|
2315
2352
|
patch[f.key] = this.normalizeBooleanMultiSelection(patch[f.key], f);
|
|
2316
2353
|
continue;
|
|
2317
2354
|
}
|
|
2318
2355
|
if (t === 'multiselect') {
|
|
2319
2356
|
const val = patch[f.key];
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2357
|
+
const arr = val == null ? [] : Array.isArray(val) ? val : [val];
|
|
2358
|
+
const validValues = new Set(options.map((o) => o?.[ov]));
|
|
2359
|
+
const filtered = arr.filter((x) => validValues.has(x));
|
|
2360
|
+
if (filtered.length !== arr.length)
|
|
2361
|
+
changed = true;
|
|
2362
|
+
patch[f.key] = filtered;
|
|
2324
2363
|
continue;
|
|
2325
2364
|
}
|
|
2326
2365
|
if (t === 'checkbox') {
|
|
2327
2366
|
patch[f.key] = !!patch[f.key];
|
|
2328
2367
|
continue;
|
|
2329
2368
|
}
|
|
2369
|
+
if (t === 'dropdown' || t === 'person-dropdown') {
|
|
2370
|
+
const val = patch[f.key];
|
|
2371
|
+
const isEmpty = val == null || val === '';
|
|
2372
|
+
const isValid = isEmpty || options.some((o) => o?.[ov] === val);
|
|
2373
|
+
if (!isValid) {
|
|
2374
|
+
patch[f.key] = '';
|
|
2375
|
+
changed = true;
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2330
2378
|
if (t === 'range') {
|
|
2331
2379
|
// persisted payload is { type:'range', min, max }; map -> [from, to]
|
|
2332
2380
|
const stored = patch[f.key];
|
|
@@ -2340,6 +2388,11 @@ class TableCaptionComponent {
|
|
|
2340
2388
|
}
|
|
2341
2389
|
}
|
|
2342
2390
|
this.filtersForm.patchValue(patch, { emitEvent: false });
|
|
2391
|
+
// Persist the pruned state so stale (non-existent) values don't linger in storage.
|
|
2392
|
+
if (changed) {
|
|
2393
|
+
const payload = this.normalizeOutgoingPayload(this.filtersForm.getRawValue(), cfg);
|
|
2394
|
+
this.persistState(payload, this.FILTER_KEY);
|
|
2395
|
+
}
|
|
2343
2396
|
}
|
|
2344
2397
|
catch {
|
|
2345
2398
|
// ignore invalid state
|
|
@@ -2433,7 +2486,9 @@ class TableCaptionComponent {
|
|
|
2433
2486
|
/** Reset a range filter back to its full [min, max] bounds. */
|
|
2434
2487
|
resetRangeFilter(control) {
|
|
2435
2488
|
const [min, max] = this.rangeBounds(control);
|
|
2436
|
-
this.filtersForm
|
|
2489
|
+
this.filtersForm
|
|
2490
|
+
.get(control.key)
|
|
2491
|
+
?.setValue([min, max], { emitEvent: true });
|
|
2437
2492
|
}
|
|
2438
2493
|
resetFilters() {
|
|
2439
2494
|
this.filtersForm.reset({}, { emitEvent: true });
|
|
@@ -2443,7 +2498,9 @@ class TableCaptionComponent {
|
|
|
2443
2498
|
persistState(state, key) {
|
|
2444
2499
|
const isEmpty = state == null ||
|
|
2445
2500
|
(Array.isArray(state) && state.length === 0) ||
|
|
2446
|
-
(typeof state === 'object' &&
|
|
2501
|
+
(typeof state === 'object' &&
|
|
2502
|
+
!Array.isArray(state) &&
|
|
2503
|
+
Object.keys(state).length === 0);
|
|
2447
2504
|
if (isEmpty)
|
|
2448
2505
|
localStorage.removeItem(key);
|
|
2449
2506
|
else
|
|
@@ -2456,7 +2513,7 @@ class TableCaptionComponent {
|
|
|
2456
2513
|
const arr = Array.isArray(value) ? value : [];
|
|
2457
2514
|
const labelField = this.getOptionLabelField(control);
|
|
2458
2515
|
return arr
|
|
2459
|
-
.map((x) =>
|
|
2516
|
+
.map((x) => x && typeof x === 'object' ? x[labelField] : String(x ?? ''))
|
|
2460
2517
|
.filter((s) => !!s)
|
|
2461
2518
|
.join(', ');
|
|
2462
2519
|
}
|
|
@@ -3245,7 +3302,14 @@ class TableComponent {
|
|
|
3245
3302
|
}
|
|
3246
3303
|
}
|
|
3247
3304
|
}
|
|
3248
|
-
|
|
3305
|
+
// Re-apply any active filters to the refreshed dataset. Previously this
|
|
3306
|
+
// reset to the full (unfiltered) originalData, so every SSE / reload dropped
|
|
3307
|
+
// the user's active filters until the caption happened to re-emit — the
|
|
3308
|
+
// "filters lost on reload" bug that consumers worked around app-side.
|
|
3309
|
+
this.allData =
|
|
3310
|
+
Object.keys(this.currentFilters).length > 0
|
|
3311
|
+
? this.getFilteredData()
|
|
3312
|
+
: [...this.originalData];
|
|
3249
3313
|
this.lastSortKey = '';
|
|
3250
3314
|
// allow re-applying initial sort for new dataset
|
|
3251
3315
|
this.initialSortApplied = false;
|
|
@@ -3270,8 +3334,16 @@ class TableComponent {
|
|
|
3270
3334
|
if (changes['filters']) {
|
|
3271
3335
|
this.enrichRangeFilters();
|
|
3272
3336
|
}
|
|
3273
|
-
|
|
3274
|
-
|
|
3337
|
+
// Keep the sort key on the same scheme as filters/columns (see
|
|
3338
|
+
// table-caption): honour `stateKey` (fallback `key`) and `stateKeyPrefix`
|
|
3339
|
+
// so sort state neither collides across tables sharing `key` nor leaks
|
|
3340
|
+
// between users on a shared machine.
|
|
3341
|
+
const sortBase = this.tableConfiguration?.stateKey ?? this.tableConfiguration?.key;
|
|
3342
|
+
if (sortBase) {
|
|
3343
|
+
const sortPrefix = this.tableConfiguration?.stateKeyPrefix
|
|
3344
|
+
? `${this.tableConfiguration.stateKeyPrefix}.`
|
|
3345
|
+
: '';
|
|
3346
|
+
this.SORT_KEY = `sort_${sortPrefix}${sortBase}`;
|
|
3275
3347
|
}
|
|
3276
3348
|
// Build columnTypeMap for sorting normalization
|
|
3277
3349
|
if (this.columns?.length > 0) {
|
|
@@ -8449,6 +8521,49 @@ var SimpleButtonType;
|
|
|
8449
8521
|
SimpleButtonType["SPLIT"] = "split";
|
|
8450
8522
|
})(SimpleButtonType || (SimpleButtonType = {}));
|
|
8451
8523
|
|
|
8524
|
+
/**
|
|
8525
|
+
* localStorage key prefixes the data-table persists its per-view UI state under.
|
|
8526
|
+
* Kept in sync with the key scheme in table-caption (filters/columns) and
|
|
8527
|
+
* table.component (sort): `${prefix}${stateKeyPrefix}.${stateKey ?? key}`.
|
|
8528
|
+
*/
|
|
8529
|
+
const TABLE_STATE_PREFIXES = ['filters_', 'columns_', 'sort_'];
|
|
8530
|
+
/**
|
|
8531
|
+
* Remove the data-table's persisted UI state (filters, selected columns, sort)
|
|
8532
|
+
* from localStorage. Call this on logout so a user's saved table views do not
|
|
8533
|
+
* carry over into the next login on a shared machine.
|
|
8534
|
+
*
|
|
8535
|
+
* @param userPrefix When the app persists state user-scoped
|
|
8536
|
+
* (`tableConfiguration.stateKeyPrefix`, e.g. the user id), pass the SAME value
|
|
8537
|
+
* to clear only that user's state — other users' saved views on this browser
|
|
8538
|
+
* stay intact. Omit to clear all persisted table state on this browser.
|
|
8539
|
+
*/
|
|
8540
|
+
function clearPersistedTableState(userPrefix) {
|
|
8541
|
+
let store;
|
|
8542
|
+
try {
|
|
8543
|
+
store = localStorage;
|
|
8544
|
+
}
|
|
8545
|
+
catch {
|
|
8546
|
+
return; // storage disabled / unavailable — nothing to clear
|
|
8547
|
+
}
|
|
8548
|
+
// Scoped keys look like `filters_<userPrefix>.<base>`; the trailing dot makes
|
|
8549
|
+
// the match exact so prefix "1" never also clears user "12"'s state.
|
|
8550
|
+
const scoped = userPrefix ? `${userPrefix}.` : '';
|
|
8551
|
+
const doomed = [];
|
|
8552
|
+
for (let i = 0; i < store.length; i++) {
|
|
8553
|
+
const key = store.key(i);
|
|
8554
|
+
if (!key)
|
|
8555
|
+
continue;
|
|
8556
|
+
for (const p of TABLE_STATE_PREFIXES) {
|
|
8557
|
+
if (key.startsWith(p + scoped)) {
|
|
8558
|
+
doomed.push(key);
|
|
8559
|
+
break;
|
|
8560
|
+
}
|
|
8561
|
+
}
|
|
8562
|
+
}
|
|
8563
|
+
for (const key of doomed)
|
|
8564
|
+
store.removeItem(key);
|
|
8565
|
+
}
|
|
8566
|
+
|
|
8452
8567
|
class StripHtmlSafePipe {
|
|
8453
8568
|
transform(value) {
|
|
8454
8569
|
if (value === null || value === undefined)
|
|
@@ -11253,5 +11368,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
|
|
|
11253
11368
|
* Generated bundle index. Do not edit.
|
|
11254
11369
|
*/
|
|
11255
11370
|
|
|
11256
|
-
export { ActionTypes, ActionsComponent, CardComponent, ContexObjectComponent, ControlType, GroupsFormComponent, InnerHeaderComponent, META_FORM_ASYNC_EXECUTOR, MetaFormButtonsComponent, MetaFormButtonsV2Component, MetaFormComponent, MetaFormFieldV2Component, MetaFormGroupV2Component, MetaFormService, MetaFormV2Component, MetaSubmitValidatorService, ObjectItemDialogComponent, QuickPickComponent, QuickPickSidePanelComponent, SearchBarComponent, ShellComponent, SidebarComponent, SidebarItemComponent, SimpleButtonType, StatusBarComponent, StatusColType, StatusHeaderComponent, StatusTooltipType, TableComponent, TopbarComponent, UserComponent, tableActionType, tableButtonContext, tableButtonFormat, tableColumnType, tableFilterType, tableSelectionType };
|
|
11371
|
+
export { ActionTypes, ActionsComponent, CardComponent, ContexObjectComponent, ControlType, GroupsFormComponent, InnerHeaderComponent, META_FORM_ASYNC_EXECUTOR, MetaFormButtonsComponent, MetaFormButtonsV2Component, MetaFormComponent, MetaFormFieldV2Component, MetaFormGroupV2Component, MetaFormService, MetaFormV2Component, MetaSubmitValidatorService, ObjectItemDialogComponent, QuickPickComponent, QuickPickSidePanelComponent, SearchBarComponent, ShellComponent, SidebarComponent, SidebarItemComponent, SimpleButtonType, StatusBarComponent, StatusColType, StatusHeaderComponent, StatusTooltipType, TableComponent, TopbarComponent, UserComponent, clearPersistedTableState, tableActionType, tableButtonContext, tableButtonFormat, tableColumnType, tableFilterType, tableSelectionType };
|
|
11257
11372
|
//# sourceMappingURL=fuentis-phoenix-ui.mjs.map
|