@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.
@@ -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'] && !changes['filterValidValues'].firstChange) {
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
- this.FILTER_KEY = `filters_${this.tableConfiguration.key ?? 'table'}`;
2035
- this.COLUMN_KEY = `columns_${this.tableConfiguration.key ?? 'table'}`;
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
- // 2) Subscribe once with debounce and emit normalized payload
2085
- this.filtersForm.valueChanges
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
- this.persistState(payload, this.FILTER_KEY);
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
- this.persistState(payload, this.FILTER_KEY);
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) ? v.filter((x) => x != null && x !== '') : [];
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
- if (val == null)
2321
- patch[f.key] = [];
2322
- else if (!Array.isArray(val))
2323
- patch[f.key] = [val];
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.get(control.key)?.setValue([min, max], { emitEvent: true });
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' && !Array.isArray(state) && Object.keys(state).length === 0);
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) => (x && typeof x === 'object' ? x[labelField] : String(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
- this.allData = [...this.originalData];
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
- if (this.tableConfiguration?.key) {
3274
- this.SORT_KEY = `sort_${this.tableConfiguration.key}`;
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