@nocobase/flow-engine 2.0.0-alpha.35 → 2.0.0-alpha.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/lib/components/FlowModelRenderer.d.ts +1 -1
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.d.ts +2 -2
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +19 -5
- package/lib/flowContext.js +14 -13
- package/lib/resources/multiRecordResource.js +17 -1
- package/lib/resources/singleRecordResource.js +16 -1
- package/lib/utils/associationObjectVariable.d.ts +3 -2
- package/lib/utils/associationObjectVariable.js +22 -2
- package/lib/utils/parsePathnameToViewParams.d.ts +1 -1
- package/lib/utils/parsePathnameToViewParams.js +41 -5
- package/lib/views/ViewNavigation.d.ts +3 -9
- package/lib/views/ViewNavigation.js +16 -2
- package/package.json +4 -4
- package/src/__tests__/objectVariable.test.ts +61 -0
- package/src/components/FlowModelRenderer.tsx +3 -3
- package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +22 -7
- package/src/flowContext.ts +12 -9
- package/src/resources/multiRecordResource.ts +18 -1
- package/src/resources/singleRecordResource.ts +16 -1
- package/src/utils/__tests__/parsePathnameToViewParams.test.ts +25 -0
- package/src/utils/associationObjectVariable.ts +19 -3
- package/src/utils/parsePathnameToViewParams.ts +50 -6
- package/src/views/ViewNavigation.ts +20 -12
- package/src/views/__tests__/ViewNavigation.test.ts +10 -0
|
@@ -24,7 +24,7 @@ export interface FlowModelRendererProps {
|
|
|
24
24
|
/**
|
|
25
25
|
* @default 'inside'
|
|
26
26
|
*/
|
|
27
|
-
toolbarPosition?: 'inside' | 'above';
|
|
27
|
+
toolbarPosition?: 'inside' | 'above' | 'below';
|
|
28
28
|
};
|
|
29
29
|
/** 流程设置的交互风格 */
|
|
30
30
|
flowSettingsVariant?: 'dropdown' | 'contextMenu' | 'modal' | 'drawer';
|
|
@@ -45,7 +45,7 @@ interface ModelProvidedProps {
|
|
|
45
45
|
/**
|
|
46
46
|
* @default 'inside'
|
|
47
47
|
*/
|
|
48
|
-
toolbarPosition?: 'inside' | 'above';
|
|
48
|
+
toolbarPosition?: 'inside' | 'above' | 'below';
|
|
49
49
|
}
|
|
50
50
|
interface ModelByIdProps {
|
|
51
51
|
uid: string;
|
|
@@ -79,7 +79,7 @@ interface ModelByIdProps {
|
|
|
79
79
|
/**
|
|
80
80
|
* @default 'inside'
|
|
81
81
|
*/
|
|
82
|
-
toolbarPosition?: 'inside' | 'above';
|
|
82
|
+
toolbarPosition?: 'inside' | 'above' | 'below';
|
|
83
83
|
}
|
|
84
84
|
type FlowsFloatContextMenuProps = ModelProvidedProps | ModelByIdProps;
|
|
85
85
|
/**
|
|
@@ -83,7 +83,24 @@ const renderToolbarItems = /* @__PURE__ */ __name((model, showDeleteButton, show
|
|
|
83
83
|
return /* @__PURE__ */ import_react.default.createElement(ItemComponent, { key: itemConfig.key, model });
|
|
84
84
|
});
|
|
85
85
|
}, "renderToolbarItems");
|
|
86
|
-
const
|
|
86
|
+
const toolbarPositionToCSS = {
|
|
87
|
+
inside: `
|
|
88
|
+
top: 2px;
|
|
89
|
+
`,
|
|
90
|
+
above: `
|
|
91
|
+
top: 0px;
|
|
92
|
+
transform: translateY(-100%);
|
|
93
|
+
padding-bottom: 2px;
|
|
94
|
+
margin-bottom: -2px;
|
|
95
|
+
`,
|
|
96
|
+
below: `
|
|
97
|
+
top: 0px;
|
|
98
|
+
transform: translateY(100%);
|
|
99
|
+
padding-top: 2px;
|
|
100
|
+
margin-top: -2px;
|
|
101
|
+
`
|
|
102
|
+
};
|
|
103
|
+
const floatContainerStyles = /* @__PURE__ */ __name(({ showBackground, showBorder, ctx, toolbarPosition = "inside" }) => import_css.css`
|
|
87
104
|
position: relative;
|
|
88
105
|
display: inline;
|
|
89
106
|
|
|
@@ -149,10 +166,7 @@ const floatContainerStyles = /* @__PURE__ */ __name(({ showBackground, showBorde
|
|
|
149
166
|
display: none; // 防止遮挡其它 icons
|
|
150
167
|
position: absolute;
|
|
151
168
|
right: 2px;
|
|
152
|
-
|
|
153
|
-
${toolbarPosition === "above" ? "transform: translateY(-100%);" : ""}
|
|
154
|
-
${toolbarPosition === "above" ? "padding-bottom: 2px;" : ""}
|
|
155
|
-
${toolbarPosition === "above" ? "margin-bottom: -2px;" : ""}
|
|
169
|
+
${toolbarPositionToCSS[toolbarPosition] || ""}
|
|
156
170
|
line-height: 16px;
|
|
157
171
|
pointer-events: all;
|
|
158
172
|
|
package/lib/flowContext.js
CHANGED
|
@@ -1054,7 +1054,7 @@ const _FlowModelContext = class _FlowModelContext extends BaseFlowModelContext {
|
|
|
1054
1054
|
}, "get")
|
|
1055
1055
|
});
|
|
1056
1056
|
this.defineMethod("openView", async function(uid, options) {
|
|
1057
|
-
var _a, _b, _c, _d, _e, _f
|
|
1057
|
+
var _a, _b, _c, _d, _e, _f;
|
|
1058
1058
|
const opts = { ...options };
|
|
1059
1059
|
if (opts.defineProperties || opts.defineMethod) {
|
|
1060
1060
|
opts.navigation = false;
|
|
@@ -1072,28 +1072,29 @@ const _FlowModelContext = class _FlowModelContext extends BaseFlowModelContext {
|
|
|
1072
1072
|
stepParams: {
|
|
1073
1073
|
popupSettings: {
|
|
1074
1074
|
openView: {
|
|
1075
|
-
|
|
1075
|
+
// 持久化首次传入的关键展示/数据参数,供后续路由与再次打开复用
|
|
1076
|
+
...import_lodash.default.pick(opts, ["dataSourceKey", "collectionName", "associationName", "mode", "size"])
|
|
1076
1077
|
}
|
|
1077
1078
|
}
|
|
1078
1079
|
}
|
|
1079
1080
|
});
|
|
1080
1081
|
await model2.save();
|
|
1081
1082
|
}
|
|
1082
|
-
if ((
|
|
1083
|
-
model2.
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1083
|
+
if ((_a = model2.getStepParams("popupSettings")) == null ? void 0 : _a.openView) {
|
|
1084
|
+
const prevOpenView = ((_b = model2.getStepParams("popupSettings")) == null ? void 0 : _b.openView) || {};
|
|
1085
|
+
const incoming = import_lodash.default.pick(opts, ["dataSourceKey", "collectionName", "associationName", "mode", "size"]);
|
|
1086
|
+
const nextOpenView = { ...prevOpenView, ...incoming };
|
|
1087
|
+
if (!import_lodash.default.isEqual(prevOpenView, nextOpenView)) {
|
|
1088
|
+
model2.setStepParams("popupSettings", { openView: nextOpenView });
|
|
1089
|
+
await model2.save();
|
|
1090
|
+
}
|
|
1090
1091
|
}
|
|
1091
|
-
const viewUid = (opts == null ? void 0 : opts.routeViewUid) ?? (opts == null ? void 0 : opts.viewUid) ?? (((
|
|
1092
|
+
const viewUid = (opts == null ? void 0 : opts.routeViewUid) ?? (opts == null ? void 0 : opts.viewUid) ?? (((_d = (_c = model2.stepParams) == null ? void 0 : _c.popupSettings) == null ? void 0 : _d.openView) ? model2.uid : this.model.uid);
|
|
1092
1093
|
const parentView = this.view;
|
|
1093
1094
|
const pendingType = (opts == null ? void 0 : opts.isMobileLayout) ? "embed" : (opts == null ? void 0 : opts.mode) || "drawer";
|
|
1094
1095
|
const pendingInputArgs = { ...opts, viewUid, navigation: opts.navigation };
|
|
1095
|
-
pendingInputArgs.filterByTk = pendingInputArgs.filterByTk || ((
|
|
1096
|
-
pendingInputArgs.sourceId = pendingInputArgs.sourceId || ((
|
|
1096
|
+
pendingInputArgs.filterByTk = pendingInputArgs.filterByTk || ((_e = this.inputArgs) == null ? void 0 : _e.filterByTk);
|
|
1097
|
+
pendingInputArgs.sourceId = pendingInputArgs.sourceId || ((_f = this.inputArgs) == null ? void 0 : _f.sourceId);
|
|
1097
1098
|
const pendingView = {
|
|
1098
1099
|
type: pendingType,
|
|
1099
1100
|
inputArgs: pendingInputArgs,
|
|
@@ -145,12 +145,28 @@ const _MultiRecordResource = class _MultiRecordResource extends import_baseRecor
|
|
|
145
145
|
return data;
|
|
146
146
|
}
|
|
147
147
|
async update(filterByTk, data, options) {
|
|
148
|
+
const collection = this.context.collection;
|
|
149
|
+
const filterTargetKey = collection.filterTargetKey;
|
|
150
|
+
let result = data;
|
|
151
|
+
const tkData = collection == null ? void 0 : collection.getFilterByTK(this.context.record);
|
|
152
|
+
if (Array.isArray(filterTargetKey)) {
|
|
153
|
+
result = {
|
|
154
|
+
...data,
|
|
155
|
+
...tkData || {}
|
|
156
|
+
};
|
|
157
|
+
} else {
|
|
158
|
+
result = {
|
|
159
|
+
...data,
|
|
160
|
+
[filterTargetKey]: tkData
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
console.log(result);
|
|
148
164
|
const config = this.mergeRequestConfig(
|
|
149
165
|
{
|
|
150
166
|
params: {
|
|
151
167
|
filterByTk
|
|
152
168
|
},
|
|
153
|
-
data
|
|
169
|
+
data: result
|
|
154
170
|
},
|
|
155
171
|
this.updateActionOptions,
|
|
156
172
|
options
|
|
@@ -60,14 +60,29 @@ const _SingleRecordResource = class _SingleRecordResource extends import_baseRec
|
|
|
60
60
|
async save(data, options) {
|
|
61
61
|
const config = this.mergeRequestConfig(this.saveActionOptions, import_lodash.default.omit(options, ["refresh"]));
|
|
62
62
|
let actionName = "create";
|
|
63
|
+
let result = data;
|
|
63
64
|
if (!this.isNewRecord) {
|
|
64
65
|
config.params = config.params || {};
|
|
65
66
|
config.params.filterByTk = this.getFilterByTk();
|
|
66
67
|
actionName = "update";
|
|
68
|
+
const collection = this.context.collection;
|
|
69
|
+
const filterTargetKey = collection.filterTargetKey;
|
|
70
|
+
const tkData = collection == null ? void 0 : collection.getFilterByTK(this.context.record);
|
|
71
|
+
if (Array.isArray(filterTargetKey)) {
|
|
72
|
+
result = {
|
|
73
|
+
...data,
|
|
74
|
+
...tkData || {}
|
|
75
|
+
};
|
|
76
|
+
} else {
|
|
77
|
+
result = {
|
|
78
|
+
...data,
|
|
79
|
+
[filterTargetKey]: tkData
|
|
80
|
+
};
|
|
81
|
+
}
|
|
67
82
|
}
|
|
68
83
|
await this.runAction(actionName, {
|
|
69
84
|
...config,
|
|
70
|
-
data
|
|
85
|
+
data: result
|
|
71
86
|
});
|
|
72
87
|
this.emit("saved", data);
|
|
73
88
|
if ((options == null ? void 0 : options.refresh) !== false) {
|
|
@@ -13,10 +13,11 @@ import type { FlowContext, PropertyMetaFactory } from '../flowContext';
|
|
|
13
13
|
* 仅当访问路径以“关联字段名”开头(且继续访问其子属性)时,返回 true 交由服务端解析;
|
|
14
14
|
* 否则在前端解析即可。
|
|
15
15
|
*
|
|
16
|
-
* @param collectionAccessor 返回当前对象所在collection
|
|
16
|
+
* @param collectionAccessor 返回当前对象所在 collection
|
|
17
|
+
* @param valueAccessor 可选,本地值访问器。若本地已存在目标子路径的值,则认为无需走后端,优先使用本地值。
|
|
17
18
|
* @returns `(subPath) => boolean` 判断是否需要服务端解析
|
|
18
19
|
*/
|
|
19
|
-
export declare function createAssociationSubpathResolver(collectionAccessor: () => Collection | null): (subPath: string) => boolean;
|
|
20
|
+
export declare function createAssociationSubpathResolver(collectionAccessor: () => Collection | null, valueAccessor?: () => unknown): (subPath: string) => boolean;
|
|
20
21
|
/**
|
|
21
22
|
* 构建“对象类变量”的 PropertyMetaFactory:
|
|
22
23
|
* - 暴露集合字段结构(通过 createCollectionContextMeta)用于变量选择器;
|
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
var __create = Object.create;
|
|
10
11
|
var __defProp = Object.defineProperty;
|
|
11
12
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
12
13
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
14
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
13
15
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
14
16
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
15
17
|
var __export = (target, all) => {
|
|
@@ -24,6 +26,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
24
26
|
}
|
|
25
27
|
return to;
|
|
26
28
|
};
|
|
29
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
30
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
31
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
32
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
33
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
34
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
35
|
+
mod
|
|
36
|
+
));
|
|
27
37
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
38
|
var associationObjectVariable_exports = {};
|
|
29
39
|
__export(associationObjectVariable_exports, {
|
|
@@ -31,6 +41,7 @@ __export(associationObjectVariable_exports, {
|
|
|
31
41
|
createAssociationSubpathResolver: () => createAssociationSubpathResolver
|
|
32
42
|
});
|
|
33
43
|
module.exports = __toCommonJS(associationObjectVariable_exports);
|
|
44
|
+
var import_lodash = __toESM(require("lodash"));
|
|
34
45
|
var import_createCollectionContextMeta = require("./createCollectionContextMeta");
|
|
35
46
|
function baseFieldNameOf(subPath) {
|
|
36
47
|
if (!subPath) return void 0;
|
|
@@ -66,14 +77,23 @@ function toFilterByTk(value, primaryKey) {
|
|
|
66
77
|
return void 0;
|
|
67
78
|
}
|
|
68
79
|
__name(toFilterByTk, "toFilterByTk");
|
|
69
|
-
function createAssociationSubpathResolver(collectionAccessor) {
|
|
80
|
+
function createAssociationSubpathResolver(collectionAccessor, valueAccessor) {
|
|
70
81
|
return (p) => {
|
|
71
82
|
if (!p || !p.includes(".")) return false;
|
|
72
83
|
const base = baseFieldNameOf(p);
|
|
73
84
|
if (!base) return false;
|
|
74
85
|
const collection = collectionAccessor();
|
|
75
86
|
const field = findFieldByName(collection, base);
|
|
76
|
-
|
|
87
|
+
const isAssoc = !!(field == null ? void 0 : field.isAssociationField());
|
|
88
|
+
if (!isAssoc) return false;
|
|
89
|
+
if (typeof valueAccessor === "function") {
|
|
90
|
+
const local = valueAccessor();
|
|
91
|
+
if (local && typeof local === "object") {
|
|
92
|
+
const v = import_lodash.default.get(local, p);
|
|
93
|
+
if (typeof v !== "undefined") return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return true;
|
|
77
97
|
};
|
|
78
98
|
}
|
|
79
99
|
__name(createAssociationSubpathResolver, "createAssociationSubpathResolver");
|
|
@@ -54,16 +54,52 @@ const parsePathnameToViewParams = /* @__PURE__ */ __name((pathname) => {
|
|
|
54
54
|
break;
|
|
55
55
|
}
|
|
56
56
|
} else if (currentView && i + 1 < segments.length) {
|
|
57
|
-
const
|
|
57
|
+
const rawValue = segments[i + 1];
|
|
58
|
+
let decoded = rawValue;
|
|
59
|
+
try {
|
|
60
|
+
decoded = decodeURIComponent(rawValue);
|
|
61
|
+
} catch (_) {
|
|
62
|
+
}
|
|
58
63
|
switch (segment) {
|
|
59
64
|
case "tab":
|
|
60
|
-
currentView.tabUid =
|
|
65
|
+
currentView.tabUid = decoded;
|
|
61
66
|
break;
|
|
62
|
-
case "filterbytk":
|
|
63
|
-
|
|
67
|
+
case "filterbytk": {
|
|
68
|
+
const parseKeyValuePairs = /* @__PURE__ */ __name((s) => {
|
|
69
|
+
const obj = {};
|
|
70
|
+
s.split("&").forEach((pair) => {
|
|
71
|
+
const [k, v = ""] = pair.split("=");
|
|
72
|
+
if (!k) return;
|
|
73
|
+
try {
|
|
74
|
+
const key = decodeURIComponent(k);
|
|
75
|
+
const val = decodeURIComponent(v);
|
|
76
|
+
obj[key] = val;
|
|
77
|
+
} catch (_) {
|
|
78
|
+
obj[k] = v;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
return obj;
|
|
82
|
+
}, "parseKeyValuePairs");
|
|
83
|
+
let parsed = decoded;
|
|
84
|
+
if (decoded && (decoded.startsWith("{") || decoded.startsWith("["))) {
|
|
85
|
+
try {
|
|
86
|
+
const maybe = JSON.parse(decoded);
|
|
87
|
+
if (maybe && typeof maybe === "object" && !Array.isArray(maybe)) {
|
|
88
|
+
parsed = maybe;
|
|
89
|
+
} else {
|
|
90
|
+
parsed = decoded;
|
|
91
|
+
}
|
|
92
|
+
} catch (_) {
|
|
93
|
+
parsed = decoded;
|
|
94
|
+
}
|
|
95
|
+
} else if (decoded && decoded.includes("=") && decoded.includes("&")) {
|
|
96
|
+
parsed = parseKeyValuePairs(decoded);
|
|
97
|
+
}
|
|
98
|
+
currentView.filterByTk = parsed;
|
|
64
99
|
break;
|
|
100
|
+
}
|
|
65
101
|
case "sourceid":
|
|
66
|
-
currentView.sourceId =
|
|
102
|
+
currentView.sourceId = decoded;
|
|
67
103
|
break;
|
|
68
104
|
default:
|
|
69
105
|
break;
|
|
@@ -7,16 +7,10 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
import { FlowEngineContext } from '../flowContext';
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
import { ViewParam as SharedViewParam } from '../utils';
|
|
11
|
+
type ViewParam = Omit<SharedViewParam, 'viewUid'> & {
|
|
12
12
|
viewUid?: string;
|
|
13
|
-
|
|
14
|
-
tabUid?: string;
|
|
15
|
-
/** 弹窗记录的 id */
|
|
16
|
-
filterByTk?: string;
|
|
17
|
-
/** source Id */
|
|
18
|
-
sourceId?: string;
|
|
19
|
-
}
|
|
13
|
+
};
|
|
20
14
|
/**
|
|
21
15
|
* 将 ViewParam 数组转换为 pathname
|
|
22
16
|
*
|
|
@@ -31,6 +31,17 @@ __export(ViewNavigation_exports, {
|
|
|
31
31
|
generatePathnameFromViewParams: () => generatePathnameFromViewParams
|
|
32
32
|
});
|
|
33
33
|
module.exports = __toCommonJS(ViewNavigation_exports);
|
|
34
|
+
function encodeFilterByTk(val) {
|
|
35
|
+
if (val === void 0 || val === null) return "";
|
|
36
|
+
if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
37
|
+
const pairs = Object.entries(val).map(([k, v]) => {
|
|
38
|
+
return `${encodeURIComponent(String(k))}=${encodeURIComponent(String(v))}`;
|
|
39
|
+
});
|
|
40
|
+
return encodeURIComponent(pairs.join("&"));
|
|
41
|
+
}
|
|
42
|
+
return encodeURIComponent(String(val));
|
|
43
|
+
}
|
|
44
|
+
__name(encodeFilterByTk, "encodeFilterByTk");
|
|
34
45
|
function generatePathnameFromViewParams(viewParams) {
|
|
35
46
|
if (!viewParams || viewParams.length === 0) {
|
|
36
47
|
return "/admin";
|
|
@@ -44,8 +55,11 @@ function generatePathnameFromViewParams(viewParams) {
|
|
|
44
55
|
if (viewParam.tabUid) {
|
|
45
56
|
segments.push("tab", viewParam.tabUid);
|
|
46
57
|
}
|
|
47
|
-
if (viewParam.filterByTk) {
|
|
48
|
-
|
|
58
|
+
if (viewParam.filterByTk != null) {
|
|
59
|
+
const encoded = encodeFilterByTk(viewParam.filterByTk);
|
|
60
|
+
if (encoded) {
|
|
61
|
+
segments.push("filterbytk", encoded);
|
|
62
|
+
}
|
|
49
63
|
}
|
|
50
64
|
if (viewParam.sourceId) {
|
|
51
65
|
segments.push("sourceid", viewParam.sourceId);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/flow-engine",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.36",
|
|
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.0.0-alpha.
|
|
12
|
-
"@nocobase/shared": "2.0.0-alpha.
|
|
11
|
+
"@nocobase/sdk": "2.0.0-alpha.36",
|
|
12
|
+
"@nocobase/shared": "2.0.0-alpha.36",
|
|
13
13
|
"ahooks": "^3.7.2",
|
|
14
14
|
"dompurify": "^3.0.2",
|
|
15
15
|
"lodash": "^4.x",
|
|
@@ -35,5 +35,5 @@
|
|
|
35
35
|
],
|
|
36
36
|
"author": "NocoBase Team",
|
|
37
37
|
"license": "AGPL-3.0",
|
|
38
|
-
"gitHead": "
|
|
38
|
+
"gitHead": "c1170bac3ba2325b7420ce65e732c1ba8bbb7767"
|
|
39
39
|
}
|
|
@@ -400,4 +400,65 @@ describe('objectVariable utilities', () => {
|
|
|
400
400
|
expect(resolved).toEqual({ a: 7 });
|
|
401
401
|
expect((ctx as any).api.request).not.toHaveBeenCalled();
|
|
402
402
|
});
|
|
403
|
+
|
|
404
|
+
it('local-first: toOne association subpath uses local value and skips server', async () => {
|
|
405
|
+
const { engine, collection } = setupEngineWithCollections();
|
|
406
|
+
const obj = { author: { id: 7, name: 'LocalName' } };
|
|
407
|
+
const ctx = engine.context as any;
|
|
408
|
+
|
|
409
|
+
(ctx as any).api = { request: vi.fn() };
|
|
410
|
+
|
|
411
|
+
const metaFactory = createAssociationAwareObjectMetaFactory(
|
|
412
|
+
() => collection,
|
|
413
|
+
'Current object',
|
|
414
|
+
() => obj,
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
ctx.defineProperty('obj', {
|
|
418
|
+
get: () => obj,
|
|
419
|
+
cache: false,
|
|
420
|
+
meta: metaFactory,
|
|
421
|
+
// 启用“本地优先”:传入 valueAccessor,以便当本地已有该子路径时跳过服务端
|
|
422
|
+
resolveOnServer: createAssociationSubpathResolver(
|
|
423
|
+
() => collection,
|
|
424
|
+
() => obj,
|
|
425
|
+
),
|
|
426
|
+
serverOnlyWhenContextParams: true,
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
const template = { x: '{{ ctx.obj.author.name }}' } as any;
|
|
430
|
+
const resolved = await (ctx as any).resolveJsonTemplate(template);
|
|
431
|
+
expect(resolved).toEqual({ x: 'LocalName' });
|
|
432
|
+
expect((ctx as any).api.request).not.toHaveBeenCalled();
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('local-first: toMany association subpath (index + dot) uses local value and skips server', async () => {
|
|
436
|
+
const { engine, collection } = setupEngineWithCollections();
|
|
437
|
+
const obj = { tags: [{ id: 11, name: 'T1' }] };
|
|
438
|
+
const ctx = engine.context as any;
|
|
439
|
+
|
|
440
|
+
(ctx as any).api = { request: vi.fn() };
|
|
441
|
+
|
|
442
|
+
const metaFactory = createAssociationAwareObjectMetaFactory(
|
|
443
|
+
() => collection,
|
|
444
|
+
'Current object',
|
|
445
|
+
() => obj,
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
ctx.defineProperty('obj', {
|
|
449
|
+
get: () => obj,
|
|
450
|
+
cache: false,
|
|
451
|
+
meta: metaFactory,
|
|
452
|
+
resolveOnServer: createAssociationSubpathResolver(
|
|
453
|
+
() => collection,
|
|
454
|
+
() => obj,
|
|
455
|
+
),
|
|
456
|
+
serverOnlyWhenContextParams: true,
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
const template = { x: '{{ ctx.obj.tags[0].name }}' } as any;
|
|
460
|
+
const resolved = await (ctx as any).resolveJsonTemplate(template);
|
|
461
|
+
expect(resolved).toEqual({ x: 'T1' });
|
|
462
|
+
expect((ctx as any).api.request).not.toHaveBeenCalled();
|
|
463
|
+
});
|
|
403
464
|
});
|
|
@@ -74,7 +74,7 @@ export interface FlowModelRendererProps {
|
|
|
74
74
|
/**
|
|
75
75
|
* @default 'inside'
|
|
76
76
|
*/
|
|
77
|
-
toolbarPosition?: 'inside' | 'above';
|
|
77
|
+
toolbarPosition?: 'inside' | 'above' | 'below';
|
|
78
78
|
}; // 默认 false
|
|
79
79
|
|
|
80
80
|
/** 流程设置的交互风格 */
|
|
@@ -114,7 +114,7 @@ const FlowModelRendererWithAutoFlows: React.FC<{
|
|
|
114
114
|
/**
|
|
115
115
|
* @default 'inside'
|
|
116
116
|
*/
|
|
117
|
-
toolbarPosition?: 'inside' | 'above';
|
|
117
|
+
toolbarPosition?: 'inside' | 'above' | 'below';
|
|
118
118
|
};
|
|
119
119
|
flowSettingsVariant: string;
|
|
120
120
|
hideRemoveInSettings: boolean;
|
|
@@ -181,7 +181,7 @@ const FlowModelRendererCore: React.FC<{
|
|
|
181
181
|
/**
|
|
182
182
|
* @default 'inside'
|
|
183
183
|
*/
|
|
184
|
-
toolbarPosition?: 'inside' | 'above';
|
|
184
|
+
toolbarPosition?: 'inside' | 'above' | 'below';
|
|
185
185
|
};
|
|
186
186
|
flowSettingsVariant: string;
|
|
187
187
|
hideRemoveInSettings: boolean;
|
|
@@ -80,8 +80,26 @@ const renderToolbarItems = (
|
|
|
80
80
|
});
|
|
81
81
|
};
|
|
82
82
|
|
|
83
|
+
const toolbarPositionToCSS = {
|
|
84
|
+
inside: `
|
|
85
|
+
top: 2px;
|
|
86
|
+
`,
|
|
87
|
+
above: `
|
|
88
|
+
top: 0px;
|
|
89
|
+
transform: translateY(-100%);
|
|
90
|
+
padding-bottom: 2px;
|
|
91
|
+
margin-bottom: -2px;
|
|
92
|
+
`,
|
|
93
|
+
below: `
|
|
94
|
+
top: 0px;
|
|
95
|
+
transform: translateY(100%);
|
|
96
|
+
padding-top: 2px;
|
|
97
|
+
margin-top: -2px;
|
|
98
|
+
`,
|
|
99
|
+
};
|
|
100
|
+
|
|
83
101
|
// 使用与 NocoBase 一致的悬浮工具栏样式
|
|
84
|
-
const floatContainerStyles = ({ showBackground, showBorder, ctx, toolbarPosition }) => css`
|
|
102
|
+
const floatContainerStyles = ({ showBackground, showBorder, ctx, toolbarPosition = 'inside' }) => css`
|
|
85
103
|
position: relative;
|
|
86
104
|
display: inline;
|
|
87
105
|
|
|
@@ -147,10 +165,7 @@ const floatContainerStyles = ({ showBackground, showBorder, ctx, toolbarPosition
|
|
|
147
165
|
display: none; // 防止遮挡其它 icons
|
|
148
166
|
position: absolute;
|
|
149
167
|
right: 2px;
|
|
150
|
-
|
|
151
|
-
${toolbarPosition === 'above' ? 'transform: translateY(-100%);' : ''}
|
|
152
|
-
${toolbarPosition === 'above' ? 'padding-bottom: 2px;' : ''}
|
|
153
|
-
${toolbarPosition === 'above' ? 'margin-bottom: -2px;' : ''}
|
|
168
|
+
${toolbarPositionToCSS[toolbarPosition] || ''}
|
|
154
169
|
line-height: 16px;
|
|
155
170
|
pointer-events: all;
|
|
156
171
|
|
|
@@ -297,7 +312,7 @@ interface ModelProvidedProps {
|
|
|
297
312
|
/**
|
|
298
313
|
* @default 'inside'
|
|
299
314
|
*/
|
|
300
|
-
toolbarPosition?: 'inside' | 'above';
|
|
315
|
+
toolbarPosition?: 'inside' | 'above' | 'below';
|
|
301
316
|
}
|
|
302
317
|
|
|
303
318
|
interface ModelByIdProps {
|
|
@@ -332,7 +347,7 @@ interface ModelByIdProps {
|
|
|
332
347
|
/**
|
|
333
348
|
* @default 'inside'
|
|
334
349
|
*/
|
|
335
|
-
toolbarPosition?: 'inside' | 'above';
|
|
350
|
+
toolbarPosition?: 'inside' | 'above' | 'below';
|
|
336
351
|
}
|
|
337
352
|
|
|
338
353
|
type FlowsFloatContextMenuProps = ModelProvidedProps | ModelByIdProps;
|
package/src/flowContext.ts
CHANGED
|
@@ -1422,21 +1422,24 @@ export class FlowModelContext extends BaseFlowModelContext {
|
|
|
1422
1422
|
stepParams: {
|
|
1423
1423
|
popupSettings: {
|
|
1424
1424
|
openView: {
|
|
1425
|
-
|
|
1425
|
+
// 持久化首次传入的关键展示/数据参数,供后续路由与再次打开复用
|
|
1426
|
+
..._.pick(opts, ['dataSourceKey', 'collectionName', 'associationName', 'mode', 'size']),
|
|
1426
1427
|
},
|
|
1427
1428
|
},
|
|
1428
1429
|
},
|
|
1429
1430
|
});
|
|
1430
1431
|
await model.save();
|
|
1431
1432
|
}
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1433
|
+
// 若已存在 openView 配置,则按需合并更新(仅覆盖本次显式传入的字段)
|
|
1434
|
+
if (model.getStepParams('popupSettings')?.openView) {
|
|
1435
|
+
const prevOpenView = model.getStepParams('popupSettings')?.openView || {};
|
|
1436
|
+
const incoming = _.pick(opts, ['dataSourceKey', 'collectionName', 'associationName', 'mode', 'size']);
|
|
1437
|
+
const nextOpenView = { ...prevOpenView, ...incoming };
|
|
1438
|
+
// 仅当有变更时才保存,避免不必要的写入
|
|
1439
|
+
if (!_.isEqual(prevOpenView, nextOpenView)) {
|
|
1440
|
+
model.setStepParams('popupSettings', { openView: nextOpenView });
|
|
1441
|
+
await model.save();
|
|
1442
|
+
}
|
|
1440
1443
|
}
|
|
1441
1444
|
|
|
1442
1445
|
// 路由层级的 viewUid:优先使用 routeViewUid(仅用于路由展示);
|
|
@@ -130,12 +130,29 @@ export class MultiRecordResource<TDataItem = any> extends BaseRecordResource<TDa
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
async update(filterByTk: string | number, data: Partial<TDataItem>, options?: AxiosRequestConfig): Promise<void> {
|
|
133
|
+
const collection = this.context.collection;
|
|
134
|
+
const filterTargetKey = collection.filterTargetKey;
|
|
135
|
+
let result = data;
|
|
136
|
+
const tkData = collection?.getFilterByTK(this.context.record);
|
|
137
|
+
if (Array.isArray(filterTargetKey)) {
|
|
138
|
+
result = {
|
|
139
|
+
...data,
|
|
140
|
+
...(tkData || {}),
|
|
141
|
+
};
|
|
142
|
+
} else {
|
|
143
|
+
result = {
|
|
144
|
+
...data,
|
|
145
|
+
[filterTargetKey]: tkData,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
console.log(result);
|
|
149
|
+
|
|
133
150
|
const config = this.mergeRequestConfig(
|
|
134
151
|
{
|
|
135
152
|
params: {
|
|
136
153
|
filterByTk,
|
|
137
154
|
},
|
|
138
|
-
data,
|
|
155
|
+
data: result,
|
|
139
156
|
},
|
|
140
157
|
this.updateActionOptions,
|
|
141
158
|
options,
|
|
@@ -33,15 +33,30 @@ export class SingleRecordResource<TData = any> extends BaseRecordResource<TData>
|
|
|
33
33
|
async save(data: TData, options?: AxiosRequestConfig & { refresh?: boolean }): Promise<void> {
|
|
34
34
|
const config = this.mergeRequestConfig(this.saveActionOptions, _.omit(options, ['refresh']));
|
|
35
35
|
let actionName = 'create';
|
|
36
|
+
let result = data;
|
|
36
37
|
// 如果有 filterByTk,则表示是更新操作
|
|
37
38
|
if (!this.isNewRecord) {
|
|
38
39
|
config.params = config.params || {};
|
|
39
40
|
config.params.filterByTk = this.getFilterByTk();
|
|
40
41
|
actionName = 'update';
|
|
42
|
+
const collection = this.context.collection;
|
|
43
|
+
const filterTargetKey = collection.filterTargetKey;
|
|
44
|
+
const tkData = collection?.getFilterByTK(this.context.record);
|
|
45
|
+
if (Array.isArray(filterTargetKey)) {
|
|
46
|
+
result = {
|
|
47
|
+
...data,
|
|
48
|
+
...(tkData || {}),
|
|
49
|
+
};
|
|
50
|
+
} else {
|
|
51
|
+
result = {
|
|
52
|
+
...data,
|
|
53
|
+
[filterTargetKey]: tkData,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
41
56
|
}
|
|
42
57
|
await this.runAction(actionName, {
|
|
43
58
|
...config,
|
|
44
|
-
data,
|
|
59
|
+
data: result,
|
|
45
60
|
});
|
|
46
61
|
this.emit('saved', data);
|
|
47
62
|
if (options?.refresh !== false) {
|
|
@@ -101,4 +101,29 @@ describe('parsePathnameToViewParams', () => {
|
|
|
101
101
|
const result = parsePathnameToViewParams('///admin//xxx//tab//yyy//');
|
|
102
102
|
expect(result).toEqual([{ viewUid: 'xxx', tabUid: 'yyy' }]);
|
|
103
103
|
});
|
|
104
|
+
|
|
105
|
+
test('should parse filterByTk from key-value encoded segment into object', () => {
|
|
106
|
+
const kv = encodeURIComponent('id=1&tenant=ac');
|
|
107
|
+
const path = `/admin/xxx/filterbytk/${kv}`;
|
|
108
|
+
const result = parsePathnameToViewParams(path);
|
|
109
|
+
expect(result).toEqual([{ viewUid: 'xxx', filterByTk: { id: '1', tenant: 'ac' } }]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('should parse filterByTk from JSON object segment', () => {
|
|
113
|
+
const json = encodeURIComponent('{"id":"1","tenant":"ac"}');
|
|
114
|
+
const path = `/admin/xxx/filterbytk/${json}`;
|
|
115
|
+
const result = parsePathnameToViewParams(path);
|
|
116
|
+
expect(result).toEqual([{ viewUid: 'xxx', filterByTk: { id: '1', tenant: 'ac' } }]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('should keep non-object JSON (array/number) as string for filterByTk', () => {
|
|
120
|
+
const arr = encodeURIComponent('["a"]');
|
|
121
|
+
const num = encodeURIComponent('123');
|
|
122
|
+
const t = encodeURIComponent('true');
|
|
123
|
+
expect(parsePathnameToViewParams(`/admin/xxx/filterbytk/${arr}`)).toEqual([
|
|
124
|
+
{ viewUid: 'xxx', filterByTk: '["a"]' },
|
|
125
|
+
]);
|
|
126
|
+
expect(parsePathnameToViewParams(`/admin/xxx/filterbytk/${num}`)).toEqual([{ viewUid: 'xxx', filterByTk: '123' }]);
|
|
127
|
+
expect(parsePathnameToViewParams(`/admin/xxx/filterbytk/${t}`)).toEqual([{ viewUid: 'xxx', filterByTk: 'true' }]);
|
|
128
|
+
});
|
|
104
129
|
});
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { Collection, CollectionField } from '../data-source';
|
|
11
|
+
import _ from 'lodash';
|
|
11
12
|
import type { FlowContext, PropertyMeta, PropertyMetaFactory } from '../flowContext';
|
|
12
13
|
import { createCollectionContextMeta } from './createCollectionContextMeta';
|
|
13
14
|
|
|
@@ -74,19 +75,34 @@ function toFilterByTk(value: unknown, primaryKey: string | string[]) {
|
|
|
74
75
|
* 仅当访问路径以“关联字段名”开头(且继续访问其子属性)时,返回 true 交由服务端解析;
|
|
75
76
|
* 否则在前端解析即可。
|
|
76
77
|
*
|
|
77
|
-
* @param collectionAccessor 返回当前对象所在collection
|
|
78
|
+
* @param collectionAccessor 返回当前对象所在 collection
|
|
79
|
+
* @param valueAccessor 可选,本地值访问器。若本地已存在目标子路径的值,则认为无需走后端,优先使用本地值。
|
|
78
80
|
* @returns `(subPath) => boolean` 判断是否需要服务端解析
|
|
79
81
|
*/
|
|
80
82
|
export function createAssociationSubpathResolver(
|
|
81
83
|
collectionAccessor: () => Collection | null,
|
|
84
|
+
valueAccessor?: () => unknown,
|
|
82
85
|
): (subPath: string) => boolean {
|
|
83
86
|
return (p: string) => {
|
|
84
|
-
|
|
87
|
+
// 仅在访问子属性时才考虑后端
|
|
88
|
+
if (!p || !p.includes('.')) return false;
|
|
85
89
|
const base = baseFieldNameOf(p);
|
|
86
90
|
if (!base) return false;
|
|
87
91
|
const collection = collectionAccessor();
|
|
88
92
|
const field = findFieldByName(collection, base);
|
|
89
|
-
|
|
93
|
+
const isAssoc = !!field?.isAssociationField();
|
|
94
|
+
if (!isAssoc) return false;
|
|
95
|
+
|
|
96
|
+
// 可选:本地优先。当提供 valueAccessor 时,若本地已有该子路径值,则不走后端
|
|
97
|
+
if (typeof valueAccessor === 'function') {
|
|
98
|
+
const local = valueAccessor();
|
|
99
|
+
if (local && typeof local === 'object') {
|
|
100
|
+
const v = _.get(local as Record<string, unknown>, p);
|
|
101
|
+
if (typeof v !== 'undefined') return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return true;
|
|
90
106
|
};
|
|
91
107
|
}
|
|
92
108
|
|
|
@@ -13,7 +13,7 @@ export interface ViewParam {
|
|
|
13
13
|
/** 标签页唯一标识符 */
|
|
14
14
|
tabUid?: string;
|
|
15
15
|
/** 弹窗记录的 id */
|
|
16
|
-
filterByTk?: string
|
|
16
|
+
filterByTk?: string | Record<string, string | number>;
|
|
17
17
|
/** source Id */
|
|
18
18
|
sourceId?: string;
|
|
19
19
|
}
|
|
@@ -70,17 +70,61 @@ export const parsePathnameToViewParams = (pathname: string): ViewParam[] => {
|
|
|
70
70
|
}
|
|
71
71
|
// 处理参数
|
|
72
72
|
else if (currentView && i + 1 < segments.length) {
|
|
73
|
-
const
|
|
73
|
+
const rawValue = segments[i + 1];
|
|
74
|
+
// 尝试对路径段进行解码
|
|
75
|
+
let decoded: string = rawValue;
|
|
76
|
+
try {
|
|
77
|
+
decoded = decodeURIComponent(rawValue);
|
|
78
|
+
} catch (_) {
|
|
79
|
+
// ignore
|
|
80
|
+
}
|
|
74
81
|
|
|
75
82
|
switch (segment) {
|
|
76
83
|
case 'tab':
|
|
77
|
-
|
|
84
|
+
// tab/sourceId 仅作为字符串处理
|
|
85
|
+
currentView.tabUid = decoded;
|
|
78
86
|
break;
|
|
79
|
-
case 'filterbytk':
|
|
80
|
-
|
|
87
|
+
case 'filterbytk': {
|
|
88
|
+
// 仅在 filterByTk 分支支持对象/JSON/键值对解析
|
|
89
|
+
const parseKeyValuePairs = (s: string): Record<string, string | number> => {
|
|
90
|
+
const obj: Record<string, string | number> = {};
|
|
91
|
+
s.split('&').forEach((pair) => {
|
|
92
|
+
const [k, v = ''] = pair.split('=');
|
|
93
|
+
if (!k) return;
|
|
94
|
+
try {
|
|
95
|
+
const key = decodeURIComponent(k);
|
|
96
|
+
const val = decodeURIComponent(v);
|
|
97
|
+
obj[key] = val;
|
|
98
|
+
} catch (_) {
|
|
99
|
+
obj[k] = v;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
return obj;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
let parsed: string | Record<string, string | number> = decoded;
|
|
106
|
+
if (decoded && (decoded.startsWith('{') || decoded.startsWith('['))) {
|
|
107
|
+
try {
|
|
108
|
+
const maybe = JSON.parse(decoded);
|
|
109
|
+
if (maybe && typeof maybe === 'object' && !Array.isArray(maybe)) {
|
|
110
|
+
parsed = maybe as Record<string, string | number>;
|
|
111
|
+
} else {
|
|
112
|
+
// 非对象 JSON(如数组/数字)按字符串保留
|
|
113
|
+
parsed = decoded;
|
|
114
|
+
}
|
|
115
|
+
} catch (_) {
|
|
116
|
+
// 解析失败,按字符串保留
|
|
117
|
+
parsed = decoded;
|
|
118
|
+
}
|
|
119
|
+
} else if (decoded && decoded.includes('=') && decoded.includes('&')) {
|
|
120
|
+
// 形如 a=b&c=d 的整体段
|
|
121
|
+
parsed = parseKeyValuePairs(decoded);
|
|
122
|
+
}
|
|
123
|
+
currentView.filterByTk = parsed;
|
|
81
124
|
break;
|
|
125
|
+
}
|
|
82
126
|
case 'sourceid':
|
|
83
|
-
currentView.sourceId =
|
|
127
|
+
currentView.sourceId = decoded;
|
|
84
128
|
break;
|
|
85
129
|
default:
|
|
86
130
|
// 未知参数,跳过
|
|
@@ -8,16 +8,21 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { FlowEngineContext } from '../flowContext';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
11
|
+
import { ViewParam as SharedViewParam } from '../utils';
|
|
12
|
+
|
|
13
|
+
type ViewParam = Omit<SharedViewParam, 'viewUid'> & { viewUid?: string };
|
|
14
|
+
|
|
15
|
+
function encodeFilterByTk(val: SharedViewParam['filterByTk']): string {
|
|
16
|
+
if (val === undefined || val === null) return '';
|
|
17
|
+
// 1.x 兼容:对象按 key1=v1&key2=v2 拼接后整体 encodeURIComponent
|
|
18
|
+
if (val && typeof val === 'object' && !Array.isArray(val)) {
|
|
19
|
+
const pairs = Object.entries(val).map(([k, v]) => {
|
|
20
|
+
return `${encodeURIComponent(String(k))}=${encodeURIComponent(String(v))}`;
|
|
21
|
+
});
|
|
22
|
+
return encodeURIComponent(pairs.join('&'));
|
|
23
|
+
}
|
|
24
|
+
// 其它情况按原单值编码(字符串)
|
|
25
|
+
return encodeURIComponent(String(val));
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
/**
|
|
@@ -53,8 +58,11 @@ export function generatePathnameFromViewParams(viewParams: ViewParam[]): string
|
|
|
53
58
|
if (viewParam.tabUid) {
|
|
54
59
|
segments.push('tab', viewParam.tabUid);
|
|
55
60
|
}
|
|
56
|
-
if (viewParam.filterByTk) {
|
|
57
|
-
|
|
61
|
+
if (viewParam.filterByTk != null) {
|
|
62
|
+
const encoded = encodeFilterByTk(viewParam.filterByTk);
|
|
63
|
+
if (encoded) {
|
|
64
|
+
segments.push('filterbytk', encoded);
|
|
65
|
+
}
|
|
58
66
|
}
|
|
59
67
|
if (viewParam.sourceId) {
|
|
60
68
|
segments.push('sourceid', viewParam.sourceId);
|
|
@@ -173,6 +173,16 @@ describe('generatePathnameFromViewParams', () => {
|
|
|
173
173
|
);
|
|
174
174
|
});
|
|
175
175
|
|
|
176
|
+
it('should encode object filterByTk as encoded key-value string', () => {
|
|
177
|
+
const path = generatePathnameFromViewParams([{ viewUid: 'xxx', filterByTk: { id: 1, tenant: 'ac' } }]);
|
|
178
|
+
expect(path).toBe('/admin/xxx/filterbytk/' + encodeURIComponent('id=1&tenant=ac'));
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should omit filterByTk segment for empty string or when absent', () => {
|
|
182
|
+
expect(generatePathnameFromViewParams([{ viewUid: 'xxx' }])).toBe('/admin/xxx');
|
|
183
|
+
expect(generatePathnameFromViewParams([{ viewUid: 'xxx', filterByTk: '' }])).toBe('/admin/xxx');
|
|
184
|
+
});
|
|
185
|
+
|
|
176
186
|
it('should match parsePathnameToViewParams test cases', () => {
|
|
177
187
|
// Test cases from parsePathnameToViewParams.test.ts
|
|
178
188
|
expect(generatePathnameFromViewParams([{ viewUid: 'xxx' }])).toBe('/admin/xxx');
|