@nocobase/client-v2 2.1.0-beta.35 → 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 (67) hide show
  1. package/es/BaseApplication.d.ts +1 -1
  2. package/es/components/PoweredBy.d.ts +18 -0
  3. package/es/components/SwitchLanguage.d.ts +11 -0
  4. package/es/components/form/DialogFormLayout.d.ts +75 -0
  5. package/es/components/form/DrawerFormLayout.d.ts +11 -11
  6. package/es/components/form/PasswordInput.d.ts +40 -0
  7. package/es/components/form/RemoteSelect.d.ts +79 -0
  8. package/es/components/form/index.d.ts +3 -0
  9. package/es/components/form/table/styles.d.ts +10 -0
  10. package/es/components/index.d.ts +2 -0
  11. package/es/flow/models/base/ActionModelCore.d.ts +6 -0
  12. package/es/flow/models/base/GridModel.d.ts +2 -0
  13. package/es/flow/utils/dataScopeFormValueClear.d.ts +14 -0
  14. package/es/flow-compat/passwordUtils.d.ts +1 -1
  15. package/es/hooks/index.d.ts +2 -0
  16. package/es/hooks/useCurrentAppInfo.d.ts +9 -0
  17. package/es/index.mjs +102 -90
  18. package/es/nocobase-buildin-plugin/index.d.ts +25 -0
  19. package/es/utils/appVersionHTML.d.ts +10 -0
  20. package/es/utils/index.d.ts +1 -0
  21. package/es/utils/remotePlugins.d.ts +4 -1
  22. package/lib/index.js +108 -96
  23. package/package.json +7 -7
  24. package/src/BaseApplication.tsx +3 -3
  25. package/src/PluginSettingsManager.ts +2 -1
  26. package/src/__tests__/PluginSettingsManager.test.ts +19 -0
  27. package/src/__tests__/PoweredBy.test.tsx +130 -0
  28. package/src/__tests__/app.test.tsx +31 -0
  29. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +39 -72
  30. package/src/__tests__/remotePlugins.test.ts +55 -0
  31. package/src/__tests__/useCurrentRoles.test.tsx +100 -0
  32. package/src/components/PoweredBy.tsx +71 -0
  33. package/src/components/README.md +314 -0
  34. package/src/components/README.zh-CN.md +312 -0
  35. package/src/components/SwitchLanguage.tsx +48 -0
  36. package/src/components/form/DialogFormLayout.tsx +111 -0
  37. package/src/components/form/DrawerFormLayout.tsx +13 -32
  38. package/src/components/form/PasswordInput.tsx +211 -0
  39. package/src/components/form/RemoteSelect.tsx +137 -0
  40. package/src/components/form/index.tsx +3 -0
  41. package/src/components/form/table/Table.tsx +2 -1
  42. package/src/components/form/table/styles.ts +19 -0
  43. package/src/components/index.ts +2 -0
  44. package/src/css-variable/CSSVariableProvider.tsx +10 -1
  45. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +96 -0
  46. package/src/flow/actions/dataScope.tsx +3 -0
  47. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +1 -4
  48. package/src/flow/admin-shell/admin-layout/HelpLite.tsx +7 -33
  49. package/src/flow/components/BlockItemCard.tsx +2 -2
  50. package/src/flow/models/base/ActionModel.tsx +8 -7
  51. package/src/flow/models/base/ActionModelCore.tsx +15 -7
  52. package/src/flow/models/base/GridModel.tsx +93 -36
  53. package/src/flow/models/base/__tests__/GridModel.visibleLayout.test.ts +83 -11
  54. package/src/flow/models/blocks/details/DetailsItemModel.tsx +2 -0
  55. package/src/flow/models/blocks/form/FormItemModel.tsx +5 -3
  56. package/src/flow/models/blocks/form/__tests__/FormItemModel.defineChildren.test.ts +108 -0
  57. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +5 -0
  58. package/src/flow/models/blocks/table/TableColumnModel.tsx +2 -0
  59. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +2 -0
  60. package/src/flow/utils/dataScopeFormValueClear.ts +278 -0
  61. package/src/hooks/index.ts +2 -0
  62. package/src/hooks/useCurrentAppInfo.ts +36 -0
  63. package/src/nocobase-buildin-plugin/index.tsx +70 -16
  64. package/src/utils/appVersionHTML.ts +28 -0
  65. package/src/utils/globalDeps.ts +2 -2
  66. package/src/utils/index.tsx +2 -0
  67. package/src/utils/remotePlugins.ts +12 -7
@@ -48,18 +48,19 @@ ActionModel.registerFlow({
48
48
  title: tExpr('Button icon'),
49
49
  }
50
50
  : undefined,
51
+ iconOnly: ctx.model.enableEditIcon
52
+ ? {
53
+ 'x-decorator': 'FormItem',
54
+ 'x-component': 'Switch',
55
+ title: tExpr('Icon only'),
56
+ }
57
+ : undefined,
51
58
  type: ctx.model.enableEditType
52
59
  ? {
53
60
  'x-decorator': 'FormItem',
54
61
  'x-component': 'Radio.Group',
55
62
  title: tExpr('Button type'),
56
- enum: [
57
- { value: 'default', label: '{{t("Default")}}' },
58
- { value: 'primary', label: '{{t("Primary")}}' },
59
- { value: 'dashed', label: '{{t("Dashed")}}' },
60
- { value: 'link', label: '{{t("Link")}}' },
61
- { value: 'text', label: '{{t("Text")}}' },
62
- ],
63
+ enum: ctx.model.buttonTypeOptions,
63
64
  }
64
65
  : undefined,
65
66
  danger: ctx.model.enableEditDanger
@@ -50,12 +50,13 @@ export const ActionSceneEnum = {
50
50
  };
51
51
 
52
52
  export class ActionModel<T extends DefaultStructure = DefaultStructure> extends FlowModel<T> {
53
- declare props: ButtonProps & { tooltip?: string };
53
+ declare props: ButtonProps & { tooltip?: string; iconOnly?: boolean };
54
54
  declare scene: ActionSceneType;
55
55
 
56
- defaultProps: ButtonProps & { tooltip?: string } = {
56
+ defaultProps: ButtonProps & { tooltip?: string; iconOnly?: boolean } = {
57
57
  type: 'default',
58
58
  title: tExpr('Action'),
59
+ iconOnly: false,
59
60
  };
60
61
 
61
62
  enableEditTooltip = true;
@@ -64,6 +65,13 @@ export class ActionModel<T extends DefaultStructure = DefaultStructure> extends
64
65
  enableEditType = true;
65
66
  enableEditDanger = true;
66
67
  enableEditColor = false;
68
+ buttonTypeOptions = [
69
+ { value: 'default', label: '{{t("Default")}}' },
70
+ { value: 'primary', label: '{{t("Primary")}}' },
71
+ { value: 'dashed', label: '{{t("Dashed")}}' },
72
+ { value: 'link', label: '{{t("Link")}}' },
73
+ { value: 'text', label: '{{t("Text")}}' },
74
+ ];
67
75
 
68
76
  static _getScene() {
69
77
  return _.castArray(this['scene'] || []);
@@ -133,12 +141,12 @@ export class ActionModel<T extends DefaultStructure = DefaultStructure> extends
133
141
  }
134
142
 
135
143
  renderButton() {
136
- const props = this.props;
144
+ const { iconOnly, ...props } = this.props;
137
145
  const icon = this.getIcon() ? <Icon type={this.getIcon() as any} /> : undefined;
138
146
 
139
147
  return (
140
148
  <Button {...props} onClick={this.onClick.bind(this)} icon={icon}>
141
- {props.children || this.getTitle()}
149
+ {iconOnly ? null : props.children || this.getTitle()}
142
150
  </Button>
143
151
  );
144
152
  }
@@ -152,13 +160,13 @@ export class ActionModel<T extends DefaultStructure = DefaultStructure> extends
152
160
  }
153
161
 
154
162
  renderHiddenInConfig(): React.ReactNode | undefined {
155
- const props = this.props;
163
+ const { iconOnly, ...props } = this.props;
156
164
  const icon = this.getIcon() ? <Icon type={this.getIcon() as any} /> : undefined;
157
165
  if (this.forbidden) {
158
166
  return (
159
167
  <ActionWithoutPermission>
160
168
  <Button {...props} onClick={this.onClick.bind(this)} icon={icon} style={{ opacity: '0.3' }}>
161
- {props.children || this.getTitle()}
169
+ {iconOnly ? null : props.children || this.getTitle()}
162
170
  </Button>
163
171
  </ActionWithoutPermission>
164
172
  );
@@ -166,7 +174,7 @@ export class ActionModel<T extends DefaultStructure = DefaultStructure> extends
166
174
  return (
167
175
  <Tooltip title={this.context.t('The button is hidden and only visible when the UI Editor is active')}>
168
176
  <Button {...props} onClick={this.onClick.bind(this)} icon={icon} style={{ opacity: '0.3' }}>
169
- {props.children || this.getTitle()}
177
+ {iconOnly ? null : props.children || this.getTitle()}
170
178
  </Button>
171
179
  </Tooltip>
172
180
  );
@@ -359,6 +359,58 @@ export class GridModel<T extends { subModels: { items: FlowModel[] } } = Default
359
359
  return rowElement?.parentElement?.clientWidth || fallbackWidth;
360
360
  }
361
361
 
362
+ private prunePlaceholderOnlyRows(layout: GridLayoutV2): GridLayoutV2 {
363
+ type RowCellEntry = {
364
+ cell: GridCellV2;
365
+ size: number;
366
+ hasRealItem: boolean;
367
+ };
368
+
369
+ const pruneRows = (rows: GridRowV2[]): GridRowV2[] => {
370
+ return rows
371
+ .map((row) => {
372
+ const cellsWithSizes = row.cells
373
+ .map((cell, index) => {
374
+ const size = row.sizes?.[index] ?? 1;
375
+ if (cell.rows?.length) {
376
+ const childRows = pruneRows(cell.rows);
377
+ if (childRows.length) {
378
+ return { cell: { ...cell, rows: childRows }, size, hasRealItem: true };
379
+ }
380
+ return null;
381
+ }
382
+
383
+ const items = cell.items || [];
384
+ const hasRealItem = items.some((uid) => uid !== EMPTY_COLUMN_UID);
385
+ const hasEmptyPlaceholder = items.includes(EMPTY_COLUMN_UID);
386
+ if (hasRealItem || hasEmptyPlaceholder) {
387
+ return { cell, size, hasRealItem };
388
+ }
389
+ return null;
390
+ })
391
+ .filter(Boolean) as RowCellEntry[];
392
+
393
+ if (!cellsWithSizes.some((entry) => entry.hasRealItem)) {
394
+ return null;
395
+ }
396
+
397
+ return {
398
+ ...row,
399
+ cells: cellsWithSizes.map((entry) => entry.cell),
400
+ sizes: cellsWithSizes.map((entry) => entry.size),
401
+ };
402
+ })
403
+ .filter(Boolean) as GridRowV2[];
404
+ };
405
+
406
+ return normalizeGridLayout({
407
+ layout: { ...layout, rows: pruneRows(layout.rows || []) },
408
+ itemUids: this.getItemUids(),
409
+ gridUid: this.uid,
410
+ logger: console,
411
+ });
412
+ }
413
+
362
414
  private resizeGridLayout({
363
415
  direction,
364
416
  resizeDistance,
@@ -428,7 +480,9 @@ export class GridModel<T extends { subModels: { items: FlowModel[] } } = Default
428
480
  this.emitter.on('onSubModelDestroyed', (model: FlowModel) => {
429
481
  const modelUid = model.uid;
430
482
  this.resetRows(true);
431
- this.setGridStepLayout(this.props.layout);
483
+ const layout = this.prunePlaceholderOnlyRows(this.props.layout);
484
+ this.setGridStepLayout(layout);
485
+ this.syncLayoutProps(layout);
432
486
 
433
487
  // 删除筛选配置
434
488
  this.context.filterManager?.removeFilterConfig({ targetId: modelUid });
@@ -855,6 +909,7 @@ export class GridModel<T extends { subModels: { items: FlowModel[] } } = Default
855
909
  * 运行态按可见 block 过滤行/列,避免“整行都是 hidden block”但依然保留行间距占位。
856
910
  * - 配置态(flowSettingsEnabled)保持原始 rows/sizes 以便拖拽和布局编辑。
857
911
  * - 运行态仅在判断为“整列/整行都不可见”时做过滤,不写回 props/stepParams,布局元数据保持不变。
912
+ * - 空列是拖拽缩窄后保存的布局占位,运行态需要保留其宽度,但 Grid 渲染层不会渲染其内容。
858
913
  */
859
914
  private getVisibleLayout() {
860
915
  const rawLayout = this.normalizeLayoutFromSource();
@@ -877,51 +932,53 @@ export class GridModel<T extends { subModels: { items: FlowModel[] } } = Default
877
932
  }
878
933
 
879
934
  const items = this.subModels?.items || [];
880
- if (!items.length) {
881
- return { layout: baseLayout, rows: baseProjection.rows, sizes: baseProjection.sizes };
882
- }
883
-
884
935
  const modelByUid = new Map(items.map((m: FlowModel) => [m.uid, m]));
936
+ type VisibleCellEntry = {
937
+ cell: GridLayoutV2['rows'][number]['cells'][number];
938
+ size: number;
939
+ hasVisibleContent: boolean;
940
+ };
885
941
 
886
942
  const filterRows = (rows: GridLayoutV2['rows']): GridLayoutV2['rows'] => {
887
943
  return rows
888
944
  .map((row) => {
889
- const cells: GridLayoutV2['rows'][number]['cells'] = [];
890
- const keptSizes: number[] = [];
891
-
892
- row.cells.forEach((cell, index) => {
893
- const sourceSize = row.sizes?.[index];
894
- const keepSize = Number.isFinite(sourceSize) && sourceSize > 0 ? sourceSize : 1;
895
- if (cell.rows) {
896
- const childRows = filterRows(cell.rows);
897
- if (childRows.length) {
898
- cells.push({ ...cell, rows: childRows });
899
- keptSizes.push(keepSize);
945
+ const cellsWithSizes = row.cells
946
+ .map((cell, index) => {
947
+ const sourceSize = row.sizes?.[index];
948
+ const keepSize = Number.isFinite(sourceSize) && sourceSize > 0 ? sourceSize : 1;
949
+ if (cell.rows) {
950
+ const childRows = filterRows(cell.rows);
951
+ if (childRows.length) {
952
+ return { cell: { ...cell, rows: childRows }, size: keepSize, hasVisibleContent: true };
953
+ }
954
+ return null;
900
955
  }
901
- return;
902
- }
903
956
 
904
- const cellItems = (cell.items || []).filter((uid) => {
905
- if (uid === EMPTY_COLUMN_UID) {
906
- return false;
957
+ const cellItems = (cell.items || []).filter((uid) => {
958
+ if (uid === EMPTY_COLUMN_UID) {
959
+ return true;
960
+ }
961
+ return modelByUid.get(uid)?.hidden !== true;
962
+ });
963
+ const hasVisibleContent = cellItems.some((uid) => {
964
+ if (uid === EMPTY_COLUMN_UID) return false;
965
+ const model = modelByUid.get(uid);
966
+ return !model || !model.hidden;
967
+ });
968
+ const hasEmptyPlaceholder = cellItems.includes(EMPTY_COLUMN_UID);
969
+ if (hasVisibleContent || hasEmptyPlaceholder) {
970
+ return { cell: { ...cell, items: cellItems }, size: keepSize, hasVisibleContent };
907
971
  }
908
- return modelByUid.get(uid)?.hidden !== true;
909
- });
910
- const hasVisibleItem = cellItems.some((uid) => {
911
- const model = modelByUid.get(uid);
912
- return !model || !model.hidden;
913
- });
914
- if (hasVisibleItem) {
915
- cells.push({ ...cell, items: cellItems });
916
- keptSizes.push(keepSize);
917
- }
918
- });
919
-
920
- return cells.length
972
+ return null;
973
+ })
974
+ .filter(Boolean) as VisibleCellEntry[];
975
+ const hasVisibleContent = cellsWithSizes.some((entry) => entry.hasVisibleContent);
976
+
977
+ return hasVisibleContent
921
978
  ? {
922
979
  ...row,
923
- cells,
924
- sizes: keptSizes,
980
+ cells: cellsWithSizes.map((entry) => entry.cell),
981
+ sizes: cellsWithSizes.map((entry) => entry.size),
925
982
  }
926
983
  : null;
927
984
  })
@@ -9,7 +9,7 @@
9
9
 
10
10
  import { EMPTY_COLUMN_UID, FlowEngine } from '@nocobase/flow-engine';
11
11
  import { beforeEach, describe, expect, it } from 'vitest';
12
- import { GridModel } from '../GridModel';
12
+ import { GRID_FLOW_KEY, GRID_STEP, GridModel } from '../GridModel';
13
13
 
14
14
  describe('GridModel.getVisibleLayout (hidden items filtering)', () => {
15
15
  let engine: FlowEngine;
@@ -228,8 +228,8 @@ describe('GridModel.getVisibleLayout (hidden items filtering)', () => {
228
228
  expect(sizes.row1).toEqual([10, 14]);
229
229
  });
230
230
 
231
- it('ignores EMPTY_COLUMN uid in runtime mode without crashing', async () => {
232
- await engine.flowSettings.disable();
231
+ it('preserves EMPTY_COLUMN placeholder width in runtime mode', () => {
232
+ engine.flowSettings.disable();
233
233
 
234
234
  const visible = engine.createModel({ use: 'FlowModel', uid: 'v' });
235
235
 
@@ -237,11 +237,18 @@ describe('GridModel.getVisibleLayout (hidden items filtering)', () => {
237
237
  use: 'GridModel',
238
238
  uid: 'grid-8',
239
239
  props: {
240
- rows: {
241
- row1: [[EMPTY_COLUMN_UID, 'ghost', 'v'], ['ghost-2']],
242
- },
243
- sizes: {
244
- row1: [8, 16],
240
+ layout: {
241
+ version: 2,
242
+ rows: [
243
+ {
244
+ id: 'row1',
245
+ cells: [
246
+ { id: 'cell1', items: ['v'] },
247
+ { id: 'cell2', items: [EMPTY_COLUMN_UID] },
248
+ ],
249
+ sizes: [8, 16],
250
+ },
251
+ ],
245
252
  },
246
253
  },
247
254
  structure: {} as any,
@@ -250,8 +257,73 @@ describe('GridModel.getVisibleLayout (hidden items filtering)', () => {
250
257
  (model as any).subModels = { items: [visible] };
251
258
 
252
259
  const { rows, sizes } = (model as any).getVisibleLayout();
253
- // 新布局归一化会移除不在 subModels.items 中的 uid,EMPTY_COLUMN_UID 也不会在运行态显示
254
- expect(rows.row1).toEqual([['v']]);
255
- expect(sizes.row1).toEqual([24]);
260
+ // 空列是拖拽缩窄区块后的布局占位;运行态也要保留其宽度,避免剩余区块被拉满整行。
261
+ expect(rows.row1).toEqual([['v'], [EMPTY_COLUMN_UID]]);
262
+ expect(sizes.row1).toEqual([8, 16]);
263
+ });
264
+
265
+ it('removes rows that only contain EMPTY_COLUMN placeholders in runtime mode when there are no items', () => {
266
+ engine.flowSettings.disable();
267
+
268
+ const model = engine.createModel<GridModel>({
269
+ use: 'GridModel',
270
+ uid: 'grid-9',
271
+ props: {
272
+ layout: {
273
+ version: 2,
274
+ rows: [
275
+ {
276
+ id: 'row1',
277
+ cells: [{ id: 'cell1', items: [EMPTY_COLUMN_UID] }],
278
+ sizes: [24],
279
+ },
280
+ ],
281
+ },
282
+ },
283
+ structure: {} as any,
284
+ });
285
+
286
+ (model as any).subModels = { items: [] };
287
+
288
+ const { rows, sizes } = (model as any).getVisibleLayout();
289
+ expect(rows).toEqual({});
290
+ expect(sizes).toEqual({});
291
+ });
292
+
293
+ it('removes the placeholder-only row after the last real item in the row is deleted', () => {
294
+ engine.flowSettings.disable();
295
+
296
+ const visible = engine.createModel({ use: 'FlowModel', uid: 'v' });
297
+ const layout = {
298
+ version: 2 as const,
299
+ rows: [
300
+ {
301
+ id: 'row1',
302
+ cells: [
303
+ { id: 'cell1', items: ['v'] },
304
+ { id: 'cell2', items: [EMPTY_COLUMN_UID] },
305
+ ],
306
+ sizes: [8, 16],
307
+ },
308
+ ],
309
+ };
310
+ const model = engine.createModel<GridModel>({
311
+ use: 'GridModel',
312
+ uid: 'grid-10',
313
+ props: { layout },
314
+ structure: {} as any,
315
+ });
316
+ (model as any).subModels = { items: [visible] };
317
+ model.setStepParams(GRID_FLOW_KEY, GRID_STEP, { layout });
318
+ model.syncLayoutProps(model.getGridLayout());
319
+ model.onMount();
320
+
321
+ (model as any).subModels = { items: [] };
322
+ model.emitter.emit('onSubModelDestroyed', visible);
323
+
324
+ expect(model.props.rows).toEqual({});
325
+ expect(model.props.sizes).toEqual({});
326
+ expect(model.props.layout.rows).toEqual([]);
327
+ expect(model.getStepParams(GRID_FLOW_KEY, GRID_STEP).layout.rows).toEqual([]);
256
328
  });
257
329
  });
@@ -227,6 +227,8 @@ export class DetailsItemModel extends DisplayItemModel<{
227
227
 
228
228
  DetailsItemModel.define({
229
229
  label: tExpr('Display fields'),
230
+ searchable: true,
231
+ searchPlaceholder: tExpr('Search fields'),
230
232
  sort: 100,
231
233
  });
232
234
 
@@ -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
  });