@nocobase/client-v2 2.1.11 → 2.2.0-alpha.2
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/PluginSettingsManager.d.ts +33 -0
- package/es/RouteRepository.d.ts +21 -6
- package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
- package/es/components/category-tabs/index.d.ts +9 -0
- package/es/components/form/TypedVariableInput.d.ts +22 -4
- package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
- package/es/components/form/filter/index.d.ts +2 -0
- package/es/components/index.d.ts +2 -0
- package/es/flow/actions/afterSuccess.d.ts +8 -1
- package/es/flow/actions/index.d.ts +1 -1
- package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +2 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.d.ts +2 -1
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +2 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.d.ts +3 -2
- package/es/flow/admin-shell/admin-layout/AdminLayoutSlotModels.d.ts +5 -0
- package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +6 -0
- package/es/flow/components/FieldAssignValueInput.d.ts +2 -0
- package/es/flow/index.d.ts +1 -0
- package/es/flow/models/base/GridModel.d.ts +1 -1
- package/es/flow/models/blocks/form/submitHandler.d.ts +1 -1
- package/es/flow/models/blocks/table/TableActionsColumnModel.d.ts +1 -0
- package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
- package/es/flow/models/blocks/table/dragSort/dragSortHooks.d.ts +1 -1
- package/es/flow/models/blocks/table/dragSort/dragSortSettings.d.ts +2 -1
- package/es/flow/models/blocks/table/dragSort/dragSortUtils.d.ts +6 -4
- package/es/flow/resolveViewParamsToViewList.d.ts +1 -1
- package/es/index.d.ts +2 -1
- package/es/index.mjs +181 -131
- package/lib/index.js +186 -136
- package/package.json +7 -7
- package/src/PluginSettingsManager.ts +53 -0
- package/src/RouteRepository.ts +126 -24
- package/src/__tests__/PluginSettingsManager.test.ts +13 -0
- package/src/__tests__/RouteRepository.test.ts +216 -0
- package/src/__tests__/browserChecker.test.ts +61 -0
- package/src/__tests__/exports.test.ts +16 -0
- package/src/__tests__/plugin-manager.test.tsx +44 -2
- package/src/__tests__/settings-center.test.tsx +40 -1
- package/src/collection-manager/field-configure.ts +1 -1
- package/src/collection-manager/interfaces/id.ts +1 -1
- package/src/collection-manager/interfaces/m2m.tsx +2 -2
- package/src/collection-manager/interfaces/m2o.tsx +2 -2
- package/src/collection-manager/interfaces/nanoid.ts +1 -1
- package/src/collection-manager/interfaces/o2m.tsx +2 -2
- package/src/collection-manager/interfaces/obo.tsx +2 -2
- package/src/collection-manager/interfaces/oho.tsx +2 -2
- package/src/collection-manager/interfaces/properties/index.ts +2 -2
- package/src/collection-manager/interfaces/uuid.ts +1 -1
- package/src/components/README.md +7 -1
- package/src/components/README.zh-CN.md +6 -1
- package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
- package/src/components/category-tabs/index.ts +10 -0
- package/src/components/form/TypedVariableInput.tsx +416 -93
- package/src/components/form/__tests__/TypedVariableInput.test.tsx +266 -9
- package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
- package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
- package/src/components/form/filter/index.ts +2 -0
- package/src/components/index.ts +2 -0
- package/src/flow/__tests__/FlowRoute.test.tsx +443 -5
- package/src/flow/__tests__/getKey.test.ts +7 -0
- package/src/flow/__tests__/resolveViewParamsToViewList.test.ts +34 -0
- package/src/flow/actions/__tests__/afterSuccess.test.ts +73 -0
- package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +104 -0
- package/src/flow/actions/afterSuccess.tsx +142 -3
- package/src/flow/actions/customVariable.tsx +1 -2
- package/src/flow/actions/index.ts +1 -1
- package/src/flow/actions/linkageRules.tsx +2 -7
- package/src/flow/actions/openView.tsx +38 -4
- package/src/flow/actions/runjs.tsx +2 -14
- package/src/flow/admin-shell/BaseLayoutModel.tsx +25 -3
- package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +5 -2
- package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +13 -0
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +34 -10
- package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +310 -0
- package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +44 -8
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.ts +5 -3
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +61 -9
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +71 -8
- package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +1 -1
- package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +23 -9
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +188 -0
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +99 -0
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +82 -0
- package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +166 -2
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +20 -0
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +33 -5
- package/src/flow/components/DefaultValue.tsx +1 -2
- package/src/flow/components/FieldAssignValueInput.tsx +10 -5
- package/src/flow/components/FlowRoute.tsx +56 -11
- package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +2 -17
- package/src/flow/components/code-editor/hooks/useCodeRunner.ts +2 -14
- package/src/flow/components/code-editor/runjsDiagnostics.ts +8 -45
- package/src/flow/getViewDiffAndUpdateHidden.tsx +1 -0
- package/src/flow/index.ts +1 -0
- package/src/flow/models/actions/JSActionModel.tsx +2 -7
- package/src/flow/models/actions/JSCollectionActionModel.tsx +2 -7
- package/src/flow/models/actions/JSItemActionModel.tsx +2 -7
- package/src/flow/models/actions/JSRecordActionModel.tsx +2 -7
- package/src/flow/models/base/ActionModel.tsx +12 -1
- package/src/flow/models/base/GridModel.tsx +38 -7
- package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +0 -4
- package/src/flow/models/base/__tests__/ActionModel.test.ts +83 -0
- package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +239 -1
- package/src/flow/models/base/__tests__/transformRowsToSingleColumn.test.ts +48 -0
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +1 -2
- package/src/flow/models/blocks/filter-form/FilterFormJSActionModel.tsx +2 -7
- package/src/flow/models/blocks/filter-form/fields/FilterFormCustomFieldModel.tsx +1 -1
- package/src/flow/models/blocks/form/FormActionModel.tsx +1 -1
- package/src/flow/models/blocks/form/JSFormActionModel.tsx +2 -7
- package/src/flow/models/blocks/form/__tests__/submitHandler.test.ts +54 -1
- package/src/flow/models/blocks/form/submitHandler.ts +17 -3
- package/src/flow/models/blocks/form/value-runtime/rules.ts +2 -2
- package/src/flow/models/blocks/js-block/JSBlock.tsx +2 -7
- package/src/flow/models/blocks/table/JSColumnModel.tsx +1 -9
- package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +36 -13
- package/src/flow/models/blocks/table/TableBlockModel.tsx +38 -17
- package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +111 -1
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.dragSort.test.ts +127 -0
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.mobileSettingsButtons.test.tsx +78 -0
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +1 -0
- package/src/flow/models/blocks/table/dragSort/dragSortHooks.tsx +28 -17
- package/src/flow/models/blocks/table/dragSort/dragSortSettings.ts +13 -6
- package/src/flow/models/blocks/table/dragSort/dragSortUtils.ts +15 -2
- package/src/flow/models/fields/JSEditableFieldModel.tsx +2 -11
- package/src/flow/models/fields/JSFieldModel.tsx +2 -7
- package/src/flow/models/fields/JSItemModel.tsx +2 -14
- package/src/flow/models/topbar/TopbarActionModel.tsx +93 -10
- package/src/flow/resolveViewParamsToViewList.tsx +11 -3
- package/src/flow-compat/Popover.tsx +43 -4
- package/src/flow-compat/__tests__/Popover.test.tsx +34 -0
- package/src/index.ts +8 -1
- package/src/layout-manager/__tests__/LayoutRoute.test.tsx +41 -1
- package/src/nocobase-buildin-plugin/index.tsx +0 -2
- package/src/settings-center/AdminSettingsLayout.tsx +10 -2
- package/src/settings-center/SystemSettingsPage.tsx +1 -2
- package/src/settings-center/plugin-manager/PluginCard.tsx +2 -2
- package/src/settings-center/utils.tsx +0 -6
package/src/flow/index.ts
CHANGED
|
@@ -98,6 +98,7 @@ export * from './FlowPage';
|
|
|
98
98
|
export * from './models';
|
|
99
99
|
export * from './utils';
|
|
100
100
|
export * from './actions';
|
|
101
|
+
export { FieldAssignValueInput } from './components/FieldAssignValueInput';
|
|
101
102
|
export * from './system-settings';
|
|
102
103
|
export * from './admin-shell/admin-layout';
|
|
103
104
|
export * from './admin-shell/BaseLayoutModel';
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { tExpr } from '@nocobase/flow-engine';
|
|
11
11
|
import type { ButtonProps } from 'antd/es/button';
|
|
12
12
|
import { CodeEditor } from '../../components/code-editor';
|
|
13
13
|
import { ActionModel } from '../base';
|
|
@@ -72,12 +72,7 @@ ctx.message.info('Hello JS action.');
|
|
|
72
72
|
},
|
|
73
73
|
async handler(ctx, params) {
|
|
74
74
|
const { code, version } = resolveRunJsParams(ctx, params);
|
|
75
|
-
|
|
76
|
-
await ctx.runjs(
|
|
77
|
-
code,
|
|
78
|
-
{ window: createSafeWindow({ navigator }), document: createSafeDocument(), navigator },
|
|
79
|
-
{ version },
|
|
80
|
-
);
|
|
75
|
+
await ctx.runjs(code, undefined, { version });
|
|
81
76
|
},
|
|
82
77
|
},
|
|
83
78
|
},
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { tExpr } from '@nocobase/flow-engine';
|
|
11
11
|
import type { ButtonProps } from 'antd/es/button';
|
|
12
12
|
import { CodeEditor } from '../../components/code-editor';
|
|
13
13
|
import { ActionModel, ActionSceneEnum } from '../base';
|
|
@@ -78,12 +78,7 @@ if (!rows.length) {
|
|
|
78
78
|
},
|
|
79
79
|
async handler(ctx, params) {
|
|
80
80
|
const { code, version } = resolveRunJsParams(ctx, params);
|
|
81
|
-
|
|
82
|
-
await ctx.runjs(
|
|
83
|
-
code,
|
|
84
|
-
{ window: createSafeWindow({ navigator }), document: createSafeDocument(), navigator },
|
|
85
|
-
{ version },
|
|
86
|
-
);
|
|
81
|
+
await ctx.runjs(code, undefined, { version });
|
|
87
82
|
},
|
|
88
83
|
},
|
|
89
84
|
},
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { ElementProxy,
|
|
10
|
+
import { ElementProxy, tExpr } from '@nocobase/flow-engine';
|
|
11
11
|
import React from 'react';
|
|
12
12
|
import { CodeEditor } from '../../components/code-editor';
|
|
13
13
|
import { ActionModel, ActionSceneEnum, CollectionActionGroupModel, RecordActionGroupModel } from '../base';
|
|
@@ -137,12 +137,7 @@ JSItemActionModel.registerFlow({
|
|
|
137
137
|
ctx.defineProperty('element', {
|
|
138
138
|
get: () => new ElementProxy(element),
|
|
139
139
|
});
|
|
140
|
-
|
|
141
|
-
await ctx.runjs(
|
|
142
|
-
code,
|
|
143
|
-
{ window: createSafeWindow({ navigator }), document: createSafeDocument(), navigator },
|
|
144
|
-
{ version },
|
|
145
|
-
);
|
|
140
|
+
await ctx.runjs(code, undefined, { version });
|
|
146
141
|
});
|
|
147
142
|
},
|
|
148
143
|
},
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { tExpr } from '@nocobase/flow-engine';
|
|
11
11
|
import type { ButtonProps } from 'antd/es/button';
|
|
12
12
|
import { CodeEditor } from '../../components/code-editor';
|
|
13
13
|
import { ActionSceneEnum, RecordActionModel } from '../base';
|
|
@@ -78,12 +78,7 @@ if (!ctx.record) {
|
|
|
78
78
|
},
|
|
79
79
|
async handler(ctx, params) {
|
|
80
80
|
const { code, version } = resolveRunJsParams(ctx, params);
|
|
81
|
-
|
|
82
|
-
await ctx.runjs(
|
|
83
|
-
code,
|
|
84
|
-
{ window: createSafeWindow({ navigator }), document: createSafeDocument(), navigator },
|
|
85
|
-
{ version },
|
|
86
|
-
);
|
|
81
|
+
await ctx.runjs(code, undefined, { version });
|
|
87
82
|
},
|
|
88
83
|
},
|
|
89
84
|
},
|
|
@@ -18,6 +18,17 @@ import { commonConditionHandler, ConditionBuilder } from '../../components/Condi
|
|
|
18
18
|
export { ActionModel, ActionSceneEnum, ActionWithoutPermission, type ActionSceneType } from './ActionModelCore';
|
|
19
19
|
import { ActionModel } from './ActionModelCore';
|
|
20
20
|
|
|
21
|
+
function getMobileDefaultProps(ctx) {
|
|
22
|
+
const defaultProps = ctx.model.defaultProps || {};
|
|
23
|
+
if (ctx.isMobileLayout && defaultProps.icon) {
|
|
24
|
+
return {
|
|
25
|
+
...defaultProps,
|
|
26
|
+
iconOnly: true,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return defaultProps;
|
|
30
|
+
}
|
|
31
|
+
|
|
21
32
|
ActionModel.registerFlow({
|
|
22
33
|
key: 'buttonSettings',
|
|
23
34
|
title: tExpr('Button settings'),
|
|
@@ -80,7 +91,7 @@ ActionModel.registerFlow({
|
|
|
80
91
|
};
|
|
81
92
|
},
|
|
82
93
|
defaultParams(ctx) {
|
|
83
|
-
const defaultProps = ctx
|
|
94
|
+
const defaultProps = getMobileDefaultProps(ctx);
|
|
84
95
|
if (!ctx.model.enableEditColor) {
|
|
85
96
|
return defaultProps;
|
|
86
97
|
}
|
|
@@ -75,6 +75,12 @@ interface DragState {
|
|
|
75
75
|
generatedIds: Map<string, string>;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
const MOBILE_GRID_DISABLED_DROP_SLOT_TYPES = new Set<LayoutSlot['type']>(['column-edge', 'item-edge', 'empty-column']);
|
|
79
|
+
|
|
80
|
+
const filterMobileGridDragSlots = (slots: LayoutSlot[]) => {
|
|
81
|
+
return slots.filter((slot) => !MOBILE_GRID_DISABLED_DROP_SLOT_TYPES.has(slot.type));
|
|
82
|
+
};
|
|
83
|
+
|
|
78
84
|
const getClientPoint = (event: any): { x: number; y: number } | null => {
|
|
79
85
|
if (!event) {
|
|
80
86
|
return null;
|
|
@@ -625,7 +631,7 @@ export class GridModel<T extends { subModels: { items: FlowModel[] } } = Default
|
|
|
625
631
|
return;
|
|
626
632
|
}
|
|
627
633
|
const snapshot = buildLayoutSnapshot({ container: this.getDragContainer() });
|
|
628
|
-
this.dragState.slots = snapshot.slots;
|
|
634
|
+
this.dragState.slots = this.context.isMobileLayout ? filterMobileGridDragSlots(snapshot.slots) : snapshot.slots;
|
|
629
635
|
this.dragState.containerRect = snapshot.containerRect;
|
|
630
636
|
}
|
|
631
637
|
|
|
@@ -1135,7 +1141,7 @@ GridModel.registerFlow({
|
|
|
1135
1141
|
/**
|
|
1136
1142
|
* 将多列栅格 rows 转换为移动端单列 rows。
|
|
1137
1143
|
* 遍历原 rows 的行与列顺序,把每个原列(过滤空白列后)变成一个新行(仅一列)。
|
|
1138
|
-
* 行 key
|
|
1144
|
+
* 行 key 使用原 rowId 与 columnIndex 生成,避免每次渲染产生随机 key 导致移动端滚动位置丢失。
|
|
1139
1145
|
* @param rows 原始多列 rows
|
|
1140
1146
|
* @returns 单列 rows
|
|
1141
1147
|
*/
|
|
@@ -1145,13 +1151,38 @@ export function transformRowsToSingleColumn(
|
|
|
1145
1151
|
): Record<string, string[][]> {
|
|
1146
1152
|
const emptyColumnUid = options?.emptyColumnUid ?? EMPTY_COLUMN_UID;
|
|
1147
1153
|
const singleColumnRows: Record<string, string[][]> = {};
|
|
1154
|
+
|
|
1155
|
+
const getAvailableRowId = (baseRowId: string) => {
|
|
1156
|
+
if (!singleColumnRows[baseRowId]) {
|
|
1157
|
+
return baseRowId;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
let duplicateIndex = 1;
|
|
1161
|
+
let rowId = `${baseRowId}:duplicate:${duplicateIndex}`;
|
|
1162
|
+
while (singleColumnRows[rowId]) {
|
|
1163
|
+
duplicateIndex += 1;
|
|
1164
|
+
rowId = `${baseRowId}:duplicate:${duplicateIndex}`;
|
|
1165
|
+
}
|
|
1166
|
+
return rowId;
|
|
1167
|
+
};
|
|
1168
|
+
|
|
1148
1169
|
Object.keys(rows).forEach((rowId) => {
|
|
1149
1170
|
const columns = rows[rowId];
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
}
|
|
1171
|
+
const effectiveColumns = columns
|
|
1172
|
+
.map((column, columnIndex) => ({
|
|
1173
|
+
columnIndex,
|
|
1174
|
+
items: column.filter((id) => id !== emptyColumnUid),
|
|
1175
|
+
}))
|
|
1176
|
+
.filter((column) => column.items.length > 0);
|
|
1177
|
+
|
|
1178
|
+
if (effectiveColumns.length === 1 && effectiveColumns[0].columnIndex === 0) {
|
|
1179
|
+
singleColumnRows[getAvailableRowId(rowId)] = [effectiveColumns[0].items];
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
effectiveColumns.forEach((column) => {
|
|
1184
|
+
const generatedRowId = `mobile:${encodeURIComponent(rowId)}:${column.columnIndex}`;
|
|
1185
|
+
singleColumnRows[getAvailableRowId(generatedRowId)] = [column.items];
|
|
1155
1186
|
});
|
|
1156
1187
|
});
|
|
1157
1188
|
return singleColumnRows;
|
|
@@ -140,13 +140,9 @@ vi.mock('@nocobase/flow-engine', () => {
|
|
|
140
140
|
jioToJoiSchema: () => ({
|
|
141
141
|
validate: (value: any) => ({ value }),
|
|
142
142
|
}),
|
|
143
|
-
createSafeWindow: () => window,
|
|
144
|
-
createSafeDocument: () => document,
|
|
145
|
-
createSafeNavigator: () => navigator,
|
|
146
143
|
isRunJSValue: () => false,
|
|
147
144
|
isVariableExpression: () => false,
|
|
148
145
|
normalizeRunJSValue: (value: any) => value,
|
|
149
|
-
runjsWithSafeGlobals: async (handler: any, ...args: any[]) => handler?.(...args),
|
|
150
146
|
parseCtxDateExpression: (value: any) => value,
|
|
151
147
|
setupRunJSContexts: () => undefined,
|
|
152
148
|
getRunJSScenesForContext: () => [],
|
|
@@ -0,0 +1,83 @@
|
|
|
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 { FlowEngine, FlowRuntimeContext } from '@nocobase/flow-engine';
|
|
11
|
+
import { describe, expect, it } from 'vitest';
|
|
12
|
+
import { ActionModel } from '../ActionModel';
|
|
13
|
+
|
|
14
|
+
class IconActionModel extends ActionModel {
|
|
15
|
+
defaultProps = {
|
|
16
|
+
type: 'default' as const,
|
|
17
|
+
title: 'Icon action',
|
|
18
|
+
icon: 'PlusOutlined',
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
class PlainActionModel extends ActionModel {
|
|
23
|
+
defaultProps = {
|
|
24
|
+
type: 'default' as const,
|
|
25
|
+
title: 'Plain action',
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getButtonSettingsDefaultParams(model: ActionModel) {
|
|
30
|
+
const defaultParams = model.getFlow('buttonSettings')?.getStep('general')?.defaultParams;
|
|
31
|
+
if (typeof defaultParams !== 'function') {
|
|
32
|
+
throw new Error('buttonSettings.general.defaultParams is unavailable.');
|
|
33
|
+
}
|
|
34
|
+
return defaultParams(new FlowRuntimeContext(model, 'buttonSettings', 'settings'));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('ActionModel button settings', () => {
|
|
38
|
+
it('uses icon-only buttons by default for mobile icon actions', () => {
|
|
39
|
+
const engine = new FlowEngine();
|
|
40
|
+
engine.registerModels({ IconActionModel });
|
|
41
|
+
const model = engine.createModel<IconActionModel>({
|
|
42
|
+
use: 'IconActionModel',
|
|
43
|
+
uid: 'mobile-icon-action',
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
model.context.defineProperty('isMobileLayout', { value: true });
|
|
47
|
+
|
|
48
|
+
expect(getButtonSettingsDefaultParams(model)).toMatchObject({
|
|
49
|
+
icon: 'PlusOutlined',
|
|
50
|
+
iconOnly: true,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('keeps button titles visible by default outside mobile layouts', () => {
|
|
55
|
+
const engine = new FlowEngine();
|
|
56
|
+
engine.registerModels({ IconActionModel });
|
|
57
|
+
const model = engine.createModel<IconActionModel>({
|
|
58
|
+
use: 'IconActionModel',
|
|
59
|
+
uid: 'desktop-icon-action',
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(getButtonSettingsDefaultParams(model)).toMatchObject({
|
|
63
|
+
icon: 'PlusOutlined',
|
|
64
|
+
});
|
|
65
|
+
expect(getButtonSettingsDefaultParams(model)).not.toHaveProperty('iconOnly', true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('does not force icon-only buttons for mobile actions without icons', () => {
|
|
69
|
+
const engine = new FlowEngine();
|
|
70
|
+
engine.registerModels({ PlainActionModel });
|
|
71
|
+
const model = engine.createModel<PlainActionModel>({
|
|
72
|
+
use: 'PlainActionModel',
|
|
73
|
+
uid: 'mobile-plain-action',
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
model.context.defineProperty('isMobileLayout', { value: true });
|
|
77
|
+
|
|
78
|
+
expect(getButtonSettingsDefaultParams(model)).toMatchObject({
|
|
79
|
+
title: 'Plain action',
|
|
80
|
+
});
|
|
81
|
+
expect(getButtonSettingsDefaultParams(model)).not.toHaveProperty('iconOnly', true);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { FlowEngine } from '@nocobase/flow-engine';
|
|
11
11
|
import React from 'react';
|
|
12
12
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
13
|
-
import { GridModel } from '../GridModel';
|
|
13
|
+
import { GridModel, transformRowsToSingleColumn } from '../GridModel';
|
|
14
14
|
|
|
15
15
|
const createMockRect = ({ top, left, width, height }: { top: number; left: number; width: number; height: number }) => {
|
|
16
16
|
return {
|
|
@@ -66,6 +66,83 @@ const createGridContainer = () => {
|
|
|
66
66
|
return container;
|
|
67
67
|
};
|
|
68
68
|
|
|
69
|
+
const createGridContainerWithEmptyColumn = () => {
|
|
70
|
+
const container = document.createElement('div');
|
|
71
|
+
container.setAttribute('data-grid-root', '');
|
|
72
|
+
const row = document.createElement('div');
|
|
73
|
+
row.setAttribute('data-grid-row-id', 'row-1');
|
|
74
|
+
container.appendChild(row);
|
|
75
|
+
|
|
76
|
+
const firstColumn = document.createElement('div');
|
|
77
|
+
firstColumn.setAttribute('data-grid-column-row-id', 'row-1');
|
|
78
|
+
firstColumn.setAttribute('data-grid-column-index', '0');
|
|
79
|
+
row.appendChild(firstColumn);
|
|
80
|
+
|
|
81
|
+
const item = document.createElement('div');
|
|
82
|
+
item.setAttribute('data-grid-item-row-id', 'row-1');
|
|
83
|
+
item.setAttribute('data-grid-column-index', '0');
|
|
84
|
+
item.setAttribute('data-grid-item-index', '0');
|
|
85
|
+
item.setAttribute('data-grid-item-uid', 'item-1');
|
|
86
|
+
firstColumn.appendChild(item);
|
|
87
|
+
|
|
88
|
+
const secondColumn = document.createElement('div');
|
|
89
|
+
secondColumn.setAttribute('data-grid-column-row-id', 'row-1');
|
|
90
|
+
secondColumn.setAttribute('data-grid-column-index', '1');
|
|
91
|
+
row.appendChild(secondColumn);
|
|
92
|
+
|
|
93
|
+
mockRect(container, { top: 0, left: 0, width: 480, height: 280 });
|
|
94
|
+
mockRect(row, { top: 20, left: 20, width: 440, height: 120 });
|
|
95
|
+
mockRect(firstColumn, { top: 20, left: 20, width: 220, height: 120 });
|
|
96
|
+
mockRect(item, { top: 30, left: 30, width: 200, height: 100 });
|
|
97
|
+
mockRect(secondColumn, { top: 20, left: 260, width: 200, height: 120 });
|
|
98
|
+
return container;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const createProjectedMobileGridContainer = (rows: Record<string, string[][]>) => {
|
|
102
|
+
const container = document.createElement('div');
|
|
103
|
+
container.setAttribute('data-grid-root', '');
|
|
104
|
+
mockRect(container, { top: 0, left: 0, width: 480, height: 320 });
|
|
105
|
+
|
|
106
|
+
let top = 20;
|
|
107
|
+
Object.entries(transformRowsToSingleColumn(rows)).forEach(([rowId, columns]) => {
|
|
108
|
+
const row = document.createElement('div');
|
|
109
|
+
row.setAttribute('data-grid-row-id', rowId);
|
|
110
|
+
container.appendChild(row);
|
|
111
|
+
|
|
112
|
+
const column = document.createElement('div');
|
|
113
|
+
column.setAttribute('data-grid-column-row-id', rowId);
|
|
114
|
+
column.setAttribute('data-grid-column-index', '0');
|
|
115
|
+
row.appendChild(column);
|
|
116
|
+
|
|
117
|
+
const items = columns[0] || [];
|
|
118
|
+
items.forEach((itemUid, itemIndex) => {
|
|
119
|
+
const item = document.createElement('div');
|
|
120
|
+
item.setAttribute('data-grid-item-row-id', rowId);
|
|
121
|
+
item.setAttribute('data-grid-column-index', '0');
|
|
122
|
+
item.setAttribute('data-grid-item-index', String(itemIndex));
|
|
123
|
+
item.setAttribute('data-grid-item-uid', itemUid);
|
|
124
|
+
column.appendChild(item);
|
|
125
|
+
mockRect(item, { top: top + 10 + itemIndex * 36, left: 30, width: 420, height: 32 });
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
mockRect(row, { top, left: 20, width: 440, height: 52 });
|
|
129
|
+
mockRect(column, { top, left: 20, width: 440, height: 52 });
|
|
130
|
+
top += 72;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return container;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const setMobileGridItems = (engine: FlowEngine, model: GridModel, itemUids: string[]) => {
|
|
137
|
+
(model as any).subModels = {
|
|
138
|
+
items: itemUids.map((itemUid) => engine.createModel({ use: 'FlowModel', uid: itemUid })),
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const getSavedTopLevelItemOrder = (model: GridModel) => {
|
|
143
|
+
return Object.values(model.props.rows || {}).map((columns: string[][]) => columns[0]?.[0]);
|
|
144
|
+
};
|
|
145
|
+
|
|
69
146
|
describe('GridModel drag snapshot container', () => {
|
|
70
147
|
let engine: FlowEngine;
|
|
71
148
|
|
|
@@ -106,6 +183,167 @@ describe('GridModel drag snapshot container', () => {
|
|
|
106
183
|
model.handleDragCancel({} as any);
|
|
107
184
|
});
|
|
108
185
|
|
|
186
|
+
it('filters multi-column drop slots from mobile drag snapshots', () => {
|
|
187
|
+
const model = engine.createModel<GridModel>({
|
|
188
|
+
use: 'GridModel',
|
|
189
|
+
uid: 'grid-mobile-drag-slots',
|
|
190
|
+
props: {
|
|
191
|
+
rows: {
|
|
192
|
+
'row-1': [['item-1'], []],
|
|
193
|
+
},
|
|
194
|
+
sizes: {
|
|
195
|
+
'row-1': [12, 12],
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
structure: {} as any,
|
|
199
|
+
});
|
|
200
|
+
model.context.defineProperty('isMobileLayout', { value: true });
|
|
201
|
+
|
|
202
|
+
const container = createGridContainerWithEmptyColumn();
|
|
203
|
+
(model.gridContainerRef as any).current = container;
|
|
204
|
+
|
|
205
|
+
model.handleDragStart({
|
|
206
|
+
active: { id: 'item-1' },
|
|
207
|
+
activatorEvent: { clientX: 80, clientY: 80 },
|
|
208
|
+
} as any);
|
|
209
|
+
|
|
210
|
+
const slotTypes = ((model as any).dragState?.slots || []).map((slot) => slot.type);
|
|
211
|
+
expect(slotTypes).toContain('column');
|
|
212
|
+
expect(slotTypes).toContain('row-gap');
|
|
213
|
+
expect(slotTypes).not.toContain('column-edge');
|
|
214
|
+
expect(slotTypes).not.toContain('item-edge');
|
|
215
|
+
expect(slotTypes).not.toContain('empty-column');
|
|
216
|
+
model.handleDragCancel({} as any);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('keeps empty-row slot available for the first mobile grid item', () => {
|
|
220
|
+
const model = engine.createModel<GridModel>({
|
|
221
|
+
use: 'GridModel',
|
|
222
|
+
uid: 'grid-mobile-empty-drag-slots',
|
|
223
|
+
props: {},
|
|
224
|
+
structure: {} as any,
|
|
225
|
+
});
|
|
226
|
+
model.context.defineProperty('isMobileLayout', { value: true });
|
|
227
|
+
|
|
228
|
+
const container = document.createElement('div');
|
|
229
|
+
mockRect(container, { top: 0, left: 0, width: 480, height: 280 });
|
|
230
|
+
(model.gridContainerRef as any).current = container;
|
|
231
|
+
|
|
232
|
+
model.handleDragStart({
|
|
233
|
+
active: { id: 'item-1' },
|
|
234
|
+
activatorEvent: { clientX: 80, clientY: 80 },
|
|
235
|
+
} as any);
|
|
236
|
+
|
|
237
|
+
expect(((model as any).dragState?.slots || []).map((slot) => slot.type)).toEqual(['empty-row']);
|
|
238
|
+
model.handleDragCancel({} as any);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('saves mobile vertical sorting when moving the second item above the first one', () => {
|
|
242
|
+
const rows = {
|
|
243
|
+
'row-a': [['item-a']],
|
|
244
|
+
'row-b': [['item-b']],
|
|
245
|
+
};
|
|
246
|
+
const model = engine.createModel<GridModel>({
|
|
247
|
+
use: 'GridModel',
|
|
248
|
+
uid: 'grid-mobile-sort-second-above-first',
|
|
249
|
+
props: {
|
|
250
|
+
rows,
|
|
251
|
+
sizes: {
|
|
252
|
+
'row-a': [24],
|
|
253
|
+
'row-b': [24],
|
|
254
|
+
},
|
|
255
|
+
rowOrder: ['row-a', 'row-b'],
|
|
256
|
+
},
|
|
257
|
+
structure: {} as any,
|
|
258
|
+
});
|
|
259
|
+
model.context.defineProperty('isMobileLayout', { value: true });
|
|
260
|
+
setMobileGridItems(engine, model, ['item-a', 'item-b']);
|
|
261
|
+
(model as any).saveStepParams = vi.fn();
|
|
262
|
+
|
|
263
|
+
const container = createProjectedMobileGridContainer(rows);
|
|
264
|
+
(model.gridContainerRef as any).current = container;
|
|
265
|
+
|
|
266
|
+
model.handleDragStart({
|
|
267
|
+
active: { id: 'item-b' },
|
|
268
|
+
activatorEvent: { clientX: 240, clientY: 118 },
|
|
269
|
+
} as any);
|
|
270
|
+
model.handleDragEnd({
|
|
271
|
+
delta: { x: 0, y: -112 },
|
|
272
|
+
} as any);
|
|
273
|
+
|
|
274
|
+
expect(getSavedTopLevelItemOrder(model)).toEqual(['item-b', 'item-a']);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('saves mobile vertical sorting when moving the first item below the last one', () => {
|
|
278
|
+
const rows = {
|
|
279
|
+
'row-a': [['item-a']],
|
|
280
|
+
'row-b': [['item-b']],
|
|
281
|
+
'row-c': [['item-c']],
|
|
282
|
+
};
|
|
283
|
+
const model = engine.createModel<GridModel>({
|
|
284
|
+
use: 'GridModel',
|
|
285
|
+
uid: 'grid-mobile-sort-first-below-last',
|
|
286
|
+
props: {
|
|
287
|
+
rows,
|
|
288
|
+
sizes: {
|
|
289
|
+
'row-a': [24],
|
|
290
|
+
'row-b': [24],
|
|
291
|
+
'row-c': [24],
|
|
292
|
+
},
|
|
293
|
+
rowOrder: ['row-a', 'row-b', 'row-c'],
|
|
294
|
+
},
|
|
295
|
+
structure: {} as any,
|
|
296
|
+
});
|
|
297
|
+
model.context.defineProperty('isMobileLayout', { value: true });
|
|
298
|
+
setMobileGridItems(engine, model, ['item-a', 'item-b', 'item-c']);
|
|
299
|
+
(model as any).saveStepParams = vi.fn();
|
|
300
|
+
|
|
301
|
+
const container = createProjectedMobileGridContainer(rows);
|
|
302
|
+
(model.gridContainerRef as any).current = container;
|
|
303
|
+
|
|
304
|
+
model.handleDragStart({
|
|
305
|
+
active: { id: 'item-a' },
|
|
306
|
+
activatorEvent: { clientX: 240, clientY: 46 },
|
|
307
|
+
} as any);
|
|
308
|
+
model.handleDragEnd({
|
|
309
|
+
delta: { x: 0, y: 184 },
|
|
310
|
+
} as any);
|
|
311
|
+
|
|
312
|
+
expect(getSavedTopLevelItemOrder(model)).toEqual(['item-b', 'item-c', 'item-a']);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('does not filter multi-column drop slots from desktop drag snapshots', () => {
|
|
316
|
+
const model = engine.createModel<GridModel>({
|
|
317
|
+
use: 'GridModel',
|
|
318
|
+
uid: 'grid-desktop-drag-slots',
|
|
319
|
+
props: {
|
|
320
|
+
rows: {
|
|
321
|
+
'row-1': [['item-1'], []],
|
|
322
|
+
},
|
|
323
|
+
sizes: {
|
|
324
|
+
'row-1': [12, 12],
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
structure: {} as any,
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const container = createGridContainerWithEmptyColumn();
|
|
331
|
+
(model.gridContainerRef as any).current = container;
|
|
332
|
+
|
|
333
|
+
model.handleDragStart({
|
|
334
|
+
active: { id: 'item-1' },
|
|
335
|
+
activatorEvent: { clientX: 80, clientY: 80 },
|
|
336
|
+
} as any);
|
|
337
|
+
|
|
338
|
+
const slotTypes = ((model as any).dragState?.slots || []).map((slot) => slot.type);
|
|
339
|
+
expect(slotTypes).toContain('column');
|
|
340
|
+
expect(slotTypes).toContain('row-gap');
|
|
341
|
+
expect(slotTypes).toContain('column-edge');
|
|
342
|
+
expect(slotTypes).toContain('item-edge');
|
|
343
|
+
expect(slotTypes).toContain('empty-column');
|
|
344
|
+
model.handleDragCancel({} as any);
|
|
345
|
+
});
|
|
346
|
+
|
|
109
347
|
it('uses native pointer position instead of scroll-adjusted drag delta', () => {
|
|
110
348
|
const model = engine.createModel<GridModel>({
|
|
111
349
|
use: 'GridModel',
|
|
@@ -35,6 +35,36 @@ describe('transformRowsToSingleColumn', () => {
|
|
|
35
35
|
expect(all).toEqual([['a'], ['b', 'c'], ['d'], ['e']]);
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
+
it('should preserve row ids for ordinary single-column mobile rows', () => {
|
|
39
|
+
const rows = {
|
|
40
|
+
row_a: [['a']],
|
|
41
|
+
row_b: [['b']],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
expect(transformRowsToSingleColumn(rows)).toEqual(rows);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should avoid adding another mobile prefix to ordinary single-column projected rows', () => {
|
|
48
|
+
const rows = {
|
|
49
|
+
'mobile:row_a:0': [['a']],
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
expect(Object.keys(transformRowsToSingleColumn(rows))).toEqual(['mobile:row_a:0']);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should keep split multi-column row keys stable and unique', () => {
|
|
56
|
+
const rows = {
|
|
57
|
+
row_a: [['a'], ['b']],
|
|
58
|
+
};
|
|
59
|
+
const result = transformRowsToSingleColumn(rows);
|
|
60
|
+
const keys = Object.keys(result);
|
|
61
|
+
|
|
62
|
+
expect(keys).toHaveLength(2);
|
|
63
|
+
expect(new Set(keys).size).toBe(2);
|
|
64
|
+
expect(keys.every((key) => key.startsWith('mobile:'))).toBe(true);
|
|
65
|
+
expect(Object.values(result).map((row) => row[0])).toEqual([['a'], ['b']]);
|
|
66
|
+
});
|
|
67
|
+
|
|
38
68
|
it('should skip columns that only contain EMPTY_COLUMN_UID', () => {
|
|
39
69
|
const rows = buildRows([[[EMPTY_COLUMN_UID], ['a']]]);
|
|
40
70
|
const result = transformRowsToSingleColumn(rows);
|
|
@@ -55,4 +85,22 @@ describe('transformRowsToSingleColumn', () => {
|
|
|
55
85
|
const all = Object.values(result).map((r) => r[0]);
|
|
56
86
|
expect(all).toEqual([['x'], ['y']]);
|
|
57
87
|
});
|
|
88
|
+
|
|
89
|
+
it('should keep generated row keys stable for the same input', () => {
|
|
90
|
+
const rows = buildRows([
|
|
91
|
+
[['a'], ['b']],
|
|
92
|
+
[['c'], ['d']],
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
expect(Object.keys(transformRowsToSingleColumn(rows))).toEqual(Object.keys(transformRowsToSingleColumn(rows)));
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should keep existing row keys stable when appending new mobile rows', () => {
|
|
99
|
+
const before = buildRows([[['a'], ['b']]]);
|
|
100
|
+
const after = buildRows([[['a'], ['b']], [['c']]]);
|
|
101
|
+
const beforeKeys = Object.keys(transformRowsToSingleColumn(before));
|
|
102
|
+
const afterKeys = Object.keys(transformRowsToSingleColumn(after));
|
|
103
|
+
|
|
104
|
+
expect(afterKeys.slice(0, beforeKeys.length)).toEqual(beforeKeys);
|
|
105
|
+
});
|
|
58
106
|
});
|
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
Droppable,
|
|
18
18
|
isRunJSValue,
|
|
19
19
|
normalizeRunJSValue,
|
|
20
|
-
runjsWithSafeGlobals,
|
|
21
20
|
tExpr,
|
|
22
21
|
FlowModelRenderer,
|
|
23
22
|
FlowSettingsButton,
|
|
@@ -493,7 +492,7 @@ export class FilterFormBlockModel extends FilterBlockModel<{
|
|
|
493
492
|
// RunJS support
|
|
494
493
|
if (isRunJSValue(raw)) {
|
|
495
494
|
const { code, version } = normalizeRunJSValue(raw);
|
|
496
|
-
const ret = await
|
|
495
|
+
const ret = await this.context.runjs(code, undefined, { version });
|
|
497
496
|
return ret?.success ? ret.value : undefined;
|
|
498
497
|
}
|
|
499
498
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { tExpr } from '@nocobase/flow-engine';
|
|
11
11
|
import { CodeEditor } from '../../../components/code-editor';
|
|
12
12
|
import { FilterFormActionModel } from './FilterFormActionModel';
|
|
13
13
|
import { resolveRunJsParams } from '../../utils/resolveRunJsParams';
|
|
@@ -60,12 +60,7 @@ FilterFormJSActionModel.registerFlow({
|
|
|
60
60
|
},
|
|
61
61
|
async handler(ctx, params) {
|
|
62
62
|
const { code, version } = resolveRunJsParams(ctx, params);
|
|
63
|
-
|
|
64
|
-
await ctx.runjs(
|
|
65
|
-
code,
|
|
66
|
-
{ window: createSafeWindow({ navigator }), document: createSafeDocument(), navigator },
|
|
67
|
-
{ version },
|
|
68
|
-
);
|
|
63
|
+
await ctx.runjs(code, undefined, { version });
|
|
69
64
|
},
|
|
70
65
|
},
|
|
71
66
|
},
|
|
@@ -210,7 +210,7 @@ FilterFormCustomFieldModel.registerFlow({
|
|
|
210
210
|
'x-decorator': 'FormItem',
|
|
211
211
|
required: true,
|
|
212
212
|
description:
|
|
213
|
-
'{{t("Randomly generated and can be modified. Support letters, numbers and underscores, must start with
|
|
213
|
+
'{{t("Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.")}}',
|
|
214
214
|
},
|
|
215
215
|
source: {
|
|
216
216
|
type: 'array',
|