@nocobase/flow-engine 2.2.0-alpha.5 → 2.2.0-alpha.7
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/lib/components/FlowContextSelector.js +2 -2
- package/lib/components/FormItem.js +11 -7
- package/lib/components/MobilePopup.js +28 -10
- package/lib/components/MobilePopup.style.js +11 -1
- package/lib/components/variables/VariableInput.js +1 -0
- package/lib/flowEngine.js +6 -0
- package/lib/utils/loadedPageCache.d.ts +1 -0
- package/lib/utils/loadedPageCache.js +6 -0
- package/package.json +4 -4
- package/src/__tests__/viewScopedFlowEngine.test.ts +72 -6
- package/src/components/FlowContextSelector.tsx +2 -2
- package/src/components/FormItem.tsx +12 -7
- package/src/components/MobilePopup.style.ts +12 -1
- package/src/components/MobilePopup.tsx +30 -10
- package/src/components/__tests__/FormItem.test.tsx +17 -2
- package/src/components/__tests__/MobilePopup.test.tsx +109 -0
- package/src/components/variables/VariableInput.tsx +1 -0
- package/src/components/variables/__tests__/VariableInput.test.tsx +12 -0
- package/src/flowEngine.ts +6 -0
- package/src/utils/loadedPageCache.ts +7 -0
|
@@ -243,8 +243,8 @@ const FlowContextSelectorComponent = /* @__PURE__ */ __name(({
|
|
|
243
243
|
}, [options, pathToPreload, triggerUpdate]);
|
|
244
244
|
const defaultChildren = (0, import_react.useMemo)(() => {
|
|
245
245
|
const hasSelected = active ?? Boolean(currentPath && currentPath.length > 0);
|
|
246
|
-
return /* @__PURE__ */ import_react.default.createElement(import_antd.Button, { type: hasSelected ? "primary" : "default", style: defaultButtonStyle }, "x");
|
|
247
|
-
}, [active, currentPath]);
|
|
246
|
+
return /* @__PURE__ */ import_react.default.createElement(import_antd.Button, { type: hasSelected ? "primary" : "default", style: defaultButtonStyle, disabled: cascaderProps.disabled }, "x");
|
|
247
|
+
}, [active, cascaderProps.disabled, currentPath]);
|
|
248
248
|
const handleChange = (0, import_react.useCallback)(
|
|
249
249
|
(selectedValues, selectedOptions) => {
|
|
250
250
|
const lastOption = selectedOptions == null ? void 0 : selectedOptions[selectedOptions.length - 1];
|
|
@@ -77,14 +77,18 @@ const formItemPropKeys = [
|
|
|
77
77
|
"required",
|
|
78
78
|
"showLabel"
|
|
79
79
|
];
|
|
80
|
+
const modelInternalPropKeys = ["globalSort"];
|
|
80
81
|
const FormItem = /* @__PURE__ */ __name(({
|
|
81
82
|
children,
|
|
82
83
|
showLabel = true,
|
|
83
84
|
labelWidth,
|
|
84
85
|
...rest
|
|
85
86
|
}) => {
|
|
87
|
+
const forwardedRest = Object.fromEntries(
|
|
88
|
+
Object.entries(rest).filter(([key]) => !modelInternalPropKeys.includes(key))
|
|
89
|
+
);
|
|
86
90
|
const childProps = Object.fromEntries(
|
|
87
|
-
Object.entries(
|
|
91
|
+
Object.entries(forwardedRest).filter(([key]) => !formItemPropKeys.includes(key))
|
|
88
92
|
);
|
|
89
93
|
const processedChildren = typeof children === "function" ? children : import_react.default.Children.map(children, (child) => {
|
|
90
94
|
if (import_react.default.isValidElement(child)) {
|
|
@@ -92,7 +96,7 @@ const FormItem = /* @__PURE__ */ __name(({
|
|
|
92
96
|
}
|
|
93
97
|
return child;
|
|
94
98
|
});
|
|
95
|
-
const { label, labelWrap, colon = true, layout } =
|
|
99
|
+
const { label, labelWrap, colon = true, layout } = forwardedRest;
|
|
96
100
|
const effectiveLabelWrap = !layout || layout === "vertical" ? true : labelWrap;
|
|
97
101
|
const labelColStyle = layout === "vertical" ? { width: labelWidth, ...verticalFormItemLabelStyle } : { width: labelWidth };
|
|
98
102
|
const renderLabel = /* @__PURE__ */ __name(() => {
|
|
@@ -136,15 +140,15 @@ const FormItem = /* @__PURE__ */ __name(({
|
|
|
136
140
|
return /* @__PURE__ */ import_react.default.createElement(
|
|
137
141
|
import_antd.Form.Item,
|
|
138
142
|
{
|
|
139
|
-
...
|
|
140
|
-
style: { ...formItemStyle, ...
|
|
143
|
+
...forwardedRest,
|
|
144
|
+
style: { ...formItemStyle, ...forwardedRest.style },
|
|
141
145
|
labelCol: { style: labelColStyle },
|
|
142
146
|
layout,
|
|
143
147
|
label: renderLabel(),
|
|
144
148
|
colon: false,
|
|
145
|
-
extra:
|
|
146
|
-
tooltip:
|
|
147
|
-
title:
|
|
149
|
+
extra: forwardedRest.extra && /* @__PURE__ */ import_react.default.createElement("span", { style: { whiteSpace: "pre-wrap" } }, forwardedRest.extra),
|
|
150
|
+
tooltip: forwardedRest.tooltip && {
|
|
151
|
+
title: forwardedRest.tooltip,
|
|
148
152
|
overlayInnerStyle: { whiteSpace: "pre-line" }
|
|
149
153
|
}
|
|
150
154
|
},
|
|
@@ -48,14 +48,34 @@ var import_lazy_helper = require("../lazy-helper");
|
|
|
48
48
|
const { Popup } = (0, import_lazy_helper.lazy)(() => import("antd-mobile"), "Popup");
|
|
49
49
|
const { CloseOutline } = (0, import_lazy_helper.lazy)(() => import("antd-mobile-icons"), "CloseOutline");
|
|
50
50
|
const MobilePopup = /* @__PURE__ */ __name((props) => {
|
|
51
|
+
var _a;
|
|
51
52
|
const { title, visible, onClose: closePopup, children, minHeight, className, footer } = props;
|
|
52
53
|
const { t } = (0, import_react_i18next.useTranslation)();
|
|
53
54
|
const { componentCls, hashId } = (0, import_MobilePopup.useMobileActionDrawerStyle)();
|
|
54
|
-
const
|
|
55
|
+
const bodyStyles = (_a = props.styles) == null ? void 0 : _a.body;
|
|
56
|
+
const popupStyle = (0, import_react.useMemo)(() => {
|
|
55
57
|
return {
|
|
56
|
-
minHeight
|
|
58
|
+
minHeight: (bodyStyles == null ? void 0 : bodyStyles.minHeight) ?? minHeight,
|
|
59
|
+
height: bodyStyles == null ? void 0 : bodyStyles.height,
|
|
60
|
+
maxHeight: bodyStyles == null ? void 0 : bodyStyles.maxHeight
|
|
57
61
|
};
|
|
58
|
-
}, [minHeight]);
|
|
62
|
+
}, [bodyStyles == null ? void 0 : bodyStyles.height, bodyStyles == null ? void 0 : bodyStyles.maxHeight, bodyStyles == null ? void 0 : bodyStyles.minHeight, minHeight]);
|
|
63
|
+
const bodyStyle = (0, import_react.useMemo)(() => {
|
|
64
|
+
return {
|
|
65
|
+
padding: 0,
|
|
66
|
+
...bodyStyles
|
|
67
|
+
};
|
|
68
|
+
}, [bodyStyles]);
|
|
69
|
+
const handleCloseKeyDown = (0, import_react.useCallback)(
|
|
70
|
+
(event) => {
|
|
71
|
+
if (event.key !== "Enter" && event.key !== " ") {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
event.preventDefault();
|
|
75
|
+
closePopup();
|
|
76
|
+
},
|
|
77
|
+
[closePopup]
|
|
78
|
+
);
|
|
59
79
|
const theme = (0, import_react.useMemo)(() => {
|
|
60
80
|
return {
|
|
61
81
|
token: {
|
|
@@ -75,21 +95,19 @@ const MobilePopup = /* @__PURE__ */ __name((props) => {
|
|
|
75
95
|
onClose: closePopup,
|
|
76
96
|
onMaskClick: closePopup,
|
|
77
97
|
bodyClassName: "nb-mobile-action-drawer-body",
|
|
78
|
-
bodyStyle
|
|
79
|
-
|
|
80
|
-
},
|
|
81
|
-
maskStyle: style,
|
|
82
|
-
style,
|
|
98
|
+
bodyStyle,
|
|
99
|
+
style: popupStyle,
|
|
83
100
|
destroyOnClose: true
|
|
84
101
|
},
|
|
85
|
-
/* @__PURE__ */ import_react.default.createElement("div", { className: "nb-mobile-action-drawer-header" }, /* @__PURE__ */ import_react.default.createElement("span", { className: "nb-mobile-action-drawer-placeholder" }, /* @__PURE__ */ import_react.default.createElement(CloseOutline, null)), /* @__PURE__ */ import_react.default.createElement("span",
|
|
102
|
+
/* @__PURE__ */ import_react.default.createElement("div", { className: "nb-mobile-action-drawer-header" }, /* @__PURE__ */ import_react.default.createElement("span", { className: "nb-mobile-action-drawer-placeholder" }, /* @__PURE__ */ import_react.default.createElement(CloseOutline, null)), /* @__PURE__ */ import_react.default.createElement("span", { className: "nb-mobile-action-drawer-title" }, title), /* @__PURE__ */ import_react.default.createElement(
|
|
86
103
|
"span",
|
|
87
104
|
{
|
|
88
105
|
className: "nb-mobile-action-drawer-close-icon",
|
|
89
106
|
onClick: closePopup,
|
|
90
107
|
role: "button",
|
|
91
108
|
tabIndex: 0,
|
|
92
|
-
"aria-label": t("Close")
|
|
109
|
+
"aria-label": t("Close"),
|
|
110
|
+
onKeyDown: handleCloseKeyDown
|
|
93
111
|
},
|
|
94
112
|
/* @__PURE__ */ import_react.default.createElement(CloseOutline, null)
|
|
95
113
|
)),
|
|
@@ -127,7 +127,7 @@ const useMobileActionDrawerStyle = genStyleHook("nb-mobile-action-drawer", (toke
|
|
|
127
127
|
borderBottom: `1px solid ${token.colorSplit}`,
|
|
128
128
|
position: "sticky",
|
|
129
129
|
top: 0,
|
|
130
|
-
backgroundColor:
|
|
130
|
+
backgroundColor: token.colorBgContainer,
|
|
131
131
|
zIndex: 1e3,
|
|
132
132
|
// to match the button named 'Add block'
|
|
133
133
|
"& + .nb-grid-container > .nb-grid > .nb-grid-warp > .ant-btn": {
|
|
@@ -137,11 +137,21 @@ const useMobileActionDrawerStyle = genStyleHook("nb-mobile-action-drawer", (toke
|
|
|
137
137
|
".nb-mobile-action-drawer-placeholder": {
|
|
138
138
|
display: "inline-block",
|
|
139
139
|
padding: 12,
|
|
140
|
+
flex: "0 0 auto",
|
|
140
141
|
visibility: "hidden"
|
|
141
142
|
},
|
|
143
|
+
".nb-mobile-action-drawer-title": {
|
|
144
|
+
flex: "1 1 auto",
|
|
145
|
+
minWidth: 0,
|
|
146
|
+
overflow: "hidden",
|
|
147
|
+
textAlign: "center",
|
|
148
|
+
textOverflow: "ellipsis",
|
|
149
|
+
whiteSpace: "nowrap"
|
|
150
|
+
},
|
|
142
151
|
".nb-mobile-action-drawer-close-icon": {
|
|
143
152
|
display: "inline-block",
|
|
144
153
|
padding: 12,
|
|
154
|
+
flex: "0 0 auto",
|
|
145
155
|
cursor: "pointer"
|
|
146
156
|
},
|
|
147
157
|
".nb-mobile-action-drawer-body": {
|
|
@@ -326,6 +326,7 @@ const VariableInputComponent = /* @__PURE__ */ __name(({
|
|
|
326
326
|
metaTree: resolvedMetaTree,
|
|
327
327
|
value: innerValue,
|
|
328
328
|
active: (0, import_utils.isVariableValue)(innerValue),
|
|
329
|
+
disabled,
|
|
329
330
|
onChange: handleVariableSelect,
|
|
330
331
|
parseValueToPath: resolvePathFromValue,
|
|
331
332
|
formatPathToValue: resolveValueFromPath,
|
package/lib/flowEngine.js
CHANGED
|
@@ -1141,6 +1141,9 @@ const _FlowEngine = class _FlowEngine {
|
|
|
1141
1141
|
if (!this.ensureModelRepository()) return;
|
|
1142
1142
|
const refresh = !!(options == null ? void 0 : options.refresh);
|
|
1143
1143
|
const bypassLoadedPageCache = this._loadedPageCache.shouldBypass(options, () => this.context.flowSettingsEnabled);
|
|
1144
|
+
if (this.context.flowSettingsEnabled) {
|
|
1145
|
+
this._loadedPageCache.markDirtyForOptions(options);
|
|
1146
|
+
}
|
|
1144
1147
|
if (!refresh && !bypassLoadedPageCache) {
|
|
1145
1148
|
const model2 = this.findModelByParentId(options.parentId, options.subKey);
|
|
1146
1149
|
if (model2) {
|
|
@@ -1201,6 +1204,9 @@ const _FlowEngine = class _FlowEngine {
|
|
|
1201
1204
|
if (!this.ensureModelRepository()) return;
|
|
1202
1205
|
const { uid, parentId, subKey } = options;
|
|
1203
1206
|
const bypassLoadedPageCache = this._loadedPageCache.shouldBypass(options, () => this.context.flowSettingsEnabled);
|
|
1207
|
+
if (this.context.flowSettingsEnabled) {
|
|
1208
|
+
this._loadedPageCache.markDirtyForOptions(options);
|
|
1209
|
+
}
|
|
1204
1210
|
if (uid && !bypassLoadedPageCache && this._modelInstances.has(uid)) {
|
|
1205
1211
|
return this._modelInstances.get(uid);
|
|
1206
1212
|
}
|
|
@@ -17,6 +17,7 @@ type DirtyKeyOptions = {
|
|
|
17
17
|
export declare const createLoadedPageCache: () => {
|
|
18
18
|
getDirtyKeyForModel(model?: FlowModel | null, options?: DirtyKeyOptions): string | undefined;
|
|
19
19
|
markDirty(key?: string): void;
|
|
20
|
+
markDirtyForOptions(options?: LoadedPageOptions): void;
|
|
20
21
|
shouldBypass(options?: LoadedPageOptions, isFlowSettingsEnabled?: () => boolean): boolean;
|
|
21
22
|
clear(options?: LoadedPageOptions): void;
|
|
22
23
|
mountModelToParent: <T extends FlowModel<import("..").DefaultStructure> = FlowModel<import("..").DefaultStructure>>(model: T, forceReplace?: boolean) => T;
|
|
@@ -113,6 +113,12 @@ const createLoadedPageCache = /* @__PURE__ */ __name(() => {
|
|
|
113
113
|
dirtyKeys.add(key);
|
|
114
114
|
}
|
|
115
115
|
},
|
|
116
|
+
markDirtyForOptions(options) {
|
|
117
|
+
const key = getLoadedPageKey(options);
|
|
118
|
+
if (key) {
|
|
119
|
+
dirtyKeys.add(key);
|
|
120
|
+
}
|
|
121
|
+
},
|
|
116
122
|
shouldBypass(options, isFlowSettingsEnabled) {
|
|
117
123
|
const key = getLoadedPageKey(options);
|
|
118
124
|
if (!key || !dirtyKeys.has(key)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/flow-engine",
|
|
3
|
-
"version": "2.2.0-alpha.
|
|
3
|
+
"version": "2.2.0-alpha.7",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A standalone flow engine for NocoBase, managing workflows, models, and actions.",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@formily/antd-v5": "1.x",
|
|
10
10
|
"@formily/reactive": "2.x",
|
|
11
|
-
"@nocobase/sdk": "2.2.0-alpha.
|
|
12
|
-
"@nocobase/shared": "2.2.0-alpha.
|
|
11
|
+
"@nocobase/sdk": "2.2.0-alpha.7",
|
|
12
|
+
"@nocobase/shared": "2.2.0-alpha.7",
|
|
13
13
|
"ahooks": "^3.7.2",
|
|
14
14
|
"axios": "^1.7.0",
|
|
15
15
|
"dayjs": "^1.11.9",
|
|
@@ -37,5 +37,5 @@
|
|
|
37
37
|
],
|
|
38
38
|
"author": "NocoBase Team",
|
|
39
39
|
"license": "Apache-2.0",
|
|
40
|
-
"gitHead": "
|
|
40
|
+
"gitHead": "34ba02960fca9ab0b6881d3db7040a8a64067bba"
|
|
41
41
|
}
|
|
@@ -340,9 +340,9 @@ describe('ViewScopedFlowEngine', () => {
|
|
|
340
340
|
const repository = new DirtyPageRepository();
|
|
341
341
|
root.setModelRepository(repository);
|
|
342
342
|
|
|
343
|
-
class ParentModel extends FlowModel {}
|
|
344
|
-
class PageModel extends FlowModel {}
|
|
345
343
|
class BlockModel extends FlowModel {}
|
|
344
|
+
class PageModel extends FlowModel<{ parent?: FlowModel; subModels: { items: BlockModel[] } }> {}
|
|
345
|
+
class ParentModel extends FlowModel<{ parent?: FlowModel; subModels: { page?: PageModel } }> {}
|
|
346
346
|
root.registerModels({ ParentModel, PageModel, BlockModel });
|
|
347
347
|
|
|
348
348
|
const parent = root.createModel<ParentModel>({ use: 'ParentModel', uid: 'popup-action' });
|
|
@@ -357,7 +357,10 @@ describe('ViewScopedFlowEngine', () => {
|
|
|
357
357
|
items: [{ use: 'BlockModel', uid: 'stale-block' }],
|
|
358
358
|
},
|
|
359
359
|
});
|
|
360
|
-
const staleBlock = stalePage.findSubModel('items'
|
|
360
|
+
const staleBlock = stalePage.findSubModel('items', (item) => item.uid === 'stale-block');
|
|
361
|
+
if (!staleBlock) {
|
|
362
|
+
throw new Error('Expected stale block to be loaded');
|
|
363
|
+
}
|
|
361
364
|
parent.setSubModel('page', stalePage);
|
|
362
365
|
oldScoped.unlinkFromStack();
|
|
363
366
|
|
|
@@ -372,7 +375,7 @@ describe('ViewScopedFlowEngine', () => {
|
|
|
372
375
|
},
|
|
373
376
|
};
|
|
374
377
|
|
|
375
|
-
root.flowSettings.enable();
|
|
378
|
+
await root.flowSettings.enable();
|
|
376
379
|
await staleBlock.saveStepParams();
|
|
377
380
|
root.flowSettings.disable();
|
|
378
381
|
repository.findOneCalls = 0;
|
|
@@ -388,8 +391,8 @@ describe('ViewScopedFlowEngine', () => {
|
|
|
388
391
|
|
|
389
392
|
expect(repository.findOneCalls).toBe(1);
|
|
390
393
|
expect(loaded).not.toBe(stalePage);
|
|
391
|
-
expect(
|
|
392
|
-
expect(loaded?.mapSubModels('items'
|
|
394
|
+
expect(parent.subModels.page).toBe(loaded);
|
|
395
|
+
expect(loaded?.mapSubModels('items', (item) => item.uid)).toEqual(['fresh-block']);
|
|
393
396
|
|
|
394
397
|
repository.findOneCalls = 0;
|
|
395
398
|
const nextRuntimeScoped = createViewScopedEngine(root);
|
|
@@ -405,6 +408,69 @@ describe('ViewScopedFlowEngine', () => {
|
|
|
405
408
|
expect(loadedAgain?.uid).toBe('popup-page');
|
|
406
409
|
});
|
|
407
410
|
|
|
411
|
+
it('reloads a page after it was loaded in flow settings mode', async () => {
|
|
412
|
+
const root = new FlowEngine();
|
|
413
|
+
const repository = new DirtyPageRepository();
|
|
414
|
+
root.setModelRepository(repository);
|
|
415
|
+
|
|
416
|
+
class ParentModel extends FlowModel {}
|
|
417
|
+
class PageModel extends FlowModel {}
|
|
418
|
+
class BlockModel extends FlowModel {}
|
|
419
|
+
root.registerModels({ ParentModel, PageModel, BlockModel });
|
|
420
|
+
|
|
421
|
+
const parent = root.createModel<ParentModel>({ use: 'ParentModel', uid: 'settings-popup-action' });
|
|
422
|
+
repository.data = {
|
|
423
|
+
use: 'PageModel',
|
|
424
|
+
uid: 'settings-popup-page',
|
|
425
|
+
parentId: parent.uid,
|
|
426
|
+
subKey: 'page',
|
|
427
|
+
subType: 'object',
|
|
428
|
+
subModels: {
|
|
429
|
+
items: [{ use: 'BlockModel', uid: 'stale-settings-block' }],
|
|
430
|
+
},
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
await root.flowSettings.enable();
|
|
434
|
+
const designScoped = createViewScopedEngine(root);
|
|
435
|
+
const designLoaded = await designScoped.loadOrCreateModel<PageModel>({
|
|
436
|
+
async: true,
|
|
437
|
+
parentId: parent.uid,
|
|
438
|
+
subKey: 'page',
|
|
439
|
+
subType: 'object',
|
|
440
|
+
use: 'PageModel',
|
|
441
|
+
});
|
|
442
|
+
expect(repository.findOneCalls).toBe(1);
|
|
443
|
+
expect(designLoaded?.mapSubModels('items', (item) => item.uid)).toEqual(['stale-settings-block']);
|
|
444
|
+
designScoped.unlinkFromStack();
|
|
445
|
+
|
|
446
|
+
repository.data = {
|
|
447
|
+
use: 'PageModel',
|
|
448
|
+
uid: 'settings-popup-page',
|
|
449
|
+
parentId: parent.uid,
|
|
450
|
+
subKey: 'page',
|
|
451
|
+
subType: 'object',
|
|
452
|
+
subModels: {
|
|
453
|
+
items: [{ use: 'BlockModel', uid: 'fresh-settings-block' }],
|
|
454
|
+
},
|
|
455
|
+
};
|
|
456
|
+
root.flowSettings.disable();
|
|
457
|
+
repository.findOneCalls = 0;
|
|
458
|
+
|
|
459
|
+
const runtimeScoped = createViewScopedEngine(root);
|
|
460
|
+
const runtimeLoaded = await runtimeScoped.loadOrCreateModel<PageModel>({
|
|
461
|
+
async: true,
|
|
462
|
+
parentId: parent.uid,
|
|
463
|
+
subKey: 'page',
|
|
464
|
+
subType: 'object',
|
|
465
|
+
use: 'PageModel',
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
expect(repository.findOneCalls).toBe(1);
|
|
469
|
+
expect(runtimeLoaded).not.toBe(designLoaded);
|
|
470
|
+
expect(parent.subModels.page).toBe(runtimeLoaded);
|
|
471
|
+
expect(runtimeLoaded?.mapSubModels('items', (item) => item.uid)).toEqual(['fresh-settings-block']);
|
|
472
|
+
});
|
|
473
|
+
|
|
408
474
|
it('does not bypass loaded page cache after a non-config save', async () => {
|
|
409
475
|
const root = new FlowEngine();
|
|
410
476
|
const repository = new DirtyPageRepository();
|
|
@@ -292,11 +292,11 @@ const FlowContextSelectorComponent: React.FC<FlowContextSelectorProps> = ({
|
|
|
292
292
|
const defaultChildren = useMemo(() => {
|
|
293
293
|
const hasSelected = active ?? Boolean(currentPath && currentPath.length > 0);
|
|
294
294
|
return (
|
|
295
|
-
<Button type={hasSelected ? 'primary' : 'default'} style={defaultButtonStyle}>
|
|
295
|
+
<Button type={hasSelected ? 'primary' : 'default'} style={defaultButtonStyle} disabled={cascaderProps.disabled}>
|
|
296
296
|
x
|
|
297
297
|
</Button>
|
|
298
298
|
);
|
|
299
|
-
}, [active, currentPath]);
|
|
299
|
+
}, [active, cascaderProps.disabled, currentPath]);
|
|
300
300
|
|
|
301
301
|
// 处理选择变化事件
|
|
302
302
|
const handleChange = useCallback(
|
|
@@ -54,15 +54,20 @@ const formItemPropKeys: (keyof ExtendedFormItemProps)[] = [
|
|
|
54
54
|
'showLabel',
|
|
55
55
|
];
|
|
56
56
|
|
|
57
|
+
const modelInternalPropKeys = ['globalSort'];
|
|
58
|
+
|
|
57
59
|
export const FormItem = ({
|
|
58
60
|
children,
|
|
59
61
|
showLabel = true,
|
|
60
62
|
labelWidth,
|
|
61
63
|
...rest
|
|
62
64
|
}: ExtendedFormItemProps & ChildExtraProps) => {
|
|
65
|
+
const forwardedRest = Object.fromEntries(
|
|
66
|
+
Object.entries(rest).filter(([key]) => !modelInternalPropKeys.includes(key)),
|
|
67
|
+
) as ExtendedFormItemProps & ChildExtraProps;
|
|
63
68
|
// 过滤掉 Form.Item 专用 props,只保留要传给子组件的
|
|
64
69
|
const childProps = Object.fromEntries(
|
|
65
|
-
Object.entries(
|
|
70
|
+
Object.entries(forwardedRest).filter(([key]) => !formItemPropKeys.includes(key as keyof ExtendedFormItemProps)),
|
|
66
71
|
);
|
|
67
72
|
|
|
68
73
|
const processedChildren =
|
|
@@ -74,7 +79,7 @@ export const FormItem = ({
|
|
|
74
79
|
}
|
|
75
80
|
return child;
|
|
76
81
|
});
|
|
77
|
-
const { label, labelWrap, colon = true, layout } =
|
|
82
|
+
const { label, labelWrap, colon = true, layout } = forwardedRest;
|
|
78
83
|
const effectiveLabelWrap = !layout || layout === 'vertical' ? true : labelWrap;
|
|
79
84
|
const labelColStyle =
|
|
80
85
|
layout === 'vertical' ? { width: labelWidth, ...verticalFormItemLabelStyle } : { width: labelWidth };
|
|
@@ -122,17 +127,17 @@ export const FormItem = ({
|
|
|
122
127
|
};
|
|
123
128
|
return (
|
|
124
129
|
<Form.Item
|
|
125
|
-
{...
|
|
126
|
-
style={{ ...formItemStyle, ...
|
|
130
|
+
{...forwardedRest}
|
|
131
|
+
style={{ ...formItemStyle, ...forwardedRest.style }}
|
|
127
132
|
labelCol={{ style: labelColStyle }}
|
|
128
133
|
layout={layout}
|
|
129
134
|
label={renderLabel()}
|
|
130
135
|
colon={false}
|
|
131
|
-
extra={
|
|
136
|
+
extra={forwardedRest.extra && <span style={{ whiteSpace: 'pre-wrap' }}>{forwardedRest.extra}</span>}
|
|
132
137
|
tooltip={
|
|
133
|
-
|
|
138
|
+
forwardedRest.tooltip &&
|
|
134
139
|
({
|
|
135
|
-
title:
|
|
140
|
+
title: forwardedRest.tooltip,
|
|
136
141
|
overlayInnerStyle: { whiteSpace: 'pre-line' },
|
|
137
142
|
} as TooltipProps)
|
|
138
143
|
}
|
|
@@ -160,7 +160,7 @@ export const useMobileActionDrawerStyle = genStyleHook('nb-mobile-action-drawer'
|
|
|
160
160
|
borderBottom: `1px solid ${token.colorSplit}`,
|
|
161
161
|
position: 'sticky',
|
|
162
162
|
top: 0,
|
|
163
|
-
backgroundColor:
|
|
163
|
+
backgroundColor: token.colorBgContainer,
|
|
164
164
|
zIndex: 1000,
|
|
165
165
|
|
|
166
166
|
// to match the button named 'Add block'
|
|
@@ -172,12 +172,23 @@ export const useMobileActionDrawerStyle = genStyleHook('nb-mobile-action-drawer'
|
|
|
172
172
|
'.nb-mobile-action-drawer-placeholder': {
|
|
173
173
|
display: 'inline-block',
|
|
174
174
|
padding: 12,
|
|
175
|
+
flex: '0 0 auto',
|
|
175
176
|
visibility: 'hidden',
|
|
176
177
|
},
|
|
177
178
|
|
|
179
|
+
'.nb-mobile-action-drawer-title': {
|
|
180
|
+
flex: '1 1 auto',
|
|
181
|
+
minWidth: 0,
|
|
182
|
+
overflow: 'hidden',
|
|
183
|
+
textAlign: 'center',
|
|
184
|
+
textOverflow: 'ellipsis',
|
|
185
|
+
whiteSpace: 'nowrap',
|
|
186
|
+
},
|
|
187
|
+
|
|
178
188
|
'.nb-mobile-action-drawer-close-icon': {
|
|
179
189
|
display: 'inline-block',
|
|
180
190
|
padding: 12,
|
|
191
|
+
flex: '0 0 auto',
|
|
181
192
|
cursor: 'pointer',
|
|
182
193
|
},
|
|
183
194
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { ConfigProvider } from 'antd';
|
|
11
|
-
import React, { FC, ReactNode, useMemo } from 'react';
|
|
11
|
+
import React, { FC, ReactNode, useCallback, useMemo } from 'react';
|
|
12
12
|
import { useMobileActionDrawerStyle } from './MobilePopup.style';
|
|
13
13
|
import { useTranslation } from 'react-i18next';
|
|
14
14
|
import { lazy } from '../lazy-helper';
|
|
@@ -31,11 +31,33 @@ export const MobilePopup: FC<MobilePopupProps> = (props) => {
|
|
|
31
31
|
const { t } = useTranslation();
|
|
32
32
|
const { componentCls, hashId } = useMobileActionDrawerStyle();
|
|
33
33
|
|
|
34
|
-
const
|
|
34
|
+
const bodyStyles = (props as MobilePopupProps & { styles?: { body?: React.CSSProperties } }).styles?.body;
|
|
35
|
+
const popupStyle = useMemo(() => {
|
|
35
36
|
return {
|
|
36
|
-
minHeight,
|
|
37
|
+
minHeight: bodyStyles?.minHeight ?? minHeight,
|
|
38
|
+
height: bodyStyles?.height,
|
|
39
|
+
maxHeight: bodyStyles?.maxHeight,
|
|
37
40
|
};
|
|
38
|
-
}, [minHeight]);
|
|
41
|
+
}, [bodyStyles?.height, bodyStyles?.maxHeight, bodyStyles?.minHeight, minHeight]);
|
|
42
|
+
|
|
43
|
+
const bodyStyle = useMemo(() => {
|
|
44
|
+
return {
|
|
45
|
+
padding: 0,
|
|
46
|
+
...bodyStyles,
|
|
47
|
+
};
|
|
48
|
+
}, [bodyStyles]);
|
|
49
|
+
|
|
50
|
+
const handleCloseKeyDown = useCallback(
|
|
51
|
+
(event: React.KeyboardEvent<HTMLSpanElement>) => {
|
|
52
|
+
if (event.key !== 'Enter' && event.key !== ' ') {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
event.preventDefault();
|
|
57
|
+
closePopup();
|
|
58
|
+
},
|
|
59
|
+
[closePopup],
|
|
60
|
+
);
|
|
39
61
|
|
|
40
62
|
const theme = useMemo(() => {
|
|
41
63
|
return {
|
|
@@ -57,11 +79,8 @@ export const MobilePopup: FC<MobilePopupProps> = (props) => {
|
|
|
57
79
|
onClose={closePopup}
|
|
58
80
|
onMaskClick={closePopup}
|
|
59
81
|
bodyClassName="nb-mobile-action-drawer-body"
|
|
60
|
-
bodyStyle={
|
|
61
|
-
|
|
62
|
-
}}
|
|
63
|
-
maskStyle={style}
|
|
64
|
-
style={style}
|
|
82
|
+
bodyStyle={bodyStyle}
|
|
83
|
+
style={popupStyle}
|
|
65
84
|
destroyOnClose
|
|
66
85
|
>
|
|
67
86
|
<div className="nb-mobile-action-drawer-header">
|
|
@@ -69,13 +88,14 @@ export const MobilePopup: FC<MobilePopupProps> = (props) => {
|
|
|
69
88
|
<span className="nb-mobile-action-drawer-placeholder">
|
|
70
89
|
<CloseOutline />
|
|
71
90
|
</span>
|
|
72
|
-
<span>{title}</span>
|
|
91
|
+
<span className="nb-mobile-action-drawer-title">{title}</span>
|
|
73
92
|
<span
|
|
74
93
|
className="nb-mobile-action-drawer-close-icon"
|
|
75
94
|
onClick={closePopup}
|
|
76
95
|
role="button"
|
|
77
96
|
tabIndex={0}
|
|
78
97
|
aria-label={t('Close')}
|
|
98
|
+
onKeyDown={handleCloseKeyDown}
|
|
79
99
|
>
|
|
80
100
|
<CloseOutline />
|
|
81
101
|
</span>
|
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
10
|
+
import { render } from '@testing-library/react';
|
|
11
|
+
import React from 'react';
|
|
12
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
13
|
+
import { FormItem, formItemStyle, verticalFormItemLabelStyle } from '../FormItem';
|
|
12
14
|
|
|
13
15
|
describe('FormItem', () => {
|
|
14
16
|
it('keeps vertical label-to-value spacing consistent with v1', () => {
|
|
@@ -22,4 +24,17 @@ describe('FormItem', () => {
|
|
|
22
24
|
marginBottom: 12,
|
|
23
25
|
});
|
|
24
26
|
});
|
|
27
|
+
|
|
28
|
+
it('does not forward model-internal globalSort props to field children', () => {
|
|
29
|
+
const Field = vi.fn(() => <input aria-label="field" />);
|
|
30
|
+
|
|
31
|
+
render(
|
|
32
|
+
<FormItem globalSort={['title']} placeholder="Title">
|
|
33
|
+
<Field />
|
|
34
|
+
</FormItem>,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
expect(Field).toHaveBeenCalledWith(expect.not.objectContaining({ globalSort: expect.anything() }), {});
|
|
38
|
+
expect(Field).toHaveBeenCalledWith(expect.objectContaining({ placeholder: 'Title' }), {});
|
|
39
|
+
});
|
|
25
40
|
});
|
|
@@ -0,0 +1,109 @@
|
|
|
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 React from 'react';
|
|
11
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
12
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
13
|
+
import { MobilePopup } from '../MobilePopup';
|
|
14
|
+
|
|
15
|
+
vi.mock('react-i18next', () => ({
|
|
16
|
+
useTranslation: () => ({
|
|
17
|
+
t: (key: string) => key,
|
|
18
|
+
}),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock('../MobilePopup.style', () => ({
|
|
22
|
+
useMobileActionDrawerStyle: () => ({
|
|
23
|
+
componentCls: 'nb-mobile-action-drawer',
|
|
24
|
+
hashId: 'hash',
|
|
25
|
+
}),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
vi.mock('../../lazy-helper', () => ({
|
|
29
|
+
lazy: (_loader: unknown, name: string) => {
|
|
30
|
+
if (name === 'Popup') {
|
|
31
|
+
const Popup = ({
|
|
32
|
+
children,
|
|
33
|
+
bodyStyle,
|
|
34
|
+
maskStyle,
|
|
35
|
+
style,
|
|
36
|
+
}: {
|
|
37
|
+
children: React.ReactNode;
|
|
38
|
+
bodyStyle?: React.CSSProperties;
|
|
39
|
+
maskStyle?: React.CSSProperties;
|
|
40
|
+
style?: React.CSSProperties;
|
|
41
|
+
}) => (
|
|
42
|
+
<div data-testid="mobile-popup" style={style}>
|
|
43
|
+
<div data-testid="mobile-popup-mask" style={maskStyle} />
|
|
44
|
+
<div data-testid="mobile-popup-body" style={bodyStyle}>
|
|
45
|
+
{children}
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
return { Popup };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
CloseOutline: () => <span data-testid="close-outline" />,
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
describe('MobilePopup', () => {
|
|
60
|
+
const MobilePopupWithDrawerStyles = MobilePopup as React.ComponentType<
|
|
61
|
+
React.ComponentProps<typeof MobilePopup> & { styles?: { body?: React.CSSProperties } }
|
|
62
|
+
>;
|
|
63
|
+
|
|
64
|
+
it('applies drawer body styles as max bounds without forcing fixed half-window height', () => {
|
|
65
|
+
render(
|
|
66
|
+
<MobilePopupWithDrawerStyles visible title="Title" styles={{ body: { maxHeight: '50vh' } }} onClose={vi.fn()}>
|
|
67
|
+
body
|
|
68
|
+
</MobilePopupWithDrawerStyles>,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
expect(screen.getByTestId('mobile-popup')).toHaveStyle({
|
|
72
|
+
maxHeight: '50vh',
|
|
73
|
+
});
|
|
74
|
+
expect(screen.getByTestId('mobile-popup-body')).toHaveStyle({
|
|
75
|
+
maxHeight: '50vh',
|
|
76
|
+
});
|
|
77
|
+
expect(screen.getByTestId('mobile-popup-mask')).not.toHaveStyle({
|
|
78
|
+
maxHeight: '50vh',
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('closes from the header close icon with Enter or Space', () => {
|
|
83
|
+
const onClose = vi.fn();
|
|
84
|
+
render(
|
|
85
|
+
<MobilePopup visible title="Title" onClose={onClose}>
|
|
86
|
+
body
|
|
87
|
+
</MobilePopup>,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const closeButton = screen.getByRole('button', { name: 'Close' });
|
|
91
|
+
fireEvent.keyDown(closeButton, { key: 'Enter' });
|
|
92
|
+
fireEvent.keyDown(closeButton, { key: ' ' });
|
|
93
|
+
|
|
94
|
+
expect(onClose).toHaveBeenCalledTimes(2);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('keeps long header titles in a constrained title element separate from the close button', () => {
|
|
98
|
+
const longTitle = 'A very long table column title that should not push the close button outside the drawer';
|
|
99
|
+
|
|
100
|
+
render(
|
|
101
|
+
<MobilePopup visible title={longTitle} onClose={vi.fn()}>
|
|
102
|
+
body
|
|
103
|
+
</MobilePopup>,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
expect(screen.getByText(longTitle)).toHaveClass('nb-mobile-action-drawer-title');
|
|
107
|
+
expect(screen.getByRole('button', { name: 'Close' })).toHaveClass('nb-mobile-action-drawer-close-icon');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -357,6 +357,7 @@ const VariableInputComponent: React.FC<VariableInputProps> = ({
|
|
|
357
357
|
metaTree={resolvedMetaTree}
|
|
358
358
|
value={innerValue}
|
|
359
359
|
active={isVariableValue(innerValue)}
|
|
360
|
+
disabled={disabled}
|
|
360
361
|
onChange={handleVariableSelect}
|
|
361
362
|
parseValueToPath={resolvePathFromValue}
|
|
362
363
|
formatPathToValue={resolveValueFromPath}
|
|
@@ -113,6 +113,18 @@ describe('VariableInput', () => {
|
|
|
113
113
|
expect(selectorButton).toBeInTheDocument();
|
|
114
114
|
});
|
|
115
115
|
|
|
116
|
+
it('disables the FlowContextSelector button when disabled', async () => {
|
|
117
|
+
const flowContext = createTestFlowContext();
|
|
118
|
+
render(
|
|
119
|
+
<TestFlowContextWrapper context={flowContext}>
|
|
120
|
+
<VariableInput value="test" metaTree={() => flowContext.getPropertyMetaTree()} disabled />
|
|
121
|
+
</TestFlowContextWrapper>,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const selectorButton = await screen.findByRole('button');
|
|
125
|
+
expect(selectorButton).toBeDisabled();
|
|
126
|
+
});
|
|
127
|
+
|
|
116
128
|
it('should not highlight the selector button for synthetic constant/null paths', async () => {
|
|
117
129
|
const flowContext = createTestFlowContext();
|
|
118
130
|
|
package/src/flowEngine.ts
CHANGED
|
@@ -1343,6 +1343,9 @@ export class FlowEngine {
|
|
|
1343
1343
|
if (!this.ensureModelRepository()) return;
|
|
1344
1344
|
const refresh = !!options?.refresh;
|
|
1345
1345
|
const bypassLoadedPageCache = this._loadedPageCache.shouldBypass(options, () => this.context.flowSettingsEnabled);
|
|
1346
|
+
if (this.context.flowSettingsEnabled) {
|
|
1347
|
+
this._loadedPageCache.markDirtyForOptions(options);
|
|
1348
|
+
}
|
|
1346
1349
|
if (!refresh && !bypassLoadedPageCache) {
|
|
1347
1350
|
const model = this.findModelByParentId(options.parentId, options.subKey);
|
|
1348
1351
|
if (model) {
|
|
@@ -1412,6 +1415,9 @@ export class FlowEngine {
|
|
|
1412
1415
|
if (!this.ensureModelRepository()) return;
|
|
1413
1416
|
const { uid, parentId, subKey } = options;
|
|
1414
1417
|
const bypassLoadedPageCache = this._loadedPageCache.shouldBypass(options, () => this.context.flowSettingsEnabled);
|
|
1418
|
+
if (this.context.flowSettingsEnabled) {
|
|
1419
|
+
this._loadedPageCache.markDirtyForOptions(options);
|
|
1420
|
+
}
|
|
1415
1421
|
if (uid && !bypassLoadedPageCache && this._modelInstances.has(uid)) {
|
|
1416
1422
|
return this._modelInstances.get(uid) as T;
|
|
1417
1423
|
}
|
|
@@ -123,6 +123,13 @@ export const createLoadedPageCache = () => {
|
|
|
123
123
|
}
|
|
124
124
|
},
|
|
125
125
|
|
|
126
|
+
markDirtyForOptions(options?: LoadedPageOptions): void {
|
|
127
|
+
const key = getLoadedPageKey(options);
|
|
128
|
+
if (key) {
|
|
129
|
+
dirtyKeys.add(key);
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
|
|
126
133
|
shouldBypass(options?: LoadedPageOptions, isFlowSettingsEnabled?: () => boolean): boolean {
|
|
127
134
|
const key = getLoadedPageKey(options);
|
|
128
135
|
if (!key || !dirtyKeys.has(key)) {
|