@object-ui/components 3.1.0 → 3.1.2

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.
@@ -9,7 +9,7 @@ export interface FilterCondition {
9
9
  id: string;
10
10
  field: string;
11
11
  operator: string;
12
- value: string | number | boolean;
12
+ value: string | number | boolean | (string | number | boolean)[];
13
13
  }
14
14
  export interface FilterGroup {
15
15
  id: string;
@@ -7,6 +7,8 @@ export interface ActionBarSchema {
7
7
  location?: ActionLocation;
8
8
  /** Maximum visible inline actions before overflow into "More" menu (default: 3) */
9
9
  maxVisible?: number;
10
+ /** Maximum visible inline actions on mobile devices (default: 1). Desktop uses maxVisible instead. */
11
+ mobileMaxVisible?: number;
10
12
  /** Visibility condition expression */
11
13
  visible?: string;
12
14
  /** Layout direction */
@@ -3,6 +3,7 @@ export interface ActionButtonProps {
3
3
  schema: ActionSchema & {
4
4
  type: string;
5
5
  className?: string;
6
+ actionType?: string;
6
7
  };
7
8
  className?: string;
8
9
  /** Override context for this specific action */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/components",
3
- "version": "3.1.0",
3
+ "version": "3.1.2",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Standard UI component library for Object UI, built with Shadcn UI + Tailwind CSS",
@@ -69,9 +69,9 @@
69
69
  "tailwind-merge": "^3.5.0",
70
70
  "tailwindcss-animate": "^1.0.7",
71
71
  "vaul": "^1.1.2",
72
- "@object-ui/core": "3.1.0",
73
- "@object-ui/react": "3.1.0",
74
- "@object-ui/types": "3.1.0"
72
+ "@object-ui/core": "3.1.2",
73
+ "@object-ui/react": "3.1.2",
74
+ "@object-ui/types": "3.1.2"
75
75
  },
76
76
  "peerDependencies": {
77
77
  "react": "^18.0.0 || ^19.0.0",
@@ -94,6 +94,40 @@ describe('ActionBar (action:bar)', () => {
94
94
  expect(container.textContent).toContain('Action 1');
95
95
  expect(container.textContent).toContain('Action 2');
96
96
  });
97
+
98
+ it('deduplicates actions by name', () => {
99
+ const { container } = renderComponent({
100
+ type: 'action:bar',
101
+ actions: [
102
+ { name: 'change_status', label: 'Change Status', type: 'script', component: 'action:button' },
103
+ { name: 'assign_user', label: 'Assign User', type: 'script', component: 'action:button' },
104
+ { name: 'change_status', label: 'Change Status', type: 'script', component: 'action:button' },
105
+ ],
106
+ });
107
+ const toolbar = container.querySelector('[role="toolbar"]');
108
+ expect(toolbar).toBeTruthy();
109
+ // Should only render 2 actions (duplicates removed)
110
+ expect(toolbar!.children.length).toBe(2);
111
+ expect(container.textContent).toContain('Change Status');
112
+ expect(container.textContent).toContain('Assign User');
113
+ });
114
+
115
+ it('deduplicates actions after location filtering', () => {
116
+ const { container } = renderComponent({
117
+ type: 'action:bar',
118
+ location: 'record_header',
119
+ actions: [
120
+ { name: 'change_status', label: 'Change Status', type: 'script', locations: ['record_header'] },
121
+ { name: 'assign_user', label: 'Assign User', type: 'script', locations: ['record_header'] },
122
+ { name: 'change_status', label: 'Change Status', type: 'script', locations: ['record_header', 'record_more'] },
123
+ { name: 'assign_user', label: 'Assign User', type: 'script', locations: ['record_header'] },
124
+ ],
125
+ });
126
+ const toolbar = container.querySelector('[role="toolbar"]');
127
+ expect(toolbar).toBeTruthy();
128
+ // Should only render 2 unique actions
129
+ expect(toolbar!.children.length).toBe(2);
130
+ });
97
131
  });
98
132
 
99
133
  describe('overflow', () => {
@@ -0,0 +1,409 @@
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, vi, beforeEach } from 'vitest';
10
+ import { render, screen, fireEvent } from '@testing-library/react';
11
+ import { FilterBuilder } from '../custom/filter-builder';
12
+
13
+ // Mock crypto.randomUUID for deterministic test IDs
14
+ let uuidCounter = 0;
15
+ beforeEach(() => {
16
+ uuidCounter = 0;
17
+ vi.spyOn(crypto, 'randomUUID').mockImplementation(
18
+ () => `test-uuid-${++uuidCounter}` as `${string}-${string}-${string}-${string}-${string}`,
19
+ );
20
+ });
21
+
22
+ const selectFields = [
23
+ {
24
+ value: 'status',
25
+ label: 'Status',
26
+ type: 'select',
27
+ options: [
28
+ { value: 'active', label: 'Active' },
29
+ { value: 'inactive', label: 'Inactive' },
30
+ { value: 'pending', label: 'Pending' },
31
+ ],
32
+ },
33
+ {
34
+ value: 'name',
35
+ label: 'Name',
36
+ type: 'text',
37
+ },
38
+ ];
39
+
40
+ const lookupFields = [
41
+ {
42
+ value: 'account',
43
+ label: 'Account',
44
+ type: 'lookup',
45
+ options: [
46
+ { value: 'acme', label: 'Acme Corp' },
47
+ { value: 'globex', label: 'Globex' },
48
+ { value: 'soylent', label: 'Soylent Corp' },
49
+ ],
50
+ },
51
+ ];
52
+
53
+ describe('FilterBuilder', () => {
54
+ describe('lookup field type', () => {
55
+ it('renders without error for lookup fields', () => {
56
+ const onChange = vi.fn();
57
+ const { container } = render(
58
+ <FilterBuilder
59
+ fields={lookupFields}
60
+ value={{
61
+ id: 'root',
62
+ logic: 'and',
63
+ conditions: [
64
+ { id: 'c1', field: 'account', operator: 'equals', value: '' },
65
+ ],
66
+ }}
67
+ onChange={onChange}
68
+ />,
69
+ );
70
+
71
+ // Should render the filter condition row
72
+ expect(container.querySelector('.space-y-3')).toBeInTheDocument();
73
+ });
74
+
75
+ it('renders multi-select checkboxes for lookup field with "in" operator', () => {
76
+ const onChange = vi.fn();
77
+ render(
78
+ <FilterBuilder
79
+ fields={lookupFields}
80
+ value={{
81
+ id: 'root',
82
+ logic: 'and',
83
+ conditions: [
84
+ { id: 'c1', field: 'account', operator: 'in', value: [] },
85
+ ],
86
+ }}
87
+ onChange={onChange}
88
+ />,
89
+ );
90
+
91
+ // Should render checkboxes for lookup options
92
+ const checkboxes = screen.getAllByRole('checkbox');
93
+ expect(checkboxes.length).toBe(3);
94
+ expect(screen.getByText('Acme Corp')).toBeInTheDocument();
95
+ expect(screen.getByText('Globex')).toBeInTheDocument();
96
+ expect(screen.getByText('Soylent Corp')).toBeInTheDocument();
97
+ });
98
+ });
99
+
100
+ describe('multi-select with in/notIn operator', () => {
101
+ it('renders checkbox list for select field with "in" operator', () => {
102
+ const onChange = vi.fn();
103
+ render(
104
+ <FilterBuilder
105
+ fields={selectFields}
106
+ value={{
107
+ id: 'root',
108
+ logic: 'and',
109
+ conditions: [
110
+ { id: 'c1', field: 'status', operator: 'in', value: [] },
111
+ ],
112
+ }}
113
+ onChange={onChange}
114
+ />,
115
+ );
116
+
117
+ // Should render checkboxes for all options
118
+ const checkboxes = screen.getAllByRole('checkbox');
119
+ expect(checkboxes.length).toBe(3);
120
+ expect(screen.getByText('Active')).toBeInTheDocument();
121
+ expect(screen.getByText('Inactive')).toBeInTheDocument();
122
+ expect(screen.getByText('Pending')).toBeInTheDocument();
123
+ });
124
+
125
+ it('checks selected items in multi-select', () => {
126
+ const onChange = vi.fn();
127
+ render(
128
+ <FilterBuilder
129
+ fields={selectFields}
130
+ value={{
131
+ id: 'root',
132
+ logic: 'and',
133
+ conditions: [
134
+ { id: 'c1', field: 'status', operator: 'in', value: ['active', 'pending'] },
135
+ ],
136
+ }}
137
+ onChange={onChange}
138
+ />,
139
+ );
140
+
141
+ const checkboxes = screen.getAllByRole('checkbox') as HTMLInputElement[];
142
+ // Active and Pending should be checked
143
+ expect(checkboxes[0]).toBeChecked(); // Active
144
+ expect(checkboxes[1]).not.toBeChecked(); // Inactive
145
+ expect(checkboxes[2]).toBeChecked(); // Pending
146
+ });
147
+
148
+ it('adds value when checkbox is checked', () => {
149
+ const onChange = vi.fn();
150
+ render(
151
+ <FilterBuilder
152
+ fields={selectFields}
153
+ value={{
154
+ id: 'root',
155
+ logic: 'and',
156
+ conditions: [
157
+ { id: 'c1', field: 'status', operator: 'in', value: ['active'] },
158
+ ],
159
+ }}
160
+ onChange={onChange}
161
+ />,
162
+ );
163
+
164
+ // Click the unchecked Inactive checkbox
165
+ const checkboxes = screen.getAllByRole('checkbox');
166
+ fireEvent.click(checkboxes[1]); // Inactive
167
+
168
+ expect(onChange).toHaveBeenCalled();
169
+ const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1][0];
170
+ const condition = lastCall.conditions[0];
171
+ expect(condition.value).toEqual(['active', 'inactive']);
172
+ });
173
+
174
+ it('removes value when checkbox is unchecked', () => {
175
+ const onChange = vi.fn();
176
+ render(
177
+ <FilterBuilder
178
+ fields={selectFields}
179
+ value={{
180
+ id: 'root',
181
+ logic: 'and',
182
+ conditions: [
183
+ { id: 'c1', field: 'status', operator: 'in', value: ['active', 'inactive'] },
184
+ ],
185
+ }}
186
+ onChange={onChange}
187
+ />,
188
+ );
189
+
190
+ // Click the checked Active checkbox
191
+ const checkboxes = screen.getAllByRole('checkbox');
192
+ fireEvent.click(checkboxes[0]); // Active
193
+
194
+ expect(onChange).toHaveBeenCalled();
195
+ const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1][0];
196
+ const condition = lastCall.conditions[0];
197
+ expect(condition.value).toEqual(['inactive']);
198
+ });
199
+
200
+ it('renders checkbox list for notIn operator', () => {
201
+ const onChange = vi.fn();
202
+ render(
203
+ <FilterBuilder
204
+ fields={selectFields}
205
+ value={{
206
+ id: 'root',
207
+ logic: 'and',
208
+ conditions: [
209
+ { id: 'c1', field: 'status', operator: 'notIn', value: [] },
210
+ ],
211
+ }}
212
+ onChange={onChange}
213
+ />,
214
+ );
215
+
216
+ const checkboxes = screen.getAllByRole('checkbox');
217
+ expect(checkboxes.length).toBe(3);
218
+ });
219
+
220
+ it('does not render checkboxes for equals operator (single select)', () => {
221
+ const onChange = vi.fn();
222
+ render(
223
+ <FilterBuilder
224
+ fields={selectFields}
225
+ value={{
226
+ id: 'root',
227
+ logic: 'and',
228
+ conditions: [
229
+ { id: 'c1', field: 'status', operator: 'equals', value: '' },
230
+ ],
231
+ }}
232
+ onChange={onChange}
233
+ />,
234
+ );
235
+
236
+ // Should NOT render checkboxes (single select via Select component)
237
+ expect(screen.queryAllByRole('checkbox').length).toBe(0);
238
+ });
239
+ });
240
+
241
+ describe('currency/percent/rating fields use number input', () => {
242
+ const numericFields = [
243
+ { value: 'amount', label: 'Amount', type: 'currency' },
244
+ { value: 'rate', label: 'Rate', type: 'percent' },
245
+ { value: 'score', label: 'Score', type: 'rating' },
246
+ ];
247
+
248
+ it('renders number input for currency field', () => {
249
+ const onChange = vi.fn();
250
+ render(
251
+ <FilterBuilder
252
+ fields={numericFields}
253
+ value={{
254
+ id: 'root',
255
+ logic: 'and',
256
+ conditions: [
257
+ { id: 'c1', field: 'amount', operator: 'equals', value: '' },
258
+ ],
259
+ }}
260
+ onChange={onChange}
261
+ />,
262
+ );
263
+
264
+ const input = screen.getByPlaceholderText('Value') as HTMLInputElement;
265
+ expect(input.type).toBe('number');
266
+ });
267
+
268
+ it('renders number input for percent field', () => {
269
+ const onChange = vi.fn();
270
+ render(
271
+ <FilterBuilder
272
+ fields={numericFields}
273
+ value={{
274
+ id: 'root',
275
+ logic: 'and',
276
+ conditions: [
277
+ { id: 'c1', field: 'rate', operator: 'equals', value: '' },
278
+ ],
279
+ }}
280
+ onChange={onChange}
281
+ />,
282
+ );
283
+
284
+ const input = screen.getByPlaceholderText('Value') as HTMLInputElement;
285
+ expect(input.type).toBe('number');
286
+ });
287
+ });
288
+
289
+ describe('datetime/time fields use appropriate input types', () => {
290
+ const dateTimeFields = [
291
+ { value: 'created_at', label: 'Created At', type: 'datetime' },
292
+ { value: 'start_time', label: 'Start Time', type: 'time' },
293
+ ];
294
+
295
+ it('renders datetime-local input for datetime field', () => {
296
+ const onChange = vi.fn();
297
+ render(
298
+ <FilterBuilder
299
+ fields={dateTimeFields}
300
+ value={{
301
+ id: 'root',
302
+ logic: 'and',
303
+ conditions: [
304
+ { id: 'c1', field: 'created_at', operator: 'equals', value: '' },
305
+ ],
306
+ }}
307
+ onChange={onChange}
308
+ />,
309
+ );
310
+
311
+ const input = screen.getByPlaceholderText('Value') as HTMLInputElement;
312
+ expect(input.type).toBe('datetime-local');
313
+ });
314
+
315
+ it('renders time input for time field', () => {
316
+ const onChange = vi.fn();
317
+ render(
318
+ <FilterBuilder
319
+ fields={dateTimeFields}
320
+ value={{
321
+ id: 'root',
322
+ logic: 'and',
323
+ conditions: [
324
+ { id: 'c1', field: 'start_time', operator: 'equals', value: '' },
325
+ ],
326
+ }}
327
+ onChange={onChange}
328
+ />,
329
+ );
330
+
331
+ const input = screen.getByPlaceholderText('Value') as HTMLInputElement;
332
+ expect(input.type).toBe('time');
333
+ });
334
+ });
335
+
336
+ describe('status field uses select operators and dropdown', () => {
337
+ const statusFields = [
338
+ {
339
+ value: 'pipeline',
340
+ label: 'Pipeline Stage',
341
+ type: 'status',
342
+ options: [
343
+ { value: 'lead', label: 'Lead' },
344
+ { value: 'qualified', label: 'Qualified' },
345
+ { value: 'won', label: 'Won' },
346
+ ],
347
+ },
348
+ ];
349
+
350
+ it('renders multi-select checkboxes for status field with "in" operator', () => {
351
+ const onChange = vi.fn();
352
+ render(
353
+ <FilterBuilder
354
+ fields={statusFields}
355
+ value={{
356
+ id: 'root',
357
+ logic: 'and',
358
+ conditions: [
359
+ { id: 'c1', field: 'pipeline', operator: 'in', value: [] },
360
+ ],
361
+ }}
362
+ onChange={onChange}
363
+ />,
364
+ );
365
+
366
+ const checkboxes = screen.getAllByRole('checkbox');
367
+ expect(checkboxes.length).toBe(3);
368
+ expect(screen.getByText('Lead')).toBeInTheDocument();
369
+ expect(screen.getByText('Qualified')).toBeInTheDocument();
370
+ expect(screen.getByText('Won')).toBeInTheDocument();
371
+ });
372
+ });
373
+
374
+ describe('user/owner field uses lookup operators and dropdown', () => {
375
+ const userFields = [
376
+ {
377
+ value: 'assigned_to',
378
+ label: 'Assigned To',
379
+ type: 'user',
380
+ options: [
381
+ { value: 'user1', label: 'Alice' },
382
+ { value: 'user2', label: 'Bob' },
383
+ ],
384
+ },
385
+ ];
386
+
387
+ it('renders multi-select checkboxes for user field with "in" operator', () => {
388
+ const onChange = vi.fn();
389
+ render(
390
+ <FilterBuilder
391
+ fields={userFields}
392
+ value={{
393
+ id: 'root',
394
+ logic: 'and',
395
+ conditions: [
396
+ { id: 'c1', field: 'assigned_to', operator: 'in', value: [] },
397
+ ],
398
+ }}
399
+ onChange={onChange}
400
+ />,
401
+ );
402
+
403
+ const checkboxes = screen.getAllByRole('checkbox');
404
+ expect(checkboxes.length).toBe(2);
405
+ expect(screen.getByText('Alice')).toBeInTheDocument();
406
+ expect(screen.getByText('Bob')).toBeInTheDocument();
407
+ });
408
+ });
409
+ });
@@ -13,6 +13,7 @@ import { X, Plus, Trash2 } from "lucide-react"
13
13
 
14
14
  import { cn } from "../lib/utils"
15
15
  import { Button } from "../ui/button"
16
+ import { Checkbox } from "../ui/checkbox"
16
17
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"
17
18
  import { Input } from "../ui/input"
18
19
 
@@ -20,7 +21,7 @@ export interface FilterCondition {
20
21
  id: string
21
22
  field: string
22
23
  operator: string
23
- value: string | number | boolean
24
+ value: string | number | boolean | (string | number | boolean)[]
24
25
  }
25
26
 
26
27
  export interface FilterGroup {
@@ -65,6 +66,23 @@ const numberOperators = ["equals", "notEquals", "greaterThan", "lessThan", "grea
65
66
  const booleanOperators = ["equals", "notEquals"]
66
67
  const dateOperators = ["equals", "notEquals", "before", "after", "between", "isEmpty", "isNotEmpty"]
67
68
  const selectOperators = ["equals", "notEquals", "in", "notIn", "isEmpty", "isNotEmpty"]
69
+ const lookupOperators = ["equals", "notEquals", "in", "notIn", "isEmpty", "isNotEmpty"]
70
+
71
+ /** Field types that share the same operator/input behavior as number (numeric comparison operators, number input) */
72
+ const numberLikeTypes = ["number", "currency", "percent", "rating"]
73
+ /** Field types that share the same operator/input behavior as date (before/after operators, date/datetime/time input) */
74
+ const dateLikeTypes = ["date", "datetime", "time"]
75
+ /** Field types that use select operators (equals/in/notIn) and render dropdown or checkbox list when options provided */
76
+ const selectLikeTypes = ["select", "status"]
77
+ /** Relational/reference field types that use lookup operators (equals/in/notIn) and render dropdown or checkbox list when options provided */
78
+ const lookupLikeTypes = ["lookup", "master_detail", "user", "owner"]
79
+
80
+ /** Normalize a filter value into an array for multi-select scenarios */
81
+ function normalizeToArray(value: FilterCondition["value"]): (string | number | boolean)[] {
82
+ if (Array.isArray(value)) return value
83
+ if (value !== undefined && value !== null && value !== "") return [value as string | number | boolean]
84
+ return []
85
+ }
68
86
 
69
87
  function FilterBuilder({
70
88
  fields = [],
@@ -139,19 +157,22 @@ function FilterBuilder({
139
157
  const field = fields.find((f) => f.value === fieldValue)
140
158
  const fieldType = field?.type || "text"
141
159
 
142
- switch (fieldType) {
143
- case "number":
144
- return defaultOperators.filter((op) => numberOperators.includes(op.value))
145
- case "boolean":
146
- return defaultOperators.filter((op) => booleanOperators.includes(op.value))
147
- case "date":
148
- return defaultOperators.filter((op) => dateOperators.includes(op.value))
149
- case "select":
150
- return defaultOperators.filter((op) => selectOperators.includes(op.value))
151
- case "text":
152
- default:
153
- return defaultOperators.filter((op) => textOperators.includes(op.value))
160
+ if (numberLikeTypes.includes(fieldType)) {
161
+ return defaultOperators.filter((op) => numberOperators.includes(op.value))
162
+ }
163
+ if (fieldType === "boolean") {
164
+ return defaultOperators.filter((op) => booleanOperators.includes(op.value))
165
+ }
166
+ if (dateLikeTypes.includes(fieldType)) {
167
+ return defaultOperators.filter((op) => dateOperators.includes(op.value))
168
+ }
169
+ if (selectLikeTypes.includes(fieldType)) {
170
+ return defaultOperators.filter((op) => selectOperators.includes(op.value))
171
+ }
172
+ if (lookupLikeTypes.includes(fieldType)) {
173
+ return defaultOperators.filter((op) => lookupOperators.includes(op.value))
154
174
  }
175
+ return defaultOperators.filter((op) => textOperators.includes(op.value))
155
176
  }
156
177
 
157
178
  const needsValueInput = (operator: string) => {
@@ -162,21 +183,51 @@ function FilterBuilder({
162
183
  const field = fields.find((f) => f.value === fieldValue)
163
184
  const fieldType = field?.type || "text"
164
185
 
165
- switch (fieldType) {
166
- case "number":
167
- return "number"
168
- case "date":
169
- return "date"
170
- default:
171
- return "text"
172
- }
186
+ if (numberLikeTypes.includes(fieldType)) return "number"
187
+ if (fieldType === "date") return "date"
188
+ if (fieldType === "datetime") return "datetime-local"
189
+ if (fieldType === "time") return "time"
190
+ return "text"
173
191
  }
174
192
 
175
193
  const renderValueInput = (condition: FilterCondition) => {
176
194
  const field = fields.find((f) => f.value === condition.field)
195
+ const isMultiOperator = ["in", "notIn"].includes(condition.operator)
177
196
 
178
- // For select fields with options
179
- if (field?.type === "select" && field.options) {
197
+ // For select/lookup fields with options and multi-select operator (in/notIn)
198
+ if (field?.options && isMultiOperator) {
199
+ const selectedValues = normalizeToArray(condition.value)
200
+ return (
201
+ <div className="max-h-40 overflow-y-auto space-y-0.5 border rounded-md p-2">
202
+ {field.options.map((opt) => {
203
+ const isChecked = selectedValues.map(String).includes(String(opt.value))
204
+ return (
205
+ <label
206
+ key={opt.value}
207
+ className={cn(
208
+ "flex items-center gap-2 text-sm py-1 px-1.5 rounded cursor-pointer",
209
+ isChecked ? "bg-primary/5 text-primary" : "hover:bg-muted",
210
+ )}
211
+ >
212
+ <Checkbox
213
+ checked={isChecked}
214
+ onCheckedChange={(checked) => {
215
+ const next = checked
216
+ ? [...selectedValues, opt.value]
217
+ : selectedValues.filter((v) => String(v) !== String(opt.value))
218
+ updateCondition(condition.id, { value: next })
219
+ }}
220
+ />
221
+ <span className="truncate">{opt.label}</span>
222
+ </label>
223
+ )
224
+ })}
225
+ </div>
226
+ )
227
+ }
228
+
229
+ // For select/lookup fields with options (single select)
230
+ if (field?.options && (selectLikeTypes.includes(field.type || "") || lookupLikeTypes.includes(field.type || ""))) {
180
231
  return (
181
232
  <Select
182
233
  value={String(condition.value || "")}
@@ -235,9 +286,9 @@ function FilterBuilder({
235
286
  const handleValueChange = (newValue: string) => {
236
287
  let convertedValue: string | number | boolean = newValue
237
288
 
238
- if (field?.type === "number" && newValue !== "") {
289
+ if (numberLikeTypes.includes(field?.type || "") && newValue !== "") {
239
290
  convertedValue = parseFloat(newValue) || 0
240
- } else if (field?.type === "date") {
291
+ } else if (dateLikeTypes.includes(field?.type || "")) {
241
292
  convertedValue = newValue // Keep as ISO string
242
293
  }
243
294