@nocobase/client-v2 2.1.0-beta.34 → 2.1.0-beta.36

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 (76) hide show
  1. package/es/BaseApplication.d.ts +7 -1
  2. package/es/PluginManager.d.ts +2 -0
  3. package/es/components/PoweredBy.d.ts +18 -0
  4. package/es/components/SwitchLanguage.d.ts +11 -0
  5. package/es/components/form/DialogFormLayout.d.ts +75 -0
  6. package/es/components/form/DrawerFormLayout.d.ts +11 -11
  7. package/es/components/form/PasswordInput.d.ts +40 -0
  8. package/es/components/form/RemoteSelect.d.ts +79 -0
  9. package/es/components/form/index.d.ts +3 -0
  10. package/es/components/form/table/styles.d.ts +10 -0
  11. package/es/components/index.d.ts +2 -0
  12. package/es/flow/models/base/ActionModelCore.d.ts +6 -0
  13. package/es/flow/models/base/GridModel.d.ts +2 -0
  14. package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +9 -1
  15. package/es/flow/utils/dataScopeFormValueClear.d.ts +14 -0
  16. package/es/flow-compat/passwordUtils.d.ts +1 -1
  17. package/es/hooks/index.d.ts +2 -0
  18. package/es/hooks/useCurrentAppInfo.d.ts +9 -0
  19. package/es/index.mjs +117 -105
  20. package/es/json-logic/globalOperators.d.ts +11 -0
  21. package/es/nocobase-buildin-plugin/index.d.ts +25 -0
  22. package/es/utils/appVersionHTML.d.ts +10 -0
  23. package/es/utils/globalDeps.d.ts +7 -0
  24. package/es/utils/index.d.ts +1 -0
  25. package/es/utils/remotePlugins.d.ts +4 -1
  26. package/lib/index.js +120 -108
  27. package/package.json +7 -6
  28. package/src/BaseApplication.tsx +11 -3
  29. package/src/PluginManager.ts +2 -0
  30. package/src/PluginSettingsManager.ts +2 -1
  31. package/src/__tests__/PluginSettingsManager.test.ts +19 -0
  32. package/src/__tests__/PoweredBy.test.tsx +130 -0
  33. package/src/__tests__/app.test.tsx +39 -0
  34. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +39 -72
  35. package/src/__tests__/remotePlugins.test.ts +203 -0
  36. package/src/__tests__/useCurrentRoles.test.tsx +100 -0
  37. package/src/components/PoweredBy.tsx +71 -0
  38. package/src/components/README.md +314 -0
  39. package/src/components/README.zh-CN.md +312 -0
  40. package/src/components/SwitchLanguage.tsx +48 -0
  41. package/src/components/form/DialogFormLayout.tsx +111 -0
  42. package/src/components/form/DrawerFormLayout.tsx +13 -32
  43. package/src/components/form/PasswordInput.tsx +211 -0
  44. package/src/components/form/RemoteSelect.tsx +137 -0
  45. package/src/components/form/index.tsx +3 -0
  46. package/src/components/form/table/Table.tsx +2 -1
  47. package/src/components/form/table/styles.ts +19 -0
  48. package/src/components/index.ts +2 -0
  49. package/src/css-variable/CSSVariableProvider.tsx +10 -1
  50. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +96 -0
  51. package/src/flow/actions/dataScope.tsx +3 -0
  52. package/src/flow/actions/filterFormDefaultValues.tsx +1 -2
  53. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +1 -4
  54. package/src/flow/admin-shell/admin-layout/HelpLite.tsx +7 -33
  55. package/src/flow/components/BlockItemCard.tsx +2 -2
  56. package/src/flow/models/base/ActionModel.tsx +8 -7
  57. package/src/flow/models/base/ActionModelCore.tsx +15 -7
  58. package/src/flow/models/base/GridModel.tsx +93 -36
  59. package/src/flow/models/base/__tests__/GridModel.visibleLayout.test.ts +83 -11
  60. package/src/flow/models/blocks/details/DetailsItemModel.tsx +2 -0
  61. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +329 -5
  62. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +337 -0
  63. package/src/flow/models/blocks/form/FormItemModel.tsx +5 -3
  64. package/src/flow/models/blocks/form/__tests__/FormItemModel.defineChildren.test.ts +108 -0
  65. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +5 -0
  66. package/src/flow/models/blocks/table/TableColumnModel.tsx +2 -0
  67. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +2 -0
  68. package/src/flow/utils/dataScopeFormValueClear.ts +278 -0
  69. package/src/hooks/index.ts +2 -0
  70. package/src/hooks/useCurrentAppInfo.ts +36 -0
  71. package/src/json-logic/globalOperators.js +731 -0
  72. package/src/nocobase-buildin-plugin/index.tsx +70 -16
  73. package/src/utils/appVersionHTML.ts +28 -0
  74. package/src/utils/globalDeps.ts +47 -31
  75. package/src/utils/index.tsx +2 -0
  76. package/src/utils/remotePlugins.ts +119 -13
@@ -7,10 +7,97 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
+ import React from 'react';
11
+ import { render, waitFor } from '@testing-library/react';
12
+ import { FlowEngine } from '@nocobase/flow-engine';
10
13
  import { describe, expect, it, vi } from 'vitest';
11
14
  import { filterFormDefaultValues } from '../../../../actions/filterFormDefaultValues';
12
15
  import { FilterFormBlockModel } from '../FilterFormBlockModel';
13
16
 
17
+ function resolveTemplateValue(raw: any, values: Record<string, any>): any {
18
+ if (typeof raw === 'string') {
19
+ const matched = raw.match(/^\{\{\s*ctx\.formValues\.([^}]+?)\s*\}\}$/);
20
+ return matched ? values[matched[1]] : raw;
21
+ }
22
+ if (Array.isArray(raw)) {
23
+ return raw.map((item) => resolveTemplateValue(item, values));
24
+ }
25
+ if (raw && typeof raw === 'object') {
26
+ return Object.fromEntries(Object.entries(raw).map(([key, value]) => [key, resolveTemplateValue(value, values)]));
27
+ }
28
+ return raw;
29
+ }
30
+
31
+ function createFilterFormDefaultValuesModel(rules: any[], initialValues: Record<string, any> = {}) {
32
+ const values = { ...initialValues };
33
+ const createItem = (fieldPath: string, uid: string) => ({
34
+ uid,
35
+ fieldPath,
36
+ props: { name: `${fieldPath}_${uid}` },
37
+ getProps() {
38
+ return this.props;
39
+ },
40
+ getStepParams(flowKey: string, stepKey: string) {
41
+ if (flowKey === 'fieldSettings' && stepKey === 'init') {
42
+ return { fieldPath };
43
+ }
44
+ return undefined;
45
+ },
46
+ subModels: {
47
+ field: {},
48
+ },
49
+ });
50
+ const model = {
51
+ defaultValuesRefreshSeq: 0,
52
+ lastDefaultValueByFieldName: new Map<string, any>(),
53
+ form: {
54
+ getFieldsValue: () => ({ ...values }),
55
+ getFieldValue: (name: string) => values[name],
56
+ setFieldValue: (name: string, value: any) => {
57
+ values[name] = value;
58
+ },
59
+ setFieldsValue: (next: Record<string, any>) => {
60
+ Object.assign(values, next);
61
+ },
62
+ },
63
+ context: {
64
+ resolveJsonTemplate: vi.fn((raw) => resolveTemplateValue(raw, values)),
65
+ app: {
66
+ jsonLogic: {
67
+ apply: vi.fn((logic: Record<string, any[]>) => {
68
+ const [[operator, args]] = Object.entries(logic);
69
+ if (operator === '$eq') return args[0] === args[1];
70
+ return true;
71
+ }),
72
+ },
73
+ },
74
+ },
75
+ subModels: {
76
+ grid: {
77
+ subModels: {
78
+ items: [createItem('nickname', 'nick'), createItem('username', 'user')],
79
+ },
80
+ },
81
+ },
82
+ getStepParams: vi.fn((flowKey: string, stepKey: string) => {
83
+ if (flowKey === 'formFilterBlockModelSettings' && stepKey === 'defaultValues') {
84
+ return { value: rules };
85
+ }
86
+ return undefined;
87
+ }),
88
+ canApplyFormDefaultValue: (FilterFormBlockModel.prototype as any).canApplyFormDefaultValue,
89
+ matchDefaultValueCondition: (FilterFormBlockModel.prototype as any).matchDefaultValueCondition,
90
+ applyFormDefaultValues: FilterFormBlockModel.prototype.applyFormDefaultValues,
91
+ handleFilterFormValuesChange: (FilterFormBlockModel.prototype as any).handleFilterFormValuesChange,
92
+ dispatchEvent: vi.fn(),
93
+ emitter: {
94
+ emit: vi.fn(),
95
+ },
96
+ };
97
+
98
+ return { model, values };
99
+ }
100
+
14
101
  describe('filter-form defaultValues wiring', () => {
15
102
  it('loads action and model modules', () => {
16
103
  expect(filterFormDefaultValues).toBeTruthy();
@@ -48,4 +135,254 @@ describe('filter-form defaultValues wiring', () => {
48
135
  expect(model.initialDefaultsPromise).toBeUndefined();
49
136
  expect(model.applyFormDefaultValues).not.toHaveBeenCalled();
50
137
  });
138
+
139
+ it('exposes current form values in the filter form variable meta tree', async () => {
140
+ const engine = new FlowEngine();
141
+
142
+ const dataSource = engine.context.dataSourceManager.getDataSource('main');
143
+ dataSource.addCollection({
144
+ name: 'users',
145
+ filterTargetKey: ['id', 'tenantId'],
146
+ fields: [
147
+ { name: 'id', type: 'integer', interface: 'number' },
148
+ { name: 'tenantId', type: 'string', interface: 'text' },
149
+ { name: 'name', type: 'string', interface: 'text' },
150
+ ],
151
+ });
152
+ dataSource.addCollection({
153
+ name: 'departments',
154
+ filterTargetKey: 'id',
155
+ fields: [
156
+ { name: 'id', type: 'integer', interface: 'number' },
157
+ { name: 'name', type: 'string', interface: 'text' },
158
+ { name: 'owner', type: 'belongsTo', target: 'users', interface: 'm2o' },
159
+ ],
160
+ });
161
+ dataSource.addCollection({
162
+ name: 'tasks',
163
+ filterTargetKey: 'id',
164
+ fields: [
165
+ { name: 'id', type: 'integer', interface: 'number' },
166
+ { name: 'title', type: 'string', interface: 'text' },
167
+ { name: 'department', type: 'belongsTo', target: 'departments', interface: 'm2o' },
168
+ ],
169
+ });
170
+
171
+ engine.registerModels({ FilterFormBlockModel });
172
+ const model = engine.createModel<FilterFormBlockModel>({
173
+ use: 'FilterFormBlockModel',
174
+ uid: 'filter-form-current-form',
175
+ subModels: {
176
+ grid: {
177
+ subModels: {
178
+ items: [],
179
+ },
180
+ },
181
+ },
182
+ } as any);
183
+
184
+ function HookCaller() {
185
+ model.useHooksBeforeRender();
186
+ return null;
187
+ }
188
+
189
+ render(React.createElement(HookCaller));
190
+
191
+ const store = {
192
+ title: 'bug',
193
+ 'department_department-filter': 1,
194
+ 'department.owner_owner-filter': { id: 7, tenantId: 'tenant-a' },
195
+ };
196
+ const fakeForm = {
197
+ getFieldsValue: () => ({ ...store }),
198
+ };
199
+ model.context.defineProperty('form', { value: fakeForm });
200
+ model.subModels.grid.subModels.items = [
201
+ {
202
+ uid: 'department-filter',
203
+ fieldPath: 'department',
204
+ props: { name: 'department_department-filter' },
205
+ subModels: {
206
+ field: {
207
+ context: {
208
+ collectionField: dataSource.getCollection('tasks').getField('department'),
209
+ },
210
+ },
211
+ },
212
+ },
213
+ {
214
+ uid: 'owner-filter',
215
+ fieldPath: 'department.owner',
216
+ props: { name: 'department.owner_owner-filter' },
217
+ subModels: {
218
+ field: {
219
+ context: {
220
+ collectionField: dataSource.getCollection('departments').getField('owner'),
221
+ },
222
+ },
223
+ },
224
+ },
225
+ ];
226
+
227
+ expect((model.context as any).formValues).toMatchObject({
228
+ ...store,
229
+ department: {
230
+ 'owner_owner-filter': { id: 7, tenantId: 'tenant-a' },
231
+ },
232
+ });
233
+
234
+ const options = (model.context as any).getPropertyOptions('formValues');
235
+ const meta = await options.meta();
236
+ const properties = await meta.properties();
237
+ const metaTree = await (model.context as any).getPropertyMetaTree();
238
+
239
+ expect(options.resolveOnServer('department_department-filter')).toBe(false);
240
+ expect(options.resolveOnServer('department_department-filter.name')).toBe(true);
241
+ expect(options.resolveOnServer('department_department-filter[0].name')).toBe(true);
242
+ expect(options.resolveOnServer('department.owner_owner-filter.name')).toBe(true);
243
+ expect(options.serverOnlyWhenContextParams).toBe(true);
244
+ expect(meta.title).toBe('Current form');
245
+ expect(properties.department.properties['owner_owner-filter'].title).toBe('owner');
246
+ expect(metaTree).toEqual(
247
+ expect.arrayContaining([
248
+ expect.objectContaining({
249
+ name: 'formValues',
250
+ title: 'Current form',
251
+ }),
252
+ ]),
253
+ );
254
+ expect(await meta.buildVariablesParams(model.context)).toMatchObject({
255
+ 'department_department-filter': { collection: 'departments', dataSourceKey: 'main', filterByTk: 1 },
256
+ department: {
257
+ 'owner_owner-filter': {
258
+ collection: 'users',
259
+ dataSourceKey: 'main',
260
+ filterByTk: { id: 7, tenantId: 'tenant-a' },
261
+ },
262
+ },
263
+ });
264
+ });
265
+
266
+ it('refreshes default values that depend on current filter form values', async () => {
267
+ const { model, values } = createFilterFormDefaultValuesModel(
268
+ [
269
+ {
270
+ key: 'username-default',
271
+ enable: true,
272
+ targetPath: 'username',
273
+ mode: 'default',
274
+ value: '{{ ctx.formValues.nickname_nick }}',
275
+ },
276
+ ],
277
+ { nickname_nick: 'Alice' },
278
+ );
279
+
280
+ await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any);
281
+ expect(values.username_user).toBe('Alice');
282
+
283
+ values.nickname_nick = 'Bob';
284
+ model.defaultValuesRefreshSeq += 1;
285
+ await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any, {
286
+ refreshSeq: model.defaultValuesRefreshSeq,
287
+ });
288
+ expect(values.username_user).toBe('Bob');
289
+
290
+ values.username_user = 'Manual';
291
+ values.nickname_nick = 'Carol';
292
+ model.defaultValuesRefreshSeq += 1;
293
+ await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any, {
294
+ refreshSeq: model.defaultValuesRefreshSeq,
295
+ });
296
+ expect(values.username_user).toBe('Manual');
297
+ });
298
+
299
+ it('applies fixed values even when the target filter field already has a value', async () => {
300
+ const { model, values } = createFilterFormDefaultValuesModel(
301
+ [
302
+ {
303
+ key: 'username-fixed',
304
+ enable: true,
305
+ targetPath: 'username',
306
+ mode: 'assign',
307
+ value: '{{ ctx.formValues.nickname_nick }}',
308
+ },
309
+ ],
310
+ { nickname_nick: 'Bob', username_user: 'Manual' },
311
+ );
312
+
313
+ await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any);
314
+
315
+ expect(values.username_user).toBe('Bob');
316
+ });
317
+
318
+ it('skips filter form field values when the rule condition does not match', async () => {
319
+ const { model, values } = createFilterFormDefaultValuesModel(
320
+ [
321
+ {
322
+ key: 'username-condition',
323
+ enable: true,
324
+ targetPath: 'username',
325
+ mode: 'assign',
326
+ condition: {
327
+ logic: '$and',
328
+ items: [{ path: '{{ ctx.formValues.nickname_nick }}', operator: '$eq', value: 'allow' }],
329
+ },
330
+ value: 'Matched',
331
+ },
332
+ ],
333
+ { nickname_nick: 'deny' },
334
+ );
335
+
336
+ await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any);
337
+ expect(values.username_user).toBeUndefined();
338
+
339
+ values.nickname_nick = 'allow';
340
+ await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any);
341
+ expect(values.username_user).toBe('Matched');
342
+ });
343
+
344
+ it('emits formValuesChange with final values after applying dependent field values', async () => {
345
+ const { model, values } = createFilterFormDefaultValuesModel(
346
+ [
347
+ {
348
+ key: 'username-fixed',
349
+ enable: true,
350
+ targetPath: 'username',
351
+ mode: 'assign',
352
+ value: '{{ ctx.formValues.nickname_nick }}',
353
+ },
354
+ ],
355
+ { nickname_nick: 'Bob' },
356
+ );
357
+
358
+ (model as any).handleFilterFormValuesChange({ nickname_nick: 'Bob' }, { nickname_nick: 'Bob' });
359
+
360
+ await waitFor(() => {
361
+ expect(values.username_user).toBe('Bob');
362
+ expect(model.dispatchEvent).toHaveBeenCalledWith(
363
+ 'formValuesChange',
364
+ {
365
+ changedValues: {
366
+ nickname_nick: 'Bob',
367
+ username_user: 'Bob',
368
+ },
369
+ allValues: {
370
+ nickname_nick: 'Bob',
371
+ username_user: 'Bob',
372
+ },
373
+ },
374
+ { debounce: true },
375
+ );
376
+ });
377
+ expect(model.emitter.emit).toHaveBeenCalledWith('formValuesChange', {
378
+ changedValues: {
379
+ nickname_nick: 'Bob',
380
+ username_user: 'Bob',
381
+ },
382
+ allValues: {
383
+ nickname_nick: 'Bob',
384
+ username_user: 'Bob',
385
+ },
386
+ });
387
+ });
51
388
  });
@@ -24,6 +24,7 @@ import { EditFormModel } from './EditFormModel';
24
24
  import _ from 'lodash';
25
25
  import { Tooltip } from 'antd';
26
26
  import { coerceForToOneField } from '../../../internal/utils/associationValueCoercion';
27
+ import { getFormItemFieldPathCandidates } from '../../../internal/utils/modelUtils';
27
28
  import { buildDynamicNamePath } from './dynamicNamePath';
28
29
 
29
30
  const interfacesOfUnsupportedDefaultValue = [
@@ -57,10 +58,9 @@ export class FormItemModel<T extends DefaultStructure = DefaultStructure> extend
57
58
  key: fullName,
58
59
  label: field.title,
59
60
  // 同步刷新 JS 字段菜单的切换状态(兼容旧路径与新路径)
60
- refreshTargets: ['FormItemModel/FormJSFieldItemModel'],
61
+ refreshTargets: ['FormJSFieldItemModel', 'FormItemModel/FormJSFieldItemModel'],
61
62
  toggleable: (subModel) => {
62
- const fieldPath = subModel.getStepParams('fieldSettings', 'init')?.fieldPath;
63
- return fieldPath === fullName;
63
+ return getFormItemFieldPathCandidates(subModel).some((fieldPath) => fieldPath === fullName);
64
64
  },
65
65
  useModel: 'FormItemModel',
66
66
  createModelOptions: () => ({
@@ -183,6 +183,8 @@ export class FormItemModel<T extends DefaultStructure = DefaultStructure> extend
183
183
 
184
184
  FormItemModel.define({
185
185
  label: tExpr('Display fields'),
186
+ searchable: true,
187
+ searchPlaceholder: tExpr('Search fields'),
186
188
  sort: 100,
187
189
  });
188
190
 
@@ -0,0 +1,108 @@
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 { describe, expect, it } from 'vitest';
11
+ import { FlowEngine, type FlowModelContext, type SubModelItem } from '@nocobase/flow-engine';
12
+ // Import from the aggregate to preserve the model initialization order used by adjacent tests.
13
+ import { FormItemModel, FormJSFieldItemModel, InputFieldModel, JSEditableFieldModel } from '../../../..';
14
+
15
+ function createFormMenuContext(prefixFieldPath = 'roles') {
16
+ const engine = new FlowEngine();
17
+ engine.registerModels({
18
+ FormItemModel,
19
+ FormJSFieldItemModel,
20
+ InputFieldModel,
21
+ JSEditableFieldModel,
22
+ });
23
+
24
+ const dataSource = engine.dataSourceManager.getDataSource('main');
25
+ dataSource.addCollection({
26
+ name: 'users',
27
+ filterTargetKey: 'id',
28
+ fields: [
29
+ { name: 'id', type: 'integer', interface: 'number', title: 'ID' },
30
+ { name: 'roles', type: 'hasMany', interface: 'o2m', target: 'roles', title: 'Roles' },
31
+ ],
32
+ });
33
+ dataSource.addCollection({
34
+ name: 'roles',
35
+ filterTargetKey: 'id',
36
+ fields: [
37
+ { name: 'id', type: 'integer', interface: 'number', title: 'ID' },
38
+ { name: 'name', type: 'string', interface: 'input', title: 'Name' },
39
+ ],
40
+ });
41
+
42
+ const blockModel = engine.createModel({ use: 'FlowModel', uid: 'users-form-block' });
43
+ (blockModel as any).collection = dataSource.getCollection('users');
44
+
45
+ const gridModel = engine.createModel({ use: 'FlowModel', uid: 'users-form-grid' });
46
+ gridModel.context.defineProperty('blockModel', { value: blockModel });
47
+ gridModel.context.defineProperty('collection', { value: dataSource.getCollection('roles') });
48
+ gridModel.context.defineProperty('prefixFieldPath', { value: prefixFieldPath });
49
+
50
+ return gridModel.context as FlowModelContext;
51
+ }
52
+
53
+ async function resolveCreateOptions(item: SubModelItem, ctx: FlowModelContext) {
54
+ return typeof item.createModelOptions === 'function' ? await item.createModelOptions(ctx) : item.createModelOptions;
55
+ }
56
+
57
+ function createModelLike(createOptions: any) {
58
+ return {
59
+ getStepParams: (flowKey: string, stepKey: string) => createOptions?.stepParams?.[flowKey]?.[stepKey],
60
+ } as any;
61
+ }
62
+
63
+ describe('FormItemModel defineChildren', () => {
64
+ it('refreshes the JS field submenu when a normal subform field is toggled', () => {
65
+ const ctx = createFormMenuContext();
66
+ const formItems = FormItemModel.defineChildren(ctx) as SubModelItem[];
67
+ const nameItem = formItems.find((item) => item.key === 'roles.name');
68
+
69
+ expect(nameItem?.refreshTargets).toEqual(['FormJSFieldItemModel', 'FormItemModel/FormJSFieldItemModel']);
70
+ });
71
+
72
+ it('recognizes JS subform fields that store associationPathName and fieldPath separately', async () => {
73
+ const ctx = createFormMenuContext();
74
+ const formItems = FormItemModel.defineChildren(ctx) as SubModelItem[];
75
+ const jsItems = (await FormJSFieldItemModel.defineChildren(ctx)) as SubModelItem[];
76
+ const normalNameItem = formItems.find((item) => item.key === 'roles.name');
77
+ const jsNameItem = jsItems.find((item) => item.key === 'roles.name');
78
+
79
+ expect(normalNameItem).toBeTruthy();
80
+ expect(jsNameItem).toBeTruthy();
81
+
82
+ const jsCreateOptions = await resolveCreateOptions(jsNameItem, ctx);
83
+ expect(jsCreateOptions?.stepParams?.fieldSettings?.init).toMatchObject({
84
+ associationPathName: 'roles',
85
+ fieldPath: 'name',
86
+ });
87
+
88
+ expect((normalNameItem?.toggleable as (model: any) => boolean)(createModelLike(jsCreateOptions))).toBe(true);
89
+ });
90
+
91
+ it('lets the JS subform menu recognize normal fields that store the full fieldPath', async () => {
92
+ const ctx = createFormMenuContext();
93
+ const formItems = FormItemModel.defineChildren(ctx) as SubModelItem[];
94
+ const jsItems = (await FormJSFieldItemModel.defineChildren(ctx)) as SubModelItem[];
95
+ const normalNameItem = formItems.find((item) => item.key === 'roles.name');
96
+ const jsNameItem = jsItems.find((item) => item.key === 'roles.name');
97
+
98
+ expect(normalNameItem).toBeTruthy();
99
+ expect(jsNameItem).toBeTruthy();
100
+
101
+ const normalCreateOptions = await resolveCreateOptions(normalNameItem, ctx);
102
+ expect(normalCreateOptions?.stepParams?.fieldSettings?.init).toMatchObject({
103
+ fieldPath: 'roles.name',
104
+ });
105
+
106
+ expect((jsNameItem?.toggleable as (model: any) => boolean)(createModelLike(normalCreateOptions))).toBe(true);
107
+ });
108
+ });
@@ -29,6 +29,10 @@ import { getRowKey } from './utils';
29
29
  import { FormBlockModel } from '../form/FormBlockModel';
30
30
 
31
31
  const recordIdentityByFork = new WeakMap<ForkFlowModel<any>, string>();
32
+ const rowActionButtonTypeOptions = [
33
+ { value: 'link', label: '{{t("Link")}}' },
34
+ { value: 'text', label: '{{t("Text")}}' },
35
+ ];
32
36
 
33
37
  const Columns = observer<any>(({ record, model, index }) => {
34
38
  const isConfigMode = !!model.context.flowSettingsEnabled;
@@ -65,6 +69,7 @@ const Columns = observer<any>(({ record, model, index }) => {
65
69
  }
66
70
 
67
71
  const fork = action.createFork({}, slotKey);
72
+ (fork as any).buttonTypeOptions = rowActionButtonTypeOptions;
68
73
  recordIdentityByFork.set(fork, recordIdentity);
69
74
 
70
75
  fork.invalidateFlowCache('beforeRender');
@@ -327,6 +327,8 @@ export class TableColumnModel extends DisplayItemModel {
327
327
 
328
328
  TableColumnModel.define({
329
329
  label: tExpr('Display fields'),
330
+ searchable: true,
331
+ searchPlaceholder: tExpr('Search fields'),
330
332
  });
331
333
 
332
334
  TableColumnModel.registerFlow({
@@ -1050,4 +1050,6 @@ SubTableColumnModel.registerFlow({
1050
1050
  SubTableColumnModel.define({
1051
1051
  hide: true,
1052
1052
  label: tExpr('Table column'),
1053
+ searchable: true,
1054
+ searchPlaceholder: tExpr('Search fields'),
1053
1055
  });