@nocobase/client-v2 2.2.0-alpha.7 → 2.2.0-beta.10

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 (50) hide show
  1. package/es/BaseApplication.d.ts +1 -1
  2. package/es/components/form/TypedVariableInput.d.ts +5 -31
  3. package/es/components/form/filter/CollectionFilter.d.ts +0 -2
  4. package/es/components/form/filter/CollectionFilterItem.d.ts +1 -11
  5. package/es/components/form/filter/CollectionFilterPanel.d.ts +0 -2
  6. package/es/components/form/filter/index.d.ts +0 -2
  7. package/es/components/form/filter/useFilterActionProps.d.ts +0 -2
  8. package/es/components/index.d.ts +0 -1
  9. package/es/flow/components/FieldAssignExactDatePicker.d.ts +0 -1
  10. package/es/flow/components/FieldAssignValueInput.d.ts +0 -3
  11. package/es/flow/components/RunJSValueEditor.d.ts +0 -1
  12. package/es/flow/index.d.ts +0 -1
  13. package/es/index.d.ts +0 -4
  14. package/es/index.mjs +99 -133
  15. package/es/utils/index.d.ts +0 -1
  16. package/lib/index.js +106 -140
  17. package/package.json +7 -8
  18. package/src/BaseApplication.tsx +6 -6
  19. package/src/__tests__/app.test.tsx +0 -26
  20. package/src/__tests__/browserChecker.test.ts +0 -61
  21. package/src/components/README.md +1 -7
  22. package/src/components/README.zh-CN.md +1 -6
  23. package/src/components/form/JsonTextArea.tsx +0 -4
  24. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +2 -145
  25. package/src/components/form/ScanInput/useCodeScanner.ts +2 -154
  26. package/src/components/form/TypedVariableInput.tsx +98 -445
  27. package/src/components/form/__tests__/TypedVariableInput.test.tsx +9 -320
  28. package/src/components/form/filter/CollectionFilter.tsx +0 -4
  29. package/src/components/form/filter/CollectionFilterItem.tsx +7 -32
  30. package/src/components/form/filter/CollectionFilterPanel.tsx +0 -4
  31. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +0 -38
  32. package/src/components/form/filter/index.ts +0 -2
  33. package/src/components/form/filter/useFilterActionProps.ts +6 -37
  34. package/src/components/index.ts +0 -1
  35. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +1 -0
  36. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +6 -253
  37. package/src/flow/components/FieldAssignExactDatePicker.tsx +11 -25
  38. package/src/flow/components/FieldAssignValueInput.tsx +9 -55
  39. package/src/flow/components/RunJSValueEditor.tsx +1 -9
  40. package/src/flow/index.ts +0 -1
  41. package/src/index.ts +0 -4
  42. package/src/utils/index.tsx +0 -1
  43. package/es/components/category-tabs/SortableCategoryTabs.d.ts +0 -55
  44. package/es/components/category-tabs/index.d.ts +0 -9
  45. package/es/utils/getRouteRuntimeVersion.d.ts +0 -23
  46. package/src/__tests__/getRouteRuntimeVersion.test.ts +0 -68
  47. package/src/components/category-tabs/SortableCategoryTabs.tsx +0 -210
  48. package/src/components/category-tabs/index.ts +0 -10
  49. package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +0 -95
  50. package/src/utils/getRouteRuntimeVersion.ts +0 -185
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { FlowContext, FlowContextProvider, type MetaTreeNode } from '@nocobase/flow-engine';
10
+ import { FlowContext, FlowContextProvider } from '@nocobase/flow-engine';
11
11
  import { fireEvent, render, screen, waitFor } from '@testing-library/react';
12
12
  import React from 'react';
13
13
  import { describe, expect, it, vi } from 'vitest';
@@ -64,7 +64,6 @@ describe('TypedVariableInput - constant rendering', () => {
64
64
  const item = screen.getByText('True');
65
65
  expect(item).toBeInTheDocument();
66
66
  });
67
- expect(screen.getByRole('button', { name: 'variable-switcher' }).className).not.toContain('ant-btn-primary');
68
67
  });
69
68
 
70
69
  it('renders the Null placeholder when value=null and nullable=true', async () => {
@@ -78,69 +77,6 @@ describe('TypedVariableInput - constant rendering', () => {
78
77
  const nullInput = await screen.findByPlaceholderText('<Null>');
79
78
  expect(nullInput).toBeInTheDocument();
80
79
  expect(nullInput.getAttribute('readonly')).not.toBeNull();
81
- expect(screen.getByRole('button', { name: 'variable-switcher' }).className).not.toContain('ant-btn-primary');
82
- });
83
-
84
- it('defaults undefined to the first constant type', async () => {
85
- const ctx = createContextWithEnv();
86
- const handleChange = vi.fn();
87
- renderWithCtx(
88
- ctx,
89
- <TypedVariableInput
90
- value={undefined}
91
- types={['string', 'number']}
92
- namespaces={['$env']}
93
- nullable
94
- onChange={handleChange}
95
- />,
96
- );
97
-
98
- expect(screen.queryByPlaceholderText('<Null>')).toBeNull();
99
- expect(screen.getByRole('textbox')).toBeInTheDocument();
100
- await waitFor(() => {
101
- expect(handleChange).toHaveBeenCalledWith('');
102
- });
103
- });
104
-
105
- it('can still opt out to keep the null placeholder for undefined', async () => {
106
- const ctx = createContextWithEnv();
107
- renderWithCtx(
108
- ctx,
109
- <TypedVariableInput
110
- value={undefined}
111
- types={['string', 'number']}
112
- namespaces={['$env']}
113
- nullable
114
- defaultToFirstConstantTypeWhenUndefined={false}
115
- onChange={() => undefined}
116
- />,
117
- );
118
-
119
- const nullInput = await screen.findByPlaceholderText('<Null>');
120
- expect(nullInput).toBeInTheDocument();
121
- });
122
-
123
- it('highlights the defaulted first constant type when the switcher opens', async () => {
124
- const ctx = createContextWithEnv();
125
- renderWithCtx(
126
- ctx,
127
- <TypedVariableInput
128
- value={undefined}
129
- types={['string', 'number']}
130
- namespaces={['$env']}
131
- nullable
132
- onChange={() => undefined}
133
- />,
134
- );
135
-
136
- fireEvent.click(screen.getByRole('button', { name: 'variable-switcher' }));
137
-
138
- await waitFor(() => {
139
- const constant = screen.getAllByText('Constant')[0];
140
- const string = screen.getAllByText('String')[0];
141
- expect(constant.closest('.ant-cascader-menu-item-active')).not.toBeNull();
142
- expect(string.closest('.ant-cascader-menu-item-active')).not.toBeNull();
143
- });
144
80
  });
145
81
  });
146
82
 
@@ -163,10 +99,10 @@ describe('TypedVariableInput - variable rendering', () => {
163
99
  });
164
100
  });
165
101
 
166
- it('clears back to default-of-first-type when the close button is clicked (nullable=true)', async () => {
102
+ it('clears back to null when the close button is clicked (nullable=true)', async () => {
167
103
  const ctx = createContextWithEnv();
168
104
  const handleChange = vi.fn();
169
- const { container } = renderWithCtx(
105
+ renderWithCtx(
170
106
  ctx,
171
107
  <TypedVariableInput
172
108
  value="{{$env.SMTP_PORT}}"
@@ -176,17 +112,15 @@ describe('TypedVariableInput - variable rendering', () => {
176
112
  onChange={handleChange}
177
113
  />,
178
114
  );
179
- const clear = container.querySelector('button.clear-button') as HTMLButtonElement | null;
180
- expect(clear).not.toBeNull();
181
- expect(clear).toHaveClass('clear-button');
182
- fireEvent.click(clear as HTMLButtonElement);
183
- expect(handleChange).toHaveBeenCalledWith(0);
115
+ const clear = await screen.findByRole('button', { name: 'icon-close' });
116
+ fireEvent.click(clear);
117
+ expect(handleChange).toHaveBeenCalledWith(null);
184
118
  });
185
119
 
186
120
  it('clears back to default-of-first-type when nullable=false', async () => {
187
121
  const ctx = createContextWithEnv();
188
122
  const handleChange = vi.fn();
189
- const { container } = renderWithCtx(
123
+ renderWithCtx(
190
124
  ctx,
191
125
  <TypedVariableInput
192
126
  value="{{$env.SMTP_PORT}}"
@@ -196,255 +130,10 @@ describe('TypedVariableInput - variable rendering', () => {
196
130
  onChange={handleChange}
197
131
  />,
198
132
  );
199
- const clear = container.querySelector('button.clear-button') as HTMLButtonElement | null;
200
- expect(clear).not.toBeNull();
201
- fireEvent.click(clear as HTMLButtonElement);
133
+ const clear = await screen.findByRole('button', { name: 'icon-close' });
134
+ fireEvent.click(clear);
202
135
  expect(handleChange).toHaveBeenCalledWith(0);
203
136
  });
204
-
205
- it('treats types=[] as variable-only mode with a readonly placeholder before selection', async () => {
206
- const ctx = createContextWithEnv();
207
- renderWithCtx(
208
- ctx,
209
- <TypedVariableInput
210
- value={undefined}
211
- types={[]}
212
- namespaces={['$env']}
213
- placeholder="Select variable"
214
- onChange={() => undefined}
215
- />,
216
- );
217
-
218
- const input = await screen.findByPlaceholderText('Select variable');
219
- expect(input).toBeInTheDocument();
220
- expect(input).toHaveAttribute('readonly');
221
- expect(screen.queryByRole('button', { name: 'variable-switcher' })).toBeInTheDocument();
222
- });
223
-
224
- it('clears a variable-only selection back to null', async () => {
225
- const ctx = createContextWithEnv();
226
- const handleChange = vi.fn();
227
- const { container } = renderWithCtx(
228
- ctx,
229
- <TypedVariableInput value="{{$env.SMTP_PORT}}" types={[]} namespaces={['$env']} onChange={handleChange} />,
230
- );
231
-
232
- const clear = container.querySelector('button.clear-button') as HTMLButtonElement | null;
233
- expect(clear).not.toBeNull();
234
- fireEvent.click(clear as HTMLButtonElement);
235
- expect(handleChange).toHaveBeenCalledWith(null);
236
- });
237
-
238
- it('hides the variable switcher when hideVariable=true in variable-only mode', async () => {
239
- const ctx = createContextWithEnv();
240
- renderWithCtx(
241
- ctx,
242
- <TypedVariableInput
243
- value="{{$env.SMTP_PORT}}"
244
- types={[]}
245
- namespaces={['$env']}
246
- hideVariable
247
- onChange={() => undefined}
248
- />,
249
- );
250
-
251
- await waitFor(() => {
252
- expect(screen.getByRole('button', { name: 'variable-tag' })).toBeInTheDocument();
253
- });
254
- expect(screen.queryByRole('button', { name: 'variable-switcher' })).toBeNull();
255
- });
256
- });
257
-
258
- describe('TypedVariableInput - object / JSON constant', () => {
259
- it('renders a JSON textarea for an object value when types include object', async () => {
260
- const ctx = createContextWithEnv();
261
- renderWithCtx(
262
- ctx,
263
- <TypedVariableInput value={{ a: 1 }} types={['object']} namespaces={['$env']} onChange={() => undefined} />,
264
- );
265
- // The object is stringified into a textarea.
266
- const textarea = await screen.findByDisplayValue(/"a": 1/);
267
- expect(textarea.tagName.toLowerCase()).toBe('textarea');
268
- });
269
-
270
- it('parses valid JSON on blur and emits the parsed object', async () => {
271
- const ctx = createContextWithEnv();
272
- const handleChange = vi.fn();
273
- renderWithCtx(
274
- ctx,
275
- <TypedVariableInput value={{}} types={['object']} namespaces={['$env']} onChange={handleChange} />,
276
- );
277
- const textarea = await screen.findByDisplayValue('{}');
278
- fireEvent.change(textarea, { target: { value: '{"x": 42}' } });
279
- fireEvent.blur(textarea);
280
- expect(handleChange).toHaveBeenLastCalledWith({ x: 42 });
281
- });
282
-
283
- it('shows an error live on change (before blur) and does not emit on invalid JSON', async () => {
284
- const ctx = createContextWithEnv();
285
- const handleChange = vi.fn();
286
- renderWithCtx(
287
- ctx,
288
- <TypedVariableInput value={{}} types={['object']} namespaces={['$env']} onChange={handleChange} />,
289
- );
290
- const textarea = await screen.findByDisplayValue('{}');
291
- // Typing an invalid value surfaces the error immediately — no blur needed (mirrors v1's `Json`, which validates on
292
- // every change).
293
- fireEvent.change(textarea, { target: { value: '{ not json' } });
294
- await waitFor(() => {
295
- // The raw `JSON.parse` error message is shown (matching v1), e.g. "Expected property name or '}' in JSON at
296
- // position …".
297
- expect(screen.getByText(/Expected property name/i)).toBeInTheDocument();
298
- });
299
- // The value is only emitted on blur, and never for an invalid value.
300
- fireEvent.blur(textarea);
301
- expect(handleChange).not.toHaveBeenCalled();
302
- });
303
-
304
- it('defaults to {} when the Constant > JSON type is picked', async () => {
305
- const ctx = createContextWithEnv();
306
- const handleChange = vi.fn();
307
- renderWithCtx(
308
- ctx,
309
- <TypedVariableInput value={null} types={['object']} namespaces={['$env']} onChange={handleChange} />,
310
- );
311
- fireEvent.click(screen.getByRole('button', { name: 'variable-switcher' }));
312
- // Constant submenu → JSON leaf.
313
- fireEvent.click(await screen.findByText('Constant'));
314
- fireEvent.click(await screen.findByText('JSON'));
315
- expect(handleChange).toHaveBeenCalledWith({});
316
- });
317
- });
318
-
319
- describe('TypedVariableInput - injected metaTree', () => {
320
- // A bare context with no global `$env` — the injected tree is the only source.
321
- function createBareContext() {
322
- const ctx = new FlowContext();
323
- (ctx as any).t = (key: string) => key;
324
- return ctx;
325
- }
326
-
327
- // Mirrors a workflow node-result tree: a root whose children load lazily.
328
- function makeLazyTree(loadChildren: () => Promise<MetaTreeNode[]>): MetaTreeNode[] {
329
- return [
330
- {
331
- name: '$jobsMapByNodeKey',
332
- title: 'Node result',
333
- type: 'object',
334
- paths: ['$jobsMapByNodeKey'],
335
- children: loadChildren,
336
- },
337
- ];
338
- }
339
-
340
- it('renders the injected tree in the switcher, not the global one', async () => {
341
- const ctx = createBareContext();
342
- const metaTree: MetaTreeNode[] = [
343
- { name: 'n1', title: 'Approval node', type: 'object', paths: ['$jobsMapByNodeKey', 'n1'] },
344
- ];
345
- renderWithCtx(ctx, <TypedVariableInput value={null} metaTree={metaTree} onChange={() => undefined} />);
346
- fireEvent.click(screen.getByRole('button', { name: 'variable-switcher' }));
347
- await waitFor(() => {
348
- expect(screen.getByText('Approval node')).toBeInTheDocument();
349
- });
350
- });
351
-
352
- it('lazily resolves function children when a node is expanded', async () => {
353
- const ctx = createBareContext();
354
- const loadChildren = vi.fn(async () => [
355
- { name: 'status', title: 'Status', type: 'string', paths: ['$jobsMapByNodeKey', 'n1', 'status'] },
356
- ]);
357
- const metaTree = makeLazyTree(loadChildren);
358
- renderWithCtx(ctx, <TypedVariableInput value={null} metaTree={metaTree} onChange={() => undefined} />);
359
-
360
- fireEvent.click(screen.getByRole('button', { name: 'variable-switcher' }));
361
- const root = await screen.findByText('Node result');
362
- // Expanding the lazy node triggers loadData → loadMetaTreeChildren.
363
- fireEvent.click(root);
364
- await waitFor(() => {
365
- expect(loadChildren).toHaveBeenCalled();
366
- expect(screen.getByText('Status')).toBeInTheDocument();
367
- });
368
- });
369
-
370
- it('surfaces lazy children on the FIRST expansion (no stuck-loading regression)', async () => {
371
- const ctx = createBareContext();
372
- // A deferred promise so loading→resolved is a real two-phase transition, reproducing the rc-cascader
373
- // reference-cache bug where the first expand stayed stuck on the spinner until the column was reopened.
374
- let resolveChildren: (nodes: MetaTreeNode[]) => void = () => undefined;
375
- const childrenPromise = new Promise<MetaTreeNode[]>((resolve) => {
376
- resolveChildren = resolve;
377
- });
378
- const loadChildren = vi.fn(() => childrenPromise);
379
- const metaTree = makeLazyTree(loadChildren);
380
- renderWithCtx(ctx, <TypedVariableInput value={null} metaTree={metaTree} onChange={() => undefined} />);
381
-
382
- fireEvent.click(screen.getByRole('button', { name: 'variable-switcher' }));
383
- const root = await screen.findByText('Node result');
384
- fireEvent.click(root);
385
- expect(loadChildren).toHaveBeenCalledTimes(1);
386
- // Resolve after the click — the child must appear without reopening.
387
- resolveChildren([
388
- { name: 'secret', title: 'SMTP_HOST', type: 'string', paths: ['$jobsMapByNodeKey', 'n1', 'secret'] },
389
- ]);
390
- await waitFor(() => {
391
- expect(screen.getByText('SMTP_HOST')).toBeInTheDocument();
392
- });
393
- });
394
-
395
- it('emits the selected variable as a {{ ... }} expression with no inner spaces', async () => {
396
- const ctx = createBareContext();
397
- const handleChange = vi.fn();
398
- const metaTree: MetaTreeNode[] = [
399
- { name: 'n1', title: 'Approval node', type: 'string', paths: ['$jobsMapByNodeKey', 'n1'] },
400
- ];
401
- renderWithCtx(ctx, <TypedVariableInput value={null} metaTree={metaTree} onChange={handleChange} />);
402
- fireEvent.click(screen.getByRole('button', { name: 'variable-switcher' }));
403
- const leaf = await screen.findByText('Approval node');
404
- fireEvent.click(leaf);
405
- await waitFor(() => {
406
- expect(handleChange).toHaveBeenCalledWith('{{$jobsMapByNodeKey.n1}}');
407
- });
408
- });
409
-
410
- it('preloads a saved variable label across a lazy level (deep label not dropped on mount)', async () => {
411
- const ctx = createBareContext();
412
- // The query-node case: a saved reference `$jobsMapByNodeKey.n1.role` points below `n1`, whose children are a lazy
413
- // thunk (relation not yet expanded). On mount the tag must show the full path "Query / Role", not stop at "Query" —
414
- // the component walks the saved path resolving each lazy level. (Regression: reopening a saved condition showed
415
- // only "节点数据 / 查询数据" and dropped "角色标识".)
416
- const loadRoleFields = vi.fn(async () => [
417
- { name: 'role', title: 'Role', type: 'string', paths: ['$jobsMapByNodeKey', 'n1', 'role'] },
418
- ]);
419
- const metaTree: MetaTreeNode[] = [
420
- {
421
- name: '$jobsMapByNodeKey',
422
- title: 'Node result',
423
- type: 'object',
424
- paths: ['$jobsMapByNodeKey'],
425
- children: [
426
- {
427
- name: 'n1',
428
- title: 'Query',
429
- type: 'object',
430
- paths: ['$jobsMapByNodeKey', 'n1'],
431
- children: loadRoleFields,
432
- },
433
- ],
434
- },
435
- ];
436
- renderWithCtx(
437
- ctx,
438
- <TypedVariableInput value="{{$jobsMapByNodeKey.n1.role}}" metaTree={metaTree} onChange={() => undefined} />,
439
- );
440
- const tag = screen.getByRole('button', { name: 'variable-tag' });
441
- // Preload resolves the lazy level so the deep label appears without expanding.
442
- await waitFor(() => {
443
- expect(loadRoleFields).toHaveBeenCalled();
444
- expect(tag.textContent).toContain('Role');
445
- });
446
- expect(tag.textContent).toContain('Query');
447
- });
448
137
  });
449
138
 
450
139
  describe('TypedVariableInput - editor onChange propagation', () => {
@@ -21,8 +21,6 @@ export interface CollectionFilterProps {
21
21
  collection: Collection | undefined;
22
22
  /** Previously compiled filter param used to seed the editable filter group. */
23
23
  initialValue?: CompiledFilter;
24
- /** Default compiled filter used both for initial empty state and Reset. */
25
- defaultValue?: CompiledFilter;
26
24
  /** Called on Submit or Reset with the compiled NocoBase filter param (`undefined` when cleared). */
27
25
  onChange: (filter: CompiledFilter) => void;
28
26
  /** Translator. Defaults to identity. */
@@ -60,7 +58,6 @@ export const CollectionFilter: FC<CollectionFilterProps> = (props) => {
60
58
  const {
61
59
  collection,
62
60
  initialValue,
63
- defaultValue,
64
61
  onChange,
65
62
  t = identity,
66
63
  filterableFieldNames,
@@ -78,7 +75,6 @@ export const CollectionFilter: FC<CollectionFilterProps> = (props) => {
78
75
  const filterAction = useFilterActionProps({
79
76
  collection,
80
77
  initialValue,
81
- defaultValue,
82
78
  filterableFieldNames,
83
79
  nonfilterableFieldNames,
84
80
  noIgnore,
@@ -58,16 +58,6 @@ export interface CollectionFilterItemProps {
58
58
  t?: (key: string) => string;
59
59
  /** Optional v2 app registry used to resolve plugin-provided operator components. */
60
60
  app?: { getComponent?: (name: string) => React.ComponentType<any> | undefined };
61
- /** Optional override for the left field picker placeholder. */
62
- fieldPlaceholder?: string;
63
- /** Optional override for the operator picker placeholder. */
64
- operatorPlaceholder?: string;
65
- /** Optional override for the value input placeholder. Pass `null` to suppress it. */
66
- valuePlaceholder?: string | null;
67
- /** Optional override for the left field picker width. Defaults to v2's original 200px. */
68
- fieldWidth?: number;
69
- /** Optional override for the operator picker min-width. Defaults to v2's original 120px. */
70
- operatorMinWidth?: number;
71
61
  }
72
62
 
73
63
  const identity = (s: string) => s;
@@ -108,12 +98,6 @@ export const CollectionFilterItem: FC<CollectionFilterItemProps> = observer(
108
98
  const { path: leftValue, operator, value: rightValue } = props.value;
109
99
  const flowEngine = useFlowEngine({ throwError: false }) as any;
110
100
  const app = props.app || flowEngine?.context?.app;
111
- const fieldPlaceholder = props.fieldPlaceholder ?? t('Select field');
112
- const operatorPlaceholder = props.operatorPlaceholder ?? t('Comparision');
113
- const valuePlaceholder =
114
- props.valuePlaceholder === undefined ? t('Enter value') : props.valuePlaceholder || undefined;
115
- const fieldWidth = props.fieldWidth ?? 200;
116
- const operatorMinWidth = props.operatorMinWidth ?? 120;
117
101
 
118
102
  const options = useFilterOptions(collection, { filterableFieldNames, nonfilterableFieldNames, noIgnore, t });
119
103
 
@@ -151,8 +135,8 @@ export const CollectionFilterItem: FC<CollectionFilterItemProps> = observer(
151
135
  return (
152
136
  <Space wrap>
153
137
  <Cascader
154
- style={{ width: fieldWidth }}
155
- placeholder={fieldPlaceholder}
138
+ style={{ width: 200 }}
139
+ placeholder={t('Select field')}
156
140
  options={cascaderOptions}
157
141
  value={fieldPath}
158
142
  onChange={handleFieldChange}
@@ -161,15 +145,15 @@ export const CollectionFilterItem: FC<CollectionFilterItemProps> = observer(
161
145
  popupClassName={cascaderPopupClass}
162
146
  />
163
147
  <Select
164
- style={{ minWidth: operatorMinWidth }}
165
- placeholder={operatorPlaceholder}
148
+ style={{ width: 120 }}
149
+ placeholder={t('Comparision')}
166
150
  value={operator || undefined}
167
151
  onChange={handleOperatorChange}
168
152
  disabled={!leftValue || operatorOptions.length === 0}
169
153
  >
170
154
  {operatorOptions.map((op) => (
171
155
  <Select.Option key={op.value} value={op.value}>
172
- {typeof op.label === 'string' ? t(op.label) : op.label}
156
+ {op.label}
173
157
  </Select.Option>
174
158
  ))}
175
159
  </Select>
@@ -178,7 +162,7 @@ export const CollectionFilterItem: FC<CollectionFilterItemProps> = observer(
178
162
  operator={selectedOperator}
179
163
  value={rightValue}
180
164
  onChange={handleValueChange}
181
- placeholder={valuePlaceholder}
165
+ placeholder={t('Enter value')}
182
166
  t={t}
183
167
  app={app}
184
168
  />
@@ -195,16 +179,7 @@ export function createCollectionFilterItem(
195
179
  collection: Collection,
196
180
  bound?: Pick<
197
181
  CollectionFilterItemProps,
198
- | 'filterableFieldNames'
199
- | 'nonfilterableFieldNames'
200
- | 'noIgnore'
201
- | 't'
202
- | 'app'
203
- | 'fieldPlaceholder'
204
- | 'operatorPlaceholder'
205
- | 'valuePlaceholder'
206
- | 'fieldWidth'
207
- | 'operatorMinWidth'
182
+ 'filterableFieldNames' | 'nonfilterableFieldNames' | 'noIgnore' | 't' | 'app'
208
183
  >,
209
184
  ) {
210
185
  const Component: FC<{ value: CollectionFilterItemValue }> = (props) => (
@@ -30,8 +30,6 @@ export interface CollectionFilterPanelProps {
30
30
  collection: Collection | undefined;
31
31
  /** Previously compiled filter param used to seed the editable filter group. */
32
32
  initialValue?: CompiledFilter;
33
- /** Default compiled filter used both for initial empty state and Reset. */
34
- defaultValue?: CompiledFilter;
35
33
  /** Called when the condition group structure changes or `reset()` is invoked. */
36
34
  onChange?: (filter: CompiledFilter) => void;
37
35
  /** Translator. Defaults to identity. */
@@ -53,7 +51,6 @@ export const CollectionFilterPanel = forwardRef<CollectionFilterPanelRef, Collec
53
51
  const {
54
52
  collection,
55
53
  initialValue,
56
- defaultValue,
57
54
  onChange,
58
55
  t = identity,
59
56
  filterableFieldNames,
@@ -64,7 +61,6 @@ export const CollectionFilterPanel = forwardRef<CollectionFilterPanelRef, Collec
64
61
  const filterAction = useFilterActionProps({
65
62
  collection,
66
63
  initialValue,
67
- defaultValue,
68
64
  filterableFieldNames,
69
65
  nonfilterableFieldNames,
70
66
  noIgnore,
@@ -244,42 +244,4 @@ describe('CollectionFilterItem', () => {
244
244
  expect(value.path).toBe('username');
245
245
  });
246
246
  });
247
-
248
- it('keeps default widths at 200/120 and honours explicit width/placeholder overrides', () => {
249
- const value = observable({ path: 'username', operator: '$eq', value: '' });
250
- const collection = buildStubCollection([{ name: 'username', title: 'Username' }]);
251
-
252
- const { container, rerender } = render(<CollectionFilterItem value={value} collection={collection} />);
253
-
254
- let selects = container.querySelectorAll('.ant-select');
255
- expect(selects[0]).toHaveStyle({ width: '200px' });
256
- expect(selects[1]).toHaveStyle({ minWidth: '120px' });
257
-
258
- rerender(
259
- <CollectionFilterItem
260
- value={observable({ path: '', operator: '', value: '' })}
261
- collection={collection}
262
- fieldWidth={160}
263
- operatorMinWidth={110}
264
- fieldPlaceholder="Choose field"
265
- operatorPlaceholder="Choose operator"
266
- />,
267
- );
268
-
269
- selects = container.querySelectorAll('.ant-select');
270
- expect(selects[0]).toHaveStyle({ width: '160px' });
271
- expect(selects[1]).toHaveStyle({ minWidth: '110px' });
272
- expect(screen.getByText('Choose field')).toBeInTheDocument();
273
- });
274
-
275
- it('lets callers suppress the fallback value placeholder', () => {
276
- const value = observable({ path: 'username', operator: '$eq', value: '' });
277
- const collection = buildStubCollection([{ name: 'username', title: 'Username' }]);
278
-
279
- const { rerender } = render(<CollectionFilterItem value={value} collection={collection} />);
280
- expect(screen.getByPlaceholderText('Enter value')).toBeInTheDocument();
281
-
282
- rerender(<CollectionFilterItem value={value} collection={collection} valuePlaceholder={null} />);
283
- expect(screen.queryByPlaceholderText('Enter value')).not.toBeInTheDocument();
284
- });
285
247
  });
@@ -11,7 +11,5 @@
11
11
  export { CollectionFilter } from './CollectionFilter';
12
12
  export type { CollectionFilterProps } from './CollectionFilter';
13
13
  export { CollectionFilterPanel } from './CollectionFilterPanel';
14
- export { CollectionFilterItem } from './CollectionFilterItem';
15
- export type { CollectionFilterItemValue } from './CollectionFilterItem';
16
14
  export type { CollectionFilterPanelProps, CollectionFilterPanelRef } from './CollectionFilterPanel';
17
15
  export type { CompiledFilter } from './useFilterActionProps';
@@ -43,14 +43,6 @@ const isCondition = (item: FilterGroupItem): item is CollectionFilterItemValue =
43
43
  typeof (item as CollectionFilterItemValue).path === 'string' &&
44
44
  Object.prototype.hasOwnProperty.call(item, 'operator');
45
45
 
46
- const cloneFilterGroupItem = (item: FilterGroupItem): FilterGroupItem =>
47
- isGroup(item) ? { logic: item.logic, items: item.items.map((child) => cloneFilterGroupItem(child)) } : { ...item };
48
-
49
- const cloneFilterGroup = (group: FilterGroupValue): FilterGroupValue => ({
50
- logic: group.logic,
51
- items: group.items.map((item) => cloneFilterGroupItem(item)),
52
- });
53
-
54
46
  /**
55
47
  * `true` when the rhs of a condition is "no real value yet" — covers `undefined` / `null` / empty string / empty array / empty plain object. Mirrors v1's `removeNullCondition` `isEmpty` predicate so half-filled rows ("Locked time → is → (no date picked yet)") get dropped on Submit instead of being sent to the server as `{lockedTs:{}}` and triggering a 500.
56
48
  */
@@ -172,8 +164,6 @@ export interface UseFilterActionPropsArgs extends UseFilterOptionsArgs {
172
164
  collection: Collection | undefined;
173
165
  /** Previously compiled filter param used to seed the editable filter group. */
174
166
  initialValue?: CompiledFilter;
175
- /** Default compiled filter used both for initial empty state and Reset. */
176
- defaultValue?: CompiledFilter;
177
167
  /**
178
168
  * Called when the user submits or resets the filter popover. Receives the compiled filter param (`undefined` when cleared) and which footer button triggered the call. Typical implementation: `(filter, action) => { listRequest.run(filter); if (action === 'submit') closePopover(); }`.
179
169
  */
@@ -226,23 +216,12 @@ export interface UseFilterActionPropsResult {
226
216
  * ```
227
217
  */
228
218
  export function useFilterActionProps(args: UseFilterActionPropsArgs): UseFilterActionPropsResult {
229
- const {
230
- collection,
231
- initialValue,
232
- defaultValue,
233
- onApply,
234
- filterableFieldNames,
235
- nonfilterableFieldNames,
236
- noIgnore,
237
- t,
238
- } = args;
239
- const seedValue = initialValue ?? defaultValue;
240
- const defaultGroup = decompileFilterGroup(defaultValue) || createEmptyGroup();
219
+ const { collection, initialValue, onApply, filterableFieldNames, nonfilterableFieldNames, noIgnore, t } = args;
241
220
 
242
221
  // Held in a ref so the group object identity is stable for the lifetime of the host component — `<FilterContent>` mutates this object directly (push/splice on `items`, swap `logic`), and a fresh observable on every render would reset that internal state.
243
222
  const valueRef = useRef<FilterGroupValue>();
244
223
  if (!valueRef.current) {
245
- valueRef.current = observable(decompileFilterGroup(seedValue) || createEmptyGroup()) as FilterGroupValue;
224
+ valueRef.current = observable(decompileFilterGroup(initialValue) || createEmptyGroup()) as FilterGroupValue;
246
225
  }
247
226
  const value = valueRef.current;
248
227
 
@@ -261,10 +240,9 @@ export function useFilterActionProps(args: UseFilterActionPropsArgs): UseFilterA
261
240
  });
262
241
 
263
242
  const onReset = useMemoizedFn(() => {
264
- const next = cloneFilterGroup(defaultGroup);
265
- value.logic = next.logic;
266
- value.items = next.items;
267
- onApply(compileFilterGroup(value), 'reset');
243
+ value.logic = '$and';
244
+ value.items = [];
245
+ onApply(undefined, 'reset');
268
246
  });
269
247
 
270
248
  const translate = useMemoizedFn((key: string) => (t ? t(key) : key));
@@ -283,16 +261,7 @@ export function useFilterActionProps(args: UseFilterActionPropsArgs): UseFilterA
283
261
  );
284
262
 
285
263
  // Re-read on each render so `observer`-wrapped hosts re-render when the reactive `items` array length changes. No useMemo needed — the `value` object's identity is stable (held in a ref), but its observable `items.length` is what we actually care about, and the eslint exhaustive-deps rule rightly complains about depending on a mutable property of a stable ref.
286
- const conditionCount = (() => {
287
- const compiled = compileFilterGroup(value);
288
- if (compiled?.$and && Array.isArray(compiled.$and)) {
289
- return compiled.$and.length;
290
- }
291
- if (compiled?.$or && Array.isArray(compiled.$or)) {
292
- return compiled.$or.length;
293
- }
294
- return 0;
295
- })();
264
+ const conditionCount = value.items.length;
296
265
 
297
266
  return { value, options, FilterItem, ctx, onSubmit, onReset, conditionCount };
298
267
  }
@@ -9,7 +9,6 @@
9
9
 
10
10
  export * from './AppComponents';
11
11
  export * from './BlankComponent';
12
- export * from './category-tabs';
13
12
  export * from './form/table/dnd';
14
13
  export * from './form';
15
14
  export * from './Icon';
@@ -712,6 +712,7 @@ export const AdminLayoutComponent = observer((props: any) => {
712
712
  <MobileMenuControlContext.Provider value={{ closeMobileMenu }}>
713
713
  <DndProvider collisionDetection={menuCollisionDetection} onDragEnd={handleMenuDragEnd}>
714
714
  <ProLayout
715
+ key={`admin-layout-menu-${menuRouteRefreshVersion}`}
715
716
  {...props}
716
717
  contentStyle={contentStyle}
717
718
  siderWidth={customToken.siderWidth || 200}