@object-ui/core 3.0.2 → 3.1.0

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.
Files changed (79) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +6 -0
  3. package/dist/actions/ActionEngine.d.ts +98 -0
  4. package/dist/actions/ActionEngine.js +222 -0
  5. package/dist/actions/UndoManager.d.ts +80 -0
  6. package/dist/actions/UndoManager.js +193 -0
  7. package/dist/actions/index.d.ts +2 -0
  8. package/dist/actions/index.js +2 -0
  9. package/dist/adapters/ApiDataSource.d.ts +2 -1
  10. package/dist/adapters/ApiDataSource.js +25 -0
  11. package/dist/adapters/ValueDataSource.d.ts +2 -1
  12. package/dist/adapters/ValueDataSource.js +99 -3
  13. package/dist/data-scope/ViewDataProvider.d.ts +143 -0
  14. package/dist/data-scope/ViewDataProvider.js +153 -0
  15. package/dist/data-scope/index.d.ts +1 -0
  16. package/dist/data-scope/index.js +1 -0
  17. package/dist/evaluator/ExpressionEvaluator.d.ts +7 -0
  18. package/dist/evaluator/ExpressionEvaluator.js +19 -0
  19. package/dist/index.d.ts +5 -0
  20. package/dist/index.js +5 -0
  21. package/dist/protocols/DndProtocol.d.ts +84 -0
  22. package/dist/protocols/DndProtocol.js +113 -0
  23. package/dist/protocols/KeyboardProtocol.d.ts +93 -0
  24. package/dist/protocols/KeyboardProtocol.js +108 -0
  25. package/dist/protocols/NotificationProtocol.d.ts +71 -0
  26. package/dist/protocols/NotificationProtocol.js +99 -0
  27. package/dist/protocols/ResponsiveProtocol.d.ts +73 -0
  28. package/dist/protocols/ResponsiveProtocol.js +158 -0
  29. package/dist/protocols/SharingProtocol.d.ts +71 -0
  30. package/dist/protocols/SharingProtocol.js +124 -0
  31. package/dist/protocols/index.d.ts +12 -0
  32. package/dist/protocols/index.js +12 -0
  33. package/dist/utils/debug-collector.d.ts +59 -0
  34. package/dist/utils/debug-collector.js +73 -0
  35. package/dist/utils/debug.d.ts +37 -2
  36. package/dist/utils/debug.js +62 -3
  37. package/dist/utils/expand-fields.d.ts +40 -0
  38. package/dist/utils/expand-fields.js +68 -0
  39. package/dist/utils/extract-records.d.ts +16 -0
  40. package/dist/utils/extract-records.js +32 -0
  41. package/dist/utils/normalize-quick-filter.d.ts +29 -0
  42. package/dist/utils/normalize-quick-filter.js +66 -0
  43. package/package.json +3 -3
  44. package/src/__tests__/protocols/DndProtocol.test.ts +186 -0
  45. package/src/__tests__/protocols/KeyboardProtocol.test.ts +177 -0
  46. package/src/__tests__/protocols/NotificationProtocol.test.ts +142 -0
  47. package/src/__tests__/protocols/ResponsiveProtocol.test.ts +176 -0
  48. package/src/__tests__/protocols/SharingProtocol.test.ts +188 -0
  49. package/src/actions/ActionEngine.ts +268 -0
  50. package/src/actions/UndoManager.ts +215 -0
  51. package/src/actions/__tests__/ActionEngine.test.ts +206 -0
  52. package/src/actions/__tests__/UndoManager.test.ts +320 -0
  53. package/src/actions/index.ts +2 -0
  54. package/src/adapters/ApiDataSource.ts +27 -0
  55. package/src/adapters/ValueDataSource.ts +109 -3
  56. package/src/adapters/__tests__/ValueDataSource.test.ts +147 -0
  57. package/src/data-scope/ViewDataProvider.ts +282 -0
  58. package/src/data-scope/__tests__/ViewDataProvider.test.ts +270 -0
  59. package/src/data-scope/index.ts +8 -0
  60. package/src/evaluator/ExpressionEvaluator.ts +22 -0
  61. package/src/evaluator/__tests__/ExpressionEvaluator.test.ts +31 -1
  62. package/src/index.ts +5 -0
  63. package/src/protocols/DndProtocol.ts +184 -0
  64. package/src/protocols/KeyboardProtocol.ts +185 -0
  65. package/src/protocols/NotificationProtocol.ts +159 -0
  66. package/src/protocols/ResponsiveProtocol.ts +210 -0
  67. package/src/protocols/SharingProtocol.ts +185 -0
  68. package/src/{index.test.ts → protocols/index.ts} +5 -7
  69. package/src/utils/__tests__/debug-collector.test.ts +102 -0
  70. package/src/utils/__tests__/debug.test.ts +52 -1
  71. package/src/utils/__tests__/expand-fields.test.ts +120 -0
  72. package/src/utils/__tests__/extract-records.test.ts +50 -0
  73. package/src/utils/__tests__/normalize-quick-filter.test.ts +123 -0
  74. package/src/utils/debug-collector.ts +100 -0
  75. package/src/utils/debug.ts +87 -6
  76. package/src/utils/expand-fields.ts +76 -0
  77. package/src/utils/extract-records.ts +33 -0
  78. package/src/utils/normalize-quick-filter.ts +78 -0
  79. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,50 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ import { describe, it, expect } from 'vitest';
10
+ import { extractRecords } from '../extract-records';
11
+
12
+ describe('extractRecords', () => {
13
+ const sampleData = [{ id: 1, name: 'A' }, { id: 2, name: 'B' }];
14
+
15
+ it('should return the array directly when results is an array', () => {
16
+ expect(extractRecords(sampleData)).toEqual(sampleData);
17
+ });
18
+
19
+ it('should extract from results.records', () => {
20
+ expect(extractRecords({ records: sampleData })).toEqual(sampleData);
21
+ });
22
+
23
+ it('should extract from results.data', () => {
24
+ expect(extractRecords({ data: sampleData })).toEqual(sampleData);
25
+ });
26
+
27
+ it('should extract from results.value', () => {
28
+ expect(extractRecords({ value: sampleData })).toEqual(sampleData);
29
+ });
30
+
31
+ it('should return empty array for null/undefined', () => {
32
+ expect(extractRecords(null)).toEqual([]);
33
+ expect(extractRecords(undefined)).toEqual([]);
34
+ });
35
+
36
+ it('should return empty array for non-array/non-object', () => {
37
+ expect(extractRecords('string')).toEqual([]);
38
+ expect(extractRecords(42)).toEqual([]);
39
+ });
40
+
41
+ it('should return empty array for object without recognized keys', () => {
42
+ expect(extractRecords({ total: 100 })).toEqual([]);
43
+ });
44
+
45
+ it('should prefer records over data and value', () => {
46
+ const records = [{ id: 1 }];
47
+ const data = [{ id: 2 }];
48
+ expect(extractRecords({ records, data })).toEqual(records);
49
+ });
50
+ });
@@ -0,0 +1,123 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ import { describe, it, expect } from 'vitest';
10
+ import { normalizeQuickFilter, normalizeQuickFilters } from '../normalize-quick-filter';
11
+
12
+ describe('normalizeQuickFilter', () => {
13
+ it('should pass through ObjectUI format unchanged', () => {
14
+ const input = { id: 'active', label: 'Active', filters: [['status', '=', 'active']] };
15
+ const result = normalizeQuickFilter(input);
16
+ expect(result).toBe(input); // same reference
17
+ expect(result.id).toBe('active');
18
+ expect(result.label).toBe('Active');
19
+ expect(result.filters).toEqual([['status', '=', 'active']]);
20
+ });
21
+
22
+ it('should convert spec format { field, operator, value } to ObjectUI format', () => {
23
+ const result = normalizeQuickFilter({
24
+ field: 'status',
25
+ operator: 'eq',
26
+ value: 'active',
27
+ label: 'Active',
28
+ });
29
+ expect(result.id).toBe('status-eq-active');
30
+ expect(result.label).toBe('Active');
31
+ expect(result.filters).toEqual([['status', '=', 'active']]);
32
+ });
33
+
34
+ it('should auto-generate label when omitted', () => {
35
+ const result = normalizeQuickFilter({
36
+ field: 'status',
37
+ operator: 'eq',
38
+ value: 'active',
39
+ });
40
+ expect(result.label).toBe('status eq active');
41
+ });
42
+
43
+ it('should handle null value', () => {
44
+ const result = normalizeQuickFilter({
45
+ field: 'archived',
46
+ operator: 'eq',
47
+ value: null,
48
+ label: 'Not Archived',
49
+ });
50
+ expect(result.id).toBe('archived-eq-');
51
+ expect(result.filters).toEqual([['archived', '=', null]]);
52
+ });
53
+
54
+ it('should map "equals" operator to "="', () => {
55
+ const result = normalizeQuickFilter({
56
+ field: 'status',
57
+ operator: 'equals',
58
+ value: 'active',
59
+ });
60
+ expect(result.filters).toEqual([['status', '=', 'active']]);
61
+ });
62
+
63
+ it('should map "gt" operator to ">"', () => {
64
+ const result = normalizeQuickFilter({
65
+ field: 'amount',
66
+ operator: 'gt',
67
+ value: 100,
68
+ });
69
+ expect(result.filters).toEqual([['amount', '>', 100]]);
70
+ });
71
+
72
+ it('should map "lte" operator to "<="', () => {
73
+ const result = normalizeQuickFilter({
74
+ field: 'age',
75
+ operator: 'lte',
76
+ value: 18,
77
+ });
78
+ expect(result.filters).toEqual([['age', '<=', 18]]);
79
+ });
80
+
81
+ it('should pass through unknown operators', () => {
82
+ const result = normalizeQuickFilter({
83
+ field: 'status',
84
+ operator: 'custom_op',
85
+ value: 'x',
86
+ });
87
+ expect(result.filters).toEqual([['status', 'custom_op', 'x']]);
88
+ });
89
+
90
+ it('should preserve icon and defaultActive', () => {
91
+ const result = normalizeQuickFilter({
92
+ field: 'status',
93
+ operator: 'eq',
94
+ value: 'active',
95
+ label: 'Active',
96
+ icon: 'check',
97
+ defaultActive: true,
98
+ });
99
+ expect(result.icon).toBe('check');
100
+ expect(result.defaultActive).toBe(true);
101
+ });
102
+ });
103
+
104
+ describe('normalizeQuickFilters', () => {
105
+ it('should return undefined for undefined input', () => {
106
+ expect(normalizeQuickFilters(undefined)).toBeUndefined();
107
+ });
108
+
109
+ it('should return undefined for empty array', () => {
110
+ expect(normalizeQuickFilters([])).toBeUndefined();
111
+ });
112
+
113
+ it('should normalize mixed format arrays', () => {
114
+ const result = normalizeQuickFilters([
115
+ { id: 'vip', label: 'VIP', filters: [['vip', '=', true]] },
116
+ { field: 'status', operator: 'eq', value: 'active', label: 'Active' },
117
+ ]);
118
+ expect(result).toHaveLength(2);
119
+ expect(result![0].id).toBe('vip');
120
+ expect(result![1].id).toBe('status-eq-active');
121
+ expect(result![1].filters).toEqual([['status', '=', 'active']]);
122
+ });
123
+ });
@@ -0,0 +1,100 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ /**
10
+ * DebugCollector — lightweight, tree-shakeable event collector
11
+ * for perf / expression / action debug data.
12
+ *
13
+ * Usage in production is a no-op when the singleton is never imported.
14
+ * Consumers call `DebugCollector.getInstance()` and subscribe via
15
+ * `.subscribe()`.
16
+ */
17
+
18
+ export interface PerfEntry {
19
+ type: string;
20
+ id?: string;
21
+ durationMs: number;
22
+ timestamp: number;
23
+ }
24
+
25
+ export interface ExprEntry {
26
+ expression: string;
27
+ result: unknown;
28
+ context?: Record<string, unknown>;
29
+ timestamp: number;
30
+ }
31
+
32
+ export interface EventEntry {
33
+ action: string;
34
+ payload?: unknown;
35
+ timestamp: number;
36
+ }
37
+
38
+ export type DebugEntry =
39
+ | { kind: 'perf'; data: PerfEntry }
40
+ | { kind: 'expr'; data: ExprEntry }
41
+ | { kind: 'event'; data: EventEntry };
42
+
43
+ type DebugSubscriber = (entry: DebugEntry) => void;
44
+
45
+ const MAX_ENTRIES = 200;
46
+
47
+ export class DebugCollector {
48
+ private static instance: DebugCollector | null = null;
49
+
50
+ private entries: DebugEntry[] = [];
51
+ private subscribers = new Set<DebugSubscriber>();
52
+
53
+ static getInstance(): DebugCollector {
54
+ if (!DebugCollector.instance) {
55
+ DebugCollector.instance = new DebugCollector();
56
+ }
57
+ return DebugCollector.instance;
58
+ }
59
+
60
+ /** Reset singleton — only used for testing */
61
+ static resetInstance(): void {
62
+ DebugCollector.instance = null;
63
+ }
64
+
65
+ addPerf(entry: PerfEntry): void {
66
+ this.push({ kind: 'perf', data: entry });
67
+ }
68
+
69
+ addExpr(entry: ExprEntry): void {
70
+ this.push({ kind: 'expr', data: entry });
71
+ }
72
+
73
+ addEvent(entry: EventEntry): void {
74
+ this.push({ kind: 'event', data: entry });
75
+ }
76
+
77
+ getEntries(kind?: DebugEntry['kind']): DebugEntry[] {
78
+ if (!kind) return this.entries.slice();
79
+ return this.entries.filter((e) => e.kind === kind);
80
+ }
81
+
82
+ clear(): void {
83
+ this.entries = [];
84
+ }
85
+
86
+ subscribe(fn: DebugSubscriber): () => void {
87
+ this.subscribers.add(fn);
88
+ return () => this.subscribers.delete(fn);
89
+ }
90
+
91
+ private push(entry: DebugEntry): void {
92
+ this.entries.push(entry);
93
+ if (this.entries.length > MAX_ENTRIES) {
94
+ this.entries = this.entries.slice(-MAX_ENTRIES);
95
+ }
96
+ for (const fn of this.subscribers) {
97
+ try { fn(entry); } catch { /* subscriber errors must not break debug flow */ }
98
+ }
99
+ }
100
+ }
@@ -6,15 +6,96 @@
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
8
 
9
- type DebugCategory = 'schema' | 'registry' | 'expression' | 'action' | 'plugin' | 'render';
9
+ export type DebugCategory = 'schema' | 'registry' | 'expression' | 'action' | 'plugin' | 'render' | 'dashboard';
10
10
 
11
- function isDebugEnabled(): boolean {
11
+ /**
12
+ * Fine-grained debug flags parsed from URL parameters.
13
+ *
14
+ * @example
15
+ * ```
16
+ * ?__debug → { enabled: true }
17
+ * ?__debug_schema → { enabled: true, schema: true }
18
+ * ?__debug_perf&__debug_data → { enabled: true, perf: true, data: true }
19
+ * ```
20
+ */
21
+ export interface DebugFlags {
22
+ /** Master switch — true when any debug parameter is present */
23
+ enabled: boolean;
24
+ schema?: boolean;
25
+ perf?: boolean;
26
+ data?: boolean;
27
+ expr?: boolean;
28
+ events?: boolean;
29
+ registry?: boolean;
30
+ }
31
+
32
+ const DEBUG_PARAM_PREFIX = '__debug';
33
+
34
+ /**
35
+ * Parse debug flags from a URL search string (e.g. `?__debug&__debug_schema`).
36
+ * SSR-safe — returns `{ enabled: false }` when `window` is unavailable.
37
+ *
38
+ * @param search — Optional search string. Defaults to `window.location.search` when available.
39
+ */
40
+ export function parseDebugFlags(search?: string): DebugFlags {
41
+ let qs: string | undefined = search;
42
+ if (qs === undefined) {
43
+ try {
44
+ qs = typeof window !== 'undefined' ? window.location.search : '';
45
+ } catch {
46
+ qs = '';
47
+ }
48
+ }
49
+
50
+ const params = new URLSearchParams(qs);
51
+ const hasMain = params.has(DEBUG_PARAM_PREFIX);
52
+ const schema = params.has(`${DEBUG_PARAM_PREFIX}_schema`);
53
+ const perf = params.has(`${DEBUG_PARAM_PREFIX}_perf`);
54
+ const data = params.has(`${DEBUG_PARAM_PREFIX}_data`);
55
+ const expr = params.has(`${DEBUG_PARAM_PREFIX}_expr`);
56
+ const events = params.has(`${DEBUG_PARAM_PREFIX}_events`);
57
+ const registry = params.has(`${DEBUG_PARAM_PREFIX}_registry`);
58
+
59
+ const anySub = schema || perf || data || expr || events || registry;
60
+ const enabled = hasMain || anySub;
61
+
62
+ return {
63
+ enabled,
64
+ ...(schema && { schema }),
65
+ ...(perf && { perf }),
66
+ ...(data && { data }),
67
+ ...(expr && { expr }),
68
+ ...(events && { events }),
69
+ ...(registry && { registry }),
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Check whether debug mode is enabled.
75
+ *
76
+ * Resolution order (first truthy wins):
77
+ * 1. URL parameter `?__debug` (browser only)
78
+ * 2. `globalThis.OBJECTUI_DEBUG`
79
+ * 3. `process.env.OBJECTUI_DEBUG`
80
+ */
81
+ export function isDebugEnabled(): boolean {
12
82
  try {
83
+ // 1. URL parameter (browser only, SSR-safe)
84
+ if (typeof window !== 'undefined') {
85
+ try {
86
+ const flags = parseDebugFlags(window.location.search);
87
+ if (flags.enabled) return true;
88
+ } catch { /* ignore */ }
89
+ }
90
+
91
+ // 2. globalThis flag
13
92
  const g = typeof globalThis !== 'undefined' && (globalThis as any).OBJECTUI_DEBUG;
14
- return (
15
- (g === true || g === 'true') ||
16
- (typeof process !== 'undefined' && process.env?.OBJECTUI_DEBUG === 'true')
17
- );
93
+ if (g === true || g === 'true') return true;
94
+
95
+ // 3. process.env
96
+ if (typeof process !== 'undefined' && process.env?.OBJECTUI_DEBUG === 'true') return true;
97
+
98
+ return false;
18
99
  } catch {
19
100
  return false;
20
101
  }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * ObjectUI — expand-fields utility
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ /**
10
+ * Build an array of field names that should be included in `$expand`
11
+ * when fetching data. This scans the given object schema fields
12
+ * (and optional column configuration) for `lookup` and `master_detail`
13
+ * field types, so the backend (e.g. objectql) returns expanded objects
14
+ * instead of raw foreign-key IDs.
15
+ *
16
+ * @param schemaFields - Object map of field metadata from `getObjectSchema()`,
17
+ * e.g. `{ account: { type: 'lookup', reference_to: 'accounts' }, ... }`.
18
+ * @param columns - Optional explicit column list. When provided, only
19
+ * lookup/master_detail fields that appear in `columns` are expanded.
20
+ * Accepts `string[]` or `ListColumn[]` (objects with a `field` property).
21
+ * @returns Array of field names to pass as `$expand`.
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * const fields = {
26
+ * name: { type: 'text' },
27
+ * account: { type: 'lookup', reference_to: 'accounts' },
28
+ * parent: { type: 'master_detail', reference_to: 'contacts' },
29
+ * };
30
+ * buildExpandFields(fields);
31
+ * // → ['account', 'parent']
32
+ *
33
+ * buildExpandFields(fields, ['name', 'account']);
34
+ * // → ['account']
35
+ * ```
36
+ */
37
+ export function buildExpandFields(
38
+ schemaFields?: Record<string, any> | null,
39
+ columns?: (string | { field?: string; name?: string; fieldName?: string })[],
40
+ ): string[] {
41
+ if (!schemaFields || typeof schemaFields !== 'object') {
42
+ return [];
43
+ }
44
+
45
+ // Collect all lookup / master_detail field names from the schema
46
+ const lookupFieldNames: string[] = [];
47
+ for (const [fieldName, fieldDef] of Object.entries(schemaFields)) {
48
+ if (
49
+ fieldDef &&
50
+ typeof fieldDef === 'object' &&
51
+ (fieldDef.type === 'lookup' || fieldDef.type === 'master_detail')
52
+ ) {
53
+ lookupFieldNames.push(fieldName);
54
+ }
55
+ }
56
+
57
+ if (lookupFieldNames.length === 0) {
58
+ return [];
59
+ }
60
+
61
+ // When columns are provided, restrict expansion to visible columns only
62
+ if (columns && Array.isArray(columns) && columns.length > 0) {
63
+ const columnFieldNames = new Set<string>();
64
+ for (const col of columns) {
65
+ if (typeof col === 'string') {
66
+ columnFieldNames.add(col);
67
+ } else if (col && typeof col === 'object') {
68
+ const name = col.field ?? col.name ?? col.fieldName;
69
+ if (name) columnFieldNames.add(name);
70
+ }
71
+ }
72
+ return lookupFieldNames.filter((f) => columnFieldNames.has(f));
73
+ }
74
+
75
+ return lookupFieldNames;
76
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ /**
10
+ * Extract an array of records from various API response formats.
11
+ * Supports: raw array, { records: [] }, { data: [] }, { value: [] }.
12
+ *
13
+ * This utility normalises the different shapes returned by ObjectStack,
14
+ * OData, and MSW mock endpoints so that every data-fetching component
15
+ * can rely on a single extraction path.
16
+ */
17
+ export function extractRecords(results: unknown): any[] {
18
+ if (Array.isArray(results)) {
19
+ return results;
20
+ }
21
+ if (results && typeof results === 'object') {
22
+ if (Array.isArray((results as any).records)) {
23
+ return (results as any).records;
24
+ }
25
+ if (Array.isArray((results as any).data)) {
26
+ return (results as any).data;
27
+ }
28
+ if (Array.isArray((results as any).value)) {
29
+ return (results as any).value;
30
+ }
31
+ }
32
+ return [];
33
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ /**
10
+ * QuickFilter Normalization Utility
11
+ *
12
+ * Adapter layer that converts between two QuickFilter formats:
13
+ * - Spec format: { field, operator, value }
14
+ * - ObjectUI format: { id, label, filters[], icon?, defaultActive? }
15
+ *
16
+ * Both formats are accepted; spec-format items are auto-converted to ObjectUI format.
17
+ */
18
+
19
+ import type { QuickFilterItem, ObjectUIQuickFilterItem } from '@object-ui/types';
20
+
21
+ /** Normalized ObjectUI QuickFilter shape (output of normalizeQuickFilter) */
22
+ export type NormalizedQuickFilter = ObjectUIQuickFilterItem;
23
+
24
+ /**
25
+ * Map a human-readable / spec operator to the ObjectStack AST operator.
26
+ */
27
+ function mapSpecOperator(op: string): string {
28
+ switch (op) {
29
+ case 'equals': case 'eq': return '=';
30
+ case 'notEquals': case 'ne': case 'neq': return '!=';
31
+ case 'contains': return 'contains';
32
+ case 'notContains': return 'notcontains';
33
+ case 'greaterThan': case 'gt': return '>';
34
+ case 'greaterOrEqual': case 'gte': return '>=';
35
+ case 'lessThan': case 'lt': return '<';
36
+ case 'lessOrEqual': case 'lte': return '<=';
37
+ case 'in': return 'in';
38
+ case 'notIn': return 'not in';
39
+ case 'before': return '<';
40
+ case 'after': return '>';
41
+ default: return op;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Normalize a single QuickFilter item.
47
+ * - If it's already in ObjectUI format (has id + label + filters), return as-is.
48
+ * - If it's in Spec format (has field + operator), convert to ObjectUI format.
49
+ */
50
+ export function normalizeQuickFilter(item: QuickFilterItem): NormalizedQuickFilter {
51
+ // Already in ObjectUI format
52
+ if ('id' in item && 'filters' in item && item.label && Array.isArray(item.filters)) {
53
+ return item as NormalizedQuickFilter;
54
+ }
55
+ // Spec format: { field, operator, value }
56
+ if ('field' in item && 'operator' in item) {
57
+ const op = mapSpecOperator(item.operator);
58
+ return {
59
+ id: `${item.field}-${item.operator}-${String(item.value ?? '')}`,
60
+ label: item.label || `${item.field} ${item.operator} ${String(item.value ?? '')}`,
61
+ filters: [[item.field, op, item.value]],
62
+ icon: item.icon,
63
+ defaultActive: item.defaultActive,
64
+ };
65
+ }
66
+ // Unknown format — return as-is
67
+ return item as NormalizedQuickFilter;
68
+ }
69
+
70
+ /**
71
+ * Normalize an array of QuickFilter items (mixed formats accepted).
72
+ */
73
+ export function normalizeQuickFilters(
74
+ items: QuickFilterItem[] | undefined,
75
+ ): NormalizedQuickFilter[] | undefined {
76
+ if (!items || items.length === 0) return undefined;
77
+ return items.map(normalizeQuickFilter);
78
+ }