@nocobase/client-v2 2.2.0-alpha.6 → 2.2.0-alpha.8
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/es/BaseApplication.d.ts +1 -0
- package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
- package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
- package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
- package/es/flow/components/FieldAssignValueInput.d.ts +7 -0
- package/es/flow/components/RunJSValueEditor.d.ts +1 -0
- package/es/flow/components/filter/FilterGroup.d.ts +1 -0
- package/es/flow/components/filter/VariableFilterItem.d.ts +1 -0
- package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
- package/es/index.mjs +89 -89
- package/lib/index.js +97 -97
- package/package.json +8 -7
- package/src/BaseApplication.tsx +11 -6
- package/src/__tests__/app.test.tsx +55 -0
- package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
- package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
- package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +145 -2
- package/src/components/form/ScanInput/useCodeScanner.ts +154 -2
- package/src/components/form/TypedVariableInput.tsx +12 -8
- package/src/components/form/__tests__/TypedVariableInput.test.tsx +44 -3
- package/src/flow/FlowPage.tsx +9 -1
- package/src/flow/__tests__/FlowPage.test.tsx +50 -3
- package/src/flow/__tests__/FlowRoute.test.tsx +2 -2
- package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
- package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +0 -1
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +253 -6
- package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
- package/src/flow/components/FieldAssignValueInput.tsx +81 -15
- package/src/flow/components/FlowRoute.tsx +7 -3
- package/src/flow/components/RunJSValueEditor.tsx +9 -1
- package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +216 -0
- package/src/flow/components/filter/FilterGroup.tsx +33 -5
- package/src/flow/components/filter/VariableFilterItem.tsx +139 -6
- package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +21 -0
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +1 -0
- package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +27 -12
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +36 -0
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +34 -0
- package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
- package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
- package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +2 -1
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +1 -0
- package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
- package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
- package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +20 -10
- package/src/flow/models/fields/mobile-components/MobileSelect.tsx +24 -12
|
@@ -62,6 +62,51 @@ async function buildCollectionLeftMetaTreeLocal(ctx: any): Promise<MetaTreeNode[
|
|
|
62
62
|
return await resolve(subTree);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
function buildCollectionFieldMetaNode(collection: any, path: string[]): MetaTreeNode | null {
|
|
66
|
+
if (!collection || !path.length) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const normalizedPath = path[0] === 'collection' ? path.slice(1) : path;
|
|
71
|
+
if (!normalizedPath.length) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const parentTitles: string[] = [];
|
|
76
|
+
let currentCollection = collection;
|
|
77
|
+
let currentNode: MetaTreeNode | null = null;
|
|
78
|
+
|
|
79
|
+
for (const segment of normalizedPath) {
|
|
80
|
+
const field =
|
|
81
|
+
currentCollection?.getField?.(segment) ??
|
|
82
|
+
currentCollection?.getFields?.()?.find?.((candidate: any) => candidate?.name === segment);
|
|
83
|
+
if (!field) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const title = field.title || field.uiSchema?.title || field.name || segment;
|
|
88
|
+
currentNode = {
|
|
89
|
+
name: field.name || segment,
|
|
90
|
+
title,
|
|
91
|
+
type: field.type,
|
|
92
|
+
interface: field.interface,
|
|
93
|
+
uiSchema: field.uiSchema,
|
|
94
|
+
options: field.options,
|
|
95
|
+
paths: ['collection', ...normalizedPath.slice(0, parentTitles.length + 1)],
|
|
96
|
+
parentTitles: [...parentTitles],
|
|
97
|
+
} as MetaTreeNode;
|
|
98
|
+
|
|
99
|
+
if (field.targetCollection) {
|
|
100
|
+
parentTitles.push(title);
|
|
101
|
+
currentCollection = field.targetCollection;
|
|
102
|
+
} else {
|
|
103
|
+
currentCollection = null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return currentNode;
|
|
108
|
+
}
|
|
109
|
+
|
|
65
110
|
export interface VariableFilterItemValue {
|
|
66
111
|
path: string;
|
|
67
112
|
operator: string;
|
|
@@ -75,6 +120,7 @@ export interface VariableFilterItemProps {
|
|
|
75
120
|
/** 筛选条件值对象 */
|
|
76
121
|
value: VariableFilterItemValue;
|
|
77
122
|
model: FlowModel;
|
|
123
|
+
disabled?: boolean;
|
|
78
124
|
/**
|
|
79
125
|
* 是否启用右侧 VariableInput(变量或静态值二合一)。
|
|
80
126
|
* 默认 false:保持原有行为,右侧仅静态输入组件。
|
|
@@ -265,11 +311,48 @@ function normalizeRightValueInput(input: unknown) {
|
|
|
265
311
|
return input;
|
|
266
312
|
}
|
|
267
313
|
|
|
314
|
+
function findMetaTreeNodeByPath(metaTree: MetaTreeNode[], targetPath: string[]): MetaTreeNode | null {
|
|
315
|
+
if (!Array.isArray(metaTree) || !targetPath.length) {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const traverseByName = (nodes: MetaTreeNode[], path: string[]): MetaTreeNode | null => {
|
|
320
|
+
if (!path.length) {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const [head, ...rest] = path;
|
|
325
|
+
const matched = nodes.find((node) => String(node.name) === String(head));
|
|
326
|
+
if (!matched) {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
if (!rest.length) {
|
|
330
|
+
return matched;
|
|
331
|
+
}
|
|
332
|
+
if (!Array.isArray(matched.children)) {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
return traverseByName(matched.children as MetaTreeNode[], rest);
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const byNames = traverseByName(metaTree, targetPath);
|
|
339
|
+
if (byNames) {
|
|
340
|
+
return byNames;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const topNames = new Set(metaTree.map((node) => String(node.name)));
|
|
344
|
+
if (!topNames.has(String(targetPath[0]))) {
|
|
345
|
+
return traverseByName(metaTree, targetPath.slice(1));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
|
|
268
351
|
/**
|
|
269
352
|
* 上下文筛选项组件
|
|
270
353
|
*/
|
|
271
354
|
export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
272
|
-
({ value, model, rightAsVariable, rightMetaTree, ignoreFieldNames, maxAssociationFieldDepth }) => {
|
|
355
|
+
({ value, model, disabled = false, rightAsVariable, rightMetaTree, ignoreFieldNames, maxAssociationFieldDepth }) => {
|
|
273
356
|
// 使用 View 上下文,确保可访问 ctx.view 的异步子树
|
|
274
357
|
const ctx = useFlowViewContext();
|
|
275
358
|
const t = model.translate;
|
|
@@ -334,6 +417,9 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
334
417
|
// 处理左侧值变化(值由 converters 决定如何解析)
|
|
335
418
|
const handleLeftChange = useCallback(
|
|
336
419
|
(variableValue: string, meta?: MetaTreeNode) => {
|
|
420
|
+
if (disabled) {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
337
423
|
const prevPath = value.path || '';
|
|
338
424
|
const nextPath = variableValue || '';
|
|
339
425
|
const changed = nextPath !== prevPath;
|
|
@@ -348,7 +434,7 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
348
434
|
setLeftMeta(meta);
|
|
349
435
|
}
|
|
350
436
|
},
|
|
351
|
-
[value],
|
|
437
|
+
[disabled, value],
|
|
352
438
|
);
|
|
353
439
|
|
|
354
440
|
// 自定义转换器来捕获 MetaTreeNode,并在选择左值时设置默认操作符
|
|
@@ -376,6 +462,9 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
376
462
|
// 处理操作符变化
|
|
377
463
|
const handleOperatorChange = useCallback(
|
|
378
464
|
(operatorValue: string) => {
|
|
465
|
+
if (disabled) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
379
468
|
value.operator = operatorValue;
|
|
380
469
|
const cur = operatorMetaList.find((op) => op.value === operatorValue);
|
|
381
470
|
if (cur?.noValue) {
|
|
@@ -386,7 +475,7 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
386
475
|
value.noValue = false;
|
|
387
476
|
}
|
|
388
477
|
},
|
|
389
|
-
[operatorMetaList, value],
|
|
478
|
+
[disabled, operatorMetaList, value],
|
|
390
479
|
);
|
|
391
480
|
|
|
392
481
|
// 使用公共静态输入渲染器(抽取到 utils)
|
|
@@ -464,9 +553,12 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
464
553
|
|
|
465
554
|
const setRightValue = useCallback(
|
|
466
555
|
(next: unknown) => {
|
|
556
|
+
if (disabled) {
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
467
559
|
value.value = normalizeRightValueInput(next) as VariableFilterItemValue['value'];
|
|
468
560
|
},
|
|
469
|
-
[value],
|
|
561
|
+
[disabled, value],
|
|
470
562
|
);
|
|
471
563
|
|
|
472
564
|
// 右侧静态输入(无变量模式)与右侧 VariableInput 的静态渲染组件,统一复用
|
|
@@ -520,6 +612,7 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
520
612
|
{...nextProps}
|
|
521
613
|
style={{ width: '100%', ...pickStyle(nextProps.style) }}
|
|
522
614
|
value={normalized}
|
|
615
|
+
disabled={disabled}
|
|
523
616
|
onChange={(vals: any) => setRightValue(vals)}
|
|
524
617
|
/>
|
|
525
618
|
</div>
|
|
@@ -529,10 +622,19 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
529
622
|
const Comp = staticInputRenderer;
|
|
530
623
|
return (
|
|
531
624
|
<div style={{ flex: '1 1 40%', minWidth: 160, maxWidth: '100%' }}>
|
|
532
|
-
<Comp value={rightValue} onChange={(val) => setRightValue(val)} />
|
|
625
|
+
<Comp value={rightValue} onChange={(val) => setRightValue(val)} disabled={disabled} />
|
|
533
626
|
</div>
|
|
534
627
|
);
|
|
535
|
-
}, [
|
|
628
|
+
}, [
|
|
629
|
+
disabled,
|
|
630
|
+
operator,
|
|
631
|
+
operatorMetaList,
|
|
632
|
+
rightValue,
|
|
633
|
+
staticInputRenderer,
|
|
634
|
+
model.context.app,
|
|
635
|
+
setRightValue,
|
|
636
|
+
enumOptions,
|
|
637
|
+
]);
|
|
536
638
|
|
|
537
639
|
// Null 占位组件(仿照 DefaultValue.tsx 的实现)
|
|
538
640
|
const NullComponent = useMemo(() => {
|
|
@@ -652,6 +754,34 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
652
754
|
};
|
|
653
755
|
}, [getFieldInterface, model, maxAssociationFieldDepth]);
|
|
654
756
|
|
|
757
|
+
useEffect(() => {
|
|
758
|
+
if (leftMeta || !value.path) {
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
let cancelled = false;
|
|
763
|
+
const restore = async () => {
|
|
764
|
+
const nodes = await enhancedMetaTree();
|
|
765
|
+
if (cancelled) {
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
const path = customConverters.resolvePathFromValue(value.path);
|
|
769
|
+
if (!path?.length) {
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
const matched =
|
|
773
|
+
findMetaTreeNodeByPath(nodes, path) || buildCollectionFieldMetaNode(model.context.collection, path);
|
|
774
|
+
if (matched) {
|
|
775
|
+
setLeftMeta(matched);
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
restore();
|
|
780
|
+
return () => {
|
|
781
|
+
cancelled = true;
|
|
782
|
+
};
|
|
783
|
+
}, [customConverters, enhancedMetaTree, leftMeta, model.context.collection, value.path]);
|
|
784
|
+
|
|
655
785
|
return (
|
|
656
786
|
<Space wrap style={{ width: '100%' }}>
|
|
657
787
|
<VariableInput
|
|
@@ -659,6 +789,7 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
659
789
|
metaTree={enhancedMetaTree}
|
|
660
790
|
onChange={handleLeftChange}
|
|
661
791
|
converters={customConverters}
|
|
792
|
+
disabled={disabled}
|
|
662
793
|
showValueComponent={false}
|
|
663
794
|
style={{ flex: '1 1 40%', minWidth: 160, maxWidth: '100%' }}
|
|
664
795
|
onlyLeafSelectable={true}
|
|
@@ -670,6 +801,7 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
670
801
|
style={{ flex: '0 0 140px', minWidth: 120, maxWidth: '100%' }}
|
|
671
802
|
placeholder={t('Comparison')}
|
|
672
803
|
value={operator || undefined}
|
|
804
|
+
disabled={disabled}
|
|
673
805
|
onChange={handleOperatorChange}
|
|
674
806
|
>
|
|
675
807
|
{operatorOptions.map((op) => (
|
|
@@ -686,6 +818,7 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
686
818
|
onChange={(v) => setRightValue(v)}
|
|
687
819
|
metaTree={mergedRightMetaTree}
|
|
688
820
|
converters={rightConverters}
|
|
821
|
+
disabled={disabled}
|
|
689
822
|
showValueComponent
|
|
690
823
|
style={{ flex: '1 1 40%', minWidth: 160, maxWidth: '100%' }}
|
|
691
824
|
placeholder={t('Enter value')}
|
|
@@ -140,4 +140,49 @@ describe('FilterGroup closeIcon', () => {
|
|
|
140
140
|
expect(onChange).toHaveBeenCalledTimes(1);
|
|
141
141
|
expect(value.items[0].items).toHaveLength(1);
|
|
142
142
|
});
|
|
143
|
+
|
|
144
|
+
it('disables close and add actions when the filter group is disabled', () => {
|
|
145
|
+
const value = {
|
|
146
|
+
logic: '$and',
|
|
147
|
+
items: [
|
|
148
|
+
{
|
|
149
|
+
path: 'name',
|
|
150
|
+
operator: 'eq',
|
|
151
|
+
value: 'test',
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
};
|
|
155
|
+
const onRemove = vi.fn();
|
|
156
|
+
const onChange = vi.fn();
|
|
157
|
+
|
|
158
|
+
renderWithProviders(
|
|
159
|
+
<FilterGroup
|
|
160
|
+
value={value}
|
|
161
|
+
FilterItem={DummyFilterItem}
|
|
162
|
+
showBorder
|
|
163
|
+
onRemove={onRemove}
|
|
164
|
+
onChange={onChange}
|
|
165
|
+
disabled
|
|
166
|
+
/>,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const closeButtons = screen
|
|
170
|
+
.getAllByLabelText('icon-close')
|
|
171
|
+
.map((element) => element.closest('button'))
|
|
172
|
+
.filter(Boolean) as HTMLButtonElement[];
|
|
173
|
+
expect(closeButtons).toHaveLength(2);
|
|
174
|
+
expect(closeButtons[0]).toBeDisabled();
|
|
175
|
+
expect(closeButtons[1]).toBeDisabled();
|
|
176
|
+
expect(screen.getByText('Add condition').closest('button')).toBeDisabled();
|
|
177
|
+
expect(screen.getByText('Add condition group').closest('button')).toBeDisabled();
|
|
178
|
+
|
|
179
|
+
fireEvent.click(closeButtons[0]);
|
|
180
|
+
fireEvent.click(closeButtons[1]);
|
|
181
|
+
fireEvent.click(screen.getByText('Add condition').closest('button') as HTMLButtonElement);
|
|
182
|
+
fireEvent.click(screen.getByText('Add condition group').closest('button') as HTMLButtonElement);
|
|
183
|
+
|
|
184
|
+
expect(onRemove).not.toHaveBeenCalled();
|
|
185
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
186
|
+
expect(value.items).toHaveLength(1);
|
|
187
|
+
});
|
|
143
188
|
});
|
|
@@ -193,4 +193,13 @@ describe('VariableFilterItem with leftMetaTree', () => {
|
|
|
193
193
|
fireEvent.click(screen.getByTestId('variable-input'));
|
|
194
194
|
expect(value.path).toBe('title');
|
|
195
195
|
});
|
|
196
|
+
|
|
197
|
+
it('restores the saved left meta so the operator select shows its label on reopen', async () => {
|
|
198
|
+
const value = { path: 'title', operator: '$eq', value: 'x' } as any;
|
|
199
|
+
const model = createModelWithCollection();
|
|
200
|
+
|
|
201
|
+
render(<VariableFilterItem value={value} model={model} rightAsVariable={false} />);
|
|
202
|
+
|
|
203
|
+
expect(await screen.findByText('Equals')).toBeInTheDocument();
|
|
204
|
+
});
|
|
196
205
|
});
|
|
@@ -158,6 +158,27 @@ describe('VariableFilterItem', () => {
|
|
|
158
158
|
expect(value.value).toBe('abc');
|
|
159
159
|
});
|
|
160
160
|
|
|
161
|
+
it('passes disabled through to the left selector, operator select, and right variable input', async () => {
|
|
162
|
+
const value: VariableFilterItemValue = { path: '', operator: '', value: '' };
|
|
163
|
+
const model = CreateModel();
|
|
164
|
+
|
|
165
|
+
render(<VariableFilterItem value={value} model={model} rightAsVariable disabled />);
|
|
166
|
+
|
|
167
|
+
const leftVariableInputProps = (globalThis as any).__LAST_VARIABLE_INPUT_PROPS__;
|
|
168
|
+
expect(leftVariableInputProps.disabled).toBe(true);
|
|
169
|
+
|
|
170
|
+
fireEvent.click(screen.getAllByTestId('variable-input')[0]);
|
|
171
|
+
|
|
172
|
+
const variableInputs = screen.getAllByTestId('variable-input');
|
|
173
|
+
expect(variableInputs.length).toBeGreaterThanOrEqual(2);
|
|
174
|
+
const rightVariableInputProps = (globalThis as any).__LAST_VARIABLE_INPUT_PROPS__;
|
|
175
|
+
expect(rightVariableInputProps.disabled).toBe(true);
|
|
176
|
+
|
|
177
|
+
const operatorSelect = document.body.querySelector('.ant-select') as HTMLDivElement | null;
|
|
178
|
+
expect(operatorSelect).not.toBeNull();
|
|
179
|
+
expect(operatorSelect).toHaveClass('ant-select-disabled');
|
|
180
|
+
});
|
|
181
|
+
|
|
161
182
|
it('uses scoped context dataSourceManager when app dataSourceManager has no field interface manager', async () => {
|
|
162
183
|
const value = observable({ path: '', operator: '', value: '' }) as any;
|
|
163
184
|
const model = CreateModel();
|
|
@@ -431,6 +431,7 @@ export class FilterFormBlockModel extends FilterBlockModel<{
|
|
|
431
431
|
|
|
432
432
|
private canApplyFormDefaultValue(name: string, current: any, force?: boolean) {
|
|
433
433
|
if (force) return true;
|
|
434
|
+
if (this.userEditedFieldNames.has(name)) return false;
|
|
434
435
|
if (isEmptyValue(current)) return true;
|
|
435
436
|
if (!this.lastDefaultValueByFieldName.has(name)) return false;
|
|
436
437
|
return isEqual(current, this.lastDefaultValueByFieldName.get(name));
|
|
@@ -62,9 +62,25 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
62
62
|
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
interface CollectionFieldWithAssociationMarker {
|
|
66
|
+
isAssociationField?: () => boolean;
|
|
67
|
+
target?: unknown;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function hasAssociationMarker(value: unknown): value is CollectionFieldWithAssociationMarker {
|
|
71
|
+
return isRecord(value) && (typeof value.isAssociationField === 'function' || 'target' in value);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const isAssociationCollectionField = (collectionField: unknown) => {
|
|
75
|
+
if (!hasAssociationMarker(collectionField)) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
return collectionField.isAssociationField ? collectionField.isAssociationField() : Boolean(collectionField.target);
|
|
79
|
+
};
|
|
80
|
+
|
|
65
81
|
const normalizeAssociationDefaultFilterValue = (value: any, fieldModel: any) => {
|
|
66
82
|
const collectionField = fieldModel?.context?.collectionField;
|
|
67
|
-
if (!collectionField
|
|
83
|
+
if (!isAssociationCollectionField(collectionField)) {
|
|
68
84
|
return value;
|
|
69
85
|
}
|
|
70
86
|
|
|
@@ -171,8 +187,7 @@ const buildFilterFormFieldItem = ({
|
|
|
171
187
|
if (!binding) {
|
|
172
188
|
return;
|
|
173
189
|
}
|
|
174
|
-
const isAssociation =
|
|
175
|
-
typeof field?.isAssociationField === 'function' ? field.isAssociationField() : Boolean(field?.target);
|
|
190
|
+
const isAssociation = isAssociationCollectionField(field);
|
|
176
191
|
const fieldModel =
|
|
177
192
|
isAssociation && ctxWithFlags.engine?.getModelClass?.('FilterFormRecordSelectFieldModel')
|
|
178
193
|
? 'FilterFormRecordSelectFieldModel'
|
|
@@ -507,10 +522,7 @@ export class FilterFormItemModel extends FilterableItemModel<{
|
|
|
507
522
|
const formValue = this.context.form?.getFieldValue(this.props.name);
|
|
508
523
|
const modelValue = fieldModel.getFilterValue ? fieldModel.getFilterValue() : formValue;
|
|
509
524
|
const collectionField = (fieldModel as any)?.context?.collectionField;
|
|
510
|
-
const isAssociationField =
|
|
511
|
-
typeof collectionField?.isAssociationField === 'function'
|
|
512
|
-
? collectionField.isAssociationField()
|
|
513
|
-
: !!collectionField?.target;
|
|
525
|
+
const isAssociationField = isAssociationCollectionField(collectionField);
|
|
514
526
|
const shouldUseFormValue =
|
|
515
527
|
isAssociationField &&
|
|
516
528
|
!this.mounted &&
|
|
@@ -547,11 +559,7 @@ export class FilterFormItemModel extends FilterableItemModel<{
|
|
|
547
559
|
return value;
|
|
548
560
|
}
|
|
549
561
|
const collectionField = (fieldModel as any)?.context?.collectionField;
|
|
550
|
-
|
|
551
|
-
typeof collectionField?.isAssociationField === 'function'
|
|
552
|
-
? collectionField.isAssociationField()
|
|
553
|
-
: !!collectionField?.target;
|
|
554
|
-
if (!isAssociation) {
|
|
562
|
+
if (!isAssociationCollectionField(collectionField)) {
|
|
555
563
|
return value;
|
|
556
564
|
}
|
|
557
565
|
const normalizedFieldNames = normalizeAssociationFieldNames(
|
|
@@ -798,6 +806,13 @@ FilterFormItemModel.registerFlow({
|
|
|
798
806
|
},
|
|
799
807
|
defaultOperator: {
|
|
800
808
|
use: 'defaultOperator',
|
|
809
|
+
hideInSettings(ctx) {
|
|
810
|
+
const collectionField =
|
|
811
|
+
ctx.collectionField ||
|
|
812
|
+
ctx.model?.context?.collectionField ||
|
|
813
|
+
ctx.model?.subModels?.field?.context?.collectionField;
|
|
814
|
+
return isAssociationCollectionField(collectionField);
|
|
815
|
+
},
|
|
801
816
|
},
|
|
802
817
|
operatorComponentProps: {
|
|
803
818
|
use: 'operatorComponentProps',
|
package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts
CHANGED
|
@@ -33,6 +33,42 @@ class DummyCollectionBlockModel extends CollectionBlockModel {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
describe('FilterFormItemModel defineChildren association fields', () => {
|
|
36
|
+
it('hides default operator setting for association filter fields', () => {
|
|
37
|
+
const engine = new FlowEngine();
|
|
38
|
+
engine.registerModels({
|
|
39
|
+
FilterFormItemModel,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const filterItem = engine.createModel<FilterFormItemModel>({
|
|
43
|
+
uid: 'association-filter-item-settings',
|
|
44
|
+
use: 'FilterFormItemModel',
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const defaultOperatorStep = filterItem.getFlow('filterFormItemSettings')?.steps?.defaultOperator as {
|
|
48
|
+
hideInSettings?: (ctx: {
|
|
49
|
+
collectionField?: unknown;
|
|
50
|
+
model?: {
|
|
51
|
+
subModels?: {
|
|
52
|
+
field?: {
|
|
53
|
+
context?: {
|
|
54
|
+
collectionField?: unknown;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
}) => boolean;
|
|
60
|
+
};
|
|
61
|
+
expect(defaultOperatorStep?.hideInSettings?.({ collectionField: { isAssociationField: () => true } })).toBe(true);
|
|
62
|
+
expect(
|
|
63
|
+
defaultOperatorStep?.hideInSettings?.({
|
|
64
|
+
model: { subModels: { field: { context: { collectionField: { target: 'departments' } } } } },
|
|
65
|
+
}),
|
|
66
|
+
).toBe(true);
|
|
67
|
+
expect(defaultOperatorStep?.hideInSettings?.({ collectionField: { interface: 'input', type: 'string' } })).toBe(
|
|
68
|
+
false,
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
36
72
|
it('groups association target fields and supports recursive paths', async () => {
|
|
37
73
|
const engine = new FlowEngine();
|
|
38
74
|
engine.registerModels({
|
|
@@ -301,6 +301,40 @@ describe('filter-form defaultValues wiring', () => {
|
|
|
301
301
|
expect(values.username_user).toBe('Manual');
|
|
302
302
|
});
|
|
303
303
|
|
|
304
|
+
it('does not reapply a filter form default value after user clears the field', async () => {
|
|
305
|
+
const { model, values } = createFilterFormDefaultValuesModel([
|
|
306
|
+
{
|
|
307
|
+
key: 'username-default',
|
|
308
|
+
enable: true,
|
|
309
|
+
targetPath: 'username',
|
|
310
|
+
mode: 'default',
|
|
311
|
+
value: 'admin',
|
|
312
|
+
},
|
|
313
|
+
]);
|
|
314
|
+
|
|
315
|
+
await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any);
|
|
316
|
+
expect(values.username_user).toBe('admin');
|
|
317
|
+
|
|
318
|
+
values.username_user = undefined;
|
|
319
|
+
(model as any).handleFilterFormValuesChange({ username_user: undefined }, { username_user: undefined });
|
|
320
|
+
|
|
321
|
+
await waitFor(() => {
|
|
322
|
+
expect(model.dispatchEvent).toHaveBeenCalledWith(
|
|
323
|
+
'formValuesChange',
|
|
324
|
+
{
|
|
325
|
+
changedValues: {
|
|
326
|
+
username_user: undefined,
|
|
327
|
+
},
|
|
328
|
+
allValues: {
|
|
329
|
+
username_user: undefined,
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
{ debounce: true },
|
|
333
|
+
);
|
|
334
|
+
});
|
|
335
|
+
expect(values.username_user).toBeUndefined();
|
|
336
|
+
});
|
|
337
|
+
|
|
304
338
|
it('applies fixed values even when the target filter field already has a value', async () => {
|
|
305
339
|
const { model, values } = createFilterFormDefaultValuesModel(
|
|
306
340
|
[
|