@nocobase/client-v2 2.1.0-alpha.45 → 2.1.0-alpha.47
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/authRedirect.d.ts +12 -0
- package/es/collection-manager/filter-operators/index.d.ts +1 -0
- package/es/components/form/TypedVariableInput.d.ts +75 -0
- package/es/components/form/filter/CollectionFilterItem.d.ts +5 -1
- package/es/components/form/filter/FilterValueInput.d.ts +5 -0
- package/es/components/form/index.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
- package/es/flow/components/ConditionBuilder.d.ts +1 -0
- package/es/flow/components/FieldAssignRulesEditor.d.ts +3 -1
- package/es/flow/components/FieldAssignValueInput.d.ts +1 -0
- package/es/flow/components/fieldAssignOptions.d.ts +10 -0
- package/es/flow/components/filter/LinkageFilterItem.d.ts +1 -0
- package/es/flow/components/filter/VariableFilterItem.d.ts +1 -0
- package/es/flow/components/filter/metaTreeAssociationDepth.d.ts +13 -0
- package/es/flow/internal/utils/operatorSchemaHelper.d.ts +26 -3
- package/es/flow/models/base/AssociationFieldGroupModel.d.ts +2 -33
- package/es/flow/models/blocks/assign-form/assignFieldValuesFlow.d.ts +8 -0
- package/es/flow/models/blocks/filter-manager/utils.d.ts +23 -1
- package/es/flow/models/blocks/table/TableColumnModel.d.ts +2 -0
- package/es/index.mjs +106 -106
- package/lib/index.js +106 -106
- package/package.json +7 -7
- package/src/__tests__/browserChecker.test.ts +103 -0
- package/src/__tests__/dataSourceRuntime.test.tsx +22 -1
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +5 -0
- package/src/authRedirect.ts +43 -0
- package/src/collection-field-interface/CollectionFieldInterface.ts +14 -7
- package/src/collection-manager/filter-operators/index.ts +29 -2
- package/src/components/README.md +48 -0
- package/src/components/README.zh-CN.md +48 -0
- package/src/components/form/TypedVariableInput.tsx +441 -0
- package/src/components/form/__tests__/TypedVariableInput.test.tsx +152 -0
- package/src/components/form/filter/CollectionFilterItem.tsx +10 -2
- package/src/components/form/filter/FilterValueInput.tsx +27 -2
- package/src/components/form/filter/__tests__/FilterValueInput.test.tsx +39 -0
- package/src/components/form/index.tsx +1 -0
- package/src/flow/__tests__/FlowRoute.test.tsx +8 -1
- package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +100 -0
- package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +369 -2
- package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +23 -1
- package/src/flow/actions/dataScope.tsx +8 -1
- package/src/flow/actions/linkageRules.tsx +61 -5
- package/src/flow/actions/linkageRulesRefresh.tsx +2 -3
- package/src/flow/actions/setTargetDataScope.tsx +1 -1
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +15 -1
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +75 -1
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +11 -0
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +2 -4
- package/src/flow/common/Markdown/Edit.tsx +32 -4
- package/src/flow/components/ConditionBuilder.tsx +12 -2
- package/src/flow/components/DefaultValue.tsx +14 -8
- package/src/flow/components/FieldAssignRulesEditor.tsx +37 -46
- package/src/flow/components/FieldAssignValueInput.tsx +14 -14
- package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +112 -1
- package/src/flow/components/__tests__/fieldAssignOptions.test.ts +71 -1
- package/src/flow/components/code-editor/__tests__/runjsCompletions.test.ts +272 -27
- package/src/flow/components/code-editor/runjsCompletions.ts +490 -9
- package/src/flow/components/fieldAssignOptions.ts +23 -6
- package/src/flow/components/filter/LinkageFilterItem.tsx +26 -12
- package/src/flow/components/filter/VariableFilterItem.tsx +32 -13
- package/src/flow/components/filter/__tests__/fieldsToOptions.test.ts +48 -0
- package/src/flow/components/filter/__tests__/metaTreeAssociationDepth.test.ts +60 -0
- package/src/flow/components/filter/fieldsToOptions.ts +9 -4
- package/src/flow/components/filter/metaTreeAssociationDepth.ts +69 -0
- package/src/flow/internal/utils/operatorSchemaHelper.ts +112 -5
- package/src/flow/models/actions/FilterActionModel.tsx +15 -3
- package/src/flow/models/actions/__tests__/AssignFormRefill.test.ts +90 -0
- package/src/flow/models/base/AssociationFieldGroupModel.tsx +20 -3
- package/src/flow/models/base/GridModel.tsx +1 -1
- package/src/flow/models/base/PageModel/ChildPageModel.tsx +5 -1
- package/src/flow/models/base/PageModel/__tests__/ChildPageModel.test.ts +12 -0
- package/src/flow/models/base/__tests__/AssociationFieldGroupModel.test.ts +86 -0
- package/src/flow/models/base/__tests__/GridModel.render.test.tsx +53 -0
- package/src/flow/models/blocks/assign-form/AssignFormGridModel.tsx +12 -3
- package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +14 -4
- package/src/flow/models/blocks/assign-form/__tests__/assignFieldValuesFlow.editor.test.tsx +216 -0
- package/src/flow/models/blocks/assign-form/assignFieldValuesFlow.tsx +142 -40
- package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +14 -12
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +6 -1
- package/src/flow/models/blocks/filter-manager/__tests__/getDefaultOperator.test.ts +35 -1
- package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/customizeFilterRender.test.tsx +148 -0
- package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/defaultOperator.test.tsx +85 -0
- package/src/flow/models/blocks/filter-manager/flow-actions/connectFields.tsx +48 -6
- package/src/flow/models/blocks/filter-manager/flow-actions/customizeFilterRender.tsx +14 -44
- package/src/flow/models/blocks/filter-manager/flow-actions/defaultOperator.tsx +3 -5
- package/src/flow/models/blocks/filter-manager/flow-actions/operatorComponentProps.tsx +2 -12
- package/src/flow/models/blocks/filter-manager/utils.ts +143 -4
- package/src/flow/models/blocks/form/FormAssociationItemModel.tsx +9 -0
- package/src/flow/models/blocks/form/FormItemModel.tsx +9 -0
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
- package/src/flow/models/blocks/form/__tests__/FormItemModel.defineChildren.test.ts +27 -2
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +29 -0
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +5 -1
- package/src/flow/models/blocks/table/TableColumnModel.tsx +15 -5
- package/src/flow/models/blocks/table/TableCustomColumnModel.tsx +2 -2
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +18 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +6 -7
- package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +5 -3
- package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +70 -0
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
useFlowEngine,
|
|
16
16
|
useFlowSettingsContext,
|
|
17
17
|
} from '@nocobase/flow-engine';
|
|
18
|
-
import React, { useEffect
|
|
18
|
+
import React, { useEffect } from 'react';
|
|
19
19
|
import { CollectionActionModel } from '../../base/CollectionActionModel';
|
|
20
20
|
import { RecordActionModel } from '../../base/RecordActionModel';
|
|
21
21
|
import { AssignFormModel } from './AssignFormModel';
|
|
@@ -38,6 +38,9 @@ type AssignFieldValuesModel = {
|
|
|
38
38
|
uid: string;
|
|
39
39
|
assignFormUid?: string;
|
|
40
40
|
context?: AssignFieldValuesContext;
|
|
41
|
+
subModels?: {
|
|
42
|
+
assignForm?: AssignFormModel;
|
|
43
|
+
};
|
|
41
44
|
getStepParams?: (flowKey: string, stepKey: string) => { assignedValues?: AssignedValues } | undefined;
|
|
42
45
|
setStepParams?: (flowKey: string, stepKey: string, params: { assignedValues: AssignedValues }) => void;
|
|
43
46
|
};
|
|
@@ -62,6 +65,81 @@ function getResourceInit(ctx: AssignFieldValuesContext): { dataSourceKey: string
|
|
|
62
65
|
return dsKey && collName ? { dataSourceKey: dsKey, collectionName: collName } : undefined;
|
|
63
66
|
}
|
|
64
67
|
|
|
68
|
+
function resolveInitFromFlowSettings(
|
|
69
|
+
blockModel: unknown,
|
|
70
|
+
actionContext: AssignFieldValuesContext | undefined,
|
|
71
|
+
): { dataSourceKey: string; collectionName: string } | undefined {
|
|
72
|
+
const maybeBlockCollection = (blockModel as { collection?: { dataSourceKey?: string; name?: string } } | undefined)
|
|
73
|
+
?.collection;
|
|
74
|
+
const collection = maybeBlockCollection || getContextCollection(actionContext);
|
|
75
|
+
const dsKey = collection?.dataSourceKey;
|
|
76
|
+
const collName = collection?.name;
|
|
77
|
+
return dsKey && collName ? { dataSourceKey: dsKey, collectionName: collName } : undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isValidResourceInit(init: unknown): init is {
|
|
81
|
+
dataSourceKey: string;
|
|
82
|
+
collectionName: string;
|
|
83
|
+
} {
|
|
84
|
+
if (!init || typeof init !== 'object') return false;
|
|
85
|
+
const obj = init as { dataSourceKey?: unknown; collectionName?: unknown };
|
|
86
|
+
return (
|
|
87
|
+
typeof obj.dataSourceKey === 'string' &&
|
|
88
|
+
!!obj.dataSourceKey &&
|
|
89
|
+
typeof obj.collectionName === 'string' &&
|
|
90
|
+
!!obj.collectionName
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
type LocalCacheContext = {
|
|
95
|
+
_observableCache?: Record<string, unknown>;
|
|
96
|
+
_cache?: Record<string, unknown>;
|
|
97
|
+
_pending?: Record<string, unknown>;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
function toLocalCacheContext(context: unknown): LocalCacheContext | undefined {
|
|
101
|
+
return context && typeof context === 'object' ? (context as LocalCacheContext) : undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function clearOwnContextCache(context: unknown, key: string) {
|
|
105
|
+
const localContext = toLocalCacheContext(context);
|
|
106
|
+
if (!localContext) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
delete localContext._observableCache?.[key];
|
|
110
|
+
delete localContext._cache?.[key];
|
|
111
|
+
delete localContext._pending?.[key];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function clearResourceContextCache(model: { context?: unknown } | undefined) {
|
|
115
|
+
clearOwnContextCache(model?.context, 'dataSource');
|
|
116
|
+
clearOwnContextCache(model?.context, 'collection');
|
|
117
|
+
clearOwnContextCache(model?.context, 'resource');
|
|
118
|
+
clearOwnContextCache(model?.context, 'association');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function resolveAssignFormModel(ctx: {
|
|
122
|
+
model: AssignFieldValuesModel;
|
|
123
|
+
engine: {
|
|
124
|
+
getModel?: (uid: string, fromRoot?: boolean) => AssignFormModel | undefined;
|
|
125
|
+
findModelByParentId?: (parentId: string, subKey: string) => AssignFormModel | undefined | null;
|
|
126
|
+
};
|
|
127
|
+
}) {
|
|
128
|
+
if (ctx.model.assignFormUid) {
|
|
129
|
+
const form = ctx.engine.getModel?.(ctx.model.assignFormUid, true);
|
|
130
|
+
if (form) {
|
|
131
|
+
return form;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const localForm = ctx.model.subModels?.assignForm;
|
|
136
|
+
if (localForm) {
|
|
137
|
+
return localForm;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return ctx.engine.findModelByParentId?.(ctx.model.uid, 'assignForm') || undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
65
143
|
export function createAssignFormSubModelOptions(ctx: AssignFieldValuesContext) {
|
|
66
144
|
return {
|
|
67
145
|
async: true,
|
|
@@ -114,21 +192,52 @@ function AssignFieldsEditor(props: { settingsFlowKey: string; clearRecordContext
|
|
|
114
192
|
const { model, blockModel } = useFlowSettingsContext();
|
|
115
193
|
const action = model as AssignFieldValuesModel;
|
|
116
194
|
const engine = useFlowEngine();
|
|
117
|
-
const initializedRef = useRef(false);
|
|
118
195
|
const [formModel, setFormModel] = React.useState<AssignFormModel | null>(null);
|
|
119
196
|
|
|
120
197
|
useEffect(() => {
|
|
121
198
|
let cancelled = false;
|
|
122
199
|
const loadAssignForm = async () => {
|
|
200
|
+
const init = resolveInitFromFlowSettings(blockModel, action?.context);
|
|
123
201
|
const loaded = (await engine.loadOrCreateModel(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
202
|
+
init
|
|
203
|
+
? {
|
|
204
|
+
parentId: action.uid,
|
|
205
|
+
subKey: 'assignForm',
|
|
206
|
+
use: 'AssignFormModel',
|
|
207
|
+
stepParams: { resourceSettings: { init } },
|
|
208
|
+
}
|
|
209
|
+
: {
|
|
210
|
+
parentId: action.uid,
|
|
211
|
+
subKey: 'assignForm',
|
|
212
|
+
use: 'AssignFormModel',
|
|
213
|
+
},
|
|
129
214
|
{ skipSave: !model.context.flowSettingsEnabled },
|
|
130
215
|
)) as AssignFormModel;
|
|
131
216
|
if (cancelled) return;
|
|
217
|
+
|
|
218
|
+
if (isValidResourceInit(init)) {
|
|
219
|
+
const existingInit = loaded.getStepParams?.('resourceSettings', 'init');
|
|
220
|
+
// Ensure `resourceSettings.init` is available before first render to avoid cached undefined `collection`
|
|
221
|
+
if (
|
|
222
|
+
!isValidResourceInit(existingInit) ||
|
|
223
|
+
existingInit.dataSourceKey !== init.dataSourceKey ||
|
|
224
|
+
existingInit.collectionName !== init.collectionName
|
|
225
|
+
) {
|
|
226
|
+
loaded.setStepParams?.('resourceSettings', 'init', init);
|
|
227
|
+
}
|
|
228
|
+
clearResourceContextCache(loaded);
|
|
229
|
+
clearResourceContextCache(loaded.subModels?.grid);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const prev = action.getStepParams?.(props.settingsFlowKey, ASSIGN_FIELD_VALUES_STEP_KEY) || {};
|
|
233
|
+
loaded.setInitialAssignedValues(prev?.assignedValues || {});
|
|
234
|
+
|
|
235
|
+
const isBulkCollectionAction = model instanceof CollectionActionModel && !(model instanceof RecordActionModel);
|
|
236
|
+
const isBulk = props.clearRecordContext || isBulkCollectionAction;
|
|
237
|
+
if (isBulk && loaded.context?.defineProperty) {
|
|
238
|
+
loaded.context.defineProperty('record', { get: () => undefined, cache: false });
|
|
239
|
+
}
|
|
240
|
+
|
|
132
241
|
setFormModel(loaded);
|
|
133
242
|
action.assignFormUid = loaded?.uid || action.assignFormUid;
|
|
134
243
|
};
|
|
@@ -136,31 +245,15 @@ function AssignFieldsEditor(props: { settingsFlowKey: string; clearRecordContext
|
|
|
136
245
|
return () => {
|
|
137
246
|
cancelled = true;
|
|
138
247
|
};
|
|
139
|
-
}, [
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const collName = coll?.name;
|
|
149
|
-
if (dsKey && collName) {
|
|
150
|
-
formModel.setStepParams('resourceSettings', 'init', {
|
|
151
|
-
dataSourceKey: dsKey,
|
|
152
|
-
collectionName: collName,
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
formModel.setInitialAssignedValues(prev?.assignedValues || {});
|
|
156
|
-
|
|
157
|
-
const isBulk =
|
|
158
|
-
props.clearRecordContext || (action instanceof CollectionActionModel && !(action instanceof RecordActionModel));
|
|
159
|
-
if (isBulk && formModel.context?.defineProperty) {
|
|
160
|
-
formModel.context.defineProperty('record', { get: () => undefined, cache: false });
|
|
161
|
-
}
|
|
162
|
-
initializedRef.current = true;
|
|
163
|
-
}, [action, blockModel?.collection, formModel, props.clearRecordContext, props.settingsFlowKey]);
|
|
248
|
+
}, [
|
|
249
|
+
action,
|
|
250
|
+
blockModel,
|
|
251
|
+
engine,
|
|
252
|
+
model,
|
|
253
|
+
model.context.flowSettingsEnabled,
|
|
254
|
+
props.clearRecordContext,
|
|
255
|
+
props.settingsFlowKey,
|
|
256
|
+
]);
|
|
164
257
|
|
|
165
258
|
return formModel ? <FlowModelRenderer model={formModel} showFlowSettings={false} /> : null;
|
|
166
259
|
}
|
|
@@ -187,14 +280,23 @@ export function createAssignFieldValuesStep(options: AssignFieldValuesStepOption
|
|
|
187
280
|
},
|
|
188
281
|
};
|
|
189
282
|
},
|
|
190
|
-
async beforeParamsSave(
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
283
|
+
async beforeParamsSave(
|
|
284
|
+
ctx: {
|
|
285
|
+
model: AssignFieldValuesModel;
|
|
286
|
+
engine: {
|
|
287
|
+
getModel?: (uid: string, fromRoot?: boolean) => AssignFormModel | undefined;
|
|
288
|
+
findModelByParentId?: (parentId: string, subKey: string) => AssignFormModel | undefined | null;
|
|
289
|
+
};
|
|
290
|
+
},
|
|
291
|
+
params?: { assignedValues?: AssignedValues },
|
|
292
|
+
previousParams?: { assignedValues?: AssignedValues },
|
|
293
|
+
) {
|
|
294
|
+
const form = resolveAssignFormModel(ctx);
|
|
295
|
+
if (!form) {
|
|
296
|
+
const assignedValues = params?.assignedValues || previousParams?.assignedValues || {};
|
|
297
|
+
ctx.model.setStepParams?.(options.settingsFlowKey, ASSIGN_FIELD_VALUES_STEP_KEY, { assignedValues });
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
198
300
|
if (options.validateBeforeSave) {
|
|
199
301
|
await form.form?.validateFields?.();
|
|
200
302
|
}
|
|
@@ -22,7 +22,12 @@ import { CollectionBlockModel, FieldModel } from '../../base';
|
|
|
22
22
|
import { RecordSelectFieldModel } from '../../fields/AssociationFieldModel/RecordSelectFieldModel';
|
|
23
23
|
import { normalizeAssociationFieldNames } from '../../fields/AssociationFieldModel/recordSelectShared';
|
|
24
24
|
import { FilterManager } from '../filter-manager';
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
getAllDataModels,
|
|
27
|
+
getDefaultOperator,
|
|
28
|
+
getFilterFormOperatorMeta,
|
|
29
|
+
isFilterValueEmpty,
|
|
30
|
+
} from '../filter-manager/utils';
|
|
26
31
|
import { FilterFormFieldModel } from './fields';
|
|
27
32
|
import { normalizeFilterValueByOperator } from './valueNormalization';
|
|
28
33
|
|
|
@@ -51,7 +56,11 @@ const getTargetFilterableFields = (field: any, collection?: Collection, model?:
|
|
|
51
56
|
return (targetCollection.getFields() || []).filter((childField: any) => childField?.filterable);
|
|
52
57
|
};
|
|
53
58
|
|
|
54
|
-
const MAX_ASSOCIATION_DEPTH =
|
|
59
|
+
const MAX_ASSOCIATION_DEPTH = 2;
|
|
60
|
+
|
|
61
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
62
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
63
|
+
}
|
|
55
64
|
|
|
56
65
|
const normalizeAssociationDefaultFilterValue = (value: any, fieldModel: any) => {
|
|
57
66
|
const collectionField = fieldModel?.context?.collectionField;
|
|
@@ -449,15 +458,7 @@ export class FilterFormItemModel extends FilterableItemModel<{
|
|
|
449
458
|
|
|
450
459
|
private getCurrentOperatorMeta() {
|
|
451
460
|
const operator = getDefaultOperator(this);
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
const operatorList = this.collectionField?.filterable?.operators;
|
|
455
|
-
|
|
456
|
-
if (!Array.isArray(operatorList)) {
|
|
457
|
-
return null;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
return operatorList.find((op) => op.value === operator) || null;
|
|
461
|
+
return getFilterFormOperatorMeta(this, operator);
|
|
461
462
|
}
|
|
462
463
|
|
|
463
464
|
onInit(options: any) {
|
|
@@ -530,7 +531,8 @@ export class FilterFormItemModel extends FilterableItemModel<{
|
|
|
530
531
|
rawValue = this.normalizeAssociationFilterValue(rawValue, fieldModel);
|
|
531
532
|
const operatorMeta = this.getCurrentOperatorMeta();
|
|
532
533
|
if (operatorMeta?.noValue) {
|
|
533
|
-
const
|
|
534
|
+
const componentProps = operatorMeta?.schema?.['x-component-props'];
|
|
535
|
+
const options = isRecord(componentProps) ? componentProps.options : undefined;
|
|
534
536
|
if (Array.isArray(options)) {
|
|
535
537
|
return rawValue;
|
|
536
538
|
}
|
package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts
CHANGED
|
@@ -136,9 +136,13 @@ describe('FilterFormItemModel defineChildren association fields', () => {
|
|
|
136
136
|
);
|
|
137
137
|
const managerGroups = await managerAssociation.children();
|
|
138
138
|
const managerFieldsGroup = managerGroups.find((group: any) => group.key === 'department.manager-fields');
|
|
139
|
+
const managerAssociationGroup = managerGroups.find(
|
|
140
|
+
(group: any) => group.key === 'department.manager-relation-fields',
|
|
141
|
+
);
|
|
139
142
|
|
|
140
143
|
const managerFieldKeys = (managerFieldsGroup?.children || []).map((item: any) => item.key);
|
|
141
144
|
expect(managerFieldKeys).toContain('department.manager.name');
|
|
145
|
+
expect(managerAssociationGroup).toBeUndefined();
|
|
142
146
|
|
|
143
147
|
const targetItem = managerFieldsGroup?.children?.find((item: any) => item.key === 'department.manager.name');
|
|
144
148
|
const createOptions = await targetItem.createModelOptions();
|
|
@@ -200,7 +204,8 @@ describe('FilterFormItemModel defineChildren association fields', () => {
|
|
|
200
204
|
});
|
|
201
205
|
|
|
202
206
|
const ds = engine.dataSourceManager.getDataSource('main');
|
|
203
|
-
ds
|
|
207
|
+
expect(ds).toBeTruthy();
|
|
208
|
+
ds?.addCollection({
|
|
204
209
|
name: 'users',
|
|
205
210
|
filterTargetKey: 'id',
|
|
206
211
|
fields: [{ name: 'id', type: 'integer', interface: 'number', filterable: { operators: [] } }],
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { describe, expect, it, vi } from 'vitest';
|
|
11
11
|
|
|
12
|
-
import { getDefaultOperator } from '../utils';
|
|
12
|
+
import { getDefaultOperator, getFilterFormOperatorList } from '../utils';
|
|
13
13
|
|
|
14
14
|
describe('getDefaultOperator', () => {
|
|
15
15
|
it('returns model.operator when it exists', () => {
|
|
@@ -66,4 +66,38 @@ describe('getDefaultOperator', () => {
|
|
|
66
66
|
expect(result).toBe('$includes');
|
|
67
67
|
expect(getStepParams).toHaveBeenCalledWith('filterFormItemSettings', 'defaultOperator');
|
|
68
68
|
});
|
|
69
|
+
|
|
70
|
+
it('falls back to field interface operators when field operators are empty', () => {
|
|
71
|
+
const getStepParams = vi.fn().mockReturnValue(undefined);
|
|
72
|
+
const model = {
|
|
73
|
+
getStepParams,
|
|
74
|
+
subModels: {},
|
|
75
|
+
collectionField: {
|
|
76
|
+
interface: 'id',
|
|
77
|
+
type: 'bigInt',
|
|
78
|
+
filterable: { operators: [] },
|
|
79
|
+
},
|
|
80
|
+
context: {
|
|
81
|
+
dataSourceManager: {
|
|
82
|
+
collectionFieldInterfaceManager: {
|
|
83
|
+
getFieldInterface: vi.fn(() => ({
|
|
84
|
+
filterable: {
|
|
85
|
+
operators: [
|
|
86
|
+
{ label: 'is', value: '$eq', selected: true },
|
|
87
|
+
{
|
|
88
|
+
label: 'is any of',
|
|
89
|
+
value: '$in',
|
|
90
|
+
schema: { 'x-component': 'MultipleKeywordsInput' },
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
})),
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
} as any;
|
|
99
|
+
|
|
100
|
+
expect(getDefaultOperator(model)).toBe('$eq');
|
|
101
|
+
expect(getFilterFormOperatorList(model).map((item) => item.value)).toEqual(['$eq', '$in']);
|
|
102
|
+
});
|
|
69
103
|
});
|
package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/customizeFilterRender.test.tsx
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
12
|
+
import { FilterFormItemModel } from '../../../filter-form/FilterFormItemModel';
|
|
13
|
+
import { customizeFilterRender } from '../customizeFilterRender';
|
|
14
|
+
|
|
15
|
+
type TestFieldModel = {
|
|
16
|
+
props: Record<string, unknown>;
|
|
17
|
+
render: () => React.ReactNode;
|
|
18
|
+
setupReactiveRender: ReturnType<typeof vi.fn>;
|
|
19
|
+
_reactiveWrapperCache?: unknown;
|
|
20
|
+
__originalRender?: () => React.ReactNode;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const MultipleKeywordsInput = (props: Record<string, unknown>) => {
|
|
24
|
+
return <div data-testid="multiple-keywords-input" {...props} />;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function createFieldModel() {
|
|
28
|
+
return {
|
|
29
|
+
props: {
|
|
30
|
+
value: ['1', '2'],
|
|
31
|
+
placeholder: 'runtime placeholder',
|
|
32
|
+
onCompositionStart: vi.fn(),
|
|
33
|
+
onCompositionEnd: vi.fn(),
|
|
34
|
+
},
|
|
35
|
+
render: () => <span data-testid="original-field" />,
|
|
36
|
+
setupReactiveRender: vi.fn(),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function createFilterItemModel({
|
|
41
|
+
fieldModel,
|
|
42
|
+
operator = '$in',
|
|
43
|
+
collectionField,
|
|
44
|
+
}: {
|
|
45
|
+
fieldModel: TestFieldModel;
|
|
46
|
+
operator?: string;
|
|
47
|
+
collectionField: Record<string, unknown>;
|
|
48
|
+
}) {
|
|
49
|
+
return {
|
|
50
|
+
uid: 'filter-item',
|
|
51
|
+
operator,
|
|
52
|
+
collectionField,
|
|
53
|
+
context: {
|
|
54
|
+
app: {
|
|
55
|
+
getComponent: (name: string) => (name === 'MultipleKeywordsInput' ? MultipleKeywordsInput : undefined),
|
|
56
|
+
},
|
|
57
|
+
collectionField,
|
|
58
|
+
},
|
|
59
|
+
subModels: {
|
|
60
|
+
field: fieldModel,
|
|
61
|
+
},
|
|
62
|
+
setProps: vi.fn(),
|
|
63
|
+
} as unknown as FilterFormItemModel;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe('customizeFilterRender action', () => {
|
|
67
|
+
it('uses operator schema components for number filter fields', () => {
|
|
68
|
+
const fieldModel = createFieldModel();
|
|
69
|
+
const model = createFilterItemModel({
|
|
70
|
+
fieldModel,
|
|
71
|
+
collectionField: {
|
|
72
|
+
interface: 'number',
|
|
73
|
+
type: 'integer',
|
|
74
|
+
filterable: {
|
|
75
|
+
operators: [
|
|
76
|
+
{ label: 'equals', value: '$eq' },
|
|
77
|
+
{
|
|
78
|
+
label: 'is any of',
|
|
79
|
+
value: '$in',
|
|
80
|
+
schema: {
|
|
81
|
+
'x-component': 'MultipleKeywordsInput',
|
|
82
|
+
'x-component-props': {
|
|
83
|
+
fieldInterface: 'number',
|
|
84
|
+
placeholder: 'schema placeholder',
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
customizeFilterRender.handler?.({ model } as any);
|
|
94
|
+
|
|
95
|
+
const element = fieldModel.render();
|
|
96
|
+
expect(React.isValidElement(element)).toBe(true);
|
|
97
|
+
expect((element as React.ReactElement).type).toBe(MultipleKeywordsInput);
|
|
98
|
+
expect((element as React.ReactElement).props.fieldInterface).toBe('number');
|
|
99
|
+
expect((element as React.ReactElement).props.placeholder).toBe('runtime placeholder');
|
|
100
|
+
expect((element as React.ReactElement).props.onCompositionStart).toBeNull();
|
|
101
|
+
expect((element as React.ReactElement).props.onCompositionEnd).toBeNull();
|
|
102
|
+
expect(fieldModel.setupReactiveRender).toHaveBeenCalled();
|
|
103
|
+
expect(model.setProps).toHaveBeenCalledWith({ key: 'filter-item-$in' });
|
|
104
|
+
|
|
105
|
+
fieldModel.props = {
|
|
106
|
+
...fieldModel.props,
|
|
107
|
+
value: ['3'],
|
|
108
|
+
placeholder: 'updated runtime placeholder',
|
|
109
|
+
};
|
|
110
|
+
const rerenderedElement = fieldModel.render();
|
|
111
|
+
expect((rerenderedElement as React.ReactElement).props.value).toEqual(['3']);
|
|
112
|
+
expect((rerenderedElement as React.ReactElement).props.placeholder).toBe('updated runtime placeholder');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('restores original render when switching to an operator without a schema component', () => {
|
|
116
|
+
const fieldModel = createFieldModel();
|
|
117
|
+
const originalRender = fieldModel.render;
|
|
118
|
+
const model = createFilterItemModel({
|
|
119
|
+
fieldModel,
|
|
120
|
+
collectionField: {
|
|
121
|
+
interface: 'number',
|
|
122
|
+
type: 'integer',
|
|
123
|
+
filterable: {
|
|
124
|
+
operators: [
|
|
125
|
+
{ label: 'equals', value: '$eq' },
|
|
126
|
+
{
|
|
127
|
+
label: 'is any of',
|
|
128
|
+
value: '$in',
|
|
129
|
+
schema: {
|
|
130
|
+
'x-component': 'MultipleKeywordsInput',
|
|
131
|
+
'x-component-props': {
|
|
132
|
+
fieldInterface: 'number',
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
customizeFilterRender.handler?.({ model } as any);
|
|
142
|
+
model.operator = '$eq';
|
|
143
|
+
customizeFilterRender.handler?.({ model } as any);
|
|
144
|
+
|
|
145
|
+
expect(fieldModel.render).toBe(originalRender);
|
|
146
|
+
expect(fieldModel.render()).toEqual(<span data-testid="original-field" />);
|
|
147
|
+
});
|
|
148
|
+
});
|
package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/defaultOperator.test.tsx
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
11
11
|
import { FlowEngine, FlowModel } from '@nocobase/flow-engine';
|
|
12
|
+
import { createMockClient, IdFieldInterface, InputFieldInterface } from '@nocobase/client-v2';
|
|
13
|
+
import React from 'react';
|
|
12
14
|
import { defaultOperator } from '../defaultOperator';
|
|
13
15
|
|
|
14
16
|
function createModel() {
|
|
@@ -17,6 +19,12 @@ function createModel() {
|
|
|
17
19
|
return model;
|
|
18
20
|
}
|
|
19
21
|
|
|
22
|
+
type OperatorOption = {
|
|
23
|
+
value: string;
|
|
24
|
+
label?: unknown;
|
|
25
|
+
schema?: Record<string, unknown>;
|
|
26
|
+
};
|
|
27
|
+
|
|
20
28
|
describe('defaultOperator action', () => {
|
|
21
29
|
beforeEach(() => {
|
|
22
30
|
document.body.innerHTML = '';
|
|
@@ -55,4 +63,81 @@ describe('defaultOperator action', () => {
|
|
|
55
63
|
expect(options2.find((o: any) => o.value === '$a')).toBeTruthy();
|
|
56
64
|
expect(options2.find((o: any) => o.value === '$b')).toBeTruthy();
|
|
57
65
|
});
|
|
66
|
+
|
|
67
|
+
it('resolves default operator options from field interfaces when collection field metadata is absent', () => {
|
|
68
|
+
const app = createMockClient();
|
|
69
|
+
app.addFieldInterfaces([InputFieldInterface]);
|
|
70
|
+
app.addFieldInterfaceOperator('input', {
|
|
71
|
+
label: 'plugin in',
|
|
72
|
+
value: '$in',
|
|
73
|
+
schema: { 'x-component': 'MultipleKeywordsInput' },
|
|
74
|
+
});
|
|
75
|
+
app.addFieldInterfaceOperator('input', {
|
|
76
|
+
label: 'plugin not in',
|
|
77
|
+
value: '$notIn',
|
|
78
|
+
schema: { 'x-component': 'MultipleKeywordsInput' },
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const model = new FlowModel({ uid: 'm-default-operator-interface', flowEngine: app.flowEngine });
|
|
82
|
+
model.context.defineProperty('filterField', {
|
|
83
|
+
value: { name: 'name', title: 'Name', interface: 'input', type: 'string' },
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const ui = defaultOperator.uiMode?.({ model } as any);
|
|
87
|
+
const options = (ui?.props?.options || []) as OperatorOption[];
|
|
88
|
+
expect(options.find((item) => item.value === '$in')?.schema?.['x-component']).toBe('MultipleKeywordsInput');
|
|
89
|
+
expect(options.find((item) => item.value === '$notIn')?.schema?.['x-component']).toBe('MultipleKeywordsInput');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('preserves React operator labels', () => {
|
|
93
|
+
const model = createModel();
|
|
94
|
+
const label = <span data-testid="operator-label">plugin label</span>;
|
|
95
|
+
|
|
96
|
+
model.context.defineProperty('collectionField', {
|
|
97
|
+
value: {
|
|
98
|
+
filterable: {
|
|
99
|
+
operators: [{ value: '$in', label, schema: { 'x-component': 'MultipleKeywordsInput' } }],
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const ui = defaultOperator.uiMode?.({ model } as any);
|
|
105
|
+
const options = (ui?.props?.options || []) as OperatorOption[];
|
|
106
|
+
expect(options.find((item) => item.value === '$in')?.label).toBe(label);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('falls back to id field interface operators when collection field operators are empty', () => {
|
|
110
|
+
const app = createMockClient();
|
|
111
|
+
app.addFieldInterfaces([IdFieldInterface]);
|
|
112
|
+
app.addFieldInterfaceOperator('id', {
|
|
113
|
+
label: 'plugin in',
|
|
114
|
+
value: '$in',
|
|
115
|
+
schema: { 'x-component': 'MultipleKeywordsInput' },
|
|
116
|
+
});
|
|
117
|
+
app.addFieldInterfaceOperator('id', {
|
|
118
|
+
label: 'plugin not in',
|
|
119
|
+
value: '$notIn',
|
|
120
|
+
schema: { 'x-component': 'MultipleKeywordsInput' },
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const model = new FlowModel({ uid: 'm-default-operator-id-interface', flowEngine: app.flowEngine });
|
|
124
|
+
model.context.defineProperty('collectionField', {
|
|
125
|
+
value: {
|
|
126
|
+
name: 'id',
|
|
127
|
+
title: 'ID',
|
|
128
|
+
interface: 'id',
|
|
129
|
+
type: 'bigInt',
|
|
130
|
+
filterable: { operators: [] },
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
model.context.defineProperty('filterField', {
|
|
134
|
+
value: { name: 'id', title: 'ID', interface: 'id', type: 'bigInt' },
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const ui = defaultOperator.uiMode?.({ model } as any);
|
|
138
|
+
const options = (ui?.props?.options || []) as OperatorOption[];
|
|
139
|
+
expect(options.find((item) => item.value === '$eq')).toBeTruthy();
|
|
140
|
+
expect(options.find((item) => item.value === '$in')?.schema?.['x-component']).toBe('MultipleKeywordsInput');
|
|
141
|
+
expect(options.find((item) => item.value === '$notIn')?.schema?.['x-component']).toBe('MultipleKeywordsInput');
|
|
142
|
+
});
|
|
58
143
|
});
|