@nocobase/flow-engine 2.1.0-beta.26 → 2.1.0-beta.29

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.
@@ -8,6 +8,8 @@
8
8
  */
9
9
 
10
10
  export { VariableInput } from './VariableInput';
11
+ export { VariableHybridInput } from './VariableHybridInput';
12
+ export type { VariableHybridInputProps, VariableHybridInputConverters } from './VariableHybridInput';
11
13
  export { SlateVariableEditor } from './SlateVariableEditor';
12
14
  export { VariableTag } from './VariableTag';
13
15
  export { InlineVariableTag } from './InlineVariableTag';
@@ -7,8 +7,8 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { describe, expect, it } from 'vitest';
11
- import { DataSource, DataSourceManager, isFieldInterfaceMatch } from '../index';
10
+ import { describe, expect, it, vi } from 'vitest';
11
+ import { DataSource, DataSourceManager, getCollectionFieldInterface, isFieldInterfaceMatch } from '../index';
12
12
  import { FlowEngine } from '../../flowEngine';
13
13
 
14
14
  describe('Collection/Field helpers', () => {
@@ -55,4 +55,43 @@ describe('Collection/Field helpers', () => {
55
55
  const field = posts.getFieldByPath('category.name');
56
56
  expect(field?.name).toBe('name');
57
57
  });
58
+
59
+ it('resolves collection field interfaces from the first available manager', () => {
60
+ const first = { collectionFieldInterfaceManager: { getFieldInterface: vi.fn((name) => ({ name })) } };
61
+ const second = { collectionFieldInterfaceManager: { getFieldInterface: vi.fn((name) => ({ name })) } };
62
+
63
+ expect(getCollectionFieldInterface('input', {}, first, second)).toEqual({ name: 'input' });
64
+ expect(first.collectionFieldInterfaceManager.getFieldInterface).toHaveBeenCalledWith('input');
65
+ expect(second.collectionFieldInterfaceManager.getFieldInterface).not.toHaveBeenCalled();
66
+ expect(getCollectionFieldInterface(undefined, first)).toBeUndefined();
67
+ expect(getCollectionFieldInterface('input', {})).toBeUndefined();
68
+ });
69
+
70
+ it('uses collection field interface resolver from getInterfaceOptions', () => {
71
+ const { ds, m } = setup();
72
+ const ctx = m.flowEngine.context;
73
+ const getOwnerFieldInterface = vi.fn((name: string) => ({ name, source: 'owner' }));
74
+ const getLegacyFieldInterface = vi.fn((name: string) => ({ name, source: 'legacy' }));
75
+
76
+ ctx.defineProperty('app', {
77
+ value: {
78
+ dataSourceManager: {
79
+ collectionFieldInterfaceManager: {
80
+ getFieldInterface: getLegacyFieldInterface,
81
+ },
82
+ },
83
+ },
84
+ });
85
+ ds.addCollection({
86
+ name: 'posts',
87
+ fields: [{ name: 'title', type: 'string', interface: 'input' }],
88
+ });
89
+
90
+ const field = ds.getCollection('posts')!.getField('title')!;
91
+ expect(field.getInterfaceOptions()).toEqual({ name: 'input', source: 'legacy' });
92
+
93
+ m.setCollectionFieldInterfaceManager({ getFieldInterface: getOwnerFieldInterface });
94
+ expect(field.getInterfaceOptions()).toEqual({ name: 'input', source: 'owner' });
95
+ expect(getLegacyFieldInterface).toHaveBeenCalledTimes(1);
96
+ });
58
97
  });
@@ -80,6 +80,40 @@ describe('DataSource & Collection APIs', () => {
80
80
  ).toThrow(/circular/);
81
81
  });
82
82
 
83
+ it('translates validation messages from data-source-main in component rules', async () => {
84
+ const { m, engine } = makeManager();
85
+ engine.context.i18n = {
86
+ t: (key: string, options?: Record<string, any>) => {
87
+ if (key === 'string.length' && options?.ns === 'data-source-main') {
88
+ return `${options.label} 长度必须为 ${options.limit} 个字符`;
89
+ }
90
+ return key;
91
+ },
92
+ } as any;
93
+
94
+ const ds = new DataSource({ key: 'main' });
95
+ m.addDataSource(ds);
96
+ ds.addCollection({
97
+ name: 'posts',
98
+ fields: [
99
+ {
100
+ name: 'title',
101
+ type: 'string',
102
+ interface: 'text',
103
+ title: '单行文本',
104
+ validation: {
105
+ type: 'string',
106
+ rules: [{ name: 'length', args: { limit: 18 } }],
107
+ },
108
+ },
109
+ ],
110
+ });
111
+
112
+ const rules = ds.getCollection('posts')!.getField('title')!.getComponentProps().rules;
113
+
114
+ await expect(rules[0].validator({}, '123')).rejects.toBe('单行文本 长度必须为 18 个字符');
115
+ });
116
+
83
117
  it('ensureLoaded, reload and data source events work for main loader', async () => {
84
118
  const { m, engine } = makeManager();
85
119
  const loadedListener = vi.fn();
@@ -295,6 +295,29 @@ export class DataSourceManager {
295
295
  }
296
296
  }
297
297
 
298
+ export type CollectionFieldInterfaceDataSourceManager = Pick<DataSourceManager, 'collectionFieldInterfaceManager'>;
299
+
300
+ export function getCollectionFieldInterface(
301
+ interfaceName: string | undefined,
302
+ ...dataSourceManagers: Array<CollectionFieldInterfaceDataSourceManager | null | undefined>
303
+ ) {
304
+ if (!interfaceName) {
305
+ return undefined;
306
+ }
307
+
308
+ // TODO: Once legacy client is removed and all runtimes share the client-v2 flow-engine
309
+ // DataSourceManager, callers should only pass the flow-engine context DataSourceManager.
310
+ for (const dataSourceManager of dataSourceManagers) {
311
+ const collectionFieldInterfaceManager = dataSourceManager?.collectionFieldInterfaceManager;
312
+ const getFieldInterface = collectionFieldInterfaceManager?.getFieldInterface;
313
+ if (typeof getFieldInterface === 'function') {
314
+ return getFieldInterface.call(collectionFieldInterfaceManager, interfaceName);
315
+ }
316
+ }
317
+
318
+ return undefined;
319
+ }
320
+
298
321
  export class DataSource {
299
322
  dataSourceManager: DataSourceManager;
300
323
  collectionManager: CollectionManager;
@@ -1112,7 +1135,21 @@ export class CollectionField {
1112
1135
  });
1113
1136
 
1114
1137
  if (error) {
1115
- const message = error.details.map((d: any) => d.message.replace(/"value"/g, `"${label}"`)).join(', ');
1138
+ const message = error.details
1139
+ .map((d: any) => {
1140
+ const translated = this.flowEngine.translate(d.type, {
1141
+ ...d.context,
1142
+ ns: 'data-source-main',
1143
+ label,
1144
+ });
1145
+
1146
+ if (translated && translated !== d.type) {
1147
+ return translated;
1148
+ }
1149
+
1150
+ return d.message.replace(/"value"/g, `"${label}"`);
1151
+ })
1152
+ .join(', ');
1116
1153
  return Promise.reject(message);
1117
1154
  }
1118
1155
 
@@ -1135,8 +1172,13 @@ export class CollectionField {
1135
1172
  }
1136
1173
 
1137
1174
  getInterfaceOptions() {
1138
- const app = this.flowEngine.context.app;
1139
- return app.dataSourceManager.collectionFieldInterfaceManager.getFieldInterface(this.interface);
1175
+ const ctx = this.flowEngine.context;
1176
+ return getCollectionFieldInterface(
1177
+ this.interface,
1178
+ this.collection?.dataSource?.dataSourceManager,
1179
+ ctx.dataSourceManager,
1180
+ ctx.app?.dataSourceManager,
1181
+ );
1140
1182
  }
1141
1183
 
1142
1184
  getFilterOperators() {
@@ -26,6 +26,10 @@ function encodeFilterByTk(val: SharedViewParam['filterByTk']): string {
26
26
  return encodeURIComponent(String(val));
27
27
  }
28
28
 
29
+ function hasUsableSourceId(sourceId: unknown): sourceId is string | number {
30
+ return sourceId !== undefined && sourceId !== null && String(sourceId) !== '';
31
+ }
32
+
29
33
  /**
30
34
  * 将 ViewParam 数组转换为 pathname
31
35
  *
@@ -65,8 +69,8 @@ export function generatePathnameFromViewParams(viewParams: ViewParams[]): string
65
69
  segments.push('filterbytk', encoded);
66
70
  }
67
71
  }
68
- if (viewParam.sourceId) {
69
- segments.push('sourceid', viewParam.sourceId);
72
+ if (hasUsableSourceId(viewParam.sourceId)) {
73
+ segments.push('sourceid', String(viewParam.sourceId));
70
74
  }
71
75
  });
72
76