@nocobase/client-v2 2.1.0-beta.32 → 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.
Files changed (39) hide show
  1. package/es/flow/components/code-editor/index.d.ts +1 -0
  2. package/es/flow/models/blocks/shared/legacyDefaultValueMigrationBase.d.ts +1 -0
  3. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +4 -0
  4. package/es/flow/models/fields/ClickableFieldModel.d.ts +3 -0
  5. package/es/flow/models/fields/DisplayEnumFieldModel.d.ts +6 -0
  6. package/es/flow/models/fields/DisplayTitleFieldModel.d.ts +2 -2
  7. package/es/index.mjs +76 -76
  8. package/lib/index.js +67 -67
  9. package/package.json +8 -5
  10. package/src/__tests__/globalDeps.test.ts +1 -0
  11. package/src/flow/actions/__tests__/formAssignRules.legacyMigration.test.tsx +173 -0
  12. package/src/flow/actions/__tests__/pattern.test.ts +134 -0
  13. package/src/flow/actions/__tests__/titleField.test.ts +45 -0
  14. package/src/flow/actions/filterFormDefaultValues.tsx +30 -9
  15. package/src/flow/actions/formAssignRules.tsx +24 -9
  16. package/src/flow/actions/pattern.tsx +41 -6
  17. package/src/flow/actions/titleField.tsx +4 -2
  18. package/src/flow/actions/validation.tsx +1 -1
  19. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +0 -4
  20. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +13 -5
  21. package/src/flow/components/DynamicFlowsIcon.tsx +87 -13
  22. package/src/flow/components/__tests__/DynamicFlowsIcon.test.tsx +195 -8
  23. package/src/flow/components/code-editor/index.tsx +12 -8
  24. package/src/flow/models/base/PageModel/RootPageModel.tsx +1 -1
  25. package/src/flow/models/blocks/filter-form/__tests__/legacyDefaultValueMigration.test.ts +3 -0
  26. package/src/flow/models/blocks/form/FormActionModel.tsx +2 -8
  27. package/src/flow/models/blocks/form/FormItemModel.tsx +1 -1
  28. package/src/flow/models/blocks/form/__tests__/legacyDefaultValueMigration.test.ts +3 -0
  29. package/src/flow/models/blocks/shared/legacyDefaultValueMigrationBase.ts +21 -5
  30. package/src/flow/models/blocks/table/TableColumnModel.tsx +5 -2
  31. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +36 -0
  32. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +144 -3
  33. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +170 -1
  34. package/src/flow/models/fields/ClickableFieldModel.tsx +46 -2
  35. package/src/flow/models/fields/DisplayEnumFieldModel.tsx +44 -0
  36. package/src/flow/models/fields/DisplayTitleFieldModel.tsx +40 -15
  37. package/src/flow/models/fields/__tests__/ClickableFieldModel.test.ts +180 -2
  38. package/src/flow/models/fields/__tests__/DisplayEnumFieldModel.test.tsx +39 -0
  39. package/src/utils/globalDeps.ts +9 -0
@@ -449,6 +449,9 @@ TableColumnModel.registerFlow({
449
449
  quickEdit: {
450
450
  title: tExpr('Enable quick edit'),
451
451
  uiMode: { type: 'switch', key: 'editable' },
452
+ hideInSettings(ctx) {
453
+ return !!ctx.model.associationPathName;
454
+ },
452
455
  defaultParams(ctx) {
453
456
  if (ctx.model.collectionField.readonly || ctx.model.associationPathName) {
454
457
  return {
@@ -460,7 +463,7 @@ TableColumnModel.registerFlow({
460
463
  };
461
464
  },
462
465
  handler(ctx, params) {
463
- ctx.model.setProps('editable', params.editable);
466
+ ctx.model.setProps('editable', ctx.model.associationPathName ? false : params.editable);
464
467
  },
465
468
  },
466
469
  model: {
@@ -541,7 +544,7 @@ TableColumnModel.registerFlow({
541
544
  ctx.model.setProps(targetCollectionField.getComponentProps());
542
545
  },
543
546
  defaultParams: (ctx: any) => {
544
- const titleField = ctx.model.context.collectionField.targetCollectionTitleFieldName;
547
+ const titleField = ctx.model?.context?.collectionField?.targetCollectionTitleFieldName;
545
548
  return {
546
549
  label: getSavedAssociationTitleField(ctx.model) || titleField,
547
550
  };
@@ -12,6 +12,42 @@ import { describe, expect, it, vi } from 'vitest';
12
12
  import { TableColumnModel } from '../TableColumnModel';
13
13
 
14
14
  describe('TableColumnModel sorter settings', () => {
15
+ it('hides quick edit setting for relation path columns added from association groups', async () => {
16
+ const engine = new FlowEngine();
17
+ const model = new TableColumnModel({ uid: 'table-column-relation-path-quick-edit', flowEngine: engine } as any);
18
+ const quickEditStep = model.getFlow('tableColumnSettings')?.steps?.quickEdit as any;
19
+
20
+ const hidden = await quickEditStep.hideInSettings({
21
+ model: {
22
+ associationPathName: 'department',
23
+ },
24
+ });
25
+
26
+ expect(hidden).toBe(true);
27
+ });
28
+
29
+ it('keeps quick edit disabled for relation path columns even when params enable it', () => {
30
+ const engine = new FlowEngine();
31
+ const model = new TableColumnModel({
32
+ uid: 'table-column-relation-path-disable-quick-edit',
33
+ flowEngine: engine,
34
+ } as any);
35
+ const quickEditStep = model.getFlow('tableColumnSettings')?.steps?.quickEdit as any;
36
+ const setProps = vi.fn();
37
+
38
+ quickEditStep.handler(
39
+ {
40
+ model: {
41
+ associationPathName: 'department',
42
+ setProps,
43
+ },
44
+ },
45
+ { editable: true },
46
+ );
47
+
48
+ expect(setProps).toHaveBeenCalledWith('editable', false);
49
+ });
50
+
15
51
  it('hides sortable setting for association fields', async () => {
16
52
  const engine = new FlowEngine();
17
53
  const model = new TableColumnModel({ uid: 'table-column-association-sorter', flowEngine: engine } as any);
@@ -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.props.readPretty) {
341
- fork.setProps({ value });
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={rowForkKey}
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 { getLatestSubTableRowRecord, buildRowPathFromFieldIndex } from '../SubTableColumnModel';
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,6 +8,7 @@
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';
@@ -38,9 +39,49 @@ const hasAssociationPathName = (parent: unknown): parent is { associationPathNam
38
39
 
39
40
  const hasUsableSourceId = (sourceId: unknown) => sourceId !== undefined && sourceId !== null && String(sourceId) !== '';
40
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
+
41
78
  export class ClickableFieldModel extends FieldModel {
42
79
  get collectionField(): CollectionField {
43
- return this.context.collectionField;
80
+ const collectionField = this.context.collectionField;
81
+ if (collectionField?.isAssociationField?.()) {
82
+ return collectionField;
83
+ }
84
+ return getParentAssociationField(this) || collectionField;
44
85
  }
45
86
 
46
87
  /**
@@ -294,8 +335,11 @@ ClickableFieldModel.registerFlow({
294
335
  hideInSettings(ctx) {
295
336
  return ctx.disableFieldClickToOpen;
296
337
  },
338
+ async afterParamsSave(ctx, params) {
339
+ await applyClickToOpenSetting(ctx, params);
340
+ },
297
341
  handler(ctx, params) {
298
- ctx.model.setProps({ clickToOpen: params.clickToOpen, ...ctx.collectionField.getComponentProps() });
342
+ applyClickToOpenProps(ctx, params);
299
343
  },
300
344
  },
301
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,12 +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 { FieldModel } from '../base';
16
- import { hasDisplayValue, normalizeDisplayValue } from '../utils/displayValueUtils';
15
+ import { openViewFlow } from '../../flows/openViewFlow';
16
+ import { applyClickToOpenProps, applyClickToOpenSetting, ClickableFieldModel } from './ClickableFieldModel';
17
17
 
18
- export class DisplayTitleFieldModel extends FieldModel {
18
+ function isParentAssociationField(ctx: any) {
19
+ return !!ctx.model?.parent?.context?.collectionField?.isAssociationField?.();
20
+ }
21
+
22
+ export class DisplayTitleFieldModel extends ClickableFieldModel {
19
23
  get collectionField(): CollectionField {
20
- return this.context.collectionField;
24
+ const collectionField = this.context.collectionField;
25
+ if (collectionField?.isAssociationField?.()) {
26
+ return collectionField;
27
+ }
28
+ return (this.parent as any)?.context?.collectionField || collectionField;
21
29
  }
22
30
 
23
31
  renderComponent(value) {
@@ -56,20 +64,13 @@ export class DisplayTitleFieldModel extends FieldModel {
56
64
  };
57
65
  if (titleField) {
58
66
  const result = castArray(value).flatMap((v, idx) => {
59
- const titleCollectionField =
60
- this.context.collectionField?.targetCollection?.getField?.(titleField) || this.context.collectionField;
61
- const displayValue = normalizeDisplayValue(v?.[titleField], { collectionField: titleCollectionField });
62
- const result = this.renderComponent(displayValue);
63
- const node = hasDisplayValue(displayValue) ? result : 'N/A';
64
- return idx === 0 ? [node] : [<span key={`sep-${idx}`}>, </span>, node];
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];
65
70
  });
66
71
  return <Typography.Text {...typographyProps}>{result}</Typography.Text>;
67
72
  } else {
68
- const textContent = (
69
- <Typography.Text {...typographyProps}>
70
- {this.renderComponent(normalizeDisplayValue(value, { collectionField: this.context.collectionField }))}
71
- </Typography.Text>
72
- );
73
+ const textContent = <Typography.Text {...typographyProps}>{this.renderInDisplayStyle(value)}</Typography.Text>;
73
74
  return textContent;
74
75
  }
75
76
  }
@@ -83,5 +84,29 @@ DisplayTitleFieldModel.registerFlow({
83
84
  overflowMode: {
84
85
  use: 'overflowMode',
85
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
+ },
86
110
  },
87
111
  });
112
+ DisplayTitleFieldModel.registerFlow(openViewFlow);