@atscript/ui-table 0.1.132 → 0.1.133

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/dist/index.cjs CHANGED
@@ -149,13 +149,22 @@ function unescapeRegex(input) {
149
149
  //#region src/filters/filter-input-format.ts
150
150
  /**
151
151
  * Coerce a raw string value to the appropriate JS type for the column.
152
- * Number columns get numeric values; everything else stays as string.
152
+ * Number columns get numeric values, boolean columns understand the
153
+ * `true` / `false` vocabulary (case-insensitive) so the condition carries a
154
+ * real boolean and round-trips through the URL as one. Anything else stays
155
+ * a string. Since 0.1.133 for booleans.
153
156
  */
154
157
  function coerceValue(raw, columnType) {
155
158
  if (columnType === "number") {
156
159
  const n = Number(raw);
157
160
  return Number.isNaN(n) ? raw : n;
158
161
  }
162
+ if (columnType === "boolean") {
163
+ const lower = raw.toLowerCase();
164
+ if (lower === "true") return true;
165
+ if (lower === "false") return false;
166
+ return raw;
167
+ }
159
168
  return raw;
160
169
  }
161
170
  /** Default condition type when no symbol matches the input. */
@@ -580,6 +589,10 @@ function derivePresetAspects(content) {
580
589
  * Convert in-memory dict-form snapshot to the wire form persisted on the
581
590
  * server. Entries-arrays are sorted by `field` so consumers (server-side
582
591
  * aspect derivation, dirty detection, equality checks) see a stable order.
592
+ *
593
+ * Empty aspects round-trip as empty (since 0.1.133): a saved view with no
594
+ * filters owns `filterOps` and clears them on apply, which is not the same
595
+ * thing as a view that never claimed the aspect.
583
596
  */
584
597
  function toWireSnapshot(snapshot) {
585
598
  const wire = {};
@@ -592,7 +605,7 @@ function toWireSnapshot(snapshot) {
592
605
  field,
593
606
  width: columnWidths[field]
594
607
  });
595
- if (entries.length > 0) columns.columnWidths = entries;
608
+ columns.columnWidths = entries;
596
609
  }
597
610
  wire.columns = columns;
598
611
  }
@@ -603,7 +616,7 @@ function toWireSnapshot(snapshot) {
603
616
  field,
604
617
  conditions: snapshot.filterOps[field]
605
618
  });
606
- if (entries.length > 0) wire.filterOps = entries;
619
+ wire.filterOps = entries;
607
620
  }
608
621
  if (snapshot.sorters) wire.sorters = snapshot.sorters;
609
622
  if (snapshot.itemsPerPage !== void 0) wire.itemsPerPage = snapshot.itemsPerPage;
@@ -614,7 +627,7 @@ function fromWireSnapshot(wire) {
614
627
  if (wire.columns) {
615
628
  const { columnNames, columnWidths } = wire.columns;
616
629
  const columns = { columnNames };
617
- if (columnWidths && columnWidths.length > 0) {
630
+ if (columnWidths) {
618
631
  const dict = {};
619
632
  for (const entry of columnWidths) dict[entry.field] = entry.width;
620
633
  columns.columnWidths = dict;
@@ -622,7 +635,7 @@ function fromWireSnapshot(wire) {
622
635
  snapshot.columns = columns;
623
636
  }
624
637
  if (wire.filters) snapshot.filters = wire.filters;
625
- if (wire.filterOps && wire.filterOps.length > 0) {
638
+ if (wire.filterOps) {
626
639
  const dict = {};
627
640
  for (const entry of wire.filterOps) dict[entry.field] = entry.conditions;
628
641
  snapshot.filterOps = dict;
@@ -819,7 +832,7 @@ function draftMatchesPreset(draft, presetSnapshot, availableAspects) {
819
832
  /**
820
833
  * Framework-agnostic wrapper over `@atscript/db-client`'s `Client` for the
821
834
  * `AsPresetEntry` table. Handles wire serialisation, list-splitting by
822
- * `type`, capabilities side-channel, and 401/403 → `denied` semantics.
835
+ * `type`, capabilities side-channel, and 401/403/404 → `denied` semantics.
823
836
  *
824
837
  * Stateless: every method is a fresh request. The Vue composable layer
825
838
  * holds reactive state; this class only translates intent → HTTP.
@@ -845,8 +858,8 @@ var PresetsClient = class {
845
858
  * `(app, tableKey)`. By default also fetches `capabilities` in parallel —
846
859
  * pass `{ capabilities: false }` for refresh-after-mutation calls (fav
847
860
  * toggle, default change, save/save-as, public toggle, rename, delete)
848
- * where role-derived capabilities can't have changed. Auth errors
849
- * (401/403) collapse to `denied: true` with empty data so the UI hides
861
+ * where role-derived capabilities can't have changed. Unavailable
862
+ * responses (401/403/404) collapse to `denied: true` with empty data so the UI hides
850
863
  * itself silently.
851
864
  */
852
865
  async list(opts = {}) {
@@ -858,7 +871,7 @@ var PresetsClient = class {
858
871
  };
859
872
  try {
860
873
  const [rows, capabilities] = await Promise.all([this.client.query({ filter }), fetchCapabilities ? this.loadCapabilities().catch((err) => {
861
- if (isAuthError(err)) throw err;
874
+ if (isUnavailableError(err)) throw err;
862
875
  return null;
863
876
  }) : Promise.resolve(void 0)]);
864
877
  const presets = [];
@@ -872,7 +885,7 @@ var PresetsClient = class {
872
885
  denied: false
873
886
  };
874
887
  } catch (err) {
875
- if (isAuthError(err)) return {
888
+ if (isUnavailableError(err)) return {
876
889
  presets: [],
877
890
  userConf: null,
878
891
  capabilities: null,
@@ -988,12 +1001,22 @@ var PresetsHttpError = class extends Error {
988
1001
  this.name = "PresetsHttpError";
989
1002
  }
990
1003
  };
991
- /** True for HTTP 401/403 across both `ClientError` and `PresetsHttpError`. */
992
- function isAuthError(err) {
993
- if (err instanceof _atscript_db_client.ClientError && (err.status === 401 || err.status === 403)) return true;
994
- if (err instanceof PresetsHttpError && (err.status === 401 || err.status === 403)) return true;
995
- return false;
1004
+ /**
1005
+ * True when the presets/app-prefs surface is simply not available to this
1006
+ * client: HTTP 401/403 (not signed in / not permitted) or 404 (the
1007
+ * controller is not mounted on this server). All three collapse to
1008
+ * `denied: true` — the UI hides itself silently instead of surfacing an
1009
+ * error. Covers both `ClientError` and `PresetsHttpError`.
1010
+ */
1011
+ function isUnavailableError(err) {
1012
+ const status = err instanceof _atscript_db_client.ClientError || err instanceof PresetsHttpError ? err.status : void 0;
1013
+ return status === 401 || status === 403 || status === 404;
996
1014
  }
1015
+ /**
1016
+ * @deprecated Renamed to `isUnavailableError` in 0.1.133 — it also covers
1017
+ * 404 (feature not mounted), not just auth failures.
1018
+ */
1019
+ const isAuthError = isUnavailableError;
997
1020
  //#endregion
998
1021
  //#region src/presets/app-prefs-client.ts
999
1022
  /**
@@ -1026,7 +1049,7 @@ var AppPrefsClient = class {
1026
1049
  denied: false
1027
1050
  };
1028
1051
  } catch (err) {
1029
- if (isAuthError(err)) return {
1052
+ if (isUnavailableError(err)) return {
1030
1053
  row: null,
1031
1054
  prefs: null,
1032
1055
  denied: true
@@ -1232,6 +1255,36 @@ function stateToUrlQueryString(state, defaults) {
1232
1255
  return (0, _uniqu_url_builder.buildUrl)(query);
1233
1256
  }
1234
1257
  /**
1258
+ * The `$`-controls {@link urlQueryStringToState} actually reads. Any other
1259
+ * `$key` is ignored by the parser, so it is NOT the table's to remove.
1260
+ */
1261
+ const CONSUMED_CONTROLS = new Set([
1262
+ "$sort",
1263
+ "$search",
1264
+ "$relevance",
1265
+ "$skip"
1266
+ ]);
1267
+ /** Characters that can only appear in a uniqu filter key, never in a page flag. */
1268
+ const FILTER_OPERATOR_CHAR = /[<>!~]/;
1269
+ /**
1270
+ * Whether {@link urlQueryStringToState} would consume the query key `key` —
1271
+ * i.e. whether the key belongs to the table rather than to the page hosting
1272
+ * it. This is the ownership rule the `useTableUrlQuery` router bridge reads
1273
+ * and writes by, so both directions agree: a key the parser ignores is never
1274
+ * removed, and a key it reads is the table's to remove.
1275
+ *
1276
+ * Shape-only, and deliberately conservative for bare `field=value` keys: a
1277
+ * filter on a column and a host flag are indistinguishable without the table
1278
+ * definition, so those are owned only once the bridge has written them
1279
+ * itself (or once a `prefix` namespaces them).
1280
+ *
1281
+ * @since 0.1.133
1282
+ */
1283
+ function urlQueryConsumesKey(key) {
1284
+ if (key.startsWith("$")) return CONSUMED_CONTROLS.has(key);
1285
+ return FILTER_OPERATOR_CHAR.test(key);
1286
+ }
1287
+ /**
1235
1288
  * Parse a URL query string back into the table state subset.
1236
1289
  *
1237
1290
  * Robust by design — schema drift and copy-paste errors must not break the
@@ -1669,6 +1722,7 @@ exports.isEmptyDraft = isEmptyDraft;
1669
1722
  exports.isFilled = isFilled;
1670
1723
  exports.isSimpleEq = isSimpleEq;
1671
1724
  exports.isSystemPresetId = isSystemPresetId;
1725
+ exports.isUnavailableError = isUnavailableError;
1672
1726
  exports.mergeFilters = mergeFilters;
1673
1727
  exports.mergeSorters = mergeSorters;
1674
1728
  exports.normaliseSystemPresetId = normaliseSystemPresetId;
@@ -1691,6 +1745,7 @@ exports.togglePk = togglePk;
1691
1745
  exports.trimSelection = trimSelection;
1692
1746
  exports.unescapeRegex = unescapeRegex;
1693
1747
  exports.uniqueryFilterToFieldFilters = uniqueryFilterToFieldFilters;
1748
+ exports.urlQueryConsumesKey = urlQueryConsumesKey;
1694
1749
  exports.urlQueryStringToState = urlQueryStringToState;
1695
1750
  exports.userConfId = userConfId;
1696
1751
  exports.walkBackwardAbsorb = walkBackwardAbsorb;
package/dist/index.d.cts CHANGED
@@ -197,6 +197,10 @@ declare function derivePresetAspects(content: unknown): PresetAspect[];
197
197
  * Convert in-memory dict-form snapshot to the wire form persisted on the
198
198
  * server. Entries-arrays are sorted by `field` so consumers (server-side
199
199
  * aspect derivation, dirty detection, equality checks) see a stable order.
200
+ *
201
+ * Empty aspects round-trip as empty (since 0.1.133): a saved view with no
202
+ * filters owns `filterOps` and clears them on apply, which is not the same
203
+ * thing as a view that never claimed the aspect.
200
204
  */
201
205
  declare function toWireSnapshot(snapshot: PresetSnapshot): PresetSnapshotWire;
202
206
  declare function fromWireSnapshot(wire: PresetSnapshotWire): PresetSnapshot;
@@ -454,7 +458,7 @@ interface PresetsListResult {
454
458
  * untouched.
455
459
  */
456
460
  capabilities: PresetCapabilities | null | undefined;
457
- /** True when the controller responded 401/403 — UI silently hides. */
461
+ /** True when the controller responded 401/403/404 — UI silently hides. */
458
462
  denied: boolean;
459
463
  }
460
464
  interface PresetsSaveAsOptions {
@@ -466,7 +470,7 @@ interface PresetsSaveResult {
466
470
  /**
467
471
  * Framework-agnostic wrapper over `@atscript/db-client`'s `Client` for the
468
472
  * `AsPresetEntry` table. Handles wire serialisation, list-splitting by
469
- * `type`, capabilities side-channel, and 401/403 → `denied` semantics.
473
+ * `type`, capabilities side-channel, and 401/403/404 → `denied` semantics.
470
474
  *
471
475
  * Stateless: every method is a fresh request. The Vue composable layer
472
476
  * holds reactive state; this class only translates intent → HTTP.
@@ -483,8 +487,8 @@ declare class PresetsClient {
483
487
  * `(app, tableKey)`. By default also fetches `capabilities` in parallel —
484
488
  * pass `{ capabilities: false }` for refresh-after-mutation calls (fav
485
489
  * toggle, default change, save/save-as, public toggle, rename, delete)
486
- * where role-derived capabilities can't have changed. Auth errors
487
- * (401/403) collapse to `denied: true` with empty data so the UI hides
490
+ * where role-derived capabilities can't have changed. Unavailable
491
+ * responses (401/403/404) collapse to `denied: true` with empty data so the UI hides
488
492
  * itself silently.
489
493
  */
490
494
  list(opts?: {
@@ -525,8 +529,19 @@ declare class PresetsHttpError extends Error {
525
529
  readonly status: number;
526
530
  constructor(status: number, message: string);
527
531
  }
528
- /** True for HTTP 401/403 across both `ClientError` and `PresetsHttpError`. */
529
- declare function isAuthError(err: unknown): boolean;
532
+ /**
533
+ * True when the presets/app-prefs surface is simply not available to this
534
+ * client: HTTP 401/403 (not signed in / not permitted) or 404 (the
535
+ * controller is not mounted on this server). All three collapse to
536
+ * `denied: true` — the UI hides itself silently instead of surfacing an
537
+ * error. Covers both `ClientError` and `PresetsHttpError`.
538
+ */
539
+ declare function isUnavailableError(err: unknown): boolean;
540
+ /**
541
+ * @deprecated Renamed to `isUnavailableError` in 0.1.133 — it also covers
542
+ * 404 (feature not mounted), not just auth failures.
543
+ */
544
+ declare const isAuthError: typeof isUnavailableError;
530
545
  //#endregion
531
546
  //#region src/presets/app-prefs-client.d.ts
532
547
  /**
@@ -547,7 +562,7 @@ interface AppPrefsLoadResult {
547
562
  row: AsPresetEntryRow | null;
548
563
  /** Convenience accessor for `row.data` (the typed prefs payload), or `null`. */
549
564
  prefs: AppConfData | null;
550
- /** True when the controller responded 401/403. */
565
+ /** True when the controller responded 401/403/404 (not mounted). */
551
566
  denied: boolean;
552
567
  }
553
568
  /**
@@ -734,6 +749,21 @@ declare function resolveAspectGate(value: boolean | string[] | undefined): Aspec
734
749
  * Returns `""` (no leading `?`) for the default view.
735
750
  */
736
751
  declare function stateToUrlQueryString(state: UrlQueryStateLike, defaults: UrlQueryDefaults): string;
752
+ /**
753
+ * Whether {@link urlQueryStringToState} would consume the query key `key` —
754
+ * i.e. whether the key belongs to the table rather than to the page hosting
755
+ * it. This is the ownership rule the `useTableUrlQuery` router bridge reads
756
+ * and writes by, so both directions agree: a key the parser ignores is never
757
+ * removed, and a key it reads is the table's to remove.
758
+ *
759
+ * Shape-only, and deliberately conservative for bare `field=value` keys: a
760
+ * filter on a column and a host flag are indistinguishable without the table
761
+ * definition, so those are owned only once the bridge has written them
762
+ * itself (or once a `prefix` namespaces them).
763
+ *
764
+ * @since 0.1.133
765
+ */
766
+ declare function urlQueryConsumesKey(key: string): boolean;
737
767
  interface UrlQueryParseOptions {
738
768
  /**
739
769
  * Field paths the table knows about. Conditions on fields outside this set
@@ -1076,4 +1106,4 @@ type ColumnReorderPosition = "before" | "after";
1076
1106
  */
1077
1107
  declare function reorderColumnNames(names: string[], fromPath: string, toPath: string, position: ColumnReorderPosition): string[];
1078
1108
  //#endregion
1079
- export { APP_CONF_PREFIX, type AppConfData, AppPrefsClient, type AppPrefsClientConfig, type AppPrefsLoadResult, type AsPresetEntryData, type AsPresetEntryRow, type AsPresetsErrorCode, type AspectGate, type AspectMask, type BuildTableQueryOptions, type ColumnFilterType, type ColumnReorderPosition, type ColumnWidthEntry, type ColumnWidthsMap, type ConfigTab, DEFAULT_ROW_HEIGHT_PX, DRAFT_PERSISTED_ASPECTS, type DateShortcut, type DraftPersistedAspect, type FetchPlan, type FetchPlanMode, type FieldFilters, type FilterCondition, type FilterConditionType, MAX_DEFAULT_COLUMN_WIDTH_PX, type MergeResult, NULL_OPS, PRESET_ASPECTS, type PageAlignedBlock, type PlanFetchArgs, type PresetAspect, type PresetCapabilities, type PresetColumnWidthEntry, type PresetData, type PresetDraft, type PresetFilterOpEntry, type PresetLimitReachedBody, type PresetSnapshot, type PresetSnapshotWire, type PresetSorterEntry, PresetsClient, type PresetsClientConfig, PresetsHttpError, type PresetsListResult, type PresetsSaveAsOptions, type PresetsSaveResult, type QueryOptions, RESERVED_ID_PREFIXES, STANDARD_PRESET_ID, SYSTEM_PRESET_PREFIX, type SelectionMode, type SystemPreset, type SystemPresetInput, type TableStateData, type TableStateMethods, USER_CONF_PREFIX, type UrlQueryDefaults, type UrlQueryParseOptions, type UrlQueryStateLike, type UrlQueryStateSnapshot, type UrlQuerySync, type UserConfData, appConfId, arraysEqual, blockStartFor, buildTableQuery, clampTopIndex, columnFilterType, computeDefaultColumnWidth, conditionLabel, conditionsForType, dateShortcuts, debounce, defaultCondition, derivePresetAspects, deserializeDraft, draftMatchesPreset, escapeRegex, filledFilterCount, filterTokenLabel, filtersToUniqueryFilter, formatFilterCondition, fromWireSnapshot, hasSecondValue, isAuthError, isDirtyAgainst, isEmptyDraft, isFilled, isSimpleEq, isSystemPresetId, mergeFilters, mergeSorters, normaliseSystemPresetId, pageAlignedBlocksFor, parseFilterInput, planFetch, reconcileColumnWidthDefaults, reorderColumnNames, resolveAspectGate, resolveSystemPresets, rowsToPks, sameColumnSet, serializeDraft, setsEqual, sortersEqual, stableStringify, stateToUrlQueryString, toWireSnapshot, togglePk, trimSelection, unescapeRegex, uniqueryFilterToFieldFilters, urlQueryStringToState, userConfId, walkBackwardAbsorb, walkForwardAbsorb };
1109
+ export { APP_CONF_PREFIX, type AppConfData, AppPrefsClient, type AppPrefsClientConfig, type AppPrefsLoadResult, type AsPresetEntryData, type AsPresetEntryRow, type AsPresetsErrorCode, type AspectGate, type AspectMask, type BuildTableQueryOptions, type ColumnFilterType, type ColumnReorderPosition, type ColumnWidthEntry, type ColumnWidthsMap, type ConfigTab, DEFAULT_ROW_HEIGHT_PX, DRAFT_PERSISTED_ASPECTS, type DateShortcut, type DraftPersistedAspect, type FetchPlan, type FetchPlanMode, type FieldFilters, type FilterCondition, type FilterConditionType, MAX_DEFAULT_COLUMN_WIDTH_PX, type MergeResult, NULL_OPS, PRESET_ASPECTS, type PageAlignedBlock, type PlanFetchArgs, type PresetAspect, type PresetCapabilities, type PresetColumnWidthEntry, type PresetData, type PresetDraft, type PresetFilterOpEntry, type PresetLimitReachedBody, type PresetSnapshot, type PresetSnapshotWire, type PresetSorterEntry, PresetsClient, type PresetsClientConfig, PresetsHttpError, type PresetsListResult, type PresetsSaveAsOptions, type PresetsSaveResult, type QueryOptions, RESERVED_ID_PREFIXES, STANDARD_PRESET_ID, SYSTEM_PRESET_PREFIX, type SelectionMode, type SystemPreset, type SystemPresetInput, type TableStateData, type TableStateMethods, USER_CONF_PREFIX, type UrlQueryDefaults, type UrlQueryParseOptions, type UrlQueryStateLike, type UrlQueryStateSnapshot, type UrlQuerySync, type UserConfData, appConfId, arraysEqual, blockStartFor, buildTableQuery, clampTopIndex, columnFilterType, computeDefaultColumnWidth, conditionLabel, conditionsForType, dateShortcuts, debounce, defaultCondition, derivePresetAspects, deserializeDraft, draftMatchesPreset, escapeRegex, filledFilterCount, filterTokenLabel, filtersToUniqueryFilter, formatFilterCondition, fromWireSnapshot, hasSecondValue, isAuthError, isDirtyAgainst, isEmptyDraft, isFilled, isSimpleEq, isSystemPresetId, isUnavailableError, mergeFilters, mergeSorters, normaliseSystemPresetId, pageAlignedBlocksFor, parseFilterInput, planFetch, reconcileColumnWidthDefaults, reorderColumnNames, resolveAspectGate, resolveSystemPresets, rowsToPks, sameColumnSet, serializeDraft, setsEqual, sortersEqual, stableStringify, stateToUrlQueryString, toWireSnapshot, togglePk, trimSelection, unescapeRegex, uniqueryFilterToFieldFilters, urlQueryConsumesKey, urlQueryStringToState, userConfId, walkBackwardAbsorb, walkForwardAbsorb };
package/dist/index.d.mts CHANGED
@@ -197,6 +197,10 @@ declare function derivePresetAspects(content: unknown): PresetAspect[];
197
197
  * Convert in-memory dict-form snapshot to the wire form persisted on the
198
198
  * server. Entries-arrays are sorted by `field` so consumers (server-side
199
199
  * aspect derivation, dirty detection, equality checks) see a stable order.
200
+ *
201
+ * Empty aspects round-trip as empty (since 0.1.133): a saved view with no
202
+ * filters owns `filterOps` and clears them on apply, which is not the same
203
+ * thing as a view that never claimed the aspect.
200
204
  */
201
205
  declare function toWireSnapshot(snapshot: PresetSnapshot): PresetSnapshotWire;
202
206
  declare function fromWireSnapshot(wire: PresetSnapshotWire): PresetSnapshot;
@@ -454,7 +458,7 @@ interface PresetsListResult {
454
458
  * untouched.
455
459
  */
456
460
  capabilities: PresetCapabilities | null | undefined;
457
- /** True when the controller responded 401/403 — UI silently hides. */
461
+ /** True when the controller responded 401/403/404 — UI silently hides. */
458
462
  denied: boolean;
459
463
  }
460
464
  interface PresetsSaveAsOptions {
@@ -466,7 +470,7 @@ interface PresetsSaveResult {
466
470
  /**
467
471
  * Framework-agnostic wrapper over `@atscript/db-client`'s `Client` for the
468
472
  * `AsPresetEntry` table. Handles wire serialisation, list-splitting by
469
- * `type`, capabilities side-channel, and 401/403 → `denied` semantics.
473
+ * `type`, capabilities side-channel, and 401/403/404 → `denied` semantics.
470
474
  *
471
475
  * Stateless: every method is a fresh request. The Vue composable layer
472
476
  * holds reactive state; this class only translates intent → HTTP.
@@ -483,8 +487,8 @@ declare class PresetsClient {
483
487
  * `(app, tableKey)`. By default also fetches `capabilities` in parallel —
484
488
  * pass `{ capabilities: false }` for refresh-after-mutation calls (fav
485
489
  * toggle, default change, save/save-as, public toggle, rename, delete)
486
- * where role-derived capabilities can't have changed. Auth errors
487
- * (401/403) collapse to `denied: true` with empty data so the UI hides
490
+ * where role-derived capabilities can't have changed. Unavailable
491
+ * responses (401/403/404) collapse to `denied: true` with empty data so the UI hides
488
492
  * itself silently.
489
493
  */
490
494
  list(opts?: {
@@ -525,8 +529,19 @@ declare class PresetsHttpError extends Error {
525
529
  readonly status: number;
526
530
  constructor(status: number, message: string);
527
531
  }
528
- /** True for HTTP 401/403 across both `ClientError` and `PresetsHttpError`. */
529
- declare function isAuthError(err: unknown): boolean;
532
+ /**
533
+ * True when the presets/app-prefs surface is simply not available to this
534
+ * client: HTTP 401/403 (not signed in / not permitted) or 404 (the
535
+ * controller is not mounted on this server). All three collapse to
536
+ * `denied: true` — the UI hides itself silently instead of surfacing an
537
+ * error. Covers both `ClientError` and `PresetsHttpError`.
538
+ */
539
+ declare function isUnavailableError(err: unknown): boolean;
540
+ /**
541
+ * @deprecated Renamed to `isUnavailableError` in 0.1.133 — it also covers
542
+ * 404 (feature not mounted), not just auth failures.
543
+ */
544
+ declare const isAuthError: typeof isUnavailableError;
530
545
  //#endregion
531
546
  //#region src/presets/app-prefs-client.d.ts
532
547
  /**
@@ -547,7 +562,7 @@ interface AppPrefsLoadResult {
547
562
  row: AsPresetEntryRow | null;
548
563
  /** Convenience accessor for `row.data` (the typed prefs payload), or `null`. */
549
564
  prefs: AppConfData | null;
550
- /** True when the controller responded 401/403. */
565
+ /** True when the controller responded 401/403/404 (not mounted). */
551
566
  denied: boolean;
552
567
  }
553
568
  /**
@@ -734,6 +749,21 @@ declare function resolveAspectGate(value: boolean | string[] | undefined): Aspec
734
749
  * Returns `""` (no leading `?`) for the default view.
735
750
  */
736
751
  declare function stateToUrlQueryString(state: UrlQueryStateLike, defaults: UrlQueryDefaults): string;
752
+ /**
753
+ * Whether {@link urlQueryStringToState} would consume the query key `key` —
754
+ * i.e. whether the key belongs to the table rather than to the page hosting
755
+ * it. This is the ownership rule the `useTableUrlQuery` router bridge reads
756
+ * and writes by, so both directions agree: a key the parser ignores is never
757
+ * removed, and a key it reads is the table's to remove.
758
+ *
759
+ * Shape-only, and deliberately conservative for bare `field=value` keys: a
760
+ * filter on a column and a host flag are indistinguishable without the table
761
+ * definition, so those are owned only once the bridge has written them
762
+ * itself (or once a `prefix` namespaces them).
763
+ *
764
+ * @since 0.1.133
765
+ */
766
+ declare function urlQueryConsumesKey(key: string): boolean;
737
767
  interface UrlQueryParseOptions {
738
768
  /**
739
769
  * Field paths the table knows about. Conditions on fields outside this set
@@ -1076,4 +1106,4 @@ type ColumnReorderPosition = "before" | "after";
1076
1106
  */
1077
1107
  declare function reorderColumnNames(names: string[], fromPath: string, toPath: string, position: ColumnReorderPosition): string[];
1078
1108
  //#endregion
1079
- export { APP_CONF_PREFIX, type AppConfData, AppPrefsClient, type AppPrefsClientConfig, type AppPrefsLoadResult, type AsPresetEntryData, type AsPresetEntryRow, type AsPresetsErrorCode, type AspectGate, type AspectMask, type BuildTableQueryOptions, type ColumnFilterType, type ColumnReorderPosition, type ColumnWidthEntry, type ColumnWidthsMap, type ConfigTab, DEFAULT_ROW_HEIGHT_PX, DRAFT_PERSISTED_ASPECTS, type DateShortcut, type DraftPersistedAspect, type FetchPlan, type FetchPlanMode, type FieldFilters, type FilterCondition, type FilterConditionType, MAX_DEFAULT_COLUMN_WIDTH_PX, type MergeResult, NULL_OPS, PRESET_ASPECTS, type PageAlignedBlock, type PlanFetchArgs, type PresetAspect, type PresetCapabilities, type PresetColumnWidthEntry, type PresetData, type PresetDraft, type PresetFilterOpEntry, type PresetLimitReachedBody, type PresetSnapshot, type PresetSnapshotWire, type PresetSorterEntry, PresetsClient, type PresetsClientConfig, PresetsHttpError, type PresetsListResult, type PresetsSaveAsOptions, type PresetsSaveResult, type QueryOptions, RESERVED_ID_PREFIXES, STANDARD_PRESET_ID, SYSTEM_PRESET_PREFIX, type SelectionMode, type SystemPreset, type SystemPresetInput, type TableStateData, type TableStateMethods, USER_CONF_PREFIX, type UrlQueryDefaults, type UrlQueryParseOptions, type UrlQueryStateLike, type UrlQueryStateSnapshot, type UrlQuerySync, type UserConfData, appConfId, arraysEqual, blockStartFor, buildTableQuery, clampTopIndex, columnFilterType, computeDefaultColumnWidth, conditionLabel, conditionsForType, dateShortcuts, debounce, defaultCondition, derivePresetAspects, deserializeDraft, draftMatchesPreset, escapeRegex, filledFilterCount, filterTokenLabel, filtersToUniqueryFilter, formatFilterCondition, fromWireSnapshot, hasSecondValue, isAuthError, isDirtyAgainst, isEmptyDraft, isFilled, isSimpleEq, isSystemPresetId, mergeFilters, mergeSorters, normaliseSystemPresetId, pageAlignedBlocksFor, parseFilterInput, planFetch, reconcileColumnWidthDefaults, reorderColumnNames, resolveAspectGate, resolveSystemPresets, rowsToPks, sameColumnSet, serializeDraft, setsEqual, sortersEqual, stableStringify, stateToUrlQueryString, toWireSnapshot, togglePk, trimSelection, unescapeRegex, uniqueryFilterToFieldFilters, urlQueryStringToState, userConfId, walkBackwardAbsorb, walkForwardAbsorb };
1109
+ export { APP_CONF_PREFIX, type AppConfData, AppPrefsClient, type AppPrefsClientConfig, type AppPrefsLoadResult, type AsPresetEntryData, type AsPresetEntryRow, type AsPresetsErrorCode, type AspectGate, type AspectMask, type BuildTableQueryOptions, type ColumnFilterType, type ColumnReorderPosition, type ColumnWidthEntry, type ColumnWidthsMap, type ConfigTab, DEFAULT_ROW_HEIGHT_PX, DRAFT_PERSISTED_ASPECTS, type DateShortcut, type DraftPersistedAspect, type FetchPlan, type FetchPlanMode, type FieldFilters, type FilterCondition, type FilterConditionType, MAX_DEFAULT_COLUMN_WIDTH_PX, type MergeResult, NULL_OPS, PRESET_ASPECTS, type PageAlignedBlock, type PlanFetchArgs, type PresetAspect, type PresetCapabilities, type PresetColumnWidthEntry, type PresetData, type PresetDraft, type PresetFilterOpEntry, type PresetLimitReachedBody, type PresetSnapshot, type PresetSnapshotWire, type PresetSorterEntry, PresetsClient, type PresetsClientConfig, PresetsHttpError, type PresetsListResult, type PresetsSaveAsOptions, type PresetsSaveResult, type QueryOptions, RESERVED_ID_PREFIXES, STANDARD_PRESET_ID, SYSTEM_PRESET_PREFIX, type SelectionMode, type SystemPreset, type SystemPresetInput, type TableStateData, type TableStateMethods, USER_CONF_PREFIX, type UrlQueryDefaults, type UrlQueryParseOptions, type UrlQueryStateLike, type UrlQueryStateSnapshot, type UrlQuerySync, type UserConfData, appConfId, arraysEqual, blockStartFor, buildTableQuery, clampTopIndex, columnFilterType, computeDefaultColumnWidth, conditionLabel, conditionsForType, dateShortcuts, debounce, defaultCondition, derivePresetAspects, deserializeDraft, draftMatchesPreset, escapeRegex, filledFilterCount, filterTokenLabel, filtersToUniqueryFilter, formatFilterCondition, fromWireSnapshot, hasSecondValue, isAuthError, isDirtyAgainst, isEmptyDraft, isFilled, isSimpleEq, isSystemPresetId, isUnavailableError, mergeFilters, mergeSorters, normaliseSystemPresetId, pageAlignedBlocksFor, parseFilterInput, planFetch, reconcileColumnWidthDefaults, reorderColumnNames, resolveAspectGate, resolveSystemPresets, rowsToPks, sameColumnSet, serializeDraft, setsEqual, sortersEqual, stableStringify, stateToUrlQueryString, toWireSnapshot, togglePk, trimSelection, unescapeRegex, uniqueryFilterToFieldFilters, urlQueryConsumesKey, urlQueryStringToState, userConfId, walkBackwardAbsorb, walkForwardAbsorb };
package/dist/index.mjs CHANGED
@@ -148,13 +148,22 @@ function unescapeRegex(input) {
148
148
  //#region src/filters/filter-input-format.ts
149
149
  /**
150
150
  * Coerce a raw string value to the appropriate JS type for the column.
151
- * Number columns get numeric values; everything else stays as string.
151
+ * Number columns get numeric values, boolean columns understand the
152
+ * `true` / `false` vocabulary (case-insensitive) so the condition carries a
153
+ * real boolean and round-trips through the URL as one. Anything else stays
154
+ * a string. Since 0.1.133 for booleans.
152
155
  */
153
156
  function coerceValue(raw, columnType) {
154
157
  if (columnType === "number") {
155
158
  const n = Number(raw);
156
159
  return Number.isNaN(n) ? raw : n;
157
160
  }
161
+ if (columnType === "boolean") {
162
+ const lower = raw.toLowerCase();
163
+ if (lower === "true") return true;
164
+ if (lower === "false") return false;
165
+ return raw;
166
+ }
158
167
  return raw;
159
168
  }
160
169
  /** Default condition type when no symbol matches the input. */
@@ -579,6 +588,10 @@ function derivePresetAspects(content) {
579
588
  * Convert in-memory dict-form snapshot to the wire form persisted on the
580
589
  * server. Entries-arrays are sorted by `field` so consumers (server-side
581
590
  * aspect derivation, dirty detection, equality checks) see a stable order.
591
+ *
592
+ * Empty aspects round-trip as empty (since 0.1.133): a saved view with no
593
+ * filters owns `filterOps` and clears them on apply, which is not the same
594
+ * thing as a view that never claimed the aspect.
582
595
  */
583
596
  function toWireSnapshot(snapshot) {
584
597
  const wire = {};
@@ -591,7 +604,7 @@ function toWireSnapshot(snapshot) {
591
604
  field,
592
605
  width: columnWidths[field]
593
606
  });
594
- if (entries.length > 0) columns.columnWidths = entries;
607
+ columns.columnWidths = entries;
595
608
  }
596
609
  wire.columns = columns;
597
610
  }
@@ -602,7 +615,7 @@ function toWireSnapshot(snapshot) {
602
615
  field,
603
616
  conditions: snapshot.filterOps[field]
604
617
  });
605
- if (entries.length > 0) wire.filterOps = entries;
618
+ wire.filterOps = entries;
606
619
  }
607
620
  if (snapshot.sorters) wire.sorters = snapshot.sorters;
608
621
  if (snapshot.itemsPerPage !== void 0) wire.itemsPerPage = snapshot.itemsPerPage;
@@ -613,7 +626,7 @@ function fromWireSnapshot(wire) {
613
626
  if (wire.columns) {
614
627
  const { columnNames, columnWidths } = wire.columns;
615
628
  const columns = { columnNames };
616
- if (columnWidths && columnWidths.length > 0) {
629
+ if (columnWidths) {
617
630
  const dict = {};
618
631
  for (const entry of columnWidths) dict[entry.field] = entry.width;
619
632
  columns.columnWidths = dict;
@@ -621,7 +634,7 @@ function fromWireSnapshot(wire) {
621
634
  snapshot.columns = columns;
622
635
  }
623
636
  if (wire.filters) snapshot.filters = wire.filters;
624
- if (wire.filterOps && wire.filterOps.length > 0) {
637
+ if (wire.filterOps) {
625
638
  const dict = {};
626
639
  for (const entry of wire.filterOps) dict[entry.field] = entry.conditions;
627
640
  snapshot.filterOps = dict;
@@ -818,7 +831,7 @@ function draftMatchesPreset(draft, presetSnapshot, availableAspects) {
818
831
  /**
819
832
  * Framework-agnostic wrapper over `@atscript/db-client`'s `Client` for the
820
833
  * `AsPresetEntry` table. Handles wire serialisation, list-splitting by
821
- * `type`, capabilities side-channel, and 401/403 → `denied` semantics.
834
+ * `type`, capabilities side-channel, and 401/403/404 → `denied` semantics.
822
835
  *
823
836
  * Stateless: every method is a fresh request. The Vue composable layer
824
837
  * holds reactive state; this class only translates intent → HTTP.
@@ -844,8 +857,8 @@ var PresetsClient = class {
844
857
  * `(app, tableKey)`. By default also fetches `capabilities` in parallel —
845
858
  * pass `{ capabilities: false }` for refresh-after-mutation calls (fav
846
859
  * toggle, default change, save/save-as, public toggle, rename, delete)
847
- * where role-derived capabilities can't have changed. Auth errors
848
- * (401/403) collapse to `denied: true` with empty data so the UI hides
860
+ * where role-derived capabilities can't have changed. Unavailable
861
+ * responses (401/403/404) collapse to `denied: true` with empty data so the UI hides
849
862
  * itself silently.
850
863
  */
851
864
  async list(opts = {}) {
@@ -857,7 +870,7 @@ var PresetsClient = class {
857
870
  };
858
871
  try {
859
872
  const [rows, capabilities] = await Promise.all([this.client.query({ filter }), fetchCapabilities ? this.loadCapabilities().catch((err) => {
860
- if (isAuthError(err)) throw err;
873
+ if (isUnavailableError(err)) throw err;
861
874
  return null;
862
875
  }) : Promise.resolve(void 0)]);
863
876
  const presets = [];
@@ -871,7 +884,7 @@ var PresetsClient = class {
871
884
  denied: false
872
885
  };
873
886
  } catch (err) {
874
- if (isAuthError(err)) return {
887
+ if (isUnavailableError(err)) return {
875
888
  presets: [],
876
889
  userConf: null,
877
890
  capabilities: null,
@@ -987,12 +1000,22 @@ var PresetsHttpError = class extends Error {
987
1000
  this.name = "PresetsHttpError";
988
1001
  }
989
1002
  };
990
- /** True for HTTP 401/403 across both `ClientError` and `PresetsHttpError`. */
991
- function isAuthError(err) {
992
- if (err instanceof ClientError && (err.status === 401 || err.status === 403)) return true;
993
- if (err instanceof PresetsHttpError && (err.status === 401 || err.status === 403)) return true;
994
- return false;
1003
+ /**
1004
+ * True when the presets/app-prefs surface is simply not available to this
1005
+ * client: HTTP 401/403 (not signed in / not permitted) or 404 (the
1006
+ * controller is not mounted on this server). All three collapse to
1007
+ * `denied: true` — the UI hides itself silently instead of surfacing an
1008
+ * error. Covers both `ClientError` and `PresetsHttpError`.
1009
+ */
1010
+ function isUnavailableError(err) {
1011
+ const status = err instanceof ClientError || err instanceof PresetsHttpError ? err.status : void 0;
1012
+ return status === 401 || status === 403 || status === 404;
995
1013
  }
1014
+ /**
1015
+ * @deprecated Renamed to `isUnavailableError` in 0.1.133 — it also covers
1016
+ * 404 (feature not mounted), not just auth failures.
1017
+ */
1018
+ const isAuthError = isUnavailableError;
996
1019
  //#endregion
997
1020
  //#region src/presets/app-prefs-client.ts
998
1021
  /**
@@ -1025,7 +1048,7 @@ var AppPrefsClient = class {
1025
1048
  denied: false
1026
1049
  };
1027
1050
  } catch (err) {
1028
- if (isAuthError(err)) return {
1051
+ if (isUnavailableError(err)) return {
1029
1052
  row: null,
1030
1053
  prefs: null,
1031
1054
  denied: true
@@ -1231,6 +1254,36 @@ function stateToUrlQueryString(state, defaults) {
1231
1254
  return buildUrl(query);
1232
1255
  }
1233
1256
  /**
1257
+ * The `$`-controls {@link urlQueryStringToState} actually reads. Any other
1258
+ * `$key` is ignored by the parser, so it is NOT the table's to remove.
1259
+ */
1260
+ const CONSUMED_CONTROLS = new Set([
1261
+ "$sort",
1262
+ "$search",
1263
+ "$relevance",
1264
+ "$skip"
1265
+ ]);
1266
+ /** Characters that can only appear in a uniqu filter key, never in a page flag. */
1267
+ const FILTER_OPERATOR_CHAR = /[<>!~]/;
1268
+ /**
1269
+ * Whether {@link urlQueryStringToState} would consume the query key `key` —
1270
+ * i.e. whether the key belongs to the table rather than to the page hosting
1271
+ * it. This is the ownership rule the `useTableUrlQuery` router bridge reads
1272
+ * and writes by, so both directions agree: a key the parser ignores is never
1273
+ * removed, and a key it reads is the table's to remove.
1274
+ *
1275
+ * Shape-only, and deliberately conservative for bare `field=value` keys: a
1276
+ * filter on a column and a host flag are indistinguishable without the table
1277
+ * definition, so those are owned only once the bridge has written them
1278
+ * itself (or once a `prefix` namespaces them).
1279
+ *
1280
+ * @since 0.1.133
1281
+ */
1282
+ function urlQueryConsumesKey(key) {
1283
+ if (key.startsWith("$")) return CONSUMED_CONTROLS.has(key);
1284
+ return FILTER_OPERATOR_CHAR.test(key);
1285
+ }
1286
+ /**
1234
1287
  * Parse a URL query string back into the table state subset.
1235
1288
  *
1236
1289
  * Robust by design — schema drift and copy-paste errors must not break the
@@ -1627,4 +1680,4 @@ function reorderColumnNames(names, fromPath, toPath, position) {
1627
1680
  return without.slice(0, insertAt).concat(fromPath, without.slice(insertAt));
1628
1681
  }
1629
1682
  //#endregion
1630
- export { APP_CONF_PREFIX, AppPrefsClient, DEFAULT_ROW_HEIGHT_PX, DRAFT_PERSISTED_ASPECTS, MAX_DEFAULT_COLUMN_WIDTH_PX, NULL_OPS, PRESET_ASPECTS, PresetsClient, PresetsHttpError, RESERVED_ID_PREFIXES, STANDARD_PRESET_ID, SYSTEM_PRESET_PREFIX, USER_CONF_PREFIX, appConfId, arraysEqual, blockStartFor, buildTableQuery, clampTopIndex, columnFilterType, computeDefaultColumnWidth, conditionLabel, conditionsForType, dateShortcuts, debounce, defaultCondition, derivePresetAspects, deserializeDraft, draftMatchesPreset, escapeRegex, filledFilterCount, filterTokenLabel, filtersToUniqueryFilter, formatFilterCondition, fromWireSnapshot, hasSecondValue, isAuthError, isDirtyAgainst, isEmptyDraft, isFilled, isSimpleEq, isSystemPresetId, mergeFilters, mergeSorters, normaliseSystemPresetId, pageAlignedBlocksFor, parseFilterInput, planFetch, reconcileColumnWidthDefaults, reorderColumnNames, resolveAspectGate, resolveSystemPresets, rowsToPks, sameColumnSet, serializeDraft, setsEqual, sortersEqual, stableStringify, stateToUrlQueryString, toWireSnapshot, togglePk, trimSelection, unescapeRegex, uniqueryFilterToFieldFilters, urlQueryStringToState, userConfId, walkBackwardAbsorb, walkForwardAbsorb };
1683
+ export { APP_CONF_PREFIX, AppPrefsClient, DEFAULT_ROW_HEIGHT_PX, DRAFT_PERSISTED_ASPECTS, MAX_DEFAULT_COLUMN_WIDTH_PX, NULL_OPS, PRESET_ASPECTS, PresetsClient, PresetsHttpError, RESERVED_ID_PREFIXES, STANDARD_PRESET_ID, SYSTEM_PRESET_PREFIX, USER_CONF_PREFIX, appConfId, arraysEqual, blockStartFor, buildTableQuery, clampTopIndex, columnFilterType, computeDefaultColumnWidth, conditionLabel, conditionsForType, dateShortcuts, debounce, defaultCondition, derivePresetAspects, deserializeDraft, draftMatchesPreset, escapeRegex, filledFilterCount, filterTokenLabel, filtersToUniqueryFilter, formatFilterCondition, fromWireSnapshot, hasSecondValue, isAuthError, isDirtyAgainst, isEmptyDraft, isFilled, isSimpleEq, isSystemPresetId, isUnavailableError, mergeFilters, mergeSorters, normaliseSystemPresetId, pageAlignedBlocksFor, parseFilterInput, planFetch, reconcileColumnWidthDefaults, reorderColumnNames, resolveAspectGate, resolveSystemPresets, rowsToPks, sameColumnSet, serializeDraft, setsEqual, sortersEqual, stableStringify, stateToUrlQueryString, toWireSnapshot, togglePk, trimSelection, unescapeRegex, uniqueryFilterToFieldFilters, urlQueryConsumesKey, urlQueryStringToState, userConfId, walkBackwardAbsorb, walkForwardAbsorb };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/ui-table",
3
- "version": "0.1.132",
3
+ "version": "0.1.133",
4
4
  "description": "Framework-agnostic filter model, filter-to-Uniquery conversion, and preset serialization for atscript tables",
5
5
  "keywords": [
6
6
  "atscript",
@@ -42,18 +42,18 @@
42
42
  "access": "public"
43
43
  },
44
44
  "dependencies": {
45
- "@atscript/ui": "0.1.132"
45
+ "@atscript/ui": "0.1.133"
46
46
  },
47
47
  "devDependencies": {
48
- "@atscript/db-client": "^0.1.127",
49
- "@uniqu/core": "^0.1.7",
50
- "@uniqu/url": "^0.1.7",
48
+ "@atscript/db-client": "^0.1.129",
49
+ "@uniqu/core": "^0.1.8",
50
+ "@uniqu/url": "^0.1.8",
51
51
  "vitest": "npm:@voidzero-dev/vite-plus-test@0.1.14"
52
52
  },
53
53
  "peerDependencies": {
54
- "@atscript/db-client": "^0.1.127",
55
- "@uniqu/core": "^0.1.7",
56
- "@uniqu/url": "^0.1.7"
54
+ "@atscript/db-client": "^0.1.129",
55
+ "@uniqu/core": "^0.1.8",
56
+ "@uniqu/url": "^0.1.8"
57
57
  },
58
58
  "scripts": {
59
59
  "build": "vp pack",