@nocobase/client-v2 2.1.0-beta.26 → 2.1.0-beta.27

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 (41) hide show
  1. package/es/flow/components/code-editor/types.d.ts +1 -0
  2. package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +15 -6
  3. package/es/flow/models/blocks/shared/filterOperators.d.ts +9 -0
  4. package/es/flow-compat/data.d.ts +9 -2
  5. package/es/flow-compat/index.d.ts +1 -1
  6. package/es/index.d.ts +1 -1
  7. package/es/index.mjs +90 -90
  8. package/lib/index.js +83 -83
  9. package/package.json +5 -5
  10. package/src/BaseApplication.tsx +1 -1
  11. package/src/__tests__/app.test.tsx +23 -6
  12. package/src/flow/actions/titleField.tsx +8 -3
  13. package/src/flow/components/FieldAssignValueInput.tsx +1 -0
  14. package/src/flow/components/code-editor/__tests__/linter.test.ts +18 -0
  15. package/src/flow/components/code-editor/__tests__/runjsDiagnostics.test.ts +23 -0
  16. package/src/flow/components/code-editor/index.tsx +18 -17
  17. package/src/flow/components/code-editor/linter.ts +222 -158
  18. package/src/flow/components/code-editor/runjsDiagnostics.ts +161 -97
  19. package/src/flow/components/code-editor/types.ts +1 -0
  20. package/src/flow/components/filter/LinkageFilterItem.tsx +6 -5
  21. package/src/flow/components/filter/VariableFilterItem.tsx +14 -13
  22. package/src/flow/components/filter/__tests__/LinkageFilterItem.test.tsx +33 -0
  23. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +48 -5
  24. package/src/flow/internal/utils/__tests__/titleFieldQuickSync.test.ts +1 -0
  25. package/src/flow/internal/utils/titleFieldQuickSync.ts +2 -2
  26. package/src/flow/models/actions/FilterActionModel.tsx +17 -9
  27. package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +200 -36
  28. package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.toggleFormFieldsCollapse.test.ts +270 -1
  29. package/src/flow/models/blocks/filter-form/__tests__/customFieldOperators.test.tsx +23 -0
  30. package/src/flow/models/blocks/filter-form/customFieldOperators.ts +12 -1
  31. package/src/flow/models/blocks/filter-form/fields/FieldComponentProps.tsx +22 -8
  32. package/src/flow/models/blocks/filter-form/fields/__tests__/FilterFormCustomFieldModel.recordSelect.test.tsx +18 -0
  33. package/src/flow/models/blocks/filter-manager/FilterManager.ts +51 -1
  34. package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +75 -0
  35. package/src/flow/models/blocks/form/FormItemModel.tsx +48 -28
  36. package/src/flow/models/blocks/shared/filterOperators.ts +14 -0
  37. package/src/flow/models/blocks/table/TableBlockModel.tsx +19 -3
  38. package/src/flow/models/fields/DividerItemModel.tsx +30 -15
  39. package/src/flow-compat/data.ts +25 -3
  40. package/src/flow-compat/index.ts +7 -1
  41. package/src/index.ts +1 -1
@@ -14,7 +14,11 @@ import { Input, Radio, Checkbox, Space, Button, Select } from 'antd';
14
14
  import { PlusOutlined, CloseOutlined } from '@ant-design/icons';
15
15
  import { useTranslation } from 'react-i18next';
16
16
  import { FilterableItemModel, useFlowContext, useFlowEngine } from '@nocobase/flow-engine';
17
- import { isTitleField } from '../../../../../flow-compat';
17
+ import {
18
+ getFlowFieldInterfaceOptions,
19
+ hasFlowFieldInterfaceLookup,
20
+ isTitleFieldInterface,
21
+ } from '../../../../../flow-compat';
18
22
 
19
23
  const RECORD_SELECT_DATA_SOURCE_KEY = 'recordSelectDataSourceKey';
20
24
  const RECORD_SELECT_COLLECTION_KEY = 'recordSelectTargetCollection';
@@ -36,7 +40,12 @@ export const FieldComponentProps: React.FC<{ fieldModel: string; source: string[
36
40
  const resolvedFieldModel = fieldModel || propsValue?.fieldModel;
37
41
  const flowEngine = useFlowEngine();
38
42
  const ctx = useFlowContext();
39
- const appDataSourceManager = ctx?.app?.dataSourceManager;
43
+ const dataSourceManager = ctx?.dataSourceManager || flowEngine?.dataSourceManager || ctx?.app?.dataSourceManager;
44
+ const hasFieldInterfaceLookup = hasFlowFieldInterfaceLookup(dataSourceManager);
45
+ const getFieldInterface = useCallback(
46
+ (interfaceName: string | undefined) => getFlowFieldInterfaceOptions(interfaceName, dataSourceManager),
47
+ [dataSourceManager],
48
+ );
40
49
 
41
50
  const getCurrentValue = () => field.value || {};
42
51
  const updateProps = (key: string, value: any) => {
@@ -165,15 +174,20 @@ export const FieldComponentProps: React.FC<{ fieldModel: string; source: string[
165
174
  }, [activeDataSource, translateLabel]);
166
175
  const titleFieldOptions = useMemo(() => {
167
176
  if (!activeCollection?.getFields) return [];
168
- const shouldKeep = (fieldItem: any) =>
169
- appDataSourceManager ? isTitleField(appDataSourceManager, fieldItem.options) : true;
177
+ const shouldKeep = (fieldItem: any) => {
178
+ if (!hasFieldInterfaceLookup) {
179
+ return true;
180
+ }
181
+ const fieldOptions = fieldItem?.options || fieldItem;
182
+ return isTitleFieldInterface(getFieldInterface(fieldOptions?.interface));
183
+ };
170
184
  return (activeCollection.getFields() || [])
171
185
  .filter((fieldItem: any) => shouldKeep(fieldItem))
172
186
  .map((fieldItem: any) => ({
173
187
  label: translateLabel(fieldItem.options?.uiSchema?.title) || translateLabel(fieldItem.title) || fieldItem.name,
174
188
  value: fieldItem.name,
175
189
  }));
176
- }, [activeCollection, appDataSourceManager, translateLabel]);
190
+ }, [activeCollection, getFieldInterface, hasFieldInterfaceLookup, translateLabel]);
177
191
  const valueFieldOptions = useMemo(() => {
178
192
  if (!activeCollection?.getFields) return [];
179
193
  const shouldKeep = (fieldItem: any) => {
@@ -182,8 +196,8 @@ export const FieldComponentProps: React.FC<{ fieldModel: string; source: string[
182
196
  if (fieldOptions.filterable === false || !interfaceName) {
183
197
  return false;
184
198
  }
185
- if (appDataSourceManager) {
186
- const fieldInterface = appDataSourceManager.collectionFieldInterfaceManager?.getFieldInterface?.(interfaceName);
199
+ if (hasFieldInterfaceLookup) {
200
+ const fieldInterface = getFieldInterface(interfaceName);
187
201
  if (!fieldInterface?.filterable) {
188
202
  return false;
189
203
  }
@@ -197,7 +211,7 @@ export const FieldComponentProps: React.FC<{ fieldModel: string; source: string[
197
211
  label: translateLabel(fieldItem.options?.uiSchema?.title) || translateLabel(fieldItem.title) || fieldItem.name,
198
212
  value: fieldItem.name,
199
213
  }));
200
- }, [activeCollection, appDataSourceManager, translateLabel]);
214
+ }, [activeCollection, getFieldInterface, hasFieldInterfaceLookup, translateLabel]);
201
215
 
202
216
  useEffect(() => {
203
217
  if (resolvedFieldModel !== 'FilterFormCustomRecordSelectFieldModel') return;
@@ -222,6 +222,17 @@ describe('FilterForm custom field record select', () => {
222
222
  it('hides association fields from value field options', async () => {
223
223
  const engine = new FlowEngine();
224
224
  engine.registerModels({ HostModel, FilterFormCustomRecordSelectFieldModel });
225
+ engine.dataSourceManager.setCollectionFieldInterfaceManager({
226
+ getFieldInterface: vi.fn((name: string) => {
227
+ if (['input', 'number'].includes(name)) {
228
+ return { titleUsable: true, filterable: { operators: [] } };
229
+ }
230
+ if (name === 'm2m') {
231
+ return { filterable: { operators: [] } };
232
+ }
233
+ return undefined;
234
+ }),
235
+ });
225
236
 
226
237
  const ds = engine.dataSourceManager.getDataSource('main');
227
238
  ds?.addCollection({
@@ -242,6 +253,12 @@ describe('FilterForm custom field record select', () => {
242
253
  title: 'Hidden text',
243
254
  filterable: false,
244
255
  },
256
+ {
257
+ name: 'jsonPayload',
258
+ type: 'json',
259
+ interface: 'json',
260
+ title: 'JSON payload',
261
+ },
245
262
  {
246
263
  name: 'roles',
247
264
  type: 'belongsToMany',
@@ -298,6 +315,7 @@ describe('FilterForm custom field record select', () => {
298
315
  );
299
316
  expect(optionTexts).toContain('nickname');
300
317
  expect(optionTexts).not.toContain('Hidden text');
318
+ expect(optionTexts).not.toContain('JSON payload');
301
319
  expect(optionTexts).not.toContain('Roles relation');
302
320
  });
303
321
  });
@@ -10,6 +10,7 @@
10
10
  import { FilterGroup, FilterItem, FlowModel } from '@nocobase/flow-engine';
11
11
  import _ from 'lodash';
12
12
  import { CollectionBlockModel } from '../../base/CollectionBlockModel';
13
+ import { isArrayLikeField } from '../shared/filterOperators';
13
14
  import { getDefaultOperator, isFilterValueEmpty } from './utils';
14
15
 
15
16
  type FilterConfig = {
@@ -23,6 +24,13 @@ type FilterConfig = {
23
24
  operator?: string;
24
25
  };
25
26
 
27
+ const ARRAY_FIELD_OPERATORS_TO_SCALAR_OPERATORS: Record<string, string> = {
28
+ $match: '$in',
29
+ $anyOf: '$in',
30
+ $notMatch: '$notIn',
31
+ $noneOf: '$notIn',
32
+ };
33
+
26
34
  export type ConnectFieldsConfig = {
27
35
  targets: {
28
36
  /** 数据区块或者图表区块的 model uid */
@@ -32,6 +40,44 @@ export type ConnectFieldsConfig = {
32
40
  }[];
33
41
  };
34
42
 
43
+ function getTargetField(targetModel: any, fieldPath: string) {
44
+ const dataSourceManager = targetModel?.context?.dataSourceManager;
45
+ const collection = targetModel?.collection;
46
+ if (!dataSourceManager || !collection?.dataSourceKey || !collection?.name || !fieldPath) {
47
+ return;
48
+ }
49
+
50
+ let collectionName = collection.name;
51
+ const fieldNames = fieldPath.split('.').filter(Boolean);
52
+ for (let index = 0; index < fieldNames.length; index += 1) {
53
+ const field = dataSourceManager.getCollectionField?.(
54
+ `${collection.dataSourceKey}.${collectionName}.${fieldNames[index]}`,
55
+ );
56
+ if (!field || index === fieldNames.length - 1) {
57
+ return field;
58
+ }
59
+
60
+ collectionName = field.target || field.targetCollection?.name;
61
+ if (!collectionName) {
62
+ return;
63
+ }
64
+ }
65
+ }
66
+
67
+ function normalizeOperatorForTargetField(operator: string, targetModel: any, fieldPath: string) {
68
+ const scalarOperator = ARRAY_FIELD_OPERATORS_TO_SCALAR_OPERATORS[operator];
69
+ if (!scalarOperator) {
70
+ return operator;
71
+ }
72
+
73
+ const targetField = getTargetField(targetModel, fieldPath);
74
+ if (!targetField || isArrayLikeField(targetField)) {
75
+ return operator;
76
+ }
77
+
78
+ return scalarOperator;
79
+ }
80
+
35
81
  export class FilterManager {
36
82
  private filterConfigs: FilterConfig[];
37
83
  private readonly gridModel: FlowModel;
@@ -300,7 +346,11 @@ export class FilterManager {
300
346
  // 构建筛选条件
301
347
  const filterConditions = config.filterPaths.map((fieldPath) => ({
302
348
  path: fieldPath,
303
- operator: config.operator || getDefaultOperator(filterModel),
349
+ operator: normalizeOperatorForTargetField(
350
+ config.operator || getDefaultOperator(filterModel),
351
+ targetModel,
352
+ fieldPath,
353
+ ),
304
354
  value: filterValue,
305
355
  }));
306
356
 
@@ -496,6 +496,81 @@ describe('FilterManager', () => {
496
496
  expect(mockTargetModel2.resource.refresh).toHaveBeenCalledTimes(1);
497
497
  });
498
498
 
499
+ it('should normalize array operators when the target field is scalar', async () => {
500
+ const filterConfigs = [
501
+ {
502
+ filterId: 'filter-1',
503
+ targetId: 'target-scalar',
504
+ filterPaths: ['status'],
505
+ operator: '$match',
506
+ },
507
+ {
508
+ filterId: 'filter-1',
509
+ targetId: 'target-array',
510
+ filterPaths: ['tags'],
511
+ operator: '$match',
512
+ },
513
+ {
514
+ filterId: 'filter-1',
515
+ targetId: 'target-deep-array',
516
+ filterPaths: ['org.company.tags'],
517
+ operator: '$match',
518
+ },
519
+ ];
520
+
521
+ (filterManager as any).filterConfigs = filterConfigs;
522
+
523
+ const createTargetModel = (getCollectionField: any) => ({
524
+ collection: {
525
+ dataSourceKey: 'main',
526
+ name: 'users',
527
+ },
528
+ context: {
529
+ dataSourceManager: {
530
+ getCollectionField,
531
+ },
532
+ },
533
+ resource: {
534
+ addFilterGroup: vi.fn(),
535
+ removeFilterGroup: vi.fn(),
536
+ refresh: vi.fn().mockResolvedValue(undefined),
537
+ },
538
+ setFilterActive: vi.fn(),
539
+ getDataLoadingMode: vi.fn().mockReturnValue('auto'),
540
+ });
541
+ const mockScalarTargetModel = createTargetModel(vi.fn().mockReturnValue({ interface: 'select', type: 'string' }));
542
+ const mockArrayTargetModel = createTargetModel(
543
+ vi.fn().mockReturnValue({ interface: 'multipleSelect', type: 'array' }),
544
+ );
545
+ const mockDeepArrayTargetModel = createTargetModel(
546
+ vi.fn((key: string) => {
547
+ const fields = {
548
+ 'main.users.org': { target: 'orgs' },
549
+ 'main.orgs.company': { target: 'companies' },
550
+ 'main.companies.tags': { interface: 'multipleSelect', type: 'array' },
551
+ };
552
+ return fields[key];
553
+ }),
554
+ );
555
+ const mockFilterModel = {
556
+ getFilterValue: vi.fn().mockReturnValue(['a1']),
557
+ };
558
+
559
+ (mockFlowModel.flowEngine.getModel as any).mockImplementation((uid: string) => {
560
+ if (uid === 'target-scalar') return mockScalarTargetModel;
561
+ if (uid === 'target-array') return mockArrayTargetModel;
562
+ if (uid === 'target-deep-array') return mockDeepArrayTargetModel;
563
+ if (uid === 'filter-1') return mockFilterModel;
564
+ return null;
565
+ });
566
+
567
+ await filterManager.refreshTargetsByFilter('filter-1');
568
+
569
+ expect(mockScalarTargetModel.resource.addFilterGroup.mock.calls[0][1].options.operator).toBe('$in');
570
+ expect(mockArrayTargetModel.resource.addFilterGroup.mock.calls[0][1].options.operator).toBe('$match');
571
+ expect(mockDeepArrayTargetModel.resource.addFilterGroup.mock.calls[0][1].options.operator).toBe('$match');
572
+ });
573
+
499
574
  it('should process multiple filterIds successfully', async () => {
500
575
  // Setup filter configs
501
576
  const filterConfigs = [
@@ -22,6 +22,7 @@ import { FieldModel } from '../../base/FieldModel';
22
22
  import { DetailsItemModel } from '../details/DetailsItemModel';
23
23
  import { EditFormModel } from './EditFormModel';
24
24
  import _ from 'lodash';
25
+ import { Tooltip } from 'antd';
25
26
  import { coerceForToOneField } from '../../../internal/utils/associationValueCoercion';
26
27
  import { buildDynamicNamePath } from './dynamicNamePath';
27
28
 
@@ -99,54 +100,63 @@ export class FormItemModel<T extends DefaultStructure = DefaultStructure> extend
99
100
 
100
101
  renderItem() {
101
102
  const fieldModel = this.subModels.field as FieldModel;
102
- // 行索引(来自数组子表单)
103
103
  const idx = this.context.fieldIndex;
104
104
  const fieldKey = this.context.fieldKey;
105
105
  const parentFieldPathArray = this.parent?.context.fieldPathArray || [];
106
+ const isHiddenReservedValuePreview =
107
+ !!this.context.flowSettingsEnabled && !!this.props.hidden && !this.hidden && !this.forbidden;
106
108
 
107
- // 嵌套场景下继续传透,为字段子模型创建 fork
108
109
  const modelForRender =
109
- idx != null
110
+ idx != null || isHiddenReservedValuePreview
110
111
  ? (() => {
111
- const fork = fieldModel.createFork({}, `${fieldKey}`);
112
- fork.context.defineProperty('fieldIndex', {
113
- get: () => idx,
114
- });
115
- fork.context.defineProperty('fieldKey', {
116
- get: () => fieldKey,
117
- });
118
- if (this.context.currentObject) {
119
- fork.context.defineProperty('currentObject', {
120
- get: () => this.context.currentObject,
112
+ const forkKey = isHiddenReservedValuePreview ? `${this.uid}:config-visible` : `${fieldKey}`;
113
+ const fork = fieldModel.createFork({}, forkKey);
114
+ if (idx != null) {
115
+ fork.context.defineProperty('fieldIndex', {
116
+ get: () => idx,
121
117
  });
122
- }
123
- const itemOptions = this.context.getPropertyOptions('item');
124
- if (this.context.item) {
125
- const { value: _value, ...rest } = (itemOptions || {}) as any;
126
- fork.context.defineProperty('item', {
127
- ...rest,
128
- get: () => this.context.item,
129
- cache: false,
118
+ fork.context.defineProperty('fieldKey', {
119
+ get: () => fieldKey,
130
120
  });
121
+ if (this.context.currentObject) {
122
+ fork.context.defineProperty('currentObject', {
123
+ get: () => this.context.currentObject,
124
+ });
125
+ }
126
+ const itemOptions = this.context.getPropertyOptions('item');
127
+ if (this.context.item) {
128
+ const { value: _value, ...rest } = (itemOptions || {}) as any;
129
+ fork.context.defineProperty('item', {
130
+ ...rest,
131
+ get: () => this.context.item,
132
+ cache: false,
133
+ });
134
+ }
135
+ if (this.context.pattern) {
136
+ fork.context.defineProperty('pattern', {
137
+ get: () => this.context.pattern,
138
+ });
139
+ }
131
140
  }
132
- if (this.context.pattern) {
133
- fork.context.defineProperty('pattern', {
134
- get: () => this.context.pattern,
135
- });
141
+ if (isHiddenReservedValuePreview) {
142
+ fork.setProps({ hidden: false });
136
143
  }
137
144
  return fork;
138
145
  })()
139
146
  : fieldModel;
140
147
  const mergedProps = this.context.pattern ? { ...this.props, pattern: this.context.pattern } : this.props;
141
- const { initialValue, ...mergedPropsWithoutInitial } = mergedProps as any;
148
+ const { initialValue, hidden, ...mergedPropsWithoutInitial } = mergedProps as any;
149
+ const formItemProps = isHiddenReservedValuePreview
150
+ ? mergedPropsWithoutInitial
151
+ : { hidden, ...mergedPropsWithoutInitial };
142
152
  const fieldPath = buildDynamicNamePath(this.props.name, idx);
143
153
  this.context.defineProperty('fieldPathArray', {
144
154
  value: [...parentFieldPathArray, ..._.castArray(fieldPath)],
145
155
  });
146
156
  const record = this.context.item?.value || this.context.record;
147
- return (
157
+ const content = (
148
158
  <FormItem
149
- {...mergedPropsWithoutInitial}
159
+ {...formItemProps}
150
160
  name={fieldPath}
151
161
  validateFirst={true}
152
162
  disabled={
@@ -158,6 +168,16 @@ export class FormItemModel<T extends DefaultStructure = DefaultStructure> extend
158
168
  <FieldModelRenderer model={modelForRender} name={fieldPath} />
159
169
  </FormItem>
160
170
  );
171
+
172
+ if (isHiddenReservedValuePreview) {
173
+ return (
174
+ <Tooltip title={this.context.t('The field is hidden and only visible when the UI Editor is active')}>
175
+ <div style={{ opacity: 0.3 }}>{content}</div>
176
+ </Tooltip>
177
+ );
178
+ }
179
+
180
+ return content;
161
181
  }
162
182
  }
163
183
 
@@ -0,0 +1,14 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ export function isArrayLikeField(field: any) {
11
+ return (
12
+ ['multipleSelect', 'checkboxGroup'].includes(field?.interface) || ['array', 'json', 'jsonb'].includes(field?.type)
13
+ );
14
+ }
@@ -563,6 +563,9 @@ export class TableBlockModel extends CollectionBlockModel<TableBlockModelStructu
563
563
  columns={this.columns.value}
564
564
  pagination={this.pagination()}
565
565
  highlightedRowKey={highlightedRowKey}
566
+ selectedRowKeysFromResource={this.resource
567
+ .getSelectedRows()
568
+ .map((row) => getRowKey(row, this.collection.filterTargetKey))}
566
569
  defaultExpandAllRows={this.props.defaultExpandAllRows}
567
570
  expandedRowKeys={this.props.expandedRowKeys}
568
571
  heightMode={heightMode}
@@ -581,6 +584,7 @@ const TableBlockContent = (props: {
581
584
  columns: any;
582
585
  pagination: any;
583
586
  highlightedRowKey: string;
587
+ selectedRowKeysFromResource: string[];
584
588
  defaultExpandAllRows?: boolean;
585
589
  expandedRowKeys?: any[];
586
590
  heightMode?: string;
@@ -594,6 +598,7 @@ const TableBlockContent = (props: {
594
598
  columns,
595
599
  pagination,
596
600
  highlightedRowKey,
601
+ selectedRowKeysFromResource,
597
602
  defaultExpandAllRows,
598
603
  expandedRowKeys,
599
604
  heightMode,
@@ -620,6 +625,7 @@ const TableBlockContent = (props: {
620
625
  columns={columns}
621
626
  pagination={pagination}
622
627
  highlightedRowKey={highlightedRowKey}
628
+ selectedRowKeysFromResource={selectedRowKeysFromResource}
623
629
  defaultExpandAllRows={defaultExpandAllRows}
624
630
  expandedRowKeys={expandedRowKeys}
625
631
  tableScroll={tableScroll}
@@ -829,6 +835,7 @@ const HighPerformanceTable = React.memo(
829
835
  columns: any;
830
836
  pagination: any;
831
837
  highlightedRowKey: string;
838
+ selectedRowKeysFromResource: string[];
832
839
  defaultExpandAllRows?: boolean;
833
840
  expandedRowKeys?: any[];
834
841
  tableScroll;
@@ -841,6 +848,7 @@ const HighPerformanceTable = React.memo(
841
848
  columns,
842
849
  pagination: _pagination,
843
850
  highlightedRowKey,
851
+ selectedRowKeysFromResource,
844
852
  defaultExpandAllRows,
845
853
  expandedRowKeys,
846
854
  tableScroll,
@@ -848,9 +856,17 @@ const HighPerformanceTable = React.memo(
848
856
  const dataSourceRef = useRef(dataSource);
849
857
  dataSourceRef.current = dataSource;
850
858
 
851
- const [selectedRowKeys, setSelectedRowKeys] = React.useState<string[]>(() =>
852
- model.resource.getSelectedRows().map((row) => getRowKey(row, model.collection.filterTargetKey)),
853
- );
859
+ const [selectedRowKeys, setSelectedRowKeys] = React.useState<string[]>(selectedRowKeysFromResource || []);
860
+
861
+ useEffect(() => {
862
+ const nextSelectedRowKeys = selectedRowKeysFromResource || [];
863
+ setSelectedRowKeys((prev) => {
864
+ if (_.isEqual(prev, nextSelectedRowKeys)) {
865
+ return prev;
866
+ }
867
+ return nextSelectedRowKeys;
868
+ });
869
+ }, [selectedRowKeysFromResource]);
854
870
 
855
871
  const getRowKeyFunc = useCallback(
856
872
  (record) => {
@@ -8,25 +8,40 @@
8
8
  */
9
9
 
10
10
  import { FormItem } from '@nocobase/flow-engine';
11
- import { Divider } from 'antd';
11
+ import { Divider, theme } from 'antd';
12
12
  import React from 'react';
13
13
  import { CommonItemModel } from '../base/CommonItemModel';
14
14
  import { NBColorPicker } from './ColorFieldModel';
15
15
 
16
+ const resolveThemeColor = (value: string | undefined, fallback: string) => {
17
+ return value ? value : fallback;
18
+ };
19
+
20
+ const DividerItem = (props: any) => {
21
+ const { token } = theme.useToken();
22
+ const { color, borderColor, label, orientation, dashed } = props;
23
+
24
+ return (
25
+ <Divider
26
+ type="horizontal"
27
+ style={{
28
+ color: resolveThemeColor(color, token.colorText),
29
+ borderColor: resolveThemeColor(borderColor, token.colorSplit),
30
+ }}
31
+ orientationMargin="0"
32
+ orientation={orientation}
33
+ dashed={dashed}
34
+ >
35
+ {label}
36
+ </Divider>
37
+ );
38
+ };
39
+
16
40
  export class DividerItemModel extends CommonItemModel {
17
41
  render() {
18
- const { color, borderColor, label, orientation, dashed } = this.props;
19
42
  return (
20
43
  <FormItem shouldUpdate showLabel={false}>
21
- <Divider
22
- type="horizontal"
23
- style={{ color, borderColor }}
24
- orientationMargin="0"
25
- orientation={orientation}
26
- dashed={dashed}
27
- >
28
- {label}
29
- </Divider>
44
+ <DividerItem {...this.props} />
30
45
  </FormItem>
31
46
  );
32
47
  }
@@ -38,12 +53,12 @@ DividerItemModel.registerFlow({
38
53
  steps: {
39
54
  title: {
40
55
  title: '{{t("Edit divider")}}',
41
- defaultParams: {
56
+ defaultParams: (ctx) => ({
42
57
  label: '{{t("Text")}}',
43
58
  orientation: 'left',
44
- color: 'rgba(0, 0, 0, 0.88)',
45
- borderColor: 'rgba(5, 5, 5, 0.06)',
46
- },
59
+ color: ctx.themeToken?.colorText,
60
+ borderColor: ctx.themeToken?.colorSplit,
61
+ }),
47
62
  uiSchema(ctx) {
48
63
  return {
49
64
  label: {
@@ -7,15 +7,37 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { DataSourceManager } from '@nocobase/flow-engine';
10
+ import { getCollectionFieldInterface } from '@nocobase/flow-engine';
11
+ import type { CollectionFieldInterfaceDataSourceManager } from '@nocobase/flow-engine';
11
12
 
12
13
  export interface CollectionFieldOptions {
13
14
  interface?: string;
14
15
  [key: string]: any;
15
16
  }
16
17
 
18
+ type FieldInterfaceOptions = { titleUsable?: boolean } | null | undefined;
19
+
17
20
  export const DEFAULT_DATA_SOURCE_KEY = 'main';
18
21
 
19
- export const isTitleField = (dm: DataSourceManager, field: CollectionFieldOptions) => {
20
- return dm.collectionFieldInterfaceManager?.getFieldInterface(field.interface)?.titleUsable;
22
+ export const getFlowFieldInterfaceOptions = (
23
+ interfaceName: string | undefined,
24
+ ...dataSourceManagers: Array<CollectionFieldInterfaceDataSourceManager | null | undefined>
25
+ ) => getCollectionFieldInterface(interfaceName, ...dataSourceManagers);
26
+
27
+ export const hasFlowFieldInterfaceLookup = (
28
+ ...dataSourceManagers: Array<CollectionFieldInterfaceDataSourceManager | null | undefined>
29
+ ) =>
30
+ dataSourceManagers.some(
31
+ (dataSourceManager) => typeof dataSourceManager?.collectionFieldInterfaceManager?.getFieldInterface === 'function',
32
+ );
33
+
34
+ export const isTitleFieldInterface = (fieldInterfaceOptions: FieldInterfaceOptions) => {
35
+ return fieldInterfaceOptions?.titleUsable;
36
+ };
37
+
38
+ export const isTitleField = (
39
+ dataSourceManager: CollectionFieldInterfaceDataSourceManager | null | undefined,
40
+ field: CollectionFieldOptions | null | undefined,
41
+ ) => {
42
+ return isTitleFieldInterface(getFlowFieldInterfaceOptions(field?.interface, dataSourceManager));
21
43
  };
@@ -11,7 +11,13 @@ export { Plugin } from '../Plugin';
11
11
  export { useApp } from '../hooks/useApp';
12
12
  export { usePlugin } from '../hooks/usePlugin';
13
13
  export { ColorPicker } from './ColorPicker';
14
- export { DEFAULT_DATA_SOURCE_KEY, isTitleField } from './data';
14
+ export {
15
+ DEFAULT_DATA_SOURCE_KEY,
16
+ getFlowFieldInterfaceOptions,
17
+ hasFlowFieldInterfaceLookup,
18
+ isTitleField,
19
+ isTitleFieldInterface,
20
+ } from './data';
15
21
  export { FieldValidation } from './FieldValidation';
16
22
  export { HighPerformanceSpin } from './HighPerformanceSpin';
17
23
  export { Icon, hasIcon, icons, registerIcon, registerIcons } from './Icon';
package/src/index.ts CHANGED
@@ -32,5 +32,5 @@ export * from './collection-field-interface/CollectionFieldInterface';
32
32
  export * from './collection-field-interface/CollectionFieldInterfaceManager';
33
33
  export * from './collection-manager/interfaces';
34
34
  export * from './flow';
35
- export { DEFAULT_DATA_SOURCE_KEY, isTitleField } from './flow-compat';
35
+ export { DEFAULT_DATA_SOURCE_KEY, isTitleField, isTitleFieldInterface } from './flow-compat';
36
36
  export { default as AntdAppProvider } from './theme/AntdAppProvider';