@nocobase/client-v2 2.1.0-beta.30 → 2.1.0-beta.33
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/PluginManager.d.ts +1 -0
- package/es/components/form/DrawerFormLayout.d.ts +49 -0
- package/es/components/form/EnvVariableInput.d.ts +42 -0
- package/es/components/form/FileSizeInput.d.ts +27 -0
- package/es/components/form/createFormRegistry.d.ts +33 -0
- package/es/components/form/index.d.ts +13 -0
- package/es/components/index.d.ts +1 -1
- package/es/flow/components/FieldAssignRulesEditor.d.ts +1 -0
- package/es/flow/components/code-editor/index.d.ts +1 -0
- package/es/flow/internal/utils/enumOptionsUtils.d.ts +5 -0
- package/es/flow/models/actions/AssociationActionUtils.d.ts +5 -0
- package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +4 -0
- package/es/flow/models/blocks/filter-manager/FilterManager.d.ts +5 -1
- package/es/flow/models/blocks/shared/legacyDefaultValueMigrationBase.d.ts +1 -0
- package/es/flow/models/blocks/table/TableSelectModel.d.ts +8 -0
- package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +4 -0
- package/es/flow/models/fields/ClickableFieldModel.d.ts +3 -0
- package/es/flow/models/fields/DisplayEnumFieldModel.d.ts +6 -0
- package/es/flow/models/fields/DisplayTitleFieldModel.d.ts +2 -2
- package/es/flow/models/utils/displayValueUtils.d.ts +12 -0
- package/es/flow-compat/passwordUtils.d.ts +1 -1
- package/es/index.mjs +119 -108
- package/es/utils/remotePlugins.d.ts +0 -4
- package/lib/index.js +122 -111
- package/package.json +9 -5
- package/src/BaseApplication.tsx +14 -8
- package/src/PluginManager.ts +1 -0
- package/src/__tests__/app.test.tsx +28 -1
- package/src/__tests__/globalDeps.test.ts +1 -0
- package/src/__tests__/remotePlugins.test.ts +29 -18
- package/src/components/form/DrawerFormLayout.tsx +103 -0
- package/src/components/form/EnvVariableInput.tsx +126 -0
- package/src/components/form/FileSizeInput.tsx +105 -0
- package/src/components/form/createFormRegistry.ts +60 -0
- package/src/components/form/index.tsx +14 -0
- package/src/components/index.ts +1 -1
- package/src/flow/actions/__tests__/dataScopeFilter.test.ts +92 -13
- package/src/flow/actions/__tests__/formAssignRules.legacyMigration.test.tsx +173 -0
- package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +476 -1
- package/src/flow/actions/__tests__/pattern.test.ts +134 -0
- package/src/flow/actions/__tests__/titleField.test.ts +45 -0
- package/src/flow/actions/filterFormDefaultValues.tsx +30 -9
- package/src/flow/actions/formAssignRules.tsx +24 -9
- package/src/flow/actions/linkageRules.tsx +240 -258
- package/src/flow/actions/pattern.tsx +41 -6
- package/src/flow/actions/setTargetDataScope.tsx +32 -3
- package/src/flow/actions/titleField.tsx +4 -2
- package/src/flow/actions/validation.tsx +1 -1
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +0 -4
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +13 -5
- package/src/flow/components/DynamicFlowsIcon.tsx +87 -13
- package/src/flow/components/FieldAssignRulesEditor.tsx +2 -0
- package/src/flow/components/__tests__/DynamicFlowsIcon.test.tsx +195 -8
- package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +81 -4
- package/src/flow/components/code-editor/index.tsx +12 -8
- package/src/flow/components/filter/LinkageFilterItem.tsx +9 -2
- package/src/flow/components/filter/VariableFilterItem.tsx +2 -6
- package/src/flow/components/filter/__tests__/LinkageFilterItem.test.tsx +71 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +48 -0
- package/src/flow/internal/utils/__tests__/enumOptionsUtils.test.ts +10 -1
- package/src/flow/internal/utils/enumOptionsUtils.ts +29 -0
- package/src/flow/models/actions/AssociateActionModel.tsx +2 -2
- package/src/flow/models/actions/AssociationActionUtils.ts +14 -0
- package/src/flow/models/actions/__tests__/AssociationActionModel.test.ts +63 -0
- package/src/flow/models/base/CollectionBlockModel.tsx +7 -0
- package/src/flow/models/base/PageModel/RootPageModel.tsx +1 -1
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +33 -9
- package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +53 -13
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +63 -3
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +33 -1
- package/src/flow/models/blocks/filter-form/__tests__/legacyDefaultValueMigration.test.ts +3 -0
- package/src/flow/models/blocks/filter-manager/FilterManager.ts +66 -2
- package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +270 -0
- package/src/flow/models/blocks/form/FormActionModel.tsx +2 -8
- package/src/flow/models/blocks/form/FormBlockModel.tsx +8 -5
- package/src/flow/models/blocks/form/FormItemModel.tsx +1 -1
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +30 -0
- package/src/flow/models/blocks/form/__tests__/legacyDefaultValueMigration.test.ts +3 -0
- package/src/flow/models/blocks/form/value-runtime/rules.ts +6 -1
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +6 -1
- package/src/flow/models/blocks/shared/legacyDefaultValueMigrationBase.ts +21 -5
- package/src/flow/models/blocks/table/TableBlockModel.tsx +11 -6
- package/src/flow/models/blocks/table/TableColumnModel.tsx +8 -2
- package/src/flow/models/blocks/table/TableSelectModel.tsx +36 -26
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowClick.test.ts +69 -0
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +132 -1
- package/src/flow/models/blocks/table/__tests__/TableSelectModel.test.ts +41 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +144 -3
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +170 -1
- package/src/flow/models/fields/ClickableFieldModel.tsx +55 -6
- package/src/flow/models/fields/DisplayEnumFieldModel.tsx +44 -0
- package/src/flow/models/fields/DisplayTitleFieldModel.tsx +40 -7
- package/src/flow/models/fields/SelectFieldModel.tsx +31 -1
- package/src/flow/models/fields/__tests__/ClickableFieldModel.test.ts +202 -1
- package/src/flow/models/fields/__tests__/DisplayEnumFieldModel.test.tsx +39 -0
- package/src/flow/models/fields/mobile-components/MobileSelect.tsx +2 -1
- package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +7 -0
- package/src/flow/models/utils/displayValueUtils.ts +57 -0
- package/src/utils/globalDeps.ts +11 -0
- package/src/utils/remotePlugins.ts +7 -27
package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx
CHANGED
|
@@ -32,9 +32,11 @@ import { ErrorBoundary } from 'react-error-boundary';
|
|
|
32
32
|
import React, { useRef, useMemo, useEffect } from 'react';
|
|
33
33
|
import { SubTableFieldModel } from '.';
|
|
34
34
|
import { FieldModel } from '../../../base/FieldModel';
|
|
35
|
+
import { DetailsItemModel } from '../../../blocks/details/DetailsItemModel';
|
|
35
36
|
import { FieldDeletePlaceholder, CustomWidth } from '../../../blocks/table/TableColumnModel';
|
|
36
37
|
import { buildDynamicNamePath } from '../../../blocks/form/dynamicNamePath';
|
|
37
38
|
import { getSubTableRowIdentity } from './rowIdentity';
|
|
39
|
+
import { getFieldBindingUse, rebuildFieldSubModel } from '../../../../internal/utils/rebuildFieldSubModel';
|
|
38
40
|
|
|
39
41
|
export const SUB_TABLE_COLUMN_FIELD_COMPONENT_CONTEXT = 'subTableColumn';
|
|
40
42
|
|
|
@@ -218,6 +220,41 @@ function shouldCommitImmediately(value: any) {
|
|
|
218
220
|
return false;
|
|
219
221
|
}
|
|
220
222
|
|
|
223
|
+
export function isSubTableColumnReadPretty(parent: any) {
|
|
224
|
+
return !!parent?.props?.readPretty || parent?.props?.pattern === 'readPretty';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function isSubTableColumnConfiguredReadPretty(parent: any) {
|
|
228
|
+
return (
|
|
229
|
+
isSubTableColumnReadPretty(parent) ||
|
|
230
|
+
parent?.getStepParams?.('subTableColumnSettings', 'pattern')?.pattern === 'readPretty'
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function getSubTableColumnTitleField(parent: any) {
|
|
235
|
+
return (
|
|
236
|
+
parent?.props?.titleField ||
|
|
237
|
+
parent?.subModels?.field?.props?.titleField ||
|
|
238
|
+
parent?.subModels?.field?.props?.fieldNames?.label ||
|
|
239
|
+
parent?.collectionField?.targetCollectionTitleFieldName
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function getSubTableColumnReadPrettyFieldProps(parent: any, value: any) {
|
|
244
|
+
const fieldProps: Record<string, any> = { value };
|
|
245
|
+
const titleField = getSubTableColumnTitleField(parent);
|
|
246
|
+
const fieldNames = parent?.props?.fieldNames || parent?.subModels?.field?.props?.fieldNames;
|
|
247
|
+
|
|
248
|
+
if (titleField) {
|
|
249
|
+
fieldProps.titleField = titleField;
|
|
250
|
+
}
|
|
251
|
+
if (fieldNames) {
|
|
252
|
+
fieldProps.fieldNames = fieldNames;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return fieldProps;
|
|
256
|
+
}
|
|
257
|
+
|
|
221
258
|
const FieldModelRendererOptimize = React.memo((props: any) => {
|
|
222
259
|
const { model, onChange, value, commitOnChange, ...rest } = props;
|
|
223
260
|
const pendingValueRef = React.useRef<any>(props?.value);
|
|
@@ -337,8 +374,8 @@ const MemoCell: React.FC<CellProps> = React.memo(
|
|
|
337
374
|
});
|
|
338
375
|
}
|
|
339
376
|
|
|
340
|
-
if (parent
|
|
341
|
-
fork.setProps(
|
|
377
|
+
if (isSubTableColumnReadPretty(parent)) {
|
|
378
|
+
fork.setProps(getSubTableColumnReadPrettyFieldProps(parent, value));
|
|
342
379
|
return <React.Fragment key={id}>{fork.render()}</React.Fragment>;
|
|
343
380
|
}
|
|
344
381
|
|
|
@@ -598,6 +635,24 @@ export class SubTableColumnModel<
|
|
|
598
635
|
(this.parent as any)?.collection?.filterTargetKey ?? (this.parent as any)?.context?.collection?.filterTargetKey;
|
|
599
636
|
const rowIdentity = getSubTableRowIdentity(record, filterTargetKey) ?? `row:${String(rowIdx)}`;
|
|
600
637
|
const rowForkKey = `row:${baseIndexKey}:${rowIdentity}:${String(rowIdx)}`;
|
|
638
|
+
const fieldModel: any = this.subModels.field;
|
|
639
|
+
const cellModeKey = [
|
|
640
|
+
rowForkKey,
|
|
641
|
+
this.props.pattern,
|
|
642
|
+
this.props.readPretty,
|
|
643
|
+
this.props.titleField,
|
|
644
|
+
this.props.__displayFieldRefreshKey,
|
|
645
|
+
fieldModel?.uid,
|
|
646
|
+
fieldModel?.use,
|
|
647
|
+
fieldModel?.constructor?.name,
|
|
648
|
+
fieldModel?.props?.clickToOpen,
|
|
649
|
+
fieldModel?.props?.displayStyle,
|
|
650
|
+
fieldModel?.props?.overflowMode,
|
|
651
|
+
fieldModel?.props?.titleField,
|
|
652
|
+
fieldModel?.props?.fieldNames?.label,
|
|
653
|
+
]
|
|
654
|
+
.filter((item) => item !== undefined && item !== null)
|
|
655
|
+
.join(':');
|
|
601
656
|
const rowFork: any = (() => {
|
|
602
657
|
const fork = this.createFork({}, rowForkKey);
|
|
603
658
|
fork.context.defineProperty('subTableRowFork', {
|
|
@@ -651,7 +706,7 @@ export class SubTableColumnModel<
|
|
|
651
706
|
parentFieldIndex={baseArr}
|
|
652
707
|
parentItem={parentItem}
|
|
653
708
|
rowFork={rowFork}
|
|
654
|
-
memoKey={
|
|
709
|
+
memoKey={cellModeKey}
|
|
655
710
|
width={this.props.width}
|
|
656
711
|
commitOnChange={this.hasFormulaColumn}
|
|
657
712
|
/>
|
|
@@ -683,6 +738,7 @@ SubTableColumnModel.registerFlow({
|
|
|
683
738
|
ctx.model.setProps(collectionField.getComponentProps());
|
|
684
739
|
ctx.model.setProps('title', collectionField.title);
|
|
685
740
|
ctx.model.setProps('dataIndex', collectionField.name);
|
|
741
|
+
await ctx.model.applySubModelsBeforeRenderFlows('field');
|
|
686
742
|
const currentBlockModel = ctx.model.context.blockModel;
|
|
687
743
|
// 避免强依赖 EditFormModel(减少循环依赖风险):仅在存在该能力时调用
|
|
688
744
|
currentBlockModel?.addAppends?.(ctx.model.fieldPath);
|
|
@@ -896,6 +952,91 @@ SubTableColumnModel.registerFlow({
|
|
|
896
952
|
use: 'fieldComponent',
|
|
897
953
|
title: tExpr('Field component'),
|
|
898
954
|
},
|
|
955
|
+
fieldNames: {
|
|
956
|
+
use: 'titleField',
|
|
957
|
+
title: tExpr('Title field'),
|
|
958
|
+
hideInSettings(ctx) {
|
|
959
|
+
return (
|
|
960
|
+
!ctx.collectionField ||
|
|
961
|
+
!ctx.collectionField.isAssociationField() ||
|
|
962
|
+
!isSubTableColumnConfiguredReadPretty(ctx.model) ||
|
|
963
|
+
(ctx.model.subModels.field as any)?.disableTitleField
|
|
964
|
+
);
|
|
965
|
+
},
|
|
966
|
+
defaultParams(ctx) {
|
|
967
|
+
return {
|
|
968
|
+
label: getSubTableColumnTitleField(ctx.model),
|
|
969
|
+
};
|
|
970
|
+
},
|
|
971
|
+
beforeParamsSave: async (ctx, params, previousParams) => {
|
|
972
|
+
if (!ctx.collectionField || !ctx.collectionField.isAssociationField()) {
|
|
973
|
+
return null;
|
|
974
|
+
}
|
|
975
|
+
if (!isSubTableColumnConfiguredReadPretty(ctx.model)) {
|
|
976
|
+
return null;
|
|
977
|
+
}
|
|
978
|
+
if (!params.label || params.label === previousParams.label) {
|
|
979
|
+
return null;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
const targetCollection = ctx.collectionField.targetCollection;
|
|
983
|
+
const targetCollectionField = targetCollection?.getField(params.label);
|
|
984
|
+
if (!targetCollectionField) {
|
|
985
|
+
return null;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
const binding = DetailsItemModel.getDefaultBindingByField(ctx, targetCollectionField);
|
|
989
|
+
const fieldModel: any = ctx.model.subModels.field;
|
|
990
|
+
const currentUse = getFieldBindingUse(fieldModel) || fieldModel?.use;
|
|
991
|
+
const targetUse = binding?.modelName || currentUse;
|
|
992
|
+
const fieldSettingsInit = {
|
|
993
|
+
dataSourceKey: ctx.model.collectionField.dataSourceKey,
|
|
994
|
+
collectionName: targetCollection.name,
|
|
995
|
+
fieldPath: params.label,
|
|
996
|
+
};
|
|
997
|
+
const defaultProps =
|
|
998
|
+
typeof binding?.defaultProps === 'function'
|
|
999
|
+
? binding.defaultProps(ctx, targetCollectionField)
|
|
1000
|
+
: binding?.defaultProps;
|
|
1001
|
+
|
|
1002
|
+
ctx.model.setProps({
|
|
1003
|
+
titleField: params.label,
|
|
1004
|
+
...targetCollectionField.getComponentProps?.(),
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
if (targetUse && targetUse !== currentUse) {
|
|
1008
|
+
await rebuildFieldSubModel({
|
|
1009
|
+
parentModel: ctx.model as any,
|
|
1010
|
+
targetUse,
|
|
1011
|
+
defaultProps: {
|
|
1012
|
+
...(defaultProps || {}),
|
|
1013
|
+
titleField: params.label,
|
|
1014
|
+
},
|
|
1015
|
+
fieldSettingsInit,
|
|
1016
|
+
});
|
|
1017
|
+
} else if (fieldModel) {
|
|
1018
|
+
fieldModel.setProps({
|
|
1019
|
+
...(defaultProps || {}),
|
|
1020
|
+
titleField: params.label,
|
|
1021
|
+
});
|
|
1022
|
+
fieldModel.setStepParams('fieldSettings', 'init', fieldSettingsInit);
|
|
1023
|
+
await fieldModel.dispatchEvent('beforeRender', undefined, { useCache: false });
|
|
1024
|
+
}
|
|
1025
|
+
},
|
|
1026
|
+
handler(ctx, params) {
|
|
1027
|
+
if (
|
|
1028
|
+
!ctx.collectionField ||
|
|
1029
|
+
!ctx.collectionField.isAssociationField() ||
|
|
1030
|
+
!isSubTableColumnConfiguredReadPretty(ctx.model)
|
|
1031
|
+
) {
|
|
1032
|
+
return null;
|
|
1033
|
+
}
|
|
1034
|
+
ctx.model.setProps({
|
|
1035
|
+
titleField: params.label,
|
|
1036
|
+
...ctx.collectionField.targetCollection?.getField(params.label)?.getComponentProps?.(),
|
|
1037
|
+
});
|
|
1038
|
+
},
|
|
1039
|
+
},
|
|
899
1040
|
pattern: {
|
|
900
1041
|
use: 'pattern',
|
|
901
1042
|
},
|
|
@@ -8,7 +8,18 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { describe, expect, it, vi } from 'vitest';
|
|
11
|
-
import {
|
|
11
|
+
import { FlowEngine } from '@nocobase/flow-engine';
|
|
12
|
+
import { DisplayTitleFieldModel } from '../../../DisplayTitleFieldModel';
|
|
13
|
+
import { titleField } from '../../../../../actions/titleField';
|
|
14
|
+
import {
|
|
15
|
+
SubTableColumnModel,
|
|
16
|
+
getLatestSubTableRowRecord,
|
|
17
|
+
buildRowPathFromFieldIndex,
|
|
18
|
+
isSubTableColumnConfiguredReadPretty,
|
|
19
|
+
getSubTableColumnTitleField,
|
|
20
|
+
getSubTableColumnReadPrettyFieldProps,
|
|
21
|
+
isSubTableColumnReadPretty,
|
|
22
|
+
} from '../SubTableColumnModel';
|
|
12
23
|
|
|
13
24
|
describe('SubTableColumnModel row record helpers', () => {
|
|
14
25
|
it('builds the row path from fieldIndex entries', () => {
|
|
@@ -39,4 +50,162 @@ describe('SubTableColumnModel row record helpers', () => {
|
|
|
39
50
|
|
|
40
51
|
expect(getLatestSubTableRowRecord(form, ['roles:0'], fallback)).toBe(fallback);
|
|
41
52
|
});
|
|
53
|
+
|
|
54
|
+
it('treats a display-only column pattern as read-pretty mode', () => {
|
|
55
|
+
expect(isSubTableColumnReadPretty({ props: { pattern: 'readPretty' } })).toBe(true);
|
|
56
|
+
expect(isSubTableColumnReadPretty({ props: { readPretty: true } })).toBe(true);
|
|
57
|
+
expect(isSubTableColumnReadPretty({ props: { pattern: 'editable' } })).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('treats a saved display-only column pattern as read-pretty during beforeRender restore', () => {
|
|
61
|
+
expect(
|
|
62
|
+
isSubTableColumnConfiguredReadPretty({
|
|
63
|
+
props: {},
|
|
64
|
+
getStepParams: vi.fn(() => ({ pattern: 'readPretty' })),
|
|
65
|
+
}),
|
|
66
|
+
).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('passes the association title field to read-pretty cell field models', () => {
|
|
70
|
+
const relationValue = { id: 1, name: 'Alice' };
|
|
71
|
+
expect(
|
|
72
|
+
getSubTableColumnReadPrettyFieldProps(
|
|
73
|
+
{
|
|
74
|
+
props: {},
|
|
75
|
+
collectionField: {
|
|
76
|
+
targetCollectionTitleFieldName: 'name',
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
relationValue,
|
|
80
|
+
),
|
|
81
|
+
).toEqual({
|
|
82
|
+
value: relationValue,
|
|
83
|
+
titleField: 'name',
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('resolves the saved title field before the target collection default', () => {
|
|
88
|
+
expect(
|
|
89
|
+
getSubTableColumnTitleField({
|
|
90
|
+
props: { titleField: 'nickname' },
|
|
91
|
+
subModels: {
|
|
92
|
+
field: {
|
|
93
|
+
props: {
|
|
94
|
+
titleField: 'name',
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
collectionField: {
|
|
99
|
+
targetCollectionTitleFieldName: 'title',
|
|
100
|
+
},
|
|
101
|
+
}),
|
|
102
|
+
).toBe('nickname');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('applies the configured title field to a display-only association column', async () => {
|
|
106
|
+
const engine = new FlowEngine();
|
|
107
|
+
engine.registerModels({ SubTableColumnModel, DisplayTitleFieldModel });
|
|
108
|
+
engine.registerActions({ titleField });
|
|
109
|
+
|
|
110
|
+
const rolesField = {
|
|
111
|
+
name: 'roles',
|
|
112
|
+
title: 'Roles',
|
|
113
|
+
collection: { name: 'users' },
|
|
114
|
+
targetCollectionTitleFieldName: 'name',
|
|
115
|
+
targetCollection: {
|
|
116
|
+
name: 'roles',
|
|
117
|
+
getField: vi.fn((name: string) => ({
|
|
118
|
+
name,
|
|
119
|
+
getComponentProps: () => ({ componentField: name }),
|
|
120
|
+
})),
|
|
121
|
+
},
|
|
122
|
+
isAssociationField: () => true,
|
|
123
|
+
getComponentProps: () => ({}),
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const column = engine.createModel<SubTableColumnModel>({
|
|
127
|
+
use: SubTableColumnModel,
|
|
128
|
+
uid: 'roles-display-column-title-field',
|
|
129
|
+
stepParams: {
|
|
130
|
+
fieldSettings: {
|
|
131
|
+
init: {
|
|
132
|
+
dataSourceKey: 'main',
|
|
133
|
+
collectionName: 'users',
|
|
134
|
+
fieldPath: 'roles',
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
subTableColumnSettings: {
|
|
138
|
+
pattern: {
|
|
139
|
+
pattern: 'readPretty',
|
|
140
|
+
},
|
|
141
|
+
fieldNames: {
|
|
142
|
+
label: 'nickname',
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
column.context.defineProperty('collectionField', { value: rolesField });
|
|
148
|
+
column.context.defineProperty('blockModel', { value: { addAppends: vi.fn() } });
|
|
149
|
+
column.setSubModel('field', {
|
|
150
|
+
use: DisplayTitleFieldModel,
|
|
151
|
+
uid: 'roles-display-field-title-field',
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
await column.dispatchEvent('beforeRender');
|
|
155
|
+
|
|
156
|
+
expect(column.props.titleField).toBe('nickname');
|
|
157
|
+
expect(column.props.componentField).toBe('nickname');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('applies saved display field settings to the inner field during column beforeRender', async () => {
|
|
161
|
+
const engine = new FlowEngine();
|
|
162
|
+
engine.registerModels({ SubTableColumnModel, DisplayTitleFieldModel });
|
|
163
|
+
|
|
164
|
+
const rolesCollection = {
|
|
165
|
+
name: 'roles',
|
|
166
|
+
filterTargetKey: 'id',
|
|
167
|
+
};
|
|
168
|
+
const rolesField = {
|
|
169
|
+
name: 'roles',
|
|
170
|
+
title: 'Roles',
|
|
171
|
+
collection: { name: 'users' },
|
|
172
|
+
targetCollection: rolesCollection,
|
|
173
|
+
isAssociationField: () => true,
|
|
174
|
+
getComponentProps: () => ({ titleField: 'name' }),
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const column = engine.createModel<SubTableColumnModel>({
|
|
178
|
+
use: SubTableColumnModel,
|
|
179
|
+
uid: 'roles-title-column',
|
|
180
|
+
stepParams: {
|
|
181
|
+
fieldSettings: {
|
|
182
|
+
init: {
|
|
183
|
+
dataSourceKey: 'main',
|
|
184
|
+
collectionName: 'users',
|
|
185
|
+
fieldPath: 'roles',
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
column.context.defineProperty('collectionField', { value: rolesField });
|
|
191
|
+
column.context.defineProperty('blockModel', { value: { addAppends: vi.fn() } });
|
|
192
|
+
|
|
193
|
+
const field = column.setSubModel('field', {
|
|
194
|
+
use: DisplayTitleFieldModel,
|
|
195
|
+
uid: 'roles-title-display',
|
|
196
|
+
stepParams: {
|
|
197
|
+
displayFieldSettings: {
|
|
198
|
+
clickToOpen: {
|
|
199
|
+
clickToOpen: true,
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
}) as DisplayTitleFieldModel;
|
|
204
|
+
|
|
205
|
+
expect(field.props.clickToOpen).toBeUndefined();
|
|
206
|
+
|
|
207
|
+
await column.dispatchEvent('beforeRender');
|
|
208
|
+
|
|
209
|
+
expect(field.props.clickToOpen).toBe(true);
|
|
210
|
+
});
|
|
42
211
|
});
|
|
@@ -8,12 +8,14 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { CollectionField, tExpr } from '@nocobase/flow-engine';
|
|
11
|
+
import { uid } from '@formily/shared';
|
|
11
12
|
import { Tag } from 'antd';
|
|
12
13
|
import { castArray, get } from 'lodash';
|
|
13
14
|
import React from 'react';
|
|
14
15
|
import { EllipsisWithTooltip } from '../../components/EllipsisWithTooltip';
|
|
15
16
|
import { openViewFlow } from '../../flows/openViewFlow';
|
|
16
17
|
import { FieldModel } from '../base/FieldModel';
|
|
18
|
+
import { hasDisplayValue, normalizeDisplayValue } from '../utils/displayValueUtils';
|
|
17
19
|
|
|
18
20
|
export function transformNestedData(inputData) {
|
|
19
21
|
const resultArray = [];
|
|
@@ -37,9 +39,49 @@ const hasAssociationPathName = (parent: unknown): parent is { associationPathNam
|
|
|
37
39
|
|
|
38
40
|
const hasUsableSourceId = (sourceId: unknown) => sourceId !== undefined && sourceId !== null && String(sourceId) !== '';
|
|
39
41
|
|
|
42
|
+
function getParentAssociationField(model: FieldModel): CollectionField | null {
|
|
43
|
+
const parentCollectionField =
|
|
44
|
+
(model.parent as any)?.context?.collectionField || (model.parent as any)?.collectionField;
|
|
45
|
+
return parentCollectionField?.isAssociationField?.() ? parentCollectionField : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function applyClickToOpenProps(ctx: any, params: any) {
|
|
49
|
+
const collectionField = ctx.collectionField?.isAssociationField?.()
|
|
50
|
+
? ctx.collectionField
|
|
51
|
+
: ctx.model?.parent?.context?.collectionField || ctx.collectionField;
|
|
52
|
+
ctx.model.setProps({
|
|
53
|
+
clickToOpen: params.clickToOpen,
|
|
54
|
+
...(collectionField?.getComponentProps?.() || {}),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function refreshClickToOpenRuntime(ctx: any) {
|
|
59
|
+
ctx.model.invalidateFlowCache?.('beforeRender', true);
|
|
60
|
+
await ctx.model.rerender?.();
|
|
61
|
+
|
|
62
|
+
const parent = ctx.model.parent;
|
|
63
|
+
if (!parent) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
parent.invalidateFlowCache?.('beforeRender', true);
|
|
67
|
+
parent.setProps?.({
|
|
68
|
+
__displayFieldRefreshKey: uid(),
|
|
69
|
+
});
|
|
70
|
+
await parent.rerender?.();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function applyClickToOpenSetting(ctx: any, params: any) {
|
|
74
|
+
applyClickToOpenProps(ctx, params);
|
|
75
|
+
await refreshClickToOpenRuntime(ctx);
|
|
76
|
+
}
|
|
77
|
+
|
|
40
78
|
export class ClickableFieldModel extends FieldModel {
|
|
41
79
|
get collectionField(): CollectionField {
|
|
42
|
-
|
|
80
|
+
const collectionField = this.context.collectionField;
|
|
81
|
+
if (collectionField?.isAssociationField?.()) {
|
|
82
|
+
return collectionField;
|
|
83
|
+
}
|
|
84
|
+
return getParentAssociationField(this) || collectionField;
|
|
43
85
|
}
|
|
44
86
|
|
|
45
87
|
/**
|
|
@@ -150,11 +192,15 @@ export class ClickableFieldModel extends FieldModel {
|
|
|
150
192
|
|
|
151
193
|
renderInDisplayStyle(value, record?, isToMany?, wrap?) {
|
|
152
194
|
const { clickToOpen = false, displayStyle, titleField, overflowMode, disabled, ...restProps } = this.props;
|
|
153
|
-
|
|
195
|
+
const titleCollectionField = titleField
|
|
196
|
+
? this.context.collectionField?.targetCollection?.getField?.(titleField) || this.context.collectionField
|
|
197
|
+
: this.context.collectionField;
|
|
198
|
+
const displayValue = normalizeDisplayValue(value, { collectionField: titleCollectionField });
|
|
199
|
+
if (!hasDisplayValue(displayValue) && value && typeof value === 'object' && restProps.target) {
|
|
154
200
|
return;
|
|
155
201
|
}
|
|
156
|
-
const result = this.renderComponent(
|
|
157
|
-
const display = record ? (
|
|
202
|
+
const result = this.renderComponent(displayValue, wrap);
|
|
203
|
+
const display = record ? (hasDisplayValue(displayValue) ? result : 'N/A') : result;
|
|
158
204
|
const isTag = displayStyle === 'tag';
|
|
159
205
|
const handleClick = (e) => {
|
|
160
206
|
clickToOpen && this.onClick(e, record);
|
|
@@ -170,7 +216,7 @@ export class ClickableFieldModel extends FieldModel {
|
|
|
170
216
|
|
|
171
217
|
if (isTag) {
|
|
172
218
|
return (
|
|
173
|
-
|
|
219
|
+
hasDisplayValue(displayValue) && (
|
|
174
220
|
<Tag {...restProps} style={commonStyle} onClick={handleClick} className={restProps.className}>
|
|
175
221
|
{display}
|
|
176
222
|
</Tag>
|
|
@@ -289,8 +335,11 @@ ClickableFieldModel.registerFlow({
|
|
|
289
335
|
hideInSettings(ctx) {
|
|
290
336
|
return ctx.disableFieldClickToOpen;
|
|
291
337
|
},
|
|
338
|
+
async afterParamsSave(ctx, params) {
|
|
339
|
+
await applyClickToOpenSetting(ctx, params);
|
|
340
|
+
},
|
|
292
341
|
handler(ctx, params) {
|
|
293
|
-
ctx
|
|
342
|
+
applyClickToOpenProps(ctx, params);
|
|
294
343
|
},
|
|
295
344
|
},
|
|
296
345
|
},
|
|
@@ -48,6 +48,13 @@ export class DisplayEnumFieldModel extends ClickableFieldModel {
|
|
|
48
48
|
return value === null || value === undefined || value === '';
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
private isEmptyEnumValue(value: any) {
|
|
52
|
+
if (Array.isArray(value)) {
|
|
53
|
+
return value.length === 0;
|
|
54
|
+
}
|
|
55
|
+
return this.isEmpty(value);
|
|
56
|
+
}
|
|
57
|
+
|
|
51
58
|
public renderComponent(value) {
|
|
52
59
|
const { options = [], dataSource } = this.props;
|
|
53
60
|
const currentOptions = getCurrentOptions(value, dataSource || options, fieldNames);
|
|
@@ -61,6 +68,43 @@ export class DisplayEnumFieldModel extends ClickableFieldModel {
|
|
|
61
68
|
</Tag>
|
|
62
69
|
));
|
|
63
70
|
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Keep array values for multipleSelect/checkboxGroup.
|
|
74
|
+
* ClickableFieldModel will normalize arrays to joined strings, which breaks option label mapping.
|
|
75
|
+
*/
|
|
76
|
+
renderInDisplayStyle(value, record?, isToMany?, wrap?) {
|
|
77
|
+
const { clickToOpen = false, ...restProps } = this.props;
|
|
78
|
+
void wrap;
|
|
79
|
+
|
|
80
|
+
const result = this.renderComponent(value);
|
|
81
|
+
const display = record ? (this.isEmptyEnumValue(value) ? 'N/A' : result) : result;
|
|
82
|
+
|
|
83
|
+
const commonStyle = {
|
|
84
|
+
cursor: clickToOpen ? 'pointer' : 'default',
|
|
85
|
+
alignItems: 'center',
|
|
86
|
+
gap: 4,
|
|
87
|
+
display: isToMany && 'inline-block',
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const handleClick = (e) => {
|
|
91
|
+
clickToOpen && this.onClick(e, record);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
if (clickToOpen) {
|
|
95
|
+
return (
|
|
96
|
+
<a {...restProps} style={commonStyle} onClick={handleClick}>
|
|
97
|
+
{display}
|
|
98
|
+
</a>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<span {...restProps} style={commonStyle} className={restProps.className}>
|
|
104
|
+
{display}
|
|
105
|
+
</span>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
64
108
|
}
|
|
65
109
|
DisplayEnumFieldModel.define({
|
|
66
110
|
label: tExpr('Select'),
|
|
@@ -12,11 +12,20 @@ import { Typography } from 'antd';
|
|
|
12
12
|
import { castArray } from 'lodash';
|
|
13
13
|
import { css } from '@emotion/css';
|
|
14
14
|
import React from 'react';
|
|
15
|
-
import {
|
|
15
|
+
import { openViewFlow } from '../../flows/openViewFlow';
|
|
16
|
+
import { applyClickToOpenProps, applyClickToOpenSetting, ClickableFieldModel } from './ClickableFieldModel';
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
function isParentAssociationField(ctx: any) {
|
|
19
|
+
return !!ctx.model?.parent?.context?.collectionField?.isAssociationField?.();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class DisplayTitleFieldModel extends ClickableFieldModel {
|
|
18
23
|
get collectionField(): CollectionField {
|
|
19
|
-
|
|
24
|
+
const collectionField = this.context.collectionField;
|
|
25
|
+
if (collectionField?.isAssociationField?.()) {
|
|
26
|
+
return collectionField;
|
|
27
|
+
}
|
|
28
|
+
return (this.parent as any)?.context?.collectionField || collectionField;
|
|
20
29
|
}
|
|
21
30
|
|
|
22
31
|
renderComponent(value) {
|
|
@@ -55,13 +64,13 @@ export class DisplayTitleFieldModel extends FieldModel {
|
|
|
55
64
|
};
|
|
56
65
|
if (titleField) {
|
|
57
66
|
const result = castArray(value).flatMap((v, idx) => {
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
return idx === 0 ? [
|
|
67
|
+
const node = this.renderInDisplayStyle(v?.[titleField], v, Array.isArray(value));
|
|
68
|
+
const keyedNode = React.isValidElement(node) ? React.cloneElement(node, { key: `item-${idx}` }) : node;
|
|
69
|
+
return idx === 0 ? [keyedNode] : [<span key={`sep-${idx}`}>, </span>, keyedNode];
|
|
61
70
|
});
|
|
62
71
|
return <Typography.Text {...typographyProps}>{result}</Typography.Text>;
|
|
63
72
|
} else {
|
|
64
|
-
const textContent = <Typography.Text {...typographyProps}>{this.
|
|
73
|
+
const textContent = <Typography.Text {...typographyProps}>{this.renderInDisplayStyle(value)}</Typography.Text>;
|
|
65
74
|
return textContent;
|
|
66
75
|
}
|
|
67
76
|
}
|
|
@@ -75,5 +84,29 @@ DisplayTitleFieldModel.registerFlow({
|
|
|
75
84
|
overflowMode: {
|
|
76
85
|
use: 'overflowMode',
|
|
77
86
|
},
|
|
87
|
+
clickToOpen: {
|
|
88
|
+
title: tExpr('Enable click-to-open'),
|
|
89
|
+
uiMode: { type: 'switch', key: 'clickToOpen' },
|
|
90
|
+
defaultParams: (ctx) => {
|
|
91
|
+
if (ctx.disableFieldClickToOpen) {
|
|
92
|
+
return {
|
|
93
|
+
clickToOpen: false,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
clickToOpen: ctx.collectionField?.isAssociationField?.() || isParentAssociationField(ctx),
|
|
98
|
+
};
|
|
99
|
+
},
|
|
100
|
+
hideInSettings(ctx) {
|
|
101
|
+
return ctx.disableFieldClickToOpen;
|
|
102
|
+
},
|
|
103
|
+
async afterParamsSave(ctx, params) {
|
|
104
|
+
await applyClickToOpenSetting(ctx, params);
|
|
105
|
+
},
|
|
106
|
+
handler(ctx, params) {
|
|
107
|
+
applyClickToOpenProps(ctx, params);
|
|
108
|
+
},
|
|
109
|
+
},
|
|
78
110
|
},
|
|
79
111
|
});
|
|
112
|
+
DisplayTitleFieldModel.registerFlow(openViewFlow);
|
|
@@ -12,25 +12,55 @@ import { Select, Tag, Tooltip } from 'antd';
|
|
|
12
12
|
import React from 'react';
|
|
13
13
|
import { FieldModel } from '../base/FieldModel';
|
|
14
14
|
import { MobileSelect } from './mobile-components/MobileSelect';
|
|
15
|
+
import { enumToOptions, getSelectedEnumLabels } from '../../internal/utils/enumOptionsUtils';
|
|
16
|
+
|
|
17
|
+
const getOriginalEnumOptions = (model: SelectFieldModel) => {
|
|
18
|
+
const fromEnum = enumToOptions(model.context.collectionField?.uiSchema?.enum, (text) => text) || [];
|
|
19
|
+
if (fromEnum.length > 0) {
|
|
20
|
+
return fromEnum.map((option) => ({ ...option }));
|
|
21
|
+
}
|
|
22
|
+
const current = Array.isArray(model.props.options) ? model.props.options : [];
|
|
23
|
+
return current.map((option) => ({ ...option }));
|
|
24
|
+
};
|
|
15
25
|
|
|
16
26
|
export class SelectFieldModel extends FieldModel {
|
|
17
27
|
render() {
|
|
28
|
+
const fallbackOptions = getOriginalEnumOptions(this);
|
|
18
29
|
const options = this.props.options?.map((v) => {
|
|
19
30
|
return {
|
|
20
31
|
...v,
|
|
21
32
|
label: this.translate(v.label),
|
|
22
33
|
};
|
|
23
34
|
});
|
|
35
|
+
const selectedLabels = getSelectedEnumLabels(this.props.value, fallbackOptions).map((item) => ({
|
|
36
|
+
...item,
|
|
37
|
+
label: this.translate(item.label),
|
|
38
|
+
}));
|
|
39
|
+
const value = Array.isArray(this.props.value)
|
|
40
|
+
? selectedLabels
|
|
41
|
+
: selectedLabels.length > 0
|
|
42
|
+
? selectedLabels[0]
|
|
43
|
+
: this.props.value;
|
|
24
44
|
|
|
25
45
|
// TODO: 移动端相关的代码需迁移到单独的插件中
|
|
26
46
|
if (this.context.isMobileLayout) {
|
|
27
|
-
return <MobileSelect {...this.props} options={options} />;
|
|
47
|
+
return <MobileSelect {...this.props} options={options} displayValue={value} />;
|
|
28
48
|
}
|
|
29
49
|
|
|
30
50
|
return (
|
|
31
51
|
<Select
|
|
32
52
|
{...this.props}
|
|
53
|
+
value={value}
|
|
54
|
+
labelInValue
|
|
55
|
+
onChange={(nextValue) => {
|
|
56
|
+
if (Array.isArray(nextValue)) {
|
|
57
|
+
this.props.onChange?.(nextValue.map((item: any) => item?.value));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
this.props.onChange?.(nextValue?.value);
|
|
61
|
+
}}
|
|
33
62
|
options={options}
|
|
63
|
+
labelRender={(item) => item.label}
|
|
34
64
|
maxTagCount="responsive"
|
|
35
65
|
maxTagPlaceholder={(omittedValues) => (
|
|
36
66
|
<Tooltip
|