@stalfh233/omc-cli 0.4.75 → 0.4.77
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/coverage/m1-coverage-manifest-v1.json +35 -1
- package/dist/artifacts.js +2 -2
- package/dist/capability-overview.js +11 -1
- package/dist/cli.js +91 -40
- package/dist/command-transport.js +1 -1
- package/dist/compute.js +2 -1
- package/dist/contract-resources.js +2 -0
- package/dist/coverage-registry.js +2 -0
- package/dist/discovery.js +18 -0
- package/dist/endpoints.js +16 -0
- package/dist/form-layout.js +25 -2
- package/dist/form-support.js +52 -7
- package/dist/lanes/datarule.js +33 -9
- package/dist/lanes/folder-contract.js +59 -0
- package/dist/lanes/folder-coverage.js +16 -0
- package/dist/lanes/folder.js +241 -0
- package/dist/lanes/form-contract.js +2 -2
- package/dist/lanes/form.js +15 -7
- package/dist/lanes/lane-ids.js +1 -0
- package/dist/lanes/list-view-contract.js +3 -0
- package/dist/lanes/list-view-shapes.js +27 -1
- package/dist/lanes/list-view.js +126 -1
- package/dist/lanes/model-contract.js +5 -0
- package/dist/lanes/model-coverage.js +1 -0
- package/dist/lanes/model.js +69 -0
- package/dist/path-projection.js +129 -0
- package/dist/tools.js +2 -0
- package/dist/write.js +9 -3
- package/package.json +3 -3
- package/skills/omc-capabilities.md +13 -6
package/dist/lanes/datarule.js
CHANGED
|
@@ -453,14 +453,37 @@ function childFormulaBareReferences(formula, childFieldCodes) {
|
|
|
453
453
|
* field (→ platform 50000 on submit). Field-aware via the child schema's
|
|
454
454
|
* published fields; a caller-provided set avoids a duplicate fieldList read.
|
|
455
455
|
*/
|
|
456
|
+
/**
|
|
457
|
+
* 1.4 of issue 2026-10-23: `datarule.list` resolves a CHILD-table schema
|
|
458
|
+
* (bizdatarule/list answers rows) while `bizproperty/list?schemaCode=<child>`
|
|
459
|
+
* answers 601003 业务模型不存在 — the same schema code reads on one face and
|
|
460
|
+
* fails on the other. Do not let the field-metadata probe veto a write that the
|
|
461
|
+
* rule face itself accepts: return null (metadata unavailable) and let callers
|
|
462
|
+
* skip the field-level guards with a warning.
|
|
463
|
+
*/
|
|
464
|
+
async function readPublishedFieldCodesOrNull(session, schemaCode) {
|
|
465
|
+
try {
|
|
466
|
+
const rows = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode, isPublish: "true" } })).data);
|
|
467
|
+
return new Set(rows.map((row) => String(row.code ?? row.propertyCode ?? "")));
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
if (error instanceof WireError && error.kind === "business" && (error.errcode === 601003 || error.errcode === 10011))
|
|
471
|
+
return null;
|
|
472
|
+
throw error;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
456
475
|
async function assertChildFormulaRefs(session, args, knownChildFields) {
|
|
457
476
|
if (args.dataRuleType !== 2 || args.childRule !== true)
|
|
458
477
|
return;
|
|
459
478
|
const formula = type2FormulaText(args.options);
|
|
460
479
|
if (formula === null)
|
|
461
480
|
return;
|
|
462
|
-
const codes = knownChildFields
|
|
463
|
-
|
|
481
|
+
const codes = knownChildFields === undefined ? await readPublishedFieldCodesOrNull(session, args.schemaCode) : knownChildFields;
|
|
482
|
+
// Metadata unavailable (child-table schema on a face that only answers the
|
|
483
|
+
// rule side): the bare-reference heuristic needs the field list, so skip it
|
|
484
|
+
// rather than refusing a write the platform accepts.
|
|
485
|
+
if (codes === null)
|
|
486
|
+
return;
|
|
464
487
|
const bare = childFormulaBareReferences(formula, codes);
|
|
465
488
|
if (bare.length === 0)
|
|
466
489
|
return;
|
|
@@ -658,8 +681,8 @@ export async function syncFormConditionDataRules(session, schemaCode, fieldCode,
|
|
|
658
681
|
const existing = await listDataRules(session, schemaCode);
|
|
659
682
|
// The designer's rule rows live on the PUBLISHED face (like datarule.create's
|
|
660
683
|
// own guard); a draft-only field cannot carry a designer-visible row yet.
|
|
661
|
-
const
|
|
662
|
-
|
|
684
|
+
const publishedCodes = await readPublishedFieldCodesOrNull(session, schemaCode);
|
|
685
|
+
const published = publishedCodes === null ? true : publishedCodes.has(fieldCode);
|
|
663
686
|
for (const key of changedKeys) {
|
|
664
687
|
const dataRuleType = FORM_CONDITION_RULE_TYPES[key];
|
|
665
688
|
const next = text(nextOptions, key);
|
|
@@ -726,12 +749,13 @@ export function registerDataRuleLane(tools) {
|
|
|
726
749
|
assertDataRuleCheckType(args.dataRuleType, args.checkType);
|
|
727
750
|
assertDataRuleTarget(args);
|
|
728
751
|
const codes = dataRuleTargetCodes(args);
|
|
729
|
-
const
|
|
730
|
-
const published = new Set(fields.map((row) => String(row.code ?? row.propertyCode ?? "")));
|
|
752
|
+
const published = await readPublishedFieldCodesOrNull(session, args.schemaCode);
|
|
731
753
|
await assertChildFormulaRefs(session, args, published);
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
754
|
+
if (published !== null) {
|
|
755
|
+
const missing = codes.filter((code) => !published.has(code));
|
|
756
|
+
if (missing.length > 0)
|
|
757
|
+
throw new ToolRefusalError({ status: "refused", blocker: "datarule-property-not-published", schemaCode: args.schemaCode, propertyCodes: codes, missing, message: `字段 ${args.schemaCode}.${missing.join(",")} 不在发布面上;拒绝 datarule.create POST` });
|
|
758
|
+
}
|
|
735
759
|
const existingRules = await listDataRules(session, args.schemaCode);
|
|
736
760
|
// dataRuleType 1 (CHECK_RULE) fans out into 5 checkTypes that COEXIST on one
|
|
737
761
|
// field (the platform auto-creates a checkType 4 maxLength row per text
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { CONTRACT_FAMILY, CONTRACT_REFERENCE_VERSION, assertDescriptionCarries, behaviorBinding } from "./contract-support.js";
|
|
2
|
+
/**
|
|
3
|
+
* App-tree folder (目录/分组) wire contract — issue
|
|
4
|
+
* 2026-10-23-omc-app-folder-create-and-move. Source: AppFunctionController
|
|
5
|
+
* (/api/app/folders, 8.6.26 :90-260) and
|
|
6
|
+
* ApplicationFacadeImpl.createFolder/updateFolder/deleteFolder (:586-640).
|
|
7
|
+
*/
|
|
8
|
+
export const FOLDER_WIRE = Object.freeze({
|
|
9
|
+
endpoints: Object.freeze({
|
|
10
|
+
create: "POST /api/api/app/folders/create body {appCode,name[,name_i18n,parentId,code,icon]}",
|
|
11
|
+
get: "GET /api/api/app/folders/get?folderId=",
|
|
12
|
+
update: "POST /api/api/app/folders/update body {id,name[,name_i18n,parentId]}",
|
|
13
|
+
delete: "POST /api/api/app/folders/delete?folderId=",
|
|
14
|
+
}),
|
|
15
|
+
defaults: "createFolder forces type=Folder, defaults parentId to the app package id (root) and does NOT reject duplicate names; updateFolder refuses parentId===id and only writes parentId when non-empty; deleteFolder refuses a non-empty folder (DIRECTORY_NOT_EMPTY) — delete is NOT recursive",
|
|
16
|
+
tree: "apppackage/trees?appCode= returns a NESTED tree (folders carry children[]); reading only the top-level array makes a freshly created child folder look absent — flatten recursively and report path/depth",
|
|
17
|
+
live: "2026-10-23 customer HAR replay: folders/create answered errcode 0 成功创建目录; after the token was attached apppackage/trees returned the full tree",
|
|
18
|
+
});
|
|
19
|
+
export const FOLDER_LANE_CONTRACT = Object.freeze({
|
|
20
|
+
lane: "app-tree-folders",
|
|
21
|
+
family: CONTRACT_FAMILY,
|
|
22
|
+
referenceVersion: CONTRACT_REFERENCE_VERSION,
|
|
23
|
+
endpoints: Object.freeze({
|
|
24
|
+
tree: "apppackage/trees",
|
|
25
|
+
create: "folders/create",
|
|
26
|
+
get: "folders/get",
|
|
27
|
+
update: "folders/update",
|
|
28
|
+
delete: "folders/delete",
|
|
29
|
+
}),
|
|
30
|
+
wire: FOLDER_WIRE,
|
|
31
|
+
wireBindings: Object.freeze({
|
|
32
|
+
endpoints: Object.freeze([behaviorBinding("test/app-folder-and-model-move.test.ts#model.move reparents the model; folder.delete refuses a non-empty folder", assertFolderContract)]),
|
|
33
|
+
defaults: Object.freeze([behaviorBinding("test/app-folder-and-model-move.test.ts#model.move reparents the model; folder.delete refuses a non-empty folder", assertFolderContract)]),
|
|
34
|
+
tree: Object.freeze([behaviorBinding("test/app-folder-and-model-move.test.ts#folder.create then folder.tree sees the folder (nested tree is flattened)", assertFolderContract)]),
|
|
35
|
+
live: Object.freeze([behaviorBinding("test/app-folder-and-model-move.test.ts#folder.create then folder.tree sees the folder (nested tree is flattened)", assertFolderContract)]),
|
|
36
|
+
}),
|
|
37
|
+
evidence: Object.freeze([
|
|
38
|
+
"cp-sources/backend/decompiled-full/launcher-8.6.26/cloudpivot-engine-8.6.26/com/authine/cloudpivot/web/api/controller/app/AppFunctionController.java",
|
|
39
|
+
"cp-sources/backend/decompiled-full/launcher-8.6.26/cloudpivot-metadata-8.6.26/com/authine/cloudpivot/metadata/history/apppackage/service/ApplicationFacadeImpl.java",
|
|
40
|
+
"issue/2026-10-23-omc-app-folder-create-and-move.md",
|
|
41
|
+
]),
|
|
42
|
+
tools: Object.freeze({
|
|
43
|
+
"folder.tree": Object.freeze({ scope: "app-tree folder/model tree (flattened with path/depth)", description: "读应用树(目录/分组 + 模型):平台 apppackage/trees 返回嵌套 children,本工具递归展平并给每节点 path/depth" }),
|
|
44
|
+
"folder.create": Object.freeze({ scope: "app-tree folder create (folders/create)", description: "新建目录:folders/create 省略 parentId 建顶层目录;同父同名默认拒绝(folder-duplicate)" }),
|
|
45
|
+
"folder.update": Object.freeze({ scope: "app-tree folder rename/move (folders/update)", description: "重命名/挂载目录(不移动目录内模型,模型用 model.move);parentId 不可等于自身" }),
|
|
46
|
+
"folder.delete": Object.freeze({ scope: "app-tree folder delete (folders/delete, non-recursive)", description: "删除空目录:非递归,有子节点回 DIRECTORY_NOT_EMPTY(工具先查子节点,拒 folder-not-empty)" }),
|
|
47
|
+
}),
|
|
48
|
+
});
|
|
49
|
+
export function assertFolderContract() {
|
|
50
|
+
assertDescriptionCarries(FOLDER_LANE_CONTRACT, "folder.tree", ["children", "path", "depth"]);
|
|
51
|
+
assertDescriptionCarries(FOLDER_LANE_CONTRACT, "folder.create", ["parentId", "顶层", "folder-duplicate"]);
|
|
52
|
+
assertDescriptionCarries(FOLDER_LANE_CONTRACT, "folder.update", ["parentId", "model.move"]);
|
|
53
|
+
assertDescriptionCarries(FOLDER_LANE_CONTRACT, "folder.delete", ["非递归", "folder-not-empty"]);
|
|
54
|
+
if (!FOLDER_WIRE.defaults.includes("DIRECTORY_NOT_EMPTY"))
|
|
55
|
+
throw new Error("folder contract must pin the non-recursive delete refusal");
|
|
56
|
+
if (!FOLDER_WIRE.tree.includes("NESTED"))
|
|
57
|
+
throw new Error("folder contract must pin the nested tree shape");
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=folder-contract.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { claim, declareLaneCoverage } from "./coverage-declaration.js";
|
|
2
|
+
import { FOLDER_LANE_CONTRACT } from "./folder-contract.js";
|
|
3
|
+
const T = FOLDER_LANE_CONTRACT.tools;
|
|
4
|
+
export const FOLDER_COVERAGE = declareLaneCoverage(FOLDER_LANE_CONTRACT.lane, [
|
|
5
|
+
{
|
|
6
|
+
capability: "App-tree folders (目录/分组): read flattened tree, create, rename/move, non-recursive delete",
|
|
7
|
+
evidence: ["issue/2026-10-23-omc-app-folder-create-and-move.md", "packages/omc-cli/src/lanes/folder.ts"],
|
|
8
|
+
tools: [
|
|
9
|
+
claim("folder.tree", T["folder.tree"].scope),
|
|
10
|
+
claim("folder.create", T["folder.create"].scope),
|
|
11
|
+
claim("folder.update", T["folder.update"].scope),
|
|
12
|
+
claim("folder.delete", T["folder.delete"].scope),
|
|
13
|
+
],
|
|
14
|
+
},
|
|
15
|
+
]);
|
|
16
|
+
//# sourceMappingURL=folder-coverage.js.map
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ToolRefusalError, defineTool } from "../tool-types.js";
|
|
3
|
+
import { governedRead } from "../read.js";
|
|
4
|
+
import { writeTool } from "../write.js";
|
|
5
|
+
import { wireCall, wireRows } from "../wire.js";
|
|
6
|
+
import { ENDPOINTS } from "../endpoints.js";
|
|
7
|
+
import { capabilityArg, environmentArg } from "../args.js";
|
|
8
|
+
import { resolveApplicationCode } from "../support.js";
|
|
9
|
+
import { LANES } from "./lane-ids.js";
|
|
10
|
+
import { FOLDER_LANE_CONTRACT } from "./folder-contract.js";
|
|
11
|
+
/**
|
|
12
|
+
* App-tree folders (目录/分组) and model reparenting.
|
|
13
|
+
*
|
|
14
|
+
* issue 2026-10-23-omc-app-folder-create-and-move: a customer had to build 8
|
|
15
|
+
* folders and move 33 models by hand (or with a raw HAR-replayed script) because
|
|
16
|
+
* OMC could only attach a NEW model to an EXISTING folder at create time.
|
|
17
|
+
*
|
|
18
|
+
* Wire facts (decompiled 8.6.26 AppFunctionController :90-260 +
|
|
19
|
+
* ApplicationFacadeImpl.createFolder/updateFolder/deleteFolder :586-640, and the
|
|
20
|
+
* customer's HAR replay which answered 0「成功创建目录」):
|
|
21
|
+
* - POST /api/app/folders/create {appCode, name, name_i18n?, parentId?, code?, icon?}
|
|
22
|
+
* createFolder forces type=Folder and defaults parentId to the app package id
|
|
23
|
+
* (root) — so an omitted parentId creates a TOP-LEVEL folder, not a child.
|
|
24
|
+
* - POST /api/app/folders/update {id, name, name_i18n?, parentId?}
|
|
25
|
+
* refuses parentId === id (FOLDER_PARENTID_EQUALS_ID); parentId is only
|
|
26
|
+
* written when non-empty (omitted keeps the current parent).
|
|
27
|
+
* - POST /api/app/folders/delete ?folderId= refuses a non-empty folder
|
|
28
|
+
* (DIRECTORY_NOT_EMPTY) — it is NOT a recursive delete.
|
|
29
|
+
* - GET /api/app/folders/get ?folderId=
|
|
30
|
+
* - The tree face (apppackage/trees?appCode=) is NESTED: folders carry their
|
|
31
|
+
* children in `children[]`. Reading only the top-level array reports "folder
|
|
32
|
+
* created but not found" (the customer's first failure) — this lane flattens
|
|
33
|
+
* recursively and reports each node's depth/path.
|
|
34
|
+
*
|
|
35
|
+
* Moving a MODEL is the same bizmodels/update call model.update already makes,
|
|
36
|
+
* with the target folder as parentId — exposed as `model.move` so it has its own
|
|
37
|
+
* intent, dry-run preview, and readback.
|
|
38
|
+
*/
|
|
39
|
+
const LANE = LANES.folder;
|
|
40
|
+
const T = FOLDER_LANE_CONTRACT.tools;
|
|
41
|
+
/** Flatten the nested app tree, keeping the path so a caller can tell WHICH "合同" it means. */
|
|
42
|
+
/**
|
|
43
|
+
* Flatten the nested app tree. Live 1.1.25 readback: `apppackage/trees?appCode=`
|
|
44
|
+
* returns the TOP-LEVEL folders directly (no app-package container row), each
|
|
45
|
+
* carrying its children — so a top-level folder is depth 0 and a child is
|
|
46
|
+
* "父 / 子" at depth 1. Reading only the top array makes a freshly created child
|
|
47
|
+
* folder look absent (the customer's first failure).
|
|
48
|
+
*/
|
|
49
|
+
function flattenFolders(rows, parentPath = []) {
|
|
50
|
+
const out = [];
|
|
51
|
+
for (const raw of rows) {
|
|
52
|
+
if (raw === null || typeof raw !== "object")
|
|
53
|
+
continue;
|
|
54
|
+
const node = raw;
|
|
55
|
+
const name = String(node.name ?? node.code ?? "");
|
|
56
|
+
const path = [...parentPath, name];
|
|
57
|
+
out.push({
|
|
58
|
+
id: node.id ?? null,
|
|
59
|
+
type: node.type ?? null,
|
|
60
|
+
code: node.code ?? null,
|
|
61
|
+
name: node.name ?? null,
|
|
62
|
+
parentId: node.parentId ?? null,
|
|
63
|
+
depth: path.length - 1,
|
|
64
|
+
path: path.join(" / "),
|
|
65
|
+
});
|
|
66
|
+
if (Array.isArray(node.children) && node.children.length > 0)
|
|
67
|
+
out.push(...flattenFolders(node.children, path));
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
async function readTree(session, appCode) {
|
|
72
|
+
return flattenFolders(wireRows((await wireCall(session, { path: ENDPOINTS.applicationTree, query: { appCode } })).data));
|
|
73
|
+
}
|
|
74
|
+
function folderRefusal(schemaCode, message, extra = {}) {
|
|
75
|
+
throw new ToolRefusalError({ status: "refused", blocker: "folder-not-found", ...(schemaCode ? { applicationCode: schemaCode } : {}), ...extra, message });
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Resolve a folder by id, code, or by full path ("合同管理 / 归档"). A bare name
|
|
79
|
+
* that matches several folders is ambiguous — name them with their paths instead
|
|
80
|
+
* of guessing (the customer's 8 folders included duplicates by design).
|
|
81
|
+
*/
|
|
82
|
+
function resolveFolder(rows, ref) {
|
|
83
|
+
const byId = rows.find((row) => String(row.id ?? "") === ref);
|
|
84
|
+
if (byId)
|
|
85
|
+
return byId;
|
|
86
|
+
const byCode = rows.filter((row) => String(row.code ?? "") === ref);
|
|
87
|
+
if (byCode.length === 1)
|
|
88
|
+
return byCode[0];
|
|
89
|
+
if (byCode.length > 1)
|
|
90
|
+
folderRefusal(undefined, `目录 code「${ref}」不唯一(${byCode.length} 个)——改用 id 或完整 path:${byCode.map((row) => String(row.path)).join("、")}`);
|
|
91
|
+
const byPath = rows.filter((row) => String(row.path ?? "") === ref);
|
|
92
|
+
if (byPath.length === 1)
|
|
93
|
+
return byPath[0];
|
|
94
|
+
const byName = rows.filter((row) => String(row.name ?? "") === ref);
|
|
95
|
+
if (byName.length === 1)
|
|
96
|
+
return byName[0];
|
|
97
|
+
if (byName.length > 1)
|
|
98
|
+
folderRefusal(undefined, `目录名「${ref}」不唯一(${byName.length} 个)——用完整 path 指定:${byName.map((row) => String(row.path)).join("、")}`);
|
|
99
|
+
folderRefusal(undefined, `找不到目录「${ref}」——先 folder.tree 看 id/path;注意 create 省略 parentId 建的是顶层目录`, {
|
|
100
|
+
available: rows.filter((row) => row.type === "Folder").map((row) => ({ id: row.id, path: row.path })).slice(0, 40),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function folderTools(tools) {
|
|
104
|
+
tools.push(defineTool({
|
|
105
|
+
name: "folder.tree",
|
|
106
|
+
layer: "low",
|
|
107
|
+
kind: "read",
|
|
108
|
+
lane: LANE,
|
|
109
|
+
scope: T["folder.tree"].scope,
|
|
110
|
+
description: "读取应用树(目录/分组 + 模型节点),**递归展平**并给出每个节点的 `path` 与 `depth`(平台 `apppackage/trees?appCode=` 返回的是嵌套 children,只看顶层会误判“目录没建成”——issue 2026-10-23 的真实失败)。`foldersOnly:true` 只回目录(type=Folder)。目录的 id 就是 folder.update/delete 与 model.move 的 `folderId/parentId`。",
|
|
111
|
+
inputSchema: {
|
|
112
|
+
environment: environmentArg,
|
|
113
|
+
applicationCode: z.string().optional().describe("应用 code;省略用配置默认应用"),
|
|
114
|
+
foldersOnly: z.boolean().optional().describe("只回目录节点(type=Folder)"),
|
|
115
|
+
capability: capabilityArg,
|
|
116
|
+
},
|
|
117
|
+
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
118
|
+
const appCode = await resolveApplicationCode(session, args.applicationCode);
|
|
119
|
+
const rows = await readTree(session, appCode);
|
|
120
|
+
const nodes = args.foldersOnly === true ? rows.filter((row) => row.type === "Folder") : rows;
|
|
121
|
+
return {
|
|
122
|
+
applicationCode: appCode,
|
|
123
|
+
nodeCount: nodes.length,
|
|
124
|
+
folderCount: rows.filter((row) => row.type === "Folder").length,
|
|
125
|
+
modelCount: rows.filter((row) => row.type === "BizModel").length,
|
|
126
|
+
nodes,
|
|
127
|
+
note: "type=Folder 是目录/分组;parentId 指向父目录(顶层目录的 parentId = 应用包 id)。嵌套 children 已展平,path 用「 / 」连接。",
|
|
128
|
+
};
|
|
129
|
+
}),
|
|
130
|
+
}));
|
|
131
|
+
writeTool(tools, "folder.create", LANE, T["folder.create"].scope, "新建应用目录(分组):`POST /api/app/folders/create`(AppFunctionController:105;`ApplicationFacadeImpl.createFolder:586` 强制 `type=Folder`,**省略 `parentId` 会挂到应用根**,即顶层目录)。给 `parentId` 才是子目录。写入后按 `path` 回读确认(树是嵌套的)。同名不等于重复:平台不校验重名,工具默认按「同父目录下同名」拒绝(`folder-duplicate`),要并列同名传 `allowDuplicate:true`。", {
|
|
132
|
+
applicationCode: z.string().optional().describe("应用 code;省略用配置默认应用"),
|
|
133
|
+
name: z.string().min(1).describe("目录显示名(平台校验非空、中文长度 ≤50)"),
|
|
134
|
+
parentId: z.string().optional().describe("父目录 id;**省略=应用根(顶层目录)**。子目录必须显式给,值来自 folder.tree 的 id"),
|
|
135
|
+
code: z.string().optional().describe("目录 code(可选;平台只校验 code 唯一,空则自动生成)"),
|
|
136
|
+
name_i18n: z.string().optional().describe("多语言名(JSON 字符串,如 {\"zh-CN\":\"合同\",\"en\":\"Contract\"})"),
|
|
137
|
+
allowDuplicate: z.boolean().optional().describe("允许同父目录下同名(默认拒绝,避免建出两个同名目录后无法用 path 定位)"),
|
|
138
|
+
}, async (session, args) => {
|
|
139
|
+
const appCode = await resolveApplicationCode(session, args.applicationCode);
|
|
140
|
+
const before = await readTree(session, appCode);
|
|
141
|
+
if (args.allowDuplicate !== true) {
|
|
142
|
+
const siblings = before.filter((row) => String(row.parentId ?? "") === String(args.parentId ?? "") || (args.parentId === undefined && row.depth === 0));
|
|
143
|
+
if (siblings.some((row) => row.type === "Folder" && String(row.name ?? "") === args.name))
|
|
144
|
+
throw new ToolRefusalError({
|
|
145
|
+
status: "refused",
|
|
146
|
+
blocker: "folder-duplicate",
|
|
147
|
+
applicationCode: appCode,
|
|
148
|
+
path: `${args.parentId ?? "(root)"} / ${args.name}`,
|
|
149
|
+
message: `同一层级已有同名目录「${args.name}」——平台允许重名,但重名后 folder.tree 的 path 无法唯一定位;确认要并列同名再传 allowDuplicate:true`,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
const performed = await wireCall(session, {
|
|
153
|
+
path: ENDPOINTS.folderCreate,
|
|
154
|
+
method: "POST",
|
|
155
|
+
body: {
|
|
156
|
+
appCode,
|
|
157
|
+
name: args.name,
|
|
158
|
+
...(args.parentId !== undefined ? { parentId: args.parentId } : {}),
|
|
159
|
+
...(args.code !== undefined ? { code: args.code } : {}),
|
|
160
|
+
...(args.name_i18n !== undefined ? { name_i18n: args.name_i18n } : {}),
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
const after = await readTree(session, appCode);
|
|
164
|
+
const created = after.filter((row) => row.type === "Folder" && String(row.name ?? "") === args.name && !before.some((old) => old.id === row.id));
|
|
165
|
+
return {
|
|
166
|
+
performed: { errcode: performed.errcode },
|
|
167
|
+
readback: {
|
|
168
|
+
applicationCode: appCode,
|
|
169
|
+
present: created.length > 0,
|
|
170
|
+
created: created.map((row) => ({ id: row.id, path: row.path, parentId: row.parentId })),
|
|
171
|
+
folderCount: after.filter((row) => row.type === "Folder").length,
|
|
172
|
+
...(after.filter((row) => row.type === "Folder" && String(row.name ?? "") === args.name && String(row.parentId ?? "") === String(created[0]?.parentId ?? ""))
|
|
173
|
+
.length > 1
|
|
174
|
+
? { warning: "同层级出现了多个同名目录(allowDuplicate 生效或既有同名);后续用 id 定位,不要用 path/name" }
|
|
175
|
+
: {}),
|
|
176
|
+
hint: created.length > 0 ? "把模型放进该目录用 model.move(parentId = 这里的 id)" : "回读未见新目录:树是嵌套的,但 folder.tree 已递归展平;若确实没有,检查应用 code",
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
});
|
|
180
|
+
writeTool(tools, "folder.update", LANE, T["folder.update"].scope, "重命名目录或把目录挂到另一个目录下:`POST /api/app/folders/update`(AppFunctionController:219;`updateFolder:612` 拒绝 `parentId === id`,`parentId` 为空时保留原父级)。**只改目录节点本身,不会移动目录里的模型**——模型移动用 `model.move`。", {
|
|
181
|
+
applicationCode: z.string().optional(),
|
|
182
|
+
folderId: z.string().min(1).describe("目录 id(folder.tree 的 id)"),
|
|
183
|
+
name: z.string().min(1).optional().describe("新显示名"),
|
|
184
|
+
name_i18n: z.string().optional(),
|
|
185
|
+
parentId: z.string().min(1).optional().describe("新父目录 id(挂到根用应用包 id;不理解根 id 就省略此字段表示不改父级)"),
|
|
186
|
+
}, async (session, args) => {
|
|
187
|
+
const appCode = await resolveApplicationCode(session, args.applicationCode);
|
|
188
|
+
const before = await readTree(session, appCode);
|
|
189
|
+
const current = resolveFolder(before, args.folderId);
|
|
190
|
+
if (args.name === undefined && args.parentId === undefined)
|
|
191
|
+
throw new ToolRefusalError({ status: "refused", blocker: "folder-update-empty-patch", message: "没有可应用的补丁——至少给 name 或 parentId" });
|
|
192
|
+
if (args.parentId !== undefined && args.parentId === args.folderId)
|
|
193
|
+
throw new ToolRefusalError({ status: "refused", blocker: "folder-parent-self", message: "parentId 不能等于目录自身(平台 FOLDER_PARENTID_EQUALS_ID)" });
|
|
194
|
+
const performed = await wireCall(session, {
|
|
195
|
+
path: ENDPOINTS.folderUpdate,
|
|
196
|
+
method: "POST",
|
|
197
|
+
body: { id: args.folderId, name: args.name ?? current.name, ...(args.name_i18n !== undefined ? { name_i18n: args.name_i18n } : {}), ...(args.parentId !== undefined ? { parentId: args.parentId } : {}) },
|
|
198
|
+
});
|
|
199
|
+
const after = await readTree(session, appCode);
|
|
200
|
+
const now = after.find((row) => String(row.id ?? "") === args.folderId);
|
|
201
|
+
return {
|
|
202
|
+
performed: { errcode: performed.errcode },
|
|
203
|
+
readback: {
|
|
204
|
+
folderId: args.folderId,
|
|
205
|
+
present: now !== undefined,
|
|
206
|
+
before: { name: current.name, parentId: current.parentId, path: current.path },
|
|
207
|
+
after: now ? { name: now.name, parentId: now.parentId, path: now.path } : null,
|
|
208
|
+
note: "只改了目录节点;目录内的模型不随目录移动(模型要动请用 model.move)",
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
});
|
|
212
|
+
writeTool(tools, "folder.delete", LANE, T["folder.delete"].scope, "删除空目录:`POST /api/app/folders/delete?folderId=`(AppFunctionController:246)。**非递归**——目录里还有子目录或模型时平台回 `DIRECTORY_NOT_EMPTY`,工具在写前先查子节点并拒绝(`folder-not-empty`),避免半删。要清空请先 `model.move` 移走模型、删子目录。", {
|
|
213
|
+
applicationCode: z.string().optional(),
|
|
214
|
+
folderId: z.string().min(1),
|
|
215
|
+
}, async (session, args) => {
|
|
216
|
+
const appCode = await resolveApplicationCode(session, args.applicationCode);
|
|
217
|
+
const before = await readTree(session, appCode);
|
|
218
|
+
const current = resolveFolder(before, args.folderId);
|
|
219
|
+
if (String(current.type ?? "") !== "Folder")
|
|
220
|
+
throw new ToolRefusalError({ status: "refused", blocker: "folder-target-not-folder", message: `${args.folderId} 不是目录节点(type=${String(current.type)});删模型用 model.delete` });
|
|
221
|
+
const children = before.filter((row) => String(row.parentId ?? "") === String(args.folderId));
|
|
222
|
+
if (children.length > 0)
|
|
223
|
+
throw new ToolRefusalError({
|
|
224
|
+
status: "refused",
|
|
225
|
+
blocker: "folder-not-empty",
|
|
226
|
+
folderId: args.folderId,
|
|
227
|
+
children: children.map((row) => ({ id: row.id, type: row.type, path: row.path })).slice(0, 40),
|
|
228
|
+
message: `目录「${current.path}」还有 ${children.length} 个子节点——平台非递归删除会回 DIRECTORY_NOT_EMPTY。先 model.move 移走模型 / 删子目录`,
|
|
229
|
+
});
|
|
230
|
+
const performed = await wireCall(session, { path: ENDPOINTS.folderDelete, method: "POST", query: { folderId: args.folderId } });
|
|
231
|
+
const after = await readTree(session, appCode);
|
|
232
|
+
return {
|
|
233
|
+
performed: { errcode: performed.errcode },
|
|
234
|
+
readback: { folderId: args.folderId, deleted: !after.some((row) => String(row.id ?? "") === args.folderId), path: current.path },
|
|
235
|
+
};
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
export function registerFolderLane(tools) {
|
|
239
|
+
folderTools(tools);
|
|
240
|
+
}
|
|
241
|
+
//# sourceMappingURL=folder.js.map
|
|
@@ -19,7 +19,7 @@ export const FORM_WIRE = Object.freeze({
|
|
|
19
19
|
actionsColumns: "ticket 100: draftActionsJson/publishedActionsJson are REAL bizsheet columns (BizForm.java:95-100 @SerializableField; BizFormModel.java:59-62 serialized by bizsheet/get) — the designer writes draftActionsJson on save and both faces on publish (frontend-dist/admin/js/4280.cfa75764.js offsets 193538/185347); form.get full:true surfaces them; publish promotes draftActionsJson → publishedActionsJson",
|
|
20
20
|
});
|
|
21
21
|
const FORM_GET_DESCRIPTION = '读取默认表单(bizsheet/get)的发布状态与控件清单(紧凑默认)。**发布态以返回的 `published` / `existDraft` 布尔为准**(`published:true`=已发布;`published:false,existDraft:true`=只有草稿,**未发布**)——不要用 `publishedControlCodes` 是否非空来推断(草稿字段会让它非空,误判为已发布)。**每个模型交付前其默认表单必须 `published:true`**:用 `configure_form` 或 `form.draft(rebuild:true)`+`form.publish` 发布,并逐模型 `form.get` 核对。`full:true` 还返回 `options` 与解析后的 `customButtonJson`(表单工具栏按钮)。传 `controls:["code",…]` 只取指定控件的 options(如关联控件的 mappings);传 `full:true` 才返回 draft/published 的完整 view/attributes/html/actions JSON(actions 列为设计器保存/发布的表单级按钮动作)。体积大,仅在需要整体布局时用。';
|
|
22
|
-
const FORM_DRAFT_DESCRIPTION = "Save the default-form DRAFT (bizsheet/update). Reads the current sheet, merges the caller's explicit draftViewJson/draftAttributesJson (or preserves the existing draft); when both are absent on a fresh default form, composes the designer-standard scaffold from the model's published fields. A redundant field (relation-mapping carry-through target) always stays bound; with no workflow it is hidden in the design (options.visible=false), with a workflow it stays visible for per-node propertyPermissions. Pass `draftHtmlJson` to thread custom online-script HTML through. Empty-layout guard: explicitly passing \"[]\"/\"{}\" view/attributes is refused (form-empty-layout) — the platform designer's Save is NOT lossless and clears drafts to exactly that shape; escape with allowEmptyLayout:true, and recover cleared drafts from the form version history (/api/api/app/bizsheet/history/list).
|
|
22
|
+
const FORM_DRAFT_DESCRIPTION = "Save the default-form DRAFT (bizsheet/update). Reads the current sheet, merges the caller's explicit draftViewJson/draftAttributesJson (or preserves the existing draft); when both are absent on a fresh default form, composes the designer-standard scaffold from the model's published fields. A redundant field (relation-mapping carry-through target) always stays bound; with no workflow it is hidden in the design (options.visible=false), with a workflow it stays visible for per-node propertyPermissions. Pass `draftHtmlJson` to thread custom online-script HTML through. Empty-layout guard: explicitly passing \"[]\"/\"{}\" view/attributes is refused (form-empty-layout) — the platform designer's Save is NOT lossless and clears drafts to exactly that shape; escape with allowEmptyLayout:true, and recover cleared drafts from the form version history (/api/api/app/bizsheet/history/list). **读回原样回写**:从 `form.get` 取回的 view/attributes 直接回传时加 `fromReadback:true`——工具会与已存草稿/发布面逐字比对,一致才放行(平台自身布局可能含子表列行/系统行默认宽度),被改动过的面照常按 validation 拒绝。**表单工具栏按钮**:`buttons`(数组)或 `customButtonJson`(原样字符串)写进 `biz_sheet.options.customButtonJson`;**要到 form.publish 才生成 h_biz_button(triggerType=FORM)**,且表单页是否显示还取决于流程节点 `buttonPermission.customBtns[].enabled`(设计器「表单操作」勾选)。`useLocation` 用 FORM/ROW/LIST,表单工具栏按钮用 FORM。 **表单控件属性(含关联填充 mappings)从字段行派生**:改关联映射的事实源在字段 options.mappings(field.update → field.publish),直接改这里的控件映射会被平台按字段重算覆盖(真机 2026-10-14 实证);字段发布后如需刷新已发布表单的控件映射,用本工具 rebuild:false 重存(保留现有布局与在线JS)再 form.publish。";
|
|
23
23
|
const FORM_PUBLISH_DESCRIPTION = "Publish the default form (bizsheet/publish, proven shape: draft*Json promoted to published*Json verbatim) with published-face readback. Degrade guard: refuses to promote a draft carrying FEWER controls than the published face (form-publish-degrade) — the designer's Save clears drafts non-losslessly; recover via the form version history (/api/api/app/bizsheet/history/list) and rewrite the draft first, or pass allowDegrade:true to force the reduction. rebuild:true is the one-call refresh: discard the existing draft, re-scaffold from the published fields (non-structural control options preserved), then force-promote — use it after a field shape change. **表单工具栏按钮**:传 `buttons`(或 `customButtonJson`)→ 写进 `options.customButtonJson` 并在本次 publish 生成 `h_biz_button(triggerType=FORM, triggerCode=<formCode>)`(平台 `publishBizForm:1022`);`options.customButtonJson` 必须是**字符串化数组**(对象会 50000 syntax error);表单页是否显示再由流程节点 `buttonPermission.customBtns[].enabled` 过滤。";
|
|
24
24
|
const FORM_RUN_BUTTON_DESCRIPTION = "执行表单工具栏按钮(平台同款面:`POST /api/api/runtime/form/executeCustomButton`,body `{bizButtonModel, bizObjectList:[<完整业务对象>], reacquireDataFromDb:true}`)。**与列表行按钮不同**:列表按钮发 `launcher/domain_service_runtime/<schema>/<rule>` + `ids[]`(用 `rule.run`),表单按钮带**整条记录**,不能互换。按钮来源 `GET /api/api/system/getCustomButtons`(只列 triggerType=FORM 的 h_biz_button)。前提:按钮已由 `form.publish` 的 `buttons`/`customButtonJson` 生成。返回是执行结果信封,规则副作用用 `data.load` 另读;被规则 UPDATE 的目标行须是 COMPLETED(引擎查不到 DRAFT)。`302045` = 数据被占用。";
|
|
25
25
|
const CONFIGURE_FORM_DESCRIPTION = "High-level: bind fields into the default form and publish it (form.draft → form.publish), subject to the write permission gate. Delegates to the proven form.draft scaffold + form.publish paths so the real empty-form layout (and any caller-supplied draftHtmlJson) is persisted and promoted — never bypasses with a shortcut body. `sheetCode` is optional: omitted means the model's default sheet (sheetCode = schemaCode).";
|
|
@@ -79,7 +79,7 @@ export function assertFormContract() {
|
|
|
79
79
|
assertDescriptionCarries(FORM_LANE_CONTRACT, "form.get", ["bizsheet/get", "draft/published", "mappings", "`full:true`", "actions"]);
|
|
80
80
|
if (!FORM_WIRE.actionsColumns.includes("BizForm.java:95-100"))
|
|
81
81
|
throw new Error("form contract must pin the actions-columns evidence (ticket 100)");
|
|
82
|
-
assertDescriptionCarries(FORM_LANE_CONTRACT, "form.draft", ["bizsheet/update", "draftViewJson", "draftAttributesJson", "scaffold", "draftHtmlJson", "form-empty-layout"]);
|
|
82
|
+
assertDescriptionCarries(FORM_LANE_CONTRACT, "form.draft", ["bizsheet/update", "draftViewJson", "draftAttributesJson", "scaffold", "draftHtmlJson", "form-empty-layout", "fromReadback"]);
|
|
83
83
|
if (!FORM_WIRE.redundantFieldVisibility.includes("propertyPermissions"))
|
|
84
84
|
throw new Error("form contract must carry the redundant-field visibility (propertyPermissions) fact");
|
|
85
85
|
assertDescriptionCarries(FORM_LANE_CONTRACT, "form.publish", ["bizsheet/publish", "draft*Json", "published*Json", "verbatim", "form-publish-degrade", "history/list", "rebuild:true", "force-promote"]);
|
package/dist/lanes/form.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { ENDPOINTS } from "../endpoints.js";
|
|
3
|
+
import { decodeButtonScript } from "./list-view.js";
|
|
3
4
|
import { executeFormDraftWrite, executeFormPublishWrite, executeFormRunButtonWrite, readFormButtons, readFormOrNull, refuseEmptyCallerLayout, validateHtmlRender } from "../form-support.js";
|
|
4
5
|
import { capabilityArg, environmentArg } from "../args.js";
|
|
5
6
|
import { governedRead } from "../read.js";
|
|
@@ -51,6 +52,7 @@ export function registerFormLane(tools) {
|
|
|
51
52
|
.optional()
|
|
52
53
|
.describe("控件定义投影:true = 返回全部控件的 draft/published 定义;字符串数组 = 只返回这些控件码的定义;省略 = 只回控件码清单"),
|
|
53
54
|
full: z.boolean().optional().describe("返回全部六份布局 JSON 原文(大体积)"),
|
|
55
|
+
buttonScripts: z.boolean().optional().describe("附带表单按钮的脚本原文(options.customButtonJson 里 SCRIPT 按钮的 actionConfig.scriptData)及 sha/字节数"),
|
|
54
56
|
capability: capabilityArg,
|
|
55
57
|
},
|
|
56
58
|
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
@@ -120,18 +122,21 @@ export function registerFormLane(tools) {
|
|
|
120
122
|
// Form buttons are tiny; list them plainly in the COMPACT face so a
|
|
121
123
|
// readback never needs the artifact just to see which buttons exist.
|
|
122
124
|
const formButtons = readFormButtons(sheet.options) ?? [];
|
|
125
|
+
const withScripts = args.buttonScripts === true;
|
|
123
126
|
return {
|
|
124
127
|
...base,
|
|
125
128
|
...(formButtons.length > 0
|
|
126
129
|
? {
|
|
127
130
|
formButtonCodes: formButtons.map((button) => String(button.code ?? "")),
|
|
128
|
-
formButtons: formButtons.map((button) =>
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
131
|
+
formButtons: formButtons.map((button) => withScripts
|
|
132
|
+
? decodeButtonScript(button)
|
|
133
|
+
: {
|
|
134
|
+
code: button.code ?? null,
|
|
135
|
+
name: button.name ?? null,
|
|
136
|
+
bindAction: button.bindAction ?? null,
|
|
137
|
+
targetCode: button.targetCode ?? null,
|
|
138
|
+
useLocation: button.useLocation ?? null,
|
|
139
|
+
}),
|
|
135
140
|
}
|
|
136
141
|
: {}),
|
|
137
142
|
draftControlCodes: draftControls,
|
|
@@ -148,6 +153,7 @@ export function registerFormLane(tools) {
|
|
|
148
153
|
draftHtmlJson: z.string().optional(),
|
|
149
154
|
fieldCodes: z.array(z.string().min(1)).optional().describe("脚手架绑定的已发布字段(按此顺序);由 configure_form/intent 显式传入"),
|
|
150
155
|
rebuild: z.boolean().optional().describe("true 时丢弃现有草稿布局,按已发布字段重新生成(新增字段/子表要用它;默认保留现有草稿)"),
|
|
156
|
+
fromReadback: z.boolean().optional().describe("true = 本次 draftViewJson/draftAttributesJson 来自 form.get 的原样读回(平台自身布局可能含子表列行/系统行超宽);工具会与已存面逐字比对,只有确实一致才放行,被改动过的面照常拒绝"),
|
|
151
157
|
allowEmptyLayout: z.boolean().optional().describe("显式放行空布局(\"[]\"/\"{}\")写入;默认拒绝(form-empty-layout),防设计器 Save 清空后照写丢控件"),
|
|
152
158
|
buttons: z
|
|
153
159
|
.array(z.record(z.unknown()))
|
|
@@ -156,6 +162,7 @@ export function registerFormLane(tools) {
|
|
|
156
162
|
customButtonJson: z.string().optional().describe("buttons 的原样 JSON 数组字符串(与 buttons 互斥;用于把 form.get 读回值直接回写)"),
|
|
157
163
|
buttonsMode: z.enum(["merge", "replace"]).optional().describe("默认 merge:按 code 增量 upsert(改一个按钮不必重发全表);replace:整表替换"),
|
|
158
164
|
removeButtonCodes: z.array(z.string().min(1)).optional().describe("要删除的按钮 code(仅 merge 模式)"),
|
|
165
|
+
buttonScripts: z.array(z.object({ code: z.string().min(1), script: z.string() })).optional().describe("直接改已有表单按钮的脚本(actionConfig.scriptData),按 code 逐字替换;未知 code 拒绝。不必重发整表"),
|
|
159
166
|
}, async (session, args) => executeFormDraftWrite(session, { ...args, sheetCode: resolveSheetCode(args.schemaCode, args.sheetCode) }), (args) => {
|
|
160
167
|
// Zero-wire refusal (ticket 74): fires in semanticGate BEFORE the
|
|
161
168
|
// governed pipeline — including the rollback before-capture read.
|
|
@@ -191,6 +198,7 @@ export function registerFormLane(tools) {
|
|
|
191
198
|
customButtonJson: z.string().optional().describe("buttons 的原样 JSON 数组字符串(与 buttons 互斥)"),
|
|
192
199
|
buttonsMode: z.enum(["merge", "replace"]).optional().describe("默认 merge:按 code 增量 upsert;replace:整表替换"),
|
|
193
200
|
removeButtonCodes: z.array(z.string().min(1)).optional().describe("要删除的按钮 code(仅 merge 模式)"),
|
|
201
|
+
buttonScripts: z.array(z.object({ code: z.string().min(1), script: z.string() })).optional().describe("直接改已有表单按钮的脚本(actionConfig.scriptData),按 code 逐字替换;未知 code 拒绝。不必重发整表"),
|
|
194
202
|
}, async (session, args) => executeFormPublishWrite(session, { ...args, sheetCode: resolveSheetCode(args.schemaCode, args.sheetCode) }));
|
|
195
203
|
registerPackageTool(tools, {
|
|
196
204
|
name: "configure_form",
|
package/dist/lanes/lane-ids.js
CHANGED
|
@@ -167,6 +167,9 @@ export function assertListViewContract() {
|
|
|
167
167
|
if (!LIST_VIEW_WIRE.deleteBody.includes("query/delete"))
|
|
168
168
|
throw new Error("list-view contract must pin the delete endpoint");
|
|
169
169
|
assertDescriptionCarries(LIST_VIEW_LANE_CONTRACT, "listview.create", ["query/create", "LIST", "get_header"]);
|
|
170
|
+
// Button scripts are stored in h_biz_button.actionConfig.scriptData (a JSON
|
|
171
|
+
// STRING); listview.get/--buttons and form.get/--buttonScripts surface them
|
|
172
|
+
// without a round trip through the artifact.
|
|
170
173
|
assertDescriptionCarries(LIST_VIEW_LANE_CONTRACT, "listview.get", ["query/get", "code", "默认视图", "present"]);
|
|
171
174
|
assertDescriptionCarries(LIST_VIEW_LANE_CONTRACT, "listview.list", ["query/list", "code"]);
|
|
172
175
|
assertDescriptionCarries(LIST_VIEW_LANE_CONTRACT, "listview.configure", ["query/update", "fieldCodes", "默认视图", "querySorts", "queryConditions", "queryActions", "queryActionType", "associationType", "options", "queryPresentation", "htmlJson", "schemaRelations", "customButtons", "customButtonJson", "bindAction", "useLocation", "operateType", "showPermType", "QUERY", "pagingSetting", "listViewModeSetting", "presentationType", "queryGanttModel", "GANTT", "queryLayer", "multiTableHeaderConfig", "publish=TRUE", "field.publish", "rule.enable", "actionConfig", "targetModel", "listview-button-target-unresolved"]);
|
|
@@ -232,6 +232,10 @@ export const listViewButtonArg = z
|
|
|
232
232
|
.union([z.string(), z.record(z.string(), z.unknown())])
|
|
233
233
|
.optional()
|
|
234
234
|
.describe("BizButtonModel.actionConfig 原样透传(与 targetModel/targetForm/configureList 互斥);ADD_DATA/FORM 只接受 schemaCodeVal/formListData/configureList 子形状"),
|
|
235
|
+
script: z
|
|
236
|
+
.string()
|
|
237
|
+
.optional()
|
|
238
|
+
.describe("SCRIPT 按钮的脚本源码简写——等价于 actionConfig.scriptData;与 actionConfig 同传时以 script 为准(写进 actionConfig.scriptData)"),
|
|
235
239
|
})
|
|
236
240
|
.describe("customButtons[] entry (BizButtonModel); name/code/useLocation/bindAction/operateType are required; triggerType/triggerCode are forced to QUERY/view code and sortKey to the array position");
|
|
237
241
|
/**
|
|
@@ -1004,7 +1008,29 @@ export function normalizeButtons(buttons, schemaCode, triggerCode) {
|
|
|
1004
1008
|
// ("undefined is not iterable") blank the click. Emit an empty
|
|
1005
1009
|
// configureList for those three bind actions and `{}` otherwise.
|
|
1006
1010
|
const needsConfigureList = button.bindAction === "FLOW" || button.bindAction === "ADD_DATA" || button.bindAction === "FORM";
|
|
1007
|
-
|
|
1011
|
+
// `script` is the ergonomic spelling of actionConfig.scriptData for SCRIPT
|
|
1012
|
+
// buttons; when both are given the explicit script wins (it is the value the
|
|
1013
|
+
// caller just wrote, not a stale config blob).
|
|
1014
|
+
let actionConfig = button.actionConfig ?? (needsConfigureList ? { configureList: [] } : undefined);
|
|
1015
|
+
if (button.script !== undefined) {
|
|
1016
|
+
let base = {};
|
|
1017
|
+
if (actionConfig !== undefined) {
|
|
1018
|
+
if (typeof actionConfig === "string") {
|
|
1019
|
+
try {
|
|
1020
|
+
const parsed = JSON.parse(actionConfig);
|
|
1021
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed))
|
|
1022
|
+
base = parsed;
|
|
1023
|
+
}
|
|
1024
|
+
catch {
|
|
1025
|
+
base = {};
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
else
|
|
1029
|
+
base = { ...actionConfig };
|
|
1030
|
+
}
|
|
1031
|
+
actionConfig = { ...base, scriptData: button.script };
|
|
1032
|
+
}
|
|
1033
|
+
row.actionConfig = encodeJsonColumn(actionConfig ?? {});
|
|
1008
1034
|
return row;
|
|
1009
1035
|
});
|
|
1010
1036
|
}
|