@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.
- package/es/BaseApplication.d.ts +1 -1
- package/es/components/PoweredBy.d.ts +18 -0
- package/es/components/SwitchLanguage.d.ts +11 -0
- package/es/components/form/DialogFormLayout.d.ts +75 -0
- package/es/components/form/DrawerFormLayout.d.ts +11 -11
- package/es/components/form/PasswordInput.d.ts +40 -0
- package/es/components/form/RemoteSelect.d.ts +79 -0
- package/es/components/form/index.d.ts +3 -0
- package/es/components/form/table/styles.d.ts +10 -0
- package/es/components/index.d.ts +2 -0
- package/es/flow/models/base/ActionModelCore.d.ts +6 -0
- package/es/flow/models/base/GridModel.d.ts +2 -0
- package/es/flow/utils/dataScopeFormValueClear.d.ts +14 -0
- package/es/flow-compat/passwordUtils.d.ts +1 -1
- package/es/hooks/index.d.ts +2 -0
- package/es/hooks/useCurrentAppInfo.d.ts +9 -0
- package/es/index.mjs +102 -90
- package/es/nocobase-buildin-plugin/index.d.ts +25 -0
- package/es/utils/appVersionHTML.d.ts +10 -0
- package/es/utils/index.d.ts +1 -0
- package/es/utils/remotePlugins.d.ts +4 -1
- package/lib/index.js +108 -96
- package/package.json +7 -7
- package/src/BaseApplication.tsx +3 -3
- package/src/PluginSettingsManager.ts +2 -1
- package/src/__tests__/PluginSettingsManager.test.ts +19 -0
- package/src/__tests__/PoweredBy.test.tsx +130 -0
- package/src/__tests__/app.test.tsx +31 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +39 -72
- package/src/__tests__/remotePlugins.test.ts +55 -0
- package/src/__tests__/useCurrentRoles.test.tsx +100 -0
- package/src/components/PoweredBy.tsx +71 -0
- package/src/components/README.md +314 -0
- package/src/components/README.zh-CN.md +312 -0
- package/src/components/SwitchLanguage.tsx +48 -0
- package/src/components/form/DialogFormLayout.tsx +111 -0
- package/src/components/form/DrawerFormLayout.tsx +13 -32
- package/src/components/form/PasswordInput.tsx +211 -0
- package/src/components/form/RemoteSelect.tsx +137 -0
- package/src/components/form/index.tsx +3 -0
- package/src/components/form/table/Table.tsx +2 -1
- package/src/components/form/table/styles.ts +19 -0
- package/src/components/index.ts +2 -0
- package/src/css-variable/CSSVariableProvider.tsx +10 -1
- package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +96 -0
- package/src/flow/actions/dataScope.tsx +3 -0
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +1 -4
- package/src/flow/admin-shell/admin-layout/HelpLite.tsx +7 -33
- package/src/flow/components/BlockItemCard.tsx +2 -2
- package/src/flow/models/base/ActionModel.tsx +8 -7
- package/src/flow/models/base/ActionModelCore.tsx +15 -7
- package/src/flow/models/base/GridModel.tsx +93 -36
- package/src/flow/models/base/__tests__/GridModel.visibleLayout.test.ts +83 -11
- package/src/flow/models/blocks/details/DetailsItemModel.tsx +2 -0
- package/src/flow/models/blocks/form/FormItemModel.tsx +5 -3
- package/src/flow/models/blocks/form/__tests__/FormItemModel.defineChildren.test.ts +108 -0
- package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +5 -0
- package/src/flow/models/blocks/table/TableColumnModel.tsx +2 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +2 -0
- package/src/flow/utils/dataScopeFormValueClear.ts +278 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useCurrentAppInfo.ts +36 -0
- package/src/nocobase-buildin-plugin/index.tsx +70 -16
- package/src/utils/appVersionHTML.ts +28 -0
- package/src/utils/globalDeps.ts +2 -2
- package/src/utils/index.tsx +2 -0
- 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.
|
|
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
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
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
|
-
|
|
905
|
-
|
|
906
|
-
|
|
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
|
|
909
|
-
})
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
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:
|
|
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('
|
|
232
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
//
|
|
254
|
-
expect(rows.row1).toEqual([['v']]);
|
|
255
|
-
expect(sizes.row1).toEqual([
|
|
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
|
});
|
|
@@ -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
|
-
|
|
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');
|