@nocobase/plugin-ui-templates 2.0.0-alpha.57

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 (106) hide show
  1. package/LICENSE.txt +172 -0
  2. package/build.config.ts +12 -0
  3. package/client.js +1 -0
  4. package/dist/client/collections/flowModelTemplates.d.ts +67 -0
  5. package/dist/client/components/FlowModelTemplatesPage.d.ts +12 -0
  6. package/dist/client/components/TemplateSelectOption.d.ts +20 -0
  7. package/dist/client/constants.d.ts +9 -0
  8. package/dist/client/hooks/useFlowModelTemplateActions.d.ts +24 -0
  9. package/dist/client/index.d.ts +13 -0
  10. package/dist/client/index.js +10 -0
  11. package/dist/client/locale.d.ts +18 -0
  12. package/dist/client/menuExtensions.d.ts +9 -0
  13. package/dist/client/models/ReferenceBlockModel.d.ts +47 -0
  14. package/dist/client/models/ReferenceFormGridModel.d.ts +38 -0
  15. package/dist/client/models/SubModelTemplateImporterModel.d.ts +55 -0
  16. package/dist/client/models/referenceShared.d.ts +23 -0
  17. package/dist/client/openViewActionExtensions.d.ts +10 -0
  18. package/dist/client/schemas/flowModelTemplates.d.ts +11 -0
  19. package/dist/client/subModelMenuExtensions.d.ts +10 -0
  20. package/dist/client/utils/infiniteSelect.d.ts +28 -0
  21. package/dist/client/utils/refHost.d.ts +20 -0
  22. package/dist/client/utils/templateCompatibility.d.ts +91 -0
  23. package/dist/client.d.ts +9 -0
  24. package/dist/client.js +42 -0
  25. package/dist/externalVersion.js +24 -0
  26. package/dist/index.d.ts +10 -0
  27. package/dist/index.js +48 -0
  28. package/dist/locale/de-DE.json +14 -0
  29. package/dist/locale/en-US.json +72 -0
  30. package/dist/locale/es-ES.json +14 -0
  31. package/dist/locale/fr-FR.json +14 -0
  32. package/dist/locale/hu-HU.json +14 -0
  33. package/dist/locale/id-ID.json +14 -0
  34. package/dist/locale/it-IT.json +14 -0
  35. package/dist/locale/ja-JP.json +14 -0
  36. package/dist/locale/ko-KR.json +14 -0
  37. package/dist/locale/nl-NL.json +14 -0
  38. package/dist/locale/pt-BR.json +14 -0
  39. package/dist/locale/ru-RU.json +14 -0
  40. package/dist/locale/tr-TR.json +14 -0
  41. package/dist/locale/uk-UA.json +14 -0
  42. package/dist/locale/vi-VN.json +14 -0
  43. package/dist/locale/zh-CN.json +71 -0
  44. package/dist/locale/zh-TW.json +14 -0
  45. package/dist/server/collections/flowModelTemplateUsages.d.ts +11 -0
  46. package/dist/server/collections/flowModelTemplateUsages.js +71 -0
  47. package/dist/server/collections/flowModelTemplates.d.ts +11 -0
  48. package/dist/server/collections/flowModelTemplates.js +96 -0
  49. package/dist/server/index.d.ts +9 -0
  50. package/dist/server/index.js +42 -0
  51. package/dist/server/plugin.d.ts +17 -0
  52. package/dist/server/plugin.js +242 -0
  53. package/dist/server/resources/flowModelTemplateUsages.d.ts +19 -0
  54. package/dist/server/resources/flowModelTemplateUsages.js +91 -0
  55. package/dist/server/resources/flowModelTemplates.d.ts +20 -0
  56. package/dist/server/resources/flowModelTemplates.js +267 -0
  57. package/package.json +37 -0
  58. package/server.js +1 -0
  59. package/src/client/__tests__/openViewActionExtensions.test.ts +1208 -0
  60. package/src/client/collections/flowModelTemplates.ts +131 -0
  61. package/src/client/components/FlowModelTemplatesPage.tsx +78 -0
  62. package/src/client/components/TemplateSelectOption.tsx +106 -0
  63. package/src/client/constants.ts +10 -0
  64. package/src/client/hooks/useFlowModelTemplateActions.tsx +137 -0
  65. package/src/client/index.ts +54 -0
  66. package/src/client/locale.ts +40 -0
  67. package/src/client/menuExtensions.tsx +1033 -0
  68. package/src/client/models/ReferenceBlockModel.tsx +793 -0
  69. package/src/client/models/ReferenceFormGridModel.tsx +302 -0
  70. package/src/client/models/SubModelTemplateImporterModel.tsx +634 -0
  71. package/src/client/models/__tests__/ReferenceBlockModel.test.tsx +482 -0
  72. package/src/client/models/__tests__/ReferenceFormGridModel.test.tsx +175 -0
  73. package/src/client/models/__tests__/SubModelTemplateImporterModel.test.ts +447 -0
  74. package/src/client/models/referenceShared.tsx +99 -0
  75. package/src/client/openViewActionExtensions.tsx +981 -0
  76. package/src/client/schemas/flowModelTemplates.ts +264 -0
  77. package/src/client/subModelMenuExtensions.ts +103 -0
  78. package/src/client/utils/infiniteSelect.ts +150 -0
  79. package/src/client/utils/refHost.ts +44 -0
  80. package/src/client/utils/templateCompatibility.ts +374 -0
  81. package/src/client.ts +10 -0
  82. package/src/index.ts +11 -0
  83. package/src/locale/de-DE.json +14 -0
  84. package/src/locale/en-US.json +72 -0
  85. package/src/locale/es-ES.json +14 -0
  86. package/src/locale/fr-FR.json +14 -0
  87. package/src/locale/hu-HU.json +14 -0
  88. package/src/locale/id-ID.json +14 -0
  89. package/src/locale/it-IT.json +14 -0
  90. package/src/locale/ja-JP.json +14 -0
  91. package/src/locale/ko-KR.json +14 -0
  92. package/src/locale/nl-NL.json +14 -0
  93. package/src/locale/pt-BR.json +14 -0
  94. package/src/locale/ru-RU.json +14 -0
  95. package/src/locale/tr-TR.json +14 -0
  96. package/src/locale/uk-UA.json +14 -0
  97. package/src/locale/vi-VN.json +14 -0
  98. package/src/locale/zh-CN.json +71 -0
  99. package/src/locale/zh-TW.json +14 -0
  100. package/src/server/__tests__/template-usage.test.ts +351 -0
  101. package/src/server/collections/flowModelTemplateUsages.ts +51 -0
  102. package/src/server/collections/flowModelTemplates.ts +76 -0
  103. package/src/server/index.ts +10 -0
  104. package/src/server/plugin.ts +236 -0
  105. package/src/server/resources/flowModelTemplateUsages.ts +61 -0
  106. package/src/server/resources/flowModelTemplates.ts +251 -0
@@ -0,0 +1,264 @@
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
+ import { ISchema } from '@nocobase/client';
11
+ import { uid } from '@nocobase/utils/client';
12
+ import { flowModelTemplatesCollection } from '../collections/flowModelTemplates';
13
+ import { tStr } from '../locale';
14
+
15
+ export const flowModelTemplateEditActionSchema: ISchema = {
16
+ type: 'void',
17
+ title: tStr('Edit'),
18
+ 'x-component': 'Action.Link',
19
+ 'x-component-props': {
20
+ openMode: 'drawer',
21
+ },
22
+ properties: {
23
+ drawer: {
24
+ type: 'void',
25
+ title: tStr('Edit template'),
26
+ 'x-component': 'Action.Drawer',
27
+ 'x-decorator': 'FormV2',
28
+ 'x-use-decorator-props': 'useFlowModelTemplateEditFormProps',
29
+ properties: {
30
+ name: {
31
+ type: 'string',
32
+ title: tStr('Template name'),
33
+ 'x-decorator': 'FormItem',
34
+ 'x-component': 'Input',
35
+ required: true,
36
+ },
37
+ description: {
38
+ type: 'string',
39
+ title: tStr('Template description'),
40
+ 'x-decorator': 'FormItem',
41
+ 'x-component': 'Input.TextArea',
42
+ 'x-component-props': {
43
+ rows: 4,
44
+ },
45
+ },
46
+ footer: {
47
+ type: 'void',
48
+ 'x-component': 'Action.Drawer.Footer',
49
+ properties: {
50
+ cancel: {
51
+ title: '{{t("Cancel")}}',
52
+ 'x-component': 'Action',
53
+ 'x-component-props': {
54
+ useAction: '{{ cm.useCancelAction }}',
55
+ },
56
+ },
57
+ submit: {
58
+ title: '{{t("Submit")}}',
59
+ 'x-component': 'Action',
60
+ 'x-use-component-props': 'useFlowModelTemplateEditActionProps',
61
+ 'x-component-props': {
62
+ type: 'primary',
63
+ },
64
+ },
65
+ },
66
+ },
67
+ },
68
+ },
69
+ },
70
+ };
71
+
72
+ export const createFlowModelTemplatesSchema = (filter?: Record<string, any>): ISchema => ({
73
+ type: 'void',
74
+ name: uid(),
75
+ 'x-component': 'CardItem',
76
+ 'x-decorator': 'TableBlockProvider',
77
+ 'x-decorator-props': {
78
+ collection: flowModelTemplatesCollection.name,
79
+ action: 'list',
80
+ params: {
81
+ sort: '-createdAt',
82
+ pageSize: 50,
83
+ ...(filter ? { filter } : {}),
84
+ },
85
+ showIndex: true,
86
+ rowKey: flowModelTemplatesCollection.filterTargetKey,
87
+ },
88
+ properties: {
89
+ actions: {
90
+ type: 'void',
91
+ 'x-component': 'ActionBar',
92
+ 'x-component-props': {
93
+ style: {
94
+ marginBottom: 16,
95
+ },
96
+ },
97
+ properties: {
98
+ filter: {
99
+ type: 'void',
100
+ title: '{{ t("Filter") }}',
101
+ default: {
102
+ $and: [{ name: { $includes: '' } }],
103
+ },
104
+ 'x-action': 'filter',
105
+ 'x-component': 'Filter.Action',
106
+ 'x-use-component-props': 'useFilterActionProps',
107
+ 'x-component-props': {
108
+ icon: 'FilterOutlined',
109
+ },
110
+ 'x-align': 'left',
111
+ },
112
+ search: {
113
+ type: 'void',
114
+ 'x-component': 'Input.Search',
115
+ 'x-component-props': {
116
+ allowClear: true,
117
+ },
118
+ 'x-use-component-props': 'useFlowModelTemplateSearchProps',
119
+ 'x-align': 'left',
120
+ },
121
+ refresh: {
122
+ type: 'void',
123
+ title: '{{ t("Refresh") }}',
124
+ 'x-component': 'Action',
125
+ 'x-use-component-props': 'useRefreshActionProps',
126
+ 'x-component-props': {
127
+ icon: 'ReloadOutlined',
128
+ },
129
+ 'x-align': 'right',
130
+ },
131
+ },
132
+ },
133
+ table: {
134
+ type: 'array',
135
+ 'x-component': 'TableV2',
136
+ 'x-use-component-props': 'useTableBlockProps',
137
+ 'x-component-props': {
138
+ rowKey: flowModelTemplatesCollection.filterTargetKey,
139
+ pagination: {
140
+ pageSize: 50,
141
+ },
142
+ },
143
+ properties: {
144
+ name: {
145
+ type: 'void',
146
+ title: tStr('Template name'),
147
+ 'x-component': 'TableV2.Column',
148
+ 'x-component-props': {
149
+ width: 200,
150
+ },
151
+ properties: {
152
+ name: {
153
+ type: 'string',
154
+ 'x-component': 'CollectionField',
155
+ 'x-pattern': 'readPretty',
156
+ 'x-component-props': {
157
+ ellipsis: true,
158
+ },
159
+ },
160
+ },
161
+ },
162
+ description: {
163
+ type: 'void',
164
+ title: tStr('Template description'),
165
+ 'x-component': 'TableV2.Column',
166
+ 'x-component-props': {
167
+ width: 240,
168
+ },
169
+ properties: {
170
+ description: {
171
+ type: 'string',
172
+ 'x-component': 'CollectionField',
173
+ 'x-pattern': 'readPretty',
174
+ 'x-component-props': {
175
+ ellipsis: true,
176
+ },
177
+ },
178
+ },
179
+ },
180
+ dataSourceKey: {
181
+ type: 'void',
182
+ title: 'Data source',
183
+ 'x-component': 'TableV2.Column',
184
+ 'x-component-props': {
185
+ width: 140,
186
+ },
187
+ properties: {
188
+ dataSourceKey: {
189
+ type: 'string',
190
+ 'x-component': 'CollectionField',
191
+ 'x-pattern': 'readPretty',
192
+ 'x-component-props': {
193
+ ellipsis: true,
194
+ },
195
+ },
196
+ },
197
+ },
198
+ collectionName: {
199
+ type: 'void',
200
+ title: 'Collection',
201
+ 'x-component': 'TableV2.Column',
202
+ 'x-component-props': {
203
+ width: 160,
204
+ },
205
+ properties: {
206
+ collectionName: {
207
+ type: 'string',
208
+ 'x-component': 'CollectionField',
209
+ 'x-pattern': 'readPretty',
210
+ 'x-component-props': {
211
+ ellipsis: true,
212
+ },
213
+ },
214
+ },
215
+ },
216
+ usageCount: {
217
+ type: 'void',
218
+ title: tStr('Usage count'),
219
+ 'x-component': 'TableV2.Column',
220
+ 'x-component-props': {
221
+ width: 120,
222
+ },
223
+ properties: {
224
+ usageCount: {
225
+ type: 'number',
226
+ 'x-component': 'CollectionField',
227
+ 'x-pattern': 'readPretty',
228
+ },
229
+ },
230
+ },
231
+ actions: {
232
+ type: 'void',
233
+ title: tStr('Actions'),
234
+ 'x-component': 'TableV2.Column',
235
+ 'x-component-props': {
236
+ width: 160,
237
+ align: 'left',
238
+ },
239
+ properties: {
240
+ actions: {
241
+ type: 'void',
242
+ 'x-component': 'Space',
243
+ 'x-component-props': {
244
+ split: '|',
245
+ },
246
+ properties: {
247
+ edit: flowModelTemplateEditActionSchema,
248
+ delete: {
249
+ type: 'void',
250
+ title: tStr('Delete'),
251
+ 'x-component': 'Action.Link',
252
+ 'x-use-component-props': 'useFlowModelTemplateDeleteActionProps',
253
+ 'x-component-props': {
254
+ danger: true,
255
+ },
256
+ },
257
+ },
258
+ },
259
+ },
260
+ },
261
+ },
262
+ },
263
+ },
264
+ });
@@ -0,0 +1,103 @@
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
+ import type { FlowEngine, FlowModel, FlowModelContext } from '@nocobase/flow-engine';
11
+ import { NAMESPACE } from './locale';
12
+ import { findRefHostInfoFromAncestors } from './utils/refHost';
13
+
14
+ function findFormModel(model: FlowModel): FlowModel | undefined {
15
+ let cur: FlowModel | undefined = model;
16
+ let depth = 0;
17
+ while (cur && depth < 6) {
18
+ const use = cur?.use;
19
+ if (use === 'CreateFormModel' || use === 'EditFormModel') return cur;
20
+ cur = cur?.parent as FlowModel | undefined;
21
+ depth++;
22
+ }
23
+ return undefined;
24
+ }
25
+
26
+ function isFormInsideReferenceBlock(formModel: FlowModel | undefined): boolean {
27
+ if (!formModel) return false;
28
+ // 检查表单的 parent 是否是 ReferenceBlockModel
29
+ const parent = formModel?.parent as FlowModel | undefined;
30
+ return parent?.use === 'ReferenceBlockModel';
31
+ }
32
+
33
+ export function registerSubModelMenuExtensions(engine: FlowEngine) {
34
+ type ModelClass = typeof FlowModel & {
35
+ __subModelTemplateMenuPatched?: boolean;
36
+ defineChildren?: (ctx: FlowModelContext) => any;
37
+ };
38
+ const FormCustomItemModel = engine.getModelClass?.('FormCustomItemModel') as ModelClass | undefined;
39
+ if (!FormCustomItemModel || FormCustomItemModel.__subModelTemplateMenuPatched) return;
40
+ FormCustomItemModel.__subModelTemplateMenuPatched = true;
41
+
42
+ const originalDefineChildren = FormCustomItemModel.defineChildren;
43
+
44
+ FormCustomItemModel.defineChildren = async function patchedDefineChildren(ctx: FlowModelContext) {
45
+ const raw = originalDefineChildren ? await originalDefineChildren.call(this, ctx) : [];
46
+ const children = Array.isArray(raw) ? raw : [];
47
+
48
+ const label = ctx.t?.('Field template', { ns: [NAMESPACE, 'client'], nsMode: 'fallback' }) || 'Field template';
49
+ const formModel = findFormModel(ctx.model);
50
+ const parentUse = formModel?.use || ctx.model.parent?.use;
51
+ const expectedRootUse =
52
+ parentUse === 'CreateFormModel' || parentUse === 'EditFormModel'
53
+ ? ['CreateFormModel', 'EditFormModel']
54
+ : parentUse;
55
+ const resourceInit = formModel?.getStepParams?.('resourceSettings', 'init') || {};
56
+ const expectedDataSourceKey =
57
+ typeof resourceInit?.dataSourceKey === 'string' ? resourceInit.dataSourceKey : undefined;
58
+ const expectedCollectionName =
59
+ typeof resourceInit?.collectionName === 'string' ? resourceInit.collectionName : undefined;
60
+
61
+ const fromTemplateItem = {
62
+ key: '__fromTemplate__',
63
+ label,
64
+ sort: -999,
65
+ hide: async (innerCtx: FlowModelContext) => {
66
+ // 1) 若处于"字段模板引用"内部(当前正在渲染模板 grid),直接隐藏,避免误清空模板侧字段
67
+ const hostInfo = findRefHostInfoFromAncestors(innerCtx.model);
68
+ const hostRef = hostInfo?.ref;
69
+ if (hostRef && hostRef.mountSubKey === 'grid' && hostRef.mode !== 'copy') {
70
+ return true;
71
+ }
72
+
73
+ // 2) 若当前表单是 ReferenceBlockModel 渲染的 target,隐藏 "From template"
74
+ // 因为在 ReferenceBlockModel 内部编辑字段会直接影响被引用的模板
75
+ const fm = findFormModel(innerCtx.model);
76
+ if (isFormInsideReferenceBlock(fm)) {
77
+ return true;
78
+ }
79
+
80
+ // 3) 常规:当前表单区块已经使用引用 grid(字段模板),隐藏 "From template"
81
+ const mountTarget = fm || (innerCtx.model.parent as FlowModel | undefined);
82
+ if (!mountTarget) return false;
83
+ const grid = (mountTarget?.subModels as any)?.grid;
84
+ const isReferenceGrid = !!grid && String(grid?.use || '') === 'ReferenceFormGridModel';
85
+ return isReferenceGrid;
86
+ },
87
+ createModelOptions: () => ({
88
+ use: 'SubModelTemplateImporterModel',
89
+ props: {
90
+ // 表单字段的复用以 grid 为单位引入,保留模板里的布局
91
+ defaultSourcePath: 'subModels.grid',
92
+ defaultMountSubKey: 'grid',
93
+ mountToParentLevel: 1,
94
+ expectedRootUse,
95
+ expectedDataSourceKey,
96
+ expectedCollectionName,
97
+ },
98
+ }),
99
+ };
100
+
101
+ return [fromTemplateItem, ...children].filter(Boolean);
102
+ };
103
+ }
@@ -0,0 +1,150 @@
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
+ import debounce from 'lodash/debounce';
11
+ import { TEMPLATE_LIST_PAGE_SIZE } from './templateCompatibility';
12
+
13
+ export type PaginatedOptionsResult<T = any> = { options: T[]; hasMore: boolean };
14
+
15
+ export const defaultSelectOptionComparator = (a: any, b: any) => {
16
+ const da = a?.disabled ? 1 : 0;
17
+ const db = b?.disabled ? 1 : 0;
18
+ if (da !== db) return da - db;
19
+ return (a?.__idx ?? 0) - (b?.__idx ?? 0);
20
+ };
21
+
22
+ export function mergeSelectOptions(
23
+ prev: any[],
24
+ next: any[],
25
+ options?: { getKey?: (item: any) => string; comparator?: (a: any, b: any) => number },
26
+ ) {
27
+ const getKey = options?.getKey || ((item: any) => String(item?.value || ''));
28
+ const comparator = options?.comparator || defaultSelectOptionComparator;
29
+
30
+ const map = new Map<string, any>();
31
+ for (const item of Array.isArray(prev) ? prev : []) {
32
+ const key = getKey(item);
33
+ if (!key) continue;
34
+ map.set(key, item);
35
+ }
36
+ for (const item of Array.isArray(next) ? next : []) {
37
+ const key = getKey(item);
38
+ if (!key) continue;
39
+ const existing = map.get(key);
40
+ map.set(key, existing ? { ...existing, ...item } : item);
41
+ }
42
+
43
+ const merged = Array.from(map.values());
44
+ merged.sort(comparator);
45
+ return merged;
46
+ }
47
+
48
+ export function bindInfiniteScrollToFormilySelect(
49
+ field: any,
50
+ fetchPage: (keyword: string, page: number, pageSize: number) => Promise<PaginatedOptionsResult<any>>,
51
+ options?: {
52
+ pageSize?: number;
53
+ debounceMs?: number;
54
+ scrollThreshold?: number;
55
+ composingKey?: string;
56
+ comparator?: (a: any, b: any) => number;
57
+ },
58
+ ) {
59
+ const pageSize = Math.max(1, Number(options?.pageSize || TEMPLATE_LIST_PAGE_SIZE));
60
+ const debounceMs = Math.max(0, Number(options?.debounceMs ?? 300));
61
+ const scrollThreshold = Math.max(0, Number(options?.scrollThreshold ?? 24));
62
+ const composingKey = String(options?.composingKey || '__templateComposing');
63
+ const comparator = options?.comparator || defaultSelectOptionComparator;
64
+
65
+ let requestVersion = 0;
66
+ let currentKeyword = '';
67
+ let currentPage = 0;
68
+ let currentHasMore = true;
69
+ let loadingMore = false;
70
+
71
+ const setComposing = (v: boolean) => {
72
+ try {
73
+ field.data = { ...(field.data || {}), [composingKey]: v };
74
+ } catch {
75
+ // ignore
76
+ }
77
+ };
78
+
79
+ const isComposing = () => !!field.data?.[composingKey];
80
+
81
+ const resetAndLoad = async (keyword: string) => {
82
+ const nextVersion = (requestVersion || 0) + 1;
83
+ requestVersion = nextVersion;
84
+ currentKeyword = keyword;
85
+ currentPage = 0;
86
+ currentHasMore = true;
87
+ loadingMore = false;
88
+ field.loading = true;
89
+ try {
90
+ const { options: optionList, hasMore } = await fetchPage(keyword, 1, pageSize);
91
+ if (requestVersion !== nextVersion) return;
92
+ field.dataSource = mergeSelectOptions([], optionList, { comparator });
93
+ currentPage = 1;
94
+ currentHasMore = !!hasMore;
95
+ } finally {
96
+ if (requestVersion === nextVersion) {
97
+ field.loading = false;
98
+ }
99
+ }
100
+ };
101
+
102
+ const loadMore = async () => {
103
+ if (!currentHasMore || loadingMore) return;
104
+ const version = requestVersion;
105
+ const nextPage = Math.max(1, currentPage || 1) + 1;
106
+ loadingMore = true;
107
+ field.loading = true;
108
+ try {
109
+ const { options: optionList, hasMore } = await fetchPage(currentKeyword, nextPage, pageSize);
110
+ if (requestVersion !== version) return;
111
+ field.dataSource = mergeSelectOptions(field.dataSource || [], optionList, { comparator });
112
+ currentPage = nextPage;
113
+ currentHasMore = !!hasMore;
114
+ } finally {
115
+ loadingMore = false;
116
+ if (requestVersion === version) {
117
+ field.loading = false;
118
+ }
119
+ }
120
+ };
121
+
122
+ field.componentProps.onDropdownVisibleChange = async (open: boolean) => {
123
+ if (!open) return;
124
+ await resetAndLoad('');
125
+ };
126
+
127
+ const debouncedSearch = debounce(async (kw: string) => resetAndLoad(kw), debounceMs);
128
+
129
+ field.componentProps.onSearch = (value: string) => {
130
+ if (isComposing()) return;
131
+ const keyword = typeof value === 'string' ? value.trim() : '';
132
+ debouncedSearch(keyword);
133
+ };
134
+
135
+ field.componentProps.onCompositionStart = () => setComposing(true);
136
+ field.componentProps.onCompositionEnd = (e: any) => {
137
+ setComposing(false);
138
+ const keyword = typeof e?.target?.value === 'string' ? e.target.value.trim() : '';
139
+ debouncedSearch(keyword);
140
+ };
141
+
142
+ field.componentProps.onPopupScroll = (e: any) => {
143
+ const target = e?.target as HTMLElement | undefined;
144
+ if (!target) return;
145
+ if (target.scrollTop + target.clientHeight < target.scrollHeight - scrollThreshold) return;
146
+ loadMore();
147
+ };
148
+
149
+ return { resetAndLoad, loadMore, cancelSearch: () => debouncedSearch.cancel() };
150
+ }
@@ -0,0 +1,44 @@
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
+ import type { FlowModel } from '@nocobase/flow-engine';
11
+ import { REF_HOST_CTX_KEY } from '../constants';
12
+
13
+ export type RefHostInfo = {
14
+ ref?: {
15
+ mountSubKey?: unknown;
16
+ mode?: unknown;
17
+ [key: string]: unknown;
18
+ };
19
+ [key: string]: unknown;
20
+ };
21
+
22
+ export function findRefHostInfoFromAncestors(
23
+ model: FlowModel,
24
+ options?: { maxDepth?: number },
25
+ ): RefHostInfo | undefined {
26
+ const maxDepth = options?.maxDepth ?? 8;
27
+ let cur: FlowModel | undefined = model;
28
+ let depth = 0;
29
+ while (cur && depth < maxDepth) {
30
+ try {
31
+ const ctx = cur.context as unknown as Record<string, unknown>;
32
+ // 只读取自身 context 上定义的 host 信息,避免从 delegate 链“误捡到”其他 view/弹窗的标记
33
+ if (ctx && Object.prototype.hasOwnProperty.call(ctx, REF_HOST_CTX_KEY)) {
34
+ const v = ctx[REF_HOST_CTX_KEY] as RefHostInfo | undefined;
35
+ if (v) return v;
36
+ }
37
+ } catch {
38
+ // ignore
39
+ }
40
+ cur = cur.parent as FlowModel | undefined;
41
+ depth++;
42
+ }
43
+ return undefined;
44
+ }