@nocobase/client-v2 2.1.0-beta.43 → 2.1.0-beta.45
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 +9 -0
- package/es/authRedirect.d.ts +12 -0
- package/es/collection-manager/filter-operators/index.d.ts +1 -0
- package/es/components/form/filter/CollectionFilterItem.d.ts +5 -1
- package/es/components/form/filter/FilterValueInput.d.ts +5 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/AppListRender.d.ts +11 -0
- package/es/flow/admin-shell/admin-layout/index.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/useApplications.d.ts +3 -2
- 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/filter-manager/utils.d.ts +23 -1
- package/es/flow/models/blocks/table/TableColumnModel.d.ts +2 -0
- package/es/index.mjs +179 -101
- package/lib/index.js +187 -109
- package/package.json +7 -7
- package/src/BaseApplication.tsx +13 -0
- 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/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/flow/__tests__/FlowRoute.test.tsx +8 -1
- package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +369 -2
- package/src/flow/actions/dataScope.tsx +8 -1
- package/src/flow/actions/linkageRules.tsx +56 -1
- package/src/flow/actions/setTargetDataScope.tsx +1 -1
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +31 -3
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +15 -1
- package/src/flow/admin-shell/admin-layout/AppListRender.tsx +139 -0
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +75 -1
- package/src/flow/admin-shell/admin-layout/index.ts +1 -0
- 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/admin-shell/admin-layout/useApplications.tsx +34 -1
- 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/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/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 +11 -0
- 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
|
@@ -7,25 +7,13 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import React from 'react';
|
|
11
10
|
import { defineAction } from '@nocobase/flow-engine';
|
|
12
11
|
import { FilterFormItemModel } from '../../filter-form/FilterFormItemModel';
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
function isOperatorSupported(model: FilterFormItemModel, operator: string) {
|
|
19
|
-
const ops = model.collectionField?.filterable?.operators || [];
|
|
20
|
-
return ops.some((op) => op.value === operator);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// 重新包裹 render,确保仍由 flow-engine 的响应式包装驱动
|
|
24
|
-
function rewrapReactiveRender(fieldModel: any) {
|
|
25
|
-
if (!fieldModel) return;
|
|
26
|
-
fieldModel._reactiveWrapperCache = undefined;
|
|
27
|
-
fieldModel.setupReactiveRender?.();
|
|
28
|
-
}
|
|
12
|
+
import {
|
|
13
|
+
applyOperatorComponentRender,
|
|
14
|
+
restoreOperatorComponentRender,
|
|
15
|
+
} from '../../../../internal/utils/operatorSchemaHelper';
|
|
16
|
+
import { getFilterFormOperatorList } from '../utils';
|
|
29
17
|
|
|
30
18
|
function applyCustomizeFilterRender(model: FilterFormItemModel) {
|
|
31
19
|
const operator = model.operator;
|
|
@@ -37,37 +25,19 @@ function applyCustomizeFilterRender(model: FilterFormItemModel) {
|
|
|
37
25
|
key: `${model.uid}-${operator || ''}`,
|
|
38
26
|
});
|
|
39
27
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const originalRender = fieldModel['__originalRender'];
|
|
43
|
-
if (typeof originalRender === 'function') {
|
|
44
|
-
// 清理缓存,防止沿用上一轮的 reactive wrapper
|
|
45
|
-
(fieldModel as any)._reactiveWrapperCache = undefined;
|
|
46
|
-
fieldModel.render = originalRender;
|
|
47
|
-
}
|
|
28
|
+
if (!operator) {
|
|
29
|
+
restoreOperatorComponentRender(fieldModel);
|
|
48
30
|
return;
|
|
49
31
|
}
|
|
50
32
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
model.context.app,
|
|
33
|
+
const operatorList = getFilterFormOperatorList(model);
|
|
34
|
+
applyOperatorComponentRender({
|
|
35
|
+
app: model.context.app,
|
|
36
|
+
fieldModel,
|
|
56
37
|
operator,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const { Comp, props: xProps } = resolved;
|
|
61
|
-
|
|
62
|
-
// 缓存一次原始 render/props,用于回退
|
|
63
|
-
if (!fieldModel['__originalRender']) {
|
|
64
|
-
fieldModel['__originalRender'] = fieldModel.render;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
fieldModel.render = () => (
|
|
68
|
-
<Comp {...xProps} {...fieldModel.props} onCompositionStart={null} onCompositionEnd={null} />
|
|
69
|
-
);
|
|
70
|
-
rewrapReactiveRender(fieldModel);
|
|
38
|
+
operators: operatorList,
|
|
39
|
+
propsPriority: 'field',
|
|
40
|
+
});
|
|
71
41
|
}
|
|
72
42
|
|
|
73
43
|
export const customizeFilterRender = defineAction<FilterFormItemModel>({
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { defineAction, tExpr, FlowModel } from '@nocobase/flow-engine';
|
|
11
|
-
import { operators } from '../../../../../flow-compat';
|
|
12
11
|
import { FilterFormFieldModel } from '../../filter-form/fields';
|
|
12
|
+
import { getFilterFormOperatorList } from '../utils';
|
|
13
13
|
|
|
14
14
|
export const defaultOperator: any = defineAction<FilterFormFieldModel>({
|
|
15
15
|
name: 'defaultOperator',
|
|
@@ -37,12 +37,10 @@ export const defaultOperator: any = defineAction<FilterFormFieldModel>({
|
|
|
37
37
|
|
|
38
38
|
function getOperatorOptions(model: FlowModel) {
|
|
39
39
|
const meta = model.context.collectionField || model.context.filterField;
|
|
40
|
-
|
|
41
|
-
model.context.collectionField?.filterable?.operators || operators[model.context.filterField.type];
|
|
42
|
-
return (operatorList || [])
|
|
40
|
+
return getFilterFormOperatorList(model)
|
|
43
41
|
.filter((op) => !op.visible || op.visible(meta))
|
|
44
42
|
.map((op) => ({
|
|
45
43
|
...op,
|
|
46
|
-
label: model.translate(op.label),
|
|
44
|
+
label: typeof op.label === 'string' ? model.translate(op.label) : op.label,
|
|
47
45
|
}));
|
|
48
46
|
}
|
|
@@ -9,20 +9,10 @@
|
|
|
9
9
|
|
|
10
10
|
import { defineAction } from '@nocobase/flow-engine';
|
|
11
11
|
import { FilterFormItemModel } from '../../filter-form/FilterFormItemModel';
|
|
12
|
-
|
|
13
|
-
function getOperatorMeta(model: FilterFormItemModel) {
|
|
14
|
-
const ops = model.collectionField?.filterable?.operators || [];
|
|
15
|
-
if (!ops.length) {
|
|
16
|
-
return null;
|
|
17
|
-
}
|
|
18
|
-
if (!model.operator) {
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
21
|
-
return ops.find((op) => op.value === model.operator) || null;
|
|
22
|
-
}
|
|
12
|
+
import { getFilterFormOperatorMeta } from '../utils';
|
|
23
13
|
|
|
24
14
|
function applyOperatorComponentProps(model: FilterFormItemModel) {
|
|
25
|
-
const meta =
|
|
15
|
+
const meta = getFilterFormOperatorMeta(model, model.operator);
|
|
26
16
|
if (!meta) {
|
|
27
17
|
return;
|
|
28
18
|
}
|
|
@@ -8,14 +8,153 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { FlowModel } from '@nocobase/flow-engine';
|
|
11
|
+
import {
|
|
12
|
+
normalizeFilterableOperators,
|
|
13
|
+
type FieldFilterable,
|
|
14
|
+
type FieldFilterOperator,
|
|
15
|
+
} from '../../../../collection-manager/filter-operators';
|
|
16
|
+
import { operators } from '../../../../flow-compat';
|
|
11
17
|
import { BlockGridModel, CollectionBlockModel } from '../../base';
|
|
12
18
|
|
|
13
|
-
|
|
19
|
+
type FilterFormFieldMeta = {
|
|
20
|
+
filterable?: FieldFilterable<unknown> | false;
|
|
21
|
+
interface?: string;
|
|
22
|
+
type?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type FilterFormOperatorModel = FlowModel & {
|
|
26
|
+
operator?: string;
|
|
27
|
+
collectionField?: FilterFormFieldMeta;
|
|
28
|
+
filterField?: FilterFormFieldMeta;
|
|
29
|
+
subModels?: {
|
|
30
|
+
field?: {
|
|
31
|
+
operator?: string;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
getStepParams?: (flowKey: string, stepKey: string) => { value?: string } | undefined;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function getModelContext(model: FlowModel) {
|
|
38
|
+
return model.context as unknown as Record<string, unknown>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getModelField(model: FlowModel, key: 'collectionField' | 'filterField') {
|
|
42
|
+
const modelWithFields = model as FilterFormOperatorModel;
|
|
43
|
+
const field = modelWithFields[key] || getModelContext(model)?.[key];
|
|
44
|
+
return field && typeof field === 'object' ? (field as FilterFormFieldMeta) : undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getDataSourceManager(model: FlowModel) {
|
|
48
|
+
const contextManager = getModelContext(model)?.dataSourceManager;
|
|
49
|
+
if (contextManager) {
|
|
50
|
+
return contextManager as Record<string, unknown>;
|
|
51
|
+
}
|
|
52
|
+
return model.flowEngine?.context?.dataSourceManager as unknown as Record<string, unknown> | undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function cloneFilterable(filterable: FieldFilterable<unknown>): FieldFilterable<unknown> {
|
|
56
|
+
return {
|
|
57
|
+
operatorGroup: filterable.operatorGroup,
|
|
58
|
+
operators: Array.isArray(filterable.operators) ? [...filterable.operators] : filterable.operators,
|
|
59
|
+
operatorOverrides: Array.isArray(filterable.operatorOverrides)
|
|
60
|
+
? [...filterable.operatorOverrides]
|
|
61
|
+
: filterable.operatorOverrides,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getFilterableOperators(filterable?: FieldFilterable<unknown> | false) {
|
|
66
|
+
if (filterable === false) {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!filterable || typeof filterable !== 'object') {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const normalized = normalizeFilterableOperators(cloneFilterable(filterable));
|
|
75
|
+
if (Array.isArray(normalized?.operators) && normalized.operators.length > 0) {
|
|
76
|
+
return normalized.operators;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (Array.isArray(normalized?.operatorOverrides) && normalized.operatorOverrides.length > 0) {
|
|
80
|
+
return normalized.operatorOverrides;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getFieldInterfaceFilterable(model: FlowModel, field?: FilterFormFieldMeta) {
|
|
85
|
+
const interfaceName = typeof field?.interface === 'string' ? field.interface : undefined;
|
|
86
|
+
if (!interfaceName) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const dataSourceManager = getDataSourceManager(model);
|
|
91
|
+
const fieldInterfaceManager = dataSourceManager?.collectionFieldInterfaceManager as
|
|
92
|
+
| {
|
|
93
|
+
getFieldInterface?: (name: string) => { filterable?: FieldFilterable<unknown> | false } | undefined;
|
|
94
|
+
}
|
|
95
|
+
| undefined;
|
|
96
|
+
const fieldInterface = fieldInterfaceManager?.getFieldInterface?.(interfaceName);
|
|
97
|
+
return fieldInterface?.filterable;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function getFallbackOperators(field?: FilterFormFieldMeta) {
|
|
101
|
+
const fallbackKeys = [field?.interface, field?.type].filter((item): item is string => typeof item === 'string');
|
|
102
|
+
const standardOperators = operators as Record<string, FieldFilterOperator<unknown>[] | undefined>;
|
|
103
|
+
for (const key of fallbackKeys) {
|
|
104
|
+
const operatorList = standardOperators[key];
|
|
105
|
+
if (operatorList?.length) {
|
|
106
|
+
return operatorList;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function getFilterFormOperatorList(model: FlowModel) {
|
|
113
|
+
const collectionField = getModelField(model, 'collectionField');
|
|
114
|
+
const filterField = getModelField(model, 'filterField');
|
|
115
|
+
const field = collectionField || filterField;
|
|
116
|
+
|
|
117
|
+
if (field?.filterable === false) {
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const fieldOperators = getFilterableOperators(field?.filterable);
|
|
122
|
+
if (fieldOperators?.length) {
|
|
123
|
+
return fieldOperators;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const fieldInterfaceFilterable = getFieldInterfaceFilterable(model, field);
|
|
127
|
+
if (fieldInterfaceFilterable === false) {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const fieldInterfaceOperators = getFilterableOperators(fieldInterfaceFilterable);
|
|
132
|
+
if (fieldInterfaceOperators?.length) {
|
|
133
|
+
return fieldInterfaceOperators;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return getFallbackOperators(field);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function getFilterFormOperatorMeta(model: FlowModel, operator = getDefaultOperator(model)) {
|
|
140
|
+
if (!operator) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const meta = getModelField(model, 'collectionField') || getModelField(model, 'filterField');
|
|
145
|
+
return (
|
|
146
|
+
getFilterFormOperatorList(model).find((op) => op.value === operator && (!op.visible || op.visible(meta))) || null
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function getDefaultOperator(model: FilterFormOperatorModel) {
|
|
151
|
+
const operatorList = getFilterFormOperatorList(model);
|
|
14
152
|
return (
|
|
15
153
|
model.operator ||
|
|
16
|
-
model.getStepParams('filterFormItemSettings', 'defaultOperator')?.value ||
|
|
17
|
-
model.subModels
|
|
18
|
-
|
|
154
|
+
model.getStepParams?.('filterFormItemSettings', 'defaultOperator')?.value ||
|
|
155
|
+
model.subModels?.field?.operator ||
|
|
156
|
+
operatorList.find((op) => op.selected)?.value ||
|
|
157
|
+
operatorList[0]?.value ||
|
|
19
158
|
'$includes'
|
|
20
159
|
);
|
|
21
160
|
}
|
|
@@ -22,6 +22,8 @@ import { FieldModel } from '../../base/FieldModel';
|
|
|
22
22
|
import { rebuildFieldSubModel } from '../../../internal/utils/rebuildFieldSubModel';
|
|
23
23
|
import { useJsonTemplateResolver } from '../../../utils/useJsonTemplateResolver';
|
|
24
24
|
|
|
25
|
+
const MAX_FORM_ASSOCIATION_FIELD_DEPTH = 2;
|
|
26
|
+
|
|
25
27
|
function mergeArrays(a, b) {
|
|
26
28
|
if (!b) {
|
|
27
29
|
return a;
|
|
@@ -84,10 +86,17 @@ const AssociationItem = (props) => {
|
|
|
84
86
|
export class FormAssociationItemModel extends DisplayItemModel {
|
|
85
87
|
static defineChildren(ctx: FlowModelContext) {
|
|
86
88
|
const collection = ctx.collection as Collection;
|
|
89
|
+
const associationDepth = ctx.prefixFieldPath ? ctx.prefixFieldPath.split('.').filter(Boolean).length : 0;
|
|
87
90
|
|
|
88
91
|
return collection
|
|
89
92
|
.getFields()
|
|
90
93
|
.map((field) => {
|
|
94
|
+
if (
|
|
95
|
+
associationDepth >= MAX_FORM_ASSOCIATION_FIELD_DEPTH &&
|
|
96
|
+
(field.isAssociationField?.() || field.target || field.targetCollection)
|
|
97
|
+
) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
91
100
|
const binding = this.getDefaultBindingByField(ctx, field, { fallbackToTargetTitleField: true });
|
|
92
101
|
if (!binding) return null;
|
|
93
102
|
const fieldModel = binding.modelName;
|
|
@@ -42,12 +42,21 @@ const interfacesOfUnsupportedDefaultValue = [
|
|
|
42
42
|
'formula',
|
|
43
43
|
];
|
|
44
44
|
|
|
45
|
+
const MAX_FORM_ASSOCIATION_FIELD_DEPTH = 2;
|
|
46
|
+
|
|
45
47
|
export class FormItemModel<T extends DefaultStructure = DefaultStructure> extends EditableItemModel<T> {
|
|
46
48
|
static defineChildren(ctx: FlowModelContext) {
|
|
47
49
|
const collection = ctx.collection as Collection;
|
|
50
|
+
const associationDepth = ctx.prefixFieldPath ? ctx.prefixFieldPath.split('.').filter(Boolean).length : 0;
|
|
48
51
|
return collection
|
|
49
52
|
.getFields()
|
|
50
53
|
.map((field) => {
|
|
54
|
+
if (
|
|
55
|
+
associationDepth >= MAX_FORM_ASSOCIATION_FIELD_DEPTH &&
|
|
56
|
+
(field.isAssociationField?.() || field.target || field.targetCollection)
|
|
57
|
+
) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
51
60
|
const binding = this.getDefaultBindingByField(ctx, field);
|
|
52
61
|
if (!binding) {
|
|
53
62
|
return;
|
|
@@ -221,6 +221,23 @@ describe('FlowModel core behaviors (collected)', () => {
|
|
|
221
221
|
expect(i1).toEqual({ ping: 1 });
|
|
222
222
|
});
|
|
223
223
|
|
|
224
|
+
it('unchanged stepParams do not trigger a beforeRender rerun', async () => {
|
|
225
|
+
vi.useFakeTimers();
|
|
226
|
+
const spy = vi.fn().mockResolvedValue([]);
|
|
227
|
+
(engine as any).executor.dispatchEvent = spy;
|
|
228
|
+
const model = engine.createModel<FlowModel>({
|
|
229
|
+
use: 'FlowModel',
|
|
230
|
+
uid: 'm-flow-3-noop',
|
|
231
|
+
stepParams: { anyFlow: { anyStep: { x: 1 } } },
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
await model.dispatchEvent('beforeRender', { ping: 1 });
|
|
235
|
+
model.setStepParams('anyFlow', 'anyStep', { x: 1 });
|
|
236
|
+
await vi.advanceTimersByTimeAsync(150);
|
|
237
|
+
|
|
238
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
239
|
+
});
|
|
240
|
+
|
|
224
241
|
it('applyFlow delegates to executor.runFlow', async () => {
|
|
225
242
|
const spyRun = vi.fn().mockResolvedValue('ok');
|
|
226
243
|
(engine as any).executor.runFlow = spyRun;
|
|
@@ -12,7 +12,7 @@ import { FlowEngine, type FlowModelContext, type SubModelItem } from '@nocobase/
|
|
|
12
12
|
// Import from the aggregate to preserve the model initialization order used by adjacent tests.
|
|
13
13
|
import { FormItemModel, FormJSFieldItemModel, InputFieldModel, JSEditableFieldModel } from '../../../..';
|
|
14
14
|
|
|
15
|
-
function createFormMenuContext(prefixFieldPath = 'roles') {
|
|
15
|
+
function createFormMenuContext(prefixFieldPath = 'roles', collectionName = 'roles') {
|
|
16
16
|
const engine = new FlowEngine();
|
|
17
17
|
engine.registerModels({
|
|
18
18
|
FormItemModel,
|
|
@@ -36,15 +36,29 @@ function createFormMenuContext(prefixFieldPath = 'roles') {
|
|
|
36
36
|
fields: [
|
|
37
37
|
{ name: 'id', type: 'integer', interface: 'number', title: 'ID' },
|
|
38
38
|
{ name: 'name', type: 'string', interface: 'input', title: 'Name' },
|
|
39
|
+
{ name: 'department', type: 'belongsTo', interface: 'm2o', target: 'departments', title: 'Department' },
|
|
39
40
|
],
|
|
40
41
|
});
|
|
42
|
+
dataSource.addCollection({
|
|
43
|
+
name: 'departments',
|
|
44
|
+
filterTargetKey: 'id',
|
|
45
|
+
fields: [
|
|
46
|
+
{ name: 'name', type: 'string', interface: 'input', title: 'Name' },
|
|
47
|
+
{ name: 'company', type: 'belongsTo', interface: 'm2o', target: 'companies', title: 'Company' },
|
|
48
|
+
],
|
|
49
|
+
});
|
|
50
|
+
dataSource.addCollection({
|
|
51
|
+
name: 'companies',
|
|
52
|
+
filterTargetKey: 'id',
|
|
53
|
+
fields: [{ name: 'name', type: 'string', interface: 'input', title: 'Name' }],
|
|
54
|
+
});
|
|
41
55
|
|
|
42
56
|
const blockModel = engine.createModel({ use: 'FlowModel', uid: 'users-form-block' });
|
|
43
57
|
(blockModel as any).collection = dataSource.getCollection('users');
|
|
44
58
|
|
|
45
59
|
const gridModel = engine.createModel({ use: 'FlowModel', uid: 'users-form-grid' });
|
|
46
60
|
gridModel.context.defineProperty('blockModel', { value: blockModel });
|
|
47
|
-
gridModel.context.defineProperty('collection', { value: dataSource.getCollection(
|
|
61
|
+
gridModel.context.defineProperty('collection', { value: dataSource.getCollection(collectionName) });
|
|
48
62
|
gridModel.context.defineProperty('prefixFieldPath', { value: prefixFieldPath });
|
|
49
63
|
|
|
50
64
|
return gridModel.context as FlowModelContext;
|
|
@@ -105,4 +119,15 @@ describe('FormItemModel defineChildren', () => {
|
|
|
105
119
|
|
|
106
120
|
expect((jsNameItem?.toggleable as (model: any) => boolean)(createModelLike(normalCreateOptions))).toBe(true);
|
|
107
121
|
});
|
|
122
|
+
|
|
123
|
+
it('hides association fields after two association levels in form add-field menus', async () => {
|
|
124
|
+
const ctx = createFormMenuContext('roles.department', 'departments');
|
|
125
|
+
const formItems = FormItemModel.defineChildren(ctx) as SubModelItem[];
|
|
126
|
+
const jsItems = (await FormJSFieldItemModel.defineChildren(ctx)) as SubModelItem[];
|
|
127
|
+
|
|
128
|
+
expect(formItems.map((item) => item.key)).toContain('roles.department.name');
|
|
129
|
+
expect(formItems.map((item) => item.key)).not.toContain('roles.department.company');
|
|
130
|
+
expect(jsItems.map((item) => item.key)).toContain('roles.department.name');
|
|
131
|
+
expect(jsItems.map((item) => item.key)).not.toContain('roles.department.company');
|
|
132
|
+
});
|
|
108
133
|
});
|
|
@@ -15,6 +15,7 @@ import { observable } from '@formily/reactive';
|
|
|
15
15
|
import { get as lodashGet, merge as lodashMerge, set as lodashSet } from 'lodash';
|
|
16
16
|
import { FlowContext, JSRunner } from '@nocobase/flow-engine';
|
|
17
17
|
import { FormValueRuntime } from '..';
|
|
18
|
+
import type { FormInstance } from 'antd';
|
|
18
19
|
|
|
19
20
|
function createFormStub(initialValues: any = {}) {
|
|
20
21
|
const store: any = JSON.parse(JSON.stringify(initialValues || {}));
|
|
@@ -121,6 +122,34 @@ function createFieldContext(runtime: FormValueRuntime) {
|
|
|
121
122
|
}
|
|
122
123
|
|
|
123
124
|
describe('FormValueRuntime (default rules)', () => {
|
|
125
|
+
it('skips object patches when values are unchanged', async () => {
|
|
126
|
+
const engineEmitter = new EventEmitter();
|
|
127
|
+
const blockEmitter = new EventEmitter();
|
|
128
|
+
const formStub = createFormStub({ name: 'old' });
|
|
129
|
+
const dispatchEvent = vi.fn();
|
|
130
|
+
|
|
131
|
+
const blockModel = {
|
|
132
|
+
uid: 'form-noop-patch',
|
|
133
|
+
flowEngine: { emitter: engineEmitter },
|
|
134
|
+
emitter: blockEmitter,
|
|
135
|
+
dispatchEvent,
|
|
136
|
+
getAclActionName: () => 'create',
|
|
137
|
+
} as ConstructorParameters<typeof FormValueRuntime>[0]['model'];
|
|
138
|
+
|
|
139
|
+
const runtime = new FormValueRuntime({ model: blockModel, getForm: () => formStub as unknown as FormInstance });
|
|
140
|
+
runtime.mount({ sync: true });
|
|
141
|
+
|
|
142
|
+
const blockCtx = createFieldContext(runtime);
|
|
143
|
+
await runtime.setFormValues(blockCtx, { name: 'old' }, { source: 'system' });
|
|
144
|
+
|
|
145
|
+
expect(dispatchEvent).not.toHaveBeenCalled();
|
|
146
|
+
|
|
147
|
+
await runtime.setFormValues(blockCtx, { name: 'new' }, { source: 'system' });
|
|
148
|
+
|
|
149
|
+
expect(formStub.getFieldValue(['name'])).toBe('new');
|
|
150
|
+
expect(dispatchEvent).toHaveBeenCalledTimes(1);
|
|
151
|
+
});
|
|
152
|
+
|
|
124
153
|
it('recomputes default on dependency change when current equals last default; user change disables default permanently', async () => {
|
|
125
154
|
const engineEmitter = new EventEmitter();
|
|
126
155
|
const blockEmitter = new EventEmitter();
|
|
@@ -846,7 +846,11 @@ export class FormValueRuntime {
|
|
|
846
846
|
const changedPaths: NamePath[] = [];
|
|
847
847
|
|
|
848
848
|
if (!Array.isArray(patch)) {
|
|
849
|
-
const patchEntries = Object.entries(patch || {}).filter(([pathKey]) =>
|
|
849
|
+
const patchEntries = Object.entries(patch || {}).filter(([pathKey, rawValue]) => {
|
|
850
|
+
if (shouldSkipByLinkageScope(pathKey)) return false;
|
|
851
|
+
const value = isObservable(rawValue) ? toJS(rawValue) : rawValue;
|
|
852
|
+
return !_.isEqual(this.getFormValueAtPath([pathKey]), value);
|
|
853
|
+
});
|
|
850
854
|
const patchToApply = Object.fromEntries(patchEntries);
|
|
851
855
|
const patchKeys = patchEntries.map(([pathKey]) => pathKey);
|
|
852
856
|
if (!patchKeys.length) {
|
|
@@ -47,7 +47,7 @@ export function FieldDeletePlaceholder(props: any) {
|
|
|
47
47
|
const dataSourcePrefix = `${t(dataSource.displayName || dataSource.key)} > `;
|
|
48
48
|
const collectionPrefix = collection ? `${t(collection.title) || collection.name || collection.tableName} > ` : '';
|
|
49
49
|
return `${dataSourcePrefix}${collectionPrefix}${name}`;
|
|
50
|
-
}, []);
|
|
50
|
+
}, [collection, dataSource.displayName, dataSource.key, name, t]);
|
|
51
51
|
const content = t(`The {{type}} "{{name}}" may have been deleted. Please remove this {{blockType}}.`, {
|
|
52
52
|
type: t('Field'),
|
|
53
53
|
name: nameValue,
|
|
@@ -71,7 +71,7 @@ function FieldWithoutPermissionPlaceholder() {
|
|
|
71
71
|
const dataSourcePrefix = `${t(dataSource.displayName || dataSource.key)} > `;
|
|
72
72
|
const collectionPrefix = collection ? `${t(collection.title) || collection.name || collection.tableName} > ` : '';
|
|
73
73
|
return `${dataSourcePrefix}${collectionPrefix}${name}`;
|
|
74
|
-
}, []);
|
|
74
|
+
}, [collection, dataSource.displayName, dataSource.key, name, t]);
|
|
75
75
|
const { actionName } = model.forbidden;
|
|
76
76
|
const messageValue = useMemo(() => {
|
|
77
77
|
return t(
|
|
@@ -81,7 +81,7 @@ function FieldWithoutPermissionPlaceholder() {
|
|
|
81
81
|
actionName: t(capitalize(actionName)),
|
|
82
82
|
},
|
|
83
83
|
).replaceAll('>', '>');
|
|
84
|
-
}, [nameValue, t]);
|
|
84
|
+
}, [actionName, nameValue, t]);
|
|
85
85
|
return (
|
|
86
86
|
<Tooltip title={messageValue}>
|
|
87
87
|
<LockOutlined style={{ opacity: '0.3' }} />
|
|
@@ -89,6 +89,15 @@ function FieldWithoutPermissionPlaceholder() {
|
|
|
89
89
|
);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
export const TABLE_COLUMN_MIN_WIDTH = 10;
|
|
93
|
+
|
|
94
|
+
export function normalizeTableColumnWidth(width: number | null | undefined) {
|
|
95
|
+
if (typeof width === 'number' && width < TABLE_COLUMN_MIN_WIDTH) {
|
|
96
|
+
return TABLE_COLUMN_MIN_WIDTH;
|
|
97
|
+
}
|
|
98
|
+
return width;
|
|
99
|
+
}
|
|
100
|
+
|
|
92
101
|
export const CustomWidth = ({ setOpen, t, handleChange, defaultValue }) => {
|
|
93
102
|
const [customWidth, setCustomWidth] = useState(defaultValue);
|
|
94
103
|
return (
|
|
@@ -104,13 +113,14 @@ export const CustomWidth = ({ setOpen, t, handleChange, defaultValue }) => {
|
|
|
104
113
|
<Space.Compact block>
|
|
105
114
|
<InputNumber
|
|
106
115
|
placeholder={t('Custom column width')}
|
|
116
|
+
min={TABLE_COLUMN_MIN_WIDTH}
|
|
107
117
|
value={customWidth}
|
|
108
118
|
onChange={(val) => {
|
|
109
119
|
setCustomWidth(val);
|
|
110
120
|
}}
|
|
111
121
|
style={{ width: '100%', minWidth: 200 }}
|
|
112
122
|
/>
|
|
113
|
-
<Button type="primary" onClick={() => handleChange(customWidth)}>
|
|
123
|
+
<Button type="primary" onClick={() => handleChange(normalizeTableColumnWidth(customWidth))}>
|
|
114
124
|
OK
|
|
115
125
|
</Button>
|
|
116
126
|
</Space.Compact>
|
|
@@ -442,7 +452,7 @@ TableColumnModel.registerFlow({
|
|
|
442
452
|
width: 150,
|
|
443
453
|
},
|
|
444
454
|
handler(ctx, params) {
|
|
445
|
-
ctx.model.setProps('width', params.width);
|
|
455
|
+
ctx.model.setProps('width', normalizeTableColumnWidth(params.width));
|
|
446
456
|
},
|
|
447
457
|
},
|
|
448
458
|
aclCheck: {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { tExpr, FlowModel, ModelRenderMode } from '@nocobase/flow-engine';
|
|
11
11
|
import { Divider } from 'antd';
|
|
12
12
|
import React from 'react';
|
|
13
|
-
import { CustomWidth } from './TableColumnModel';
|
|
13
|
+
import { CustomWidth, normalizeTableColumnWidth } from './TableColumnModel';
|
|
14
14
|
export class TableCustomColumnModel extends FlowModel {
|
|
15
15
|
static renderMode: ModelRenderMode = ModelRenderMode.RenderFunction;
|
|
16
16
|
}
|
|
@@ -100,7 +100,7 @@ TableCustomColumnModel.registerFlow({
|
|
|
100
100
|
width: 150,
|
|
101
101
|
},
|
|
102
102
|
handler(ctx, params) {
|
|
103
|
-
ctx.model.setProps('width', params.width);
|
|
103
|
+
ctx.model.setProps('width', normalizeTableColumnWidth(params.width));
|
|
104
104
|
},
|
|
105
105
|
},
|
|
106
106
|
fixed: {
|
|
@@ -12,6 +12,24 @@ import { describe, expect, it, vi } from 'vitest';
|
|
|
12
12
|
import { TableColumnModel } from '../TableColumnModel';
|
|
13
13
|
|
|
14
14
|
describe('TableColumnModel sorter settings', () => {
|
|
15
|
+
it('clamps custom column width to the minimum value', () => {
|
|
16
|
+
const engine = new FlowEngine();
|
|
17
|
+
const model = new TableColumnModel({ uid: 'table-column-min-width', flowEngine: engine } as any);
|
|
18
|
+
const widthStep = model.getFlow('tableColumnSettings')?.steps?.width as any;
|
|
19
|
+
const setProps = vi.fn();
|
|
20
|
+
|
|
21
|
+
widthStep.handler(
|
|
22
|
+
{
|
|
23
|
+
model: {
|
|
24
|
+
setProps,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{ width: 0 },
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
expect(setProps).toHaveBeenCalledWith('width', 10);
|
|
31
|
+
});
|
|
32
|
+
|
|
15
33
|
it('hides quick edit setting for relation path columns added from association groups', async () => {
|
|
16
34
|
const engine = new FlowEngine();
|
|
17
35
|
const model = new TableColumnModel({ uid: 'table-column-relation-path-quick-edit', flowEngine: engine } as any);
|
package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx
CHANGED
|
@@ -33,7 +33,7 @@ import React, { useRef, useMemo, useEffect } from 'react';
|
|
|
33
33
|
import { SubTableFieldModel } from '.';
|
|
34
34
|
import { FieldModel } from '../../../base/FieldModel';
|
|
35
35
|
import { DetailsItemModel } from '../../../blocks/details/DetailsItemModel';
|
|
36
|
-
import { FieldDeletePlaceholder, CustomWidth } from '../../../blocks/table/TableColumnModel';
|
|
36
|
+
import { FieldDeletePlaceholder, CustomWidth, normalizeTableColumnWidth } from '../../../blocks/table/TableColumnModel';
|
|
37
37
|
import { buildDynamicNamePath } from '../../../blocks/form/dynamicNamePath';
|
|
38
38
|
import { getSubTableRowIdentity } from './rowIdentity';
|
|
39
39
|
import { getFieldBindingUse, rebuildFieldSubModel } from '../../../../internal/utils/rebuildFieldSubModel';
|
|
@@ -67,7 +67,7 @@ export function FieldWithoutPermissionPlaceholder({ targetModel }) {
|
|
|
67
67
|
const dataSourcePrefix = `${t(dataSource.displayName || dataSource.key)} > `;
|
|
68
68
|
const collectionPrefix = collection ? `${t(collection.title) || collection.name || collection.tableName} > ` : '';
|
|
69
69
|
return `${dataSourcePrefix}${collectionPrefix}${name}`;
|
|
70
|
-
}, []);
|
|
70
|
+
}, [collection, dataSource.displayName, dataSource.key, name, t]);
|
|
71
71
|
const { actionName } = fieldModel.forbidden || {};
|
|
72
72
|
const messageValue = useMemo(() => {
|
|
73
73
|
return t(
|
|
@@ -77,7 +77,7 @@ export function FieldWithoutPermissionPlaceholder({ targetModel }) {
|
|
|
77
77
|
actionName: t(capitalize(actionName)),
|
|
78
78
|
},
|
|
79
79
|
).replaceAll('>', '>');
|
|
80
|
-
}, [nameValue, t]);
|
|
80
|
+
}, [actionName, nameValue, t]);
|
|
81
81
|
return (
|
|
82
82
|
<Tooltip title={messageValue}>
|
|
83
83
|
<LockOutlined style={{ opacity: '0.3' }} />
|
|
@@ -100,10 +100,9 @@ const LargeFieldEdit = observer(({ model, params: { fieldPath, index }, defaultV
|
|
|
100
100
|
const handleChange = useMemo(
|
|
101
101
|
() =>
|
|
102
102
|
debounce((val) => {
|
|
103
|
-
if (props.onChange) props.onChange(val);
|
|
104
103
|
if (onChange) onChange(val);
|
|
105
104
|
}, 200),
|
|
106
|
-
[
|
|
105
|
+
[onChange],
|
|
107
106
|
);
|
|
108
107
|
|
|
109
108
|
return <FieldModelRenderer model={model} {...rest} onChange={handleChange} />;
|
|
@@ -149,7 +148,7 @@ const LargeFieldEdit = observer(({ model, params: { fieldPath, index }, defaultV
|
|
|
149
148
|
</Space>
|
|
150
149
|
);
|
|
151
150
|
}
|
|
152
|
-
}, [collectionField.interface, defaultValue, fieldModel]);
|
|
151
|
+
}, [collectionField.interface, defaultValue, disabled, fieldModel]);
|
|
153
152
|
return (
|
|
154
153
|
<div
|
|
155
154
|
ref={ref}
|
|
@@ -830,7 +829,7 @@ SubTableColumnModel.registerFlow({
|
|
|
830
829
|
width: 200,
|
|
831
830
|
},
|
|
832
831
|
handler(ctx, params) {
|
|
833
|
-
ctx.model.setProps('width', params.width);
|
|
832
|
+
ctx.model.setProps('width', normalizeTableColumnWidth(params.width));
|
|
834
833
|
},
|
|
835
834
|
},
|
|
836
835
|
aclCheck: {
|