@stalfh233/omc-cli 0.0.0-stage → 0.4.10
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/LICENSE +21 -0
- package/README.md +293 -3
- package/coverage/m1-coverage-manifest-v1.json +1278 -0
- package/dist/approval-token.js +102 -0
- package/dist/args.js +25 -0
- package/dist/artifacts.js +119 -0
- package/dist/bench/call-face-eval.js +256 -0
- package/dist/bench/context-attribution.js +151 -0
- package/dist/bench/discovery-cost-eval.js +230 -0
- package/dist/bench/driver.js +81 -0
- package/dist/bench/evals.js +236 -0
- package/dist/bench/fake-http-server.js +65 -0
- package/dist/bench/instrument.js +87 -0
- package/dist/bench/intent-face-eval.js +343 -0
- package/dist/bench/run.js +166 -0
- package/dist/bench/scenario.js +343 -0
- package/dist/bench/types.js +76 -0
- package/dist/bi-wire.js +41 -0
- package/dist/bizservice-config.js +581 -0
- package/dist/call.js +153 -0
- package/dist/capability-absences.js +23 -0
- package/dist/capability-overview.js +492 -0
- package/dist/capability-shape.js +154 -0
- package/dist/cli-contract.js +70 -0
- package/dist/cli-output.js +73 -0
- package/dist/cli.js +1418 -0
- package/dist/code-rules.js +69 -0
- package/dist/command-transport.js +155 -0
- package/dist/config-store.js +195 -0
- package/dist/context.js +21 -0
- package/dist/contract-consistency.js +66 -0
- package/dist/contract-resources.js +62 -0
- package/dist/coverage-consistency.js +62 -0
- package/dist/coverage-registry.js +76 -0
- package/dist/coverage.js +130 -0
- package/dist/data-list-filter.js +114 -0
- package/dist/discovery.js +390 -0
- package/dist/endpoints.js +154 -0
- package/dist/environment-policy.js +26 -0
- package/dist/execution-metadata.js +1092 -0
- package/dist/fake/app.js +45 -0
- package/dist/fake/b2-registration.js +594 -0
- package/dist/fake/businessrule.js +242 -0
- package/dist/fake/datarule.js +72 -0
- package/dist/fake/dictionary.js +108 -0
- package/dist/fake/environment.js +30 -0
- package/dist/fake/field.js +167 -0
- package/dist/fake/form.js +101 -0
- package/dist/fake/index.js +121 -0
- package/dist/fake/list-view.js +289 -0
- package/dist/fake/model.js +136 -0
- package/dist/fake/online-js.js +18 -0
- package/dist/fake/report.js +253 -0
- package/dist/fake/routes.js +47 -0
- package/dist/fake/rule-lifecycle.js +37 -0
- package/dist/fake/runtime-data.js +367 -0
- package/dist/fake/state.js +67 -0
- package/dist/fake/workflow.js +364 -0
- package/dist/field-change.js +200 -0
- package/dist/field-families.js +896 -0
- package/dist/form-layout.js +111 -0
- package/dist/form-support.js +821 -0
- package/dist/goal-routes.js +468 -0
- package/dist/governed-execution.js +87 -0
- package/dist/human-summary.js +212 -0
- package/dist/identity.js +62 -0
- package/dist/intent/baseline.js +57 -0
- package/dist/intent/capabilities/bizservice.js +274 -0
- package/dist/intent/capabilities/businessrule.js +493 -0
- package/dist/intent/capabilities/datarule.js +187 -0
- package/dist/intent/capabilities/field.js +545 -0
- package/dist/intent/capabilities/form.js +136 -0
- package/dist/intent/capabilities/index.js +64 -0
- package/dist/intent/capabilities/listview.js +157 -0
- package/dist/intent/capabilities/model.js +100 -0
- package/dist/intent/capabilities/onlinejs.js +108 -0
- package/dist/intent/capabilities/report.js +355 -0
- package/dist/intent/capabilities/workflow.js +458 -0
- package/dist/intent/capability.js +6 -0
- package/dist/intent/cli.js +91 -0
- package/dist/intent/compare.js +56 -0
- package/dist/intent/compiler.js +79 -0
- package/dist/intent/dsl.js +129 -0
- package/dist/intent/plan-file.js +63 -0
- package/dist/intent/readback.js +65 -0
- package/dist/intent/schema.js +158 -0
- package/dist/intent/validation.js +30 -0
- package/dist/intent/yaml.js +315 -0
- package/dist/json-column.js +68 -0
- package/dist/lanes/app-contract.js +95 -0
- package/dist/lanes/app-coverage.js +16 -0
- package/dist/lanes/app.js +174 -0
- package/dist/lanes/apply-changes.js +231 -0
- package/dist/lanes/b2-registration-contract.js +292 -0
- package/dist/lanes/b2-registration-coverage.js +48 -0
- package/dist/lanes/b2-registration.js +1187 -0
- package/dist/lanes/businessrule-contract.js +232 -0
- package/dist/lanes/businessrule-coverage.js +16 -0
- package/dist/lanes/businessrule.js +221 -0
- package/dist/lanes/contract-support.js +65 -0
- package/dist/lanes/coverage-declaration.js +9 -0
- package/dist/lanes/datarule-contract.js +155 -0
- package/dist/lanes/datarule-coverage.js +19 -0
- package/dist/lanes/datarule-protocol.js +308 -0
- package/dist/lanes/datarule.js +818 -0
- package/dist/lanes/dictionary-contract.js +78 -0
- package/dist/lanes/dictionary-coverage.js +24 -0
- package/dist/lanes/dictionary.js +235 -0
- package/dist/lanes/environment-contract.js +70 -0
- package/dist/lanes/environment-coverage.js +14 -0
- package/dist/lanes/environment.js +163 -0
- package/dist/lanes/field-contract.js +223 -0
- package/dist/lanes/field-coverage.js +27 -0
- package/dist/lanes/field.js +374 -0
- package/dist/lanes/form-contract.js +97 -0
- package/dist/lanes/form-coverage.js +16 -0
- package/dist/lanes/form.js +185 -0
- package/dist/lanes/lane-ids.js +34 -0
- package/dist/lanes/list-view-contract.js +175 -0
- package/dist/lanes/list-view-coverage.js +20 -0
- package/dist/lanes/list-view-shapes.js +1207 -0
- package/dist/lanes/list-view.js +578 -0
- package/dist/lanes/meta-contract.js +85 -0
- package/dist/lanes/meta.js +255 -0
- package/dist/lanes/model-contract.js +168 -0
- package/dist/lanes/model-coverage.js +20 -0
- package/dist/lanes/model.js +986 -0
- package/dist/lanes/online-js-contract.js +99 -0
- package/dist/lanes/online-js-coverage.js +28 -0
- package/dist/lanes/online-js.js +127 -0
- package/dist/lanes/report-contract.js +144 -0
- package/dist/lanes/report-coverage.js +21 -0
- package/dist/lanes/report.js +476 -0
- package/dist/lanes/rule-graph.js +1846 -0
- package/dist/lanes/rule-lifecycle-contract.js +92 -0
- package/dist/lanes/rule-lifecycle-coverage.js +20 -0
- package/dist/lanes/rule-lifecycle.js +176 -0
- package/dist/lanes/runtime-data-contract.js +264 -0
- package/dist/lanes/runtime-data-coverage.js +25 -0
- package/dist/lanes/runtime-data.js +1054 -0
- package/dist/lanes/workflow-contract.js +223 -0
- package/dist/lanes/workflow-coverage.js +40 -0
- package/dist/lanes/workflow.js +1813 -0
- package/dist/online-js-layout.js +58 -0
- package/dist/online-js-source.js +276 -0
- package/dist/package-tool.js +51 -0
- package/dist/package.js +73 -0
- package/dist/plan.js +73 -0
- package/dist/read.js +144 -0
- package/dist/redact.js +28 -0
- package/dist/rule-support.js +587 -0
- package/dist/runtime-support.js +134 -0
- package/dist/server.js +92 -0
- package/dist/session-manager.js +30 -0
- package/dist/session.js +149 -0
- package/dist/skills.js +112 -0
- package/dist/support.js +98 -0
- package/dist/tool-types.js +127 -0
- package/dist/tools.js +59 -0
- package/dist/usage-log.js +197 -0
- package/dist/wire.js +287 -0
- package/dist/workflow-support.js +99 -0
- package/dist/write-lease.js +26 -0
- package/dist/write-lock.js +109 -0
- package/dist/write.js +176 -0
- package/dist/zip.js +156 -0
- package/docs/tool-surface-map.md +42 -0
- package/package.json +71 -6
- package/skills/omc-acceptance-criteria.md +47 -0
- package/skills/omc-business-configuration.md +257 -0
- package/skills/omc-capabilities.md +197 -0
- package/skills/omc-capability-scouting.md +55 -0
- package/skills/omc-config-draft-review.md +171 -0
- package/skills/omc-five-piece-flow.md +35 -0
- package/skills/omc-glossary.md +86 -0
- package/skills/omc-refusals.md +110 -0
- package/skills/omc-requirement-analysis.md +161 -0
- package/skills/omc-requirement-vocabulary.md +51 -0
- package/skills/omc-start-here.md +82 -0
- package/skills/omc-tool-selection.md +119 -0
- package/skills/omc-write-hazards.md +87 -0
|
@@ -0,0 +1,821 @@
|
|
|
1
|
+
import { wireCall, wireRows, WireError } from "./wire.js";
|
|
2
|
+
import { ENDPOINTS } from "./endpoints.js";
|
|
3
|
+
import { decodeJsonObjectColumn } from "./json-column.js";
|
|
4
|
+
import { ToolRefusalError } from "./tool-types.js";
|
|
5
|
+
import { FIELD_CONTROL_TYPES, SYSTEM_FIELD_CODES, fieldControlOptions, relationDisplayField, relationMappings, } from "./field-families.js";
|
|
6
|
+
import { deriveControlOptions } from "./capability-shape.js";
|
|
7
|
+
import { validateFormLayoutShape } from "./form-layout.js";
|
|
8
|
+
/**
|
|
9
|
+
* Propagate each relation field's `options.mappings` into its form control so
|
|
10
|
+
* the form-fill carry is configured where the form renderer reads it. The
|
|
11
|
+
* field is the source of truth; only controls matching a field code with a
|
|
12
|
+
* relation mapping or display field are touched.
|
|
13
|
+
*/
|
|
14
|
+
export function propagateRelationOptions(attributesJson, fields) {
|
|
15
|
+
let attributes;
|
|
16
|
+
try {
|
|
17
|
+
attributes = JSON.parse(attributesJson);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return attributesJson;
|
|
21
|
+
}
|
|
22
|
+
for (const field of fields) {
|
|
23
|
+
if (!field.mappings && !field.displayField)
|
|
24
|
+
continue;
|
|
25
|
+
const control = attributes[field.code];
|
|
26
|
+
if (control && typeof control === "object" && control.options && typeof control.options === "object") {
|
|
27
|
+
if (field.mappings)
|
|
28
|
+
control.options.mappings = field.mappings;
|
|
29
|
+
if (field.displayField) {
|
|
30
|
+
control.options.displayField = field.displayField;
|
|
31
|
+
control.options.relativePropertyMatchCode = field.displayField;
|
|
32
|
+
// Ticket 61 / A4: the runtime resolves the displayed value from the
|
|
33
|
+
// CONTROL's relativePropertyCode, not just displayField — without it
|
|
34
|
+
// a field-level display-field change never reaches the running form.
|
|
35
|
+
control.options.relativePropertyCode = field.displayField;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return JSON.stringify(attributes);
|
|
40
|
+
}
|
|
41
|
+
export const propagateRelationMappings = propagateRelationOptions;
|
|
42
|
+
/** Validate the JSON shape consumed by the Online JS HTML renderer. */
|
|
43
|
+
export function validateHtmlRender(value, requirePlatformSkeleton = false) {
|
|
44
|
+
if (value === undefined || value === null)
|
|
45
|
+
return { ok: true, skipped: true };
|
|
46
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
47
|
+
return { ok: false, violations: [{ index: -1, problem: "html must be a non-empty JSON element array" }] };
|
|
48
|
+
}
|
|
49
|
+
let parsed;
|
|
50
|
+
try {
|
|
51
|
+
parsed = JSON.parse(value);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return { ok: false, violations: [{ index: -1, problem: "html must be valid JSON" }] };
|
|
55
|
+
}
|
|
56
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
57
|
+
return { ok: false, violations: [{ index: -1, problem: "html must be a non-empty JSON element array" }] };
|
|
58
|
+
}
|
|
59
|
+
const violations = [];
|
|
60
|
+
const ids = new Set();
|
|
61
|
+
parsed.forEach((element, index) => {
|
|
62
|
+
if (!element || typeof element !== "object" || Array.isArray(element)) {
|
|
63
|
+
violations.push({ index, problem: "element must be an object" });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const record = element;
|
|
67
|
+
const tagNameInvalid = typeof record.tagName !== "string" || record.tagName.trim() === "";
|
|
68
|
+
const attrsInvalid = !Array.isArray(record.attrs);
|
|
69
|
+
if (tagNameInvalid || attrsInvalid)
|
|
70
|
+
violations.push({ index, problem: "tagName missing or attrs not an array" });
|
|
71
|
+
if (typeof record.id === "string")
|
|
72
|
+
ids.add(record.id);
|
|
73
|
+
});
|
|
74
|
+
if (requirePlatformSkeleton) {
|
|
75
|
+
for (const id of ["toolbar", "template", "beforeCustomScript", "customScript", "afterCustomScript"]) {
|
|
76
|
+
if (!ids.has(id))
|
|
77
|
+
violations.push({ index: -1, problem: `platform skeleton missing ${id}` });
|
|
78
|
+
}
|
|
79
|
+
if (!parsed.some((element) => element && typeof element === "object" && !Array.isArray(element) && element.tagName === "style")) {
|
|
80
|
+
violations.push({ index: -1, problem: "platform skeleton missing style" });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return violations.length === 0 ? { ok: true } : { ok: false, violations };
|
|
84
|
+
}
|
|
85
|
+
export function assertHtmlRender(value, details, requirePlatformSkeleton = false) {
|
|
86
|
+
const compat = validateHtmlRender(value, requirePlatformSkeleton);
|
|
87
|
+
if (!compat.ok) {
|
|
88
|
+
throw new ToolRefusalError({
|
|
89
|
+
status: "refused",
|
|
90
|
+
blocker: "form-html-render-invalid",
|
|
91
|
+
...details,
|
|
92
|
+
htmlRenderCompat: compat,
|
|
93
|
+
message: "draftHtmlJson 未通过 Online JS HTML 渲染形状校验;拒绝写入/提拔,零 wire",
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Ticket 69 — the org-selection discriminator per field property type
|
|
99
|
+
* (人员单选 50 / 人员多选 51 / 部门单选 60 / 部门多选 61 / 混合选人 5).
|
|
100
|
+
* `options.deptVisible` + `options.multi` are what the platform reads
|
|
101
|
+
* (BizPropertyTypeUpdateUtils.disposeUpdateType,
|
|
102
|
+
* DataModelInitUtils.updateSelectionType); the control type alone is not
|
|
103
|
+
* enough — a 人员单选 without deptVisible opened a department picker.
|
|
104
|
+
*/
|
|
105
|
+
export function orgSelectorDiscriminator(fieldType) {
|
|
106
|
+
switch (fieldType) {
|
|
107
|
+
case 50: return { deptVisible: "user", multi: "false" };
|
|
108
|
+
case 51: return { deptVisible: "user", multi: "true" };
|
|
109
|
+
case 60: return { deptVisible: "org", multi: "false" };
|
|
110
|
+
case 61: return { deptVisible: "org", multi: "true" };
|
|
111
|
+
case 5: return { deptVisible: "all", multi: "true" };
|
|
112
|
+
default: return undefined;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Ticket 69 — normalise every org-selection control in an existing draft to
|
|
117
|
+
* carry the discriminator, so a form drafted before the fix (or by the
|
|
118
|
+
* designer) is repaired on the next form.draft and works after re-publish.
|
|
119
|
+
*/
|
|
120
|
+
export function normalizeOrgSelectorControls(attributesJson, fields) {
|
|
121
|
+
let attributes;
|
|
122
|
+
try {
|
|
123
|
+
attributes = JSON.parse(attributesJson);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return attributesJson;
|
|
127
|
+
}
|
|
128
|
+
for (const field of fields) {
|
|
129
|
+
const discriminator = orgSelectorDiscriminator(field.type);
|
|
130
|
+
if (!discriminator)
|
|
131
|
+
continue;
|
|
132
|
+
const control = attributes[field.code];
|
|
133
|
+
if (!control || typeof control !== "object" || typeof control.options !== "object" || control.options === null)
|
|
134
|
+
continue;
|
|
135
|
+
const defaults = deriveControlOptions(field.type);
|
|
136
|
+
for (const [key, value] of Object.entries(defaults))
|
|
137
|
+
if (control.options[key] === undefined)
|
|
138
|
+
control.options[key] = value;
|
|
139
|
+
control.options.deptVisible = discriminator.deptVisible;
|
|
140
|
+
control.options.multi = discriminator.multi;
|
|
141
|
+
if (control.options.defaultValue === undefined)
|
|
142
|
+
control.options.defaultValue = [];
|
|
143
|
+
if (control.options.defaultValueType === undefined)
|
|
144
|
+
control.options.defaultValueType = "";
|
|
145
|
+
if (control.options.orgRoot === undefined)
|
|
146
|
+
control.options.orgRoot = [];
|
|
147
|
+
if (control.options.roles === undefined)
|
|
148
|
+
control.options.roles = "";
|
|
149
|
+
if (control.options.mappings === undefined)
|
|
150
|
+
control.options.mappings = "";
|
|
151
|
+
if (control.options.displayType === undefined)
|
|
152
|
+
control.options.displayType = "tag";
|
|
153
|
+
if (control.options.selectMode === undefined)
|
|
154
|
+
control.options.selectMode = "popup";
|
|
155
|
+
}
|
|
156
|
+
return JSON.stringify(attributes);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The form-interaction condition keys are FIELD-owned: the designer stores the
|
|
160
|
+
* condition on the field (field options) and the runtime renderer reads the
|
|
161
|
+
* COPY on the control (`control.options.requiredFormula` / `readonlyCondition`,
|
|
162
|
+
* live bundle 19497: `if(t.options.requiredFormula) u.required=...`).
|
|
163
|
+
*
|
|
164
|
+
* The scaffold copies them when it first builds a control, but a
|
|
165
|
+
* `form.draft`/`configure_form` WITHOUT `rebuild` keeps every existing control
|
|
166
|
+
* verbatim — so a later `field.update` that adds/changes a condition never
|
|
167
|
+
* reached an already-built form. Live bench20: the agent set the conditions
|
|
168
|
+
* AFTER the first configure_form, re-ran configure_form without rebuild, read
|
|
169
|
+
* `ok`, and the portal showed no condition. Sync the field-owned keys into the
|
|
170
|
+
* existing controls on EVERY draft write; keys absent on the field are left
|
|
171
|
+
* untouched, so a designer-only condition survives (never clobber).
|
|
172
|
+
*/
|
|
173
|
+
const FIELD_OWNED_CONTROL_KEYS = ["requiredFormula", "readonlyFormula", "readonlyCondition", "displayFormula", "optionsSet"];
|
|
174
|
+
/**
|
|
175
|
+
* Repair relation child columns on an EXISTING child-table (201) control. The
|
|
176
|
+
* 2026-09-23 scaffold dropped `relativeCode/mappings/displayField` from child
|
|
177
|
+
* properties, so a WORK_SHEET child column was written with `schemaCode:""` and
|
|
178
|
+
* bizsheet/publish then failed 301015 "[列名]未选择关联模型" (BizFormController
|
|
179
|
+
* checkRelationCode requires options.schemaCode for column type 80/81/90).
|
|
180
|
+
* A non-rebuild draft keeps controls verbatim, so fill the missing relation keys
|
|
181
|
+
* in place from the model's subSchema — a designer-set non-empty schemaCode is
|
|
182
|
+
* never clobbered.
|
|
183
|
+
*/
|
|
184
|
+
export function repairChildTableRelationColumns(attributesJson, fields) {
|
|
185
|
+
let attributes;
|
|
186
|
+
try {
|
|
187
|
+
attributes = JSON.parse(attributesJson);
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return attributesJson;
|
|
191
|
+
}
|
|
192
|
+
for (const field of fields) {
|
|
193
|
+
if (field.type !== 8 || !field.childProperties)
|
|
194
|
+
continue;
|
|
195
|
+
const control = attributes[field.code];
|
|
196
|
+
if (!control || !Array.isArray(control.columns))
|
|
197
|
+
continue;
|
|
198
|
+
for (const column of control.columns) {
|
|
199
|
+
const child = field.childProperties.find((entry) => entry.code === column.key);
|
|
200
|
+
if (!child || (child.type !== 9 && child.type !== 11) || !child.relativeCode)
|
|
201
|
+
continue;
|
|
202
|
+
const options = column.options && typeof column.options === "object" && !Array.isArray(column.options)
|
|
203
|
+
? column.options
|
|
204
|
+
: (column.options = {});
|
|
205
|
+
if (typeof options.schemaCode === "string" && options.schemaCode.trim() !== "")
|
|
206
|
+
continue;
|
|
207
|
+
options.schemaCode = child.relativeCode;
|
|
208
|
+
options.queryCode = child.relativeCode;
|
|
209
|
+
if (options.displayField === undefined || options.displayField === "")
|
|
210
|
+
options.displayField = child.displayField ?? "name";
|
|
211
|
+
if (options.relativePropertyMatchCode === undefined || options.relativePropertyMatchCode === "")
|
|
212
|
+
options.relativePropertyMatchCode = child.displayField ?? "name";
|
|
213
|
+
if (options.mappings === undefined || options.mappings === "")
|
|
214
|
+
options.mappings = child.mappings ?? "";
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return JSON.stringify(attributes);
|
|
218
|
+
}
|
|
219
|
+
export function syncFieldDerivedControlOptions(attributesJson, fields) {
|
|
220
|
+
let attributes;
|
|
221
|
+
try {
|
|
222
|
+
attributes = JSON.parse(attributesJson);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return attributesJson;
|
|
226
|
+
}
|
|
227
|
+
for (const field of fields) {
|
|
228
|
+
if (field.fieldOptions === undefined)
|
|
229
|
+
continue;
|
|
230
|
+
const control = attributes[field.code];
|
|
231
|
+
if (!control || typeof control !== "object" || typeof control.options !== "object" || control.options === null)
|
|
232
|
+
continue;
|
|
233
|
+
for (const key of FIELD_OWNED_CONTROL_KEYS) {
|
|
234
|
+
const value = field.fieldOptions[key];
|
|
235
|
+
if (value !== undefined)
|
|
236
|
+
control.options[key] = value;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return JSON.stringify(attributes);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Ticket 66 — a model's "redundant" fields are the carry-through targets of
|
|
243
|
+
* its relation mappings (`options.mappings[{source,target}]`): values auto-filled
|
|
244
|
+
* from the related model, not hand-entered. They must still be bound on the
|
|
245
|
+
* form; only their visibility is governed (form-design hidden when the model
|
|
246
|
+
* has no workflow, else per-workflow-node permissions).
|
|
247
|
+
*/
|
|
248
|
+
export function redundantFieldCodes(fields) {
|
|
249
|
+
const targets = new Set();
|
|
250
|
+
for (const field of fields) {
|
|
251
|
+
if (field.type !== 9 && field.type !== 11)
|
|
252
|
+
continue;
|
|
253
|
+
if (!field.mappings)
|
|
254
|
+
continue;
|
|
255
|
+
let parsed;
|
|
256
|
+
try {
|
|
257
|
+
parsed = JSON.parse(field.mappings);
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (!Array.isArray(parsed))
|
|
263
|
+
continue;
|
|
264
|
+
for (const entry of parsed) {
|
|
265
|
+
const target = entry?.target;
|
|
266
|
+
if (typeof target === "string" && target.trim())
|
|
267
|
+
targets.add(target.trim());
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return targets;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Read a model's default form. CloudPivot answers errcode 302024
|
|
274
|
+
* ("表单不存在!") when the model has no form row yet — that is absence, not a
|
|
275
|
+
* failure, so callers get null and may create it.
|
|
276
|
+
*/
|
|
277
|
+
async function readFormOrNull(session, schemaCode, sheetCode) {
|
|
278
|
+
try {
|
|
279
|
+
const result = await wireCall(session, { path: ENDPOINTS.sheetGet, query: { schemaCode, sheetCode } });
|
|
280
|
+
return (result.data ?? null);
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
if (error instanceof WireError && error.errcode === 302024)
|
|
284
|
+
return null;
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Create the default form row for a model that has none (bizsheet/create;
|
|
290
|
+
* sheetType 0 = BizFormType.DEFAULT_SHEET). A model created via bizmodels/create
|
|
291
|
+
* carries no form, so form.draft must materialise it before it can hold a
|
|
292
|
+
* draft layout. The subsequent scaffold+update writes the layout.
|
|
293
|
+
*/
|
|
294
|
+
async function createDefaultForm(session, schemaCode, sheetCode) {
|
|
295
|
+
await wireCall(session, {
|
|
296
|
+
path: ENDPOINTS.sheetCreate,
|
|
297
|
+
method: "POST",
|
|
298
|
+
body: {
|
|
299
|
+
schemaCode,
|
|
300
|
+
code: sheetCode,
|
|
301
|
+
name: sheetCode,
|
|
302
|
+
name_i18n: JSON.stringify({ "zh-CN": sheetCode, en: sheetCode }),
|
|
303
|
+
sheetType: 0,
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Ticket 102 — families that occupy a full row (span 24) in real forms:
|
|
309
|
+
* long text and attachments are inherently wide (fwb evidence §rows:
|
|
310
|
+
* business rows are 1×24 long-text/attachment, 2×12 pairs of narrow fields).
|
|
311
|
+
* Every other business family packs two per row at span:12.
|
|
312
|
+
*/
|
|
313
|
+
function wideBusinessFamily(type) {
|
|
314
|
+
return type === 1 || type === 6 || type === 8;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Sheet201 form control for a CHILD_TABLE(8) field: the parent control carries
|
|
318
|
+
* the child columns as `columns`, each keyed by the parent field code. Control
|
|
319
|
+
* `options` is a plain OBJECT (like every other scaffold control — the runtime
|
|
320
|
+
* 8.6.34 renderer mutates `control.options.name_i18n` and throws
|
|
321
|
+
* "Cannot create property 'name_i18n' on string" if it is a JSON string).
|
|
322
|
+
* Child column keys are NEVER added to viewJson — only the parent key is a
|
|
323
|
+
* layout row.
|
|
324
|
+
*/
|
|
325
|
+
function childTableColumns(parentCode, children) {
|
|
326
|
+
return children.map((child) => ({
|
|
327
|
+
type: FIELD_CONTROL_TYPES[child.type] ?? 1,
|
|
328
|
+
width: "180",
|
|
329
|
+
key: child.code,
|
|
330
|
+
parentKey: parentCode,
|
|
331
|
+
// A relation child column (WORK_SHEET 9 / MULT_WORK_SHEET 11) is a WORK_SHEET
|
|
332
|
+
// form control (80/81) and MUST carry its target `schemaCode` — dropping it
|
|
333
|
+
// makes bizsheet/publish fail 301015 "[列名]未选择关联模型" (BizFormController
|
|
334
|
+
// checkRelationCode: WORK_SHEET = {80,81,90} require options.schemaCode).
|
|
335
|
+
options: fieldControlOptions(child.type, child.name, child.relativeCode, child.mappings, child.displayField ?? "name", child.fieldOptions),
|
|
336
|
+
}));
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Keep the designer's non-structural control edits across a rebuild. The
|
|
340
|
+
* freshly scaffolded control owns its label and relation target keys (they must
|
|
341
|
+
* track the field metadata), but everything else the designer changed on the
|
|
342
|
+
* draft — carry `mappings`, `visible`, `labelVisible`, `span`, conditions — is
|
|
343
|
+
* layered back from the previous draft. Child-table (201) controls are skipped:
|
|
344
|
+
* their columns are regenerated from the model's subSchema.
|
|
345
|
+
*/
|
|
346
|
+
export function preserveDesignerControlOptions(next, previous) {
|
|
347
|
+
const STRUCTURAL = new Set(["name", "name_i18n", "schemaCode", "displayField", "queryCode", "relativePropertyMatchCode"]);
|
|
348
|
+
const preserved = [];
|
|
349
|
+
for (const [key, control] of Object.entries(next)) {
|
|
350
|
+
const fresh = control;
|
|
351
|
+
const prior = previous[key];
|
|
352
|
+
if (!prior || typeof prior !== "object")
|
|
353
|
+
continue;
|
|
354
|
+
if (fresh.type === 201 || prior.type === 201)
|
|
355
|
+
continue;
|
|
356
|
+
if (!fresh.options || typeof fresh.options !== "object" || Array.isArray(fresh.options))
|
|
357
|
+
continue;
|
|
358
|
+
if (!prior.options || typeof prior.options !== "object" || Array.isArray(prior.options))
|
|
359
|
+
continue;
|
|
360
|
+
const merged = { ...fresh.options };
|
|
361
|
+
for (const [optionKey, value] of Object.entries(prior.options)) {
|
|
362
|
+
if (STRUCTURAL.has(optionKey) || value === undefined)
|
|
363
|
+
continue;
|
|
364
|
+
merged[optionKey] = value;
|
|
365
|
+
}
|
|
366
|
+
fresh.options = merged;
|
|
367
|
+
preserved.push(key);
|
|
368
|
+
}
|
|
369
|
+
return preserved;
|
|
370
|
+
}
|
|
371
|
+
function childTableParentControl(field) {
|
|
372
|
+
return {
|
|
373
|
+
key: field.code,
|
|
374
|
+
type: 201,
|
|
375
|
+
options: {
|
|
376
|
+
name: field.name,
|
|
377
|
+
name_i18n: { "zh-CN": field.name, en: field.name },
|
|
378
|
+
visible: true,
|
|
379
|
+
labelVisible: true,
|
|
380
|
+
span: 24,
|
|
381
|
+
widgetType: "子表",
|
|
382
|
+
width: 150,
|
|
383
|
+
displayPosition: "right",
|
|
384
|
+
rows: 0,
|
|
385
|
+
editable: true,
|
|
386
|
+
importable: true,
|
|
387
|
+
exportable: true,
|
|
388
|
+
addable: true,
|
|
389
|
+
deletable: true,
|
|
390
|
+
copyAble: true,
|
|
391
|
+
clearRowAble: true,
|
|
392
|
+
showTotal: false,
|
|
393
|
+
presetButtonManage: "",
|
|
394
|
+
summary: "",
|
|
395
|
+
},
|
|
396
|
+
columns: childTableColumns(field.code, field.childProperties ?? []),
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Build the designer-standard default-form scaffold (research §13): Title
|
|
401
|
+
* (203, named after the model, useDataTitle) + system row [creater(101),
|
|
402
|
+
* createdTime(104), sequenceNo(100)] + one group (200), followed by the
|
|
403
|
+
* business fields with their mapped controls, packed compactly (ticket 102:
|
|
404
|
+
* narrow fields two per row at span:12; wide families alone at span:24 —
|
|
405
|
+
* row totals never exceed 24). Relation controls carry the target schema
|
|
406
|
+
* from the field's relativeCode. Used only when the target default form's
|
|
407
|
+
* draft layout is empty.
|
|
408
|
+
*/
|
|
409
|
+
export function buildFormScaffold(fields, formName, hiddenCodes) {
|
|
410
|
+
const ts = Date.now();
|
|
411
|
+
const titleKey = `Title${ts}`;
|
|
412
|
+
const groupKey = `Group${ts}`;
|
|
413
|
+
const business = fields.filter((field) => !SYSTEM_FIELD_CODES.has(field.code) && FIELD_CONTROL_TYPES[field.type] && scaffoldControlConfigPresent(field));
|
|
414
|
+
const businessRows = [];
|
|
415
|
+
let pair = [];
|
|
416
|
+
for (const field of business) {
|
|
417
|
+
if (wideBusinessFamily(field.type)) {
|
|
418
|
+
// Flush any pending pair FIRST so the emitted row order always matches
|
|
419
|
+
// the field order (review finding: a wide family mid-pair used to jump
|
|
420
|
+
// the queue and reorder the layout).
|
|
421
|
+
if (pair.length > 0) {
|
|
422
|
+
businessRows.push(pair);
|
|
423
|
+
pair = [];
|
|
424
|
+
}
|
|
425
|
+
businessRows.push([field.code]);
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
pair.push(field.code);
|
|
429
|
+
if (pair.length === 2) {
|
|
430
|
+
businessRows.push(pair);
|
|
431
|
+
pair = [];
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (pair.length > 0)
|
|
435
|
+
businessRows.push(pair);
|
|
436
|
+
const rows = [[titleKey], ["creater", "createdTime", "sequenceNo"], [groupKey], ...businessRows];
|
|
437
|
+
const attributes = {
|
|
438
|
+
[titleKey]: { key: titleKey, type: 203, options: { name: formName, name_i18n: { en: formName, zh: formName }, visible: true, labelVisible: true, span: 24, labelAlign: "", style: "", tips: "", dataItemName: "", useDataTitle: true, styleInfo: { height: 1, bgColor: "#ffffff", bgImg: "", textAlign: "center", fontSize: 28, textColor: "#111218", blod: true, italic: false, textCss: "", textCssSwitch: false } } },
|
|
439
|
+
creater: { key: "creater", type: 101, options: { name: "创建人", name_i18n: "", visible: true, labelVisible: true, span: 8, labelAlign: "", style: "", tips: "", dataItemName: "", widgetType: "", dataItemType: "", mappings: "", verifyFormula: "", defaultValue: "" } },
|
|
440
|
+
createdTime: { key: "createdTime", type: 104, options: { name: "创建时间", name_i18n: "", visible: true, labelVisible: true, span: 8, labelAlign: "", style: "", tips: "", dataItemName: "", widgetType: "", dataItemType: "", format1: "YYYY-MM-DD HH:mm:ss", format: "YYYY-MM-DD HH:mm:ss", syncFormate: true, verifyFormula: "", defaultValue: "" } },
|
|
441
|
+
sequenceNo: { key: "sequenceNo", type: 100, options: { name: "单据号", name_i18n: "", visible: true, labelVisible: true, span: 8, labelAlign: "", style: "", tips: "", dataItemName: "", widgetType: "", dataItemType: "", seqNoSetting: [{ type: "UID", value: "aa860e64ff2e4f86851a661d663ae358" }], formPropertyType: 100, verifyFormula: "", displayFormula: "", defaultValue: "" } },
|
|
442
|
+
[groupKey]: { key: groupKey, type: 200, options: { name: "业务信息", tips: "", expand: true, align: "", style: "", styleInfo: { fontSize: 18, textColor: "#111218", blod: true, italic: false, textCss: "", textCssSwitch: false }, span: 24 } },
|
|
443
|
+
};
|
|
444
|
+
for (const field of business) {
|
|
445
|
+
if (field.type === 8) {
|
|
446
|
+
attributes[field.code] = childTableParentControl(field);
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
const control = { key: field.code, type: FIELD_CONTROL_TYPES[field.type], options: fieldControlOptions(field.type, field.name, field.relativeCode, field.mappings, field.displayField, field.fieldOptions) };
|
|
450
|
+
// Ticket 102: compact packing — narrow families span 12 (two per row),
|
|
451
|
+
// wide families keep the full 24.
|
|
452
|
+
control.options.span = wideBusinessFamily(field.type) ? 24 : 12;
|
|
453
|
+
// Ticket 66: a redundant (carry-through) field with no workflow to govern
|
|
454
|
+
// it per node is hidden in the form design; it stays bound so the carried
|
|
455
|
+
// value is still present on the form.
|
|
456
|
+
if (hiddenCodes?.has(field.code) && control.options && typeof control.options === "object") {
|
|
457
|
+
control.options.visible = false;
|
|
458
|
+
}
|
|
459
|
+
attributes[field.code] = control;
|
|
460
|
+
}
|
|
461
|
+
return { rows, attributes };
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* A control is only scaffolded when the family's strict configuration is
|
|
465
|
+
* present on the field. A FORMULA control without formulaConfig or a
|
|
466
|
+
* RELEVANCE_DATA control without relevanceField/referenceField would render an
|
|
467
|
+
* invalid control (admin validation refuses both), so such fields are skipped
|
|
468
|
+
* rather than bound with fabricated options.
|
|
469
|
+
*/
|
|
470
|
+
function scaffoldControlConfigPresent(field) {
|
|
471
|
+
if (field.type === 100)
|
|
472
|
+
return typeof field.fieldOptions?.formulaConfig === "string" && field.fieldOptions.formulaConfig.length > 0;
|
|
473
|
+
if (field.type === 16)
|
|
474
|
+
return typeof field.fieldOptions?.relevanceField === "string" && field.fieldOptions.relevanceField.length > 0 && typeof field.fieldOptions?.referenceField === "string" && field.fieldOptions.referenceField.length > 0;
|
|
475
|
+
if (field.type === 8)
|
|
476
|
+
return Array.isArray(field.childProperties) && field.childProperties.length > 0;
|
|
477
|
+
return true;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Ticket 74 — an explicitly provided layout string that parses to an empty
|
|
481
|
+
* array/object ("[]", "{}", whitespace variants). The platform designer's
|
|
482
|
+
* Save is NOT lossless: it clears draftViewJson to "[]" and
|
|
483
|
+
* draftAttributesJson to "{}", and promoting that draft loses every
|
|
484
|
+
* control. Writing such a value explicitly is refused unless the caller
|
|
485
|
+
* passes allowEmptyLayout:true.
|
|
486
|
+
*/
|
|
487
|
+
export function isEmptyLayoutJson(value) {
|
|
488
|
+
const trimmed = value.trim();
|
|
489
|
+
if (trimmed.length === 0)
|
|
490
|
+
return true;
|
|
491
|
+
try {
|
|
492
|
+
const parsed = JSON.parse(trimmed);
|
|
493
|
+
if (Array.isArray(parsed))
|
|
494
|
+
return parsed.length === 0;
|
|
495
|
+
if (parsed && typeof parsed === "object")
|
|
496
|
+
return Object.keys(parsed).length === 0;
|
|
497
|
+
return parsed === null;
|
|
498
|
+
}
|
|
499
|
+
catch {
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Distribution-grade dry-lint for a CALLER-PROVIDED form layout (args, not the
|
|
505
|
+
* stored draft). These are the shapes that render fine on readback but blank
|
|
506
|
+
* the 8.6.34 page — the CLI refuses them before any wire call so a caller who
|
|
507
|
+
* never read the docs still cannot ship a broken form. Runs on ANY provided
|
|
508
|
+
* face (one-sided included), covering configure_form/apply_changes paths that
|
|
509
|
+
* do not pass through the `form.draft` write permission gate.
|
|
510
|
+
*/
|
|
511
|
+
export function assertCallerFormLayout(args) {
|
|
512
|
+
if (args.draftViewJson === undefined && args.draftAttributesJson === undefined)
|
|
513
|
+
return;
|
|
514
|
+
const parse = (value) => {
|
|
515
|
+
if (typeof value !== "string")
|
|
516
|
+
return undefined;
|
|
517
|
+
try {
|
|
518
|
+
return JSON.parse(value);
|
|
519
|
+
}
|
|
520
|
+
catch {
|
|
521
|
+
return undefined;
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
const violations = validateFormLayoutShape(parse(args.draftViewJson), parse(args.draftAttributesJson));
|
|
525
|
+
if (violations.length > 0) {
|
|
526
|
+
throw new ToolRefusalError({
|
|
527
|
+
status: "refused",
|
|
528
|
+
blocker: "form-layout-invalid",
|
|
529
|
+
schemaCode: args.schemaCode,
|
|
530
|
+
sheetCode: args.sheetCode,
|
|
531
|
+
validation: violations,
|
|
532
|
+
message: `调用方提供的表单布局形状非法(写前零 wire 拒绝):${violations.map((v) => v.field ?? v.classification).join("、")}`,
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
/** Ticket 74 — control count on an attributes JSON face (0 when unparsable/absent). */
|
|
537
|
+
function controlCount(face) {
|
|
538
|
+
if (typeof face !== "string")
|
|
539
|
+
return 0;
|
|
540
|
+
try {
|
|
541
|
+
const parsed = JSON.parse(face);
|
|
542
|
+
return parsed && typeof parsed === "object" ? Object.keys(parsed).length : 0;
|
|
543
|
+
}
|
|
544
|
+
catch {
|
|
545
|
+
return 0;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Shared form.draft write body — read the current sheet, compose a scaffold
|
|
550
|
+
* from published fields when the form is empty, and POST the proven payload
|
|
551
|
+
* to bizsheet/update. Used by the form.draft write tool AND by configure_form,
|
|
552
|
+
* so the high-level orchestrator never bypasses the scaffold.
|
|
553
|
+
*/
|
|
554
|
+
export async function executeFormDraftWrite(session, args) {
|
|
555
|
+
if (args.draftHtmlJson !== undefined) {
|
|
556
|
+
assertHtmlRender(args.draftHtmlJson, { schemaCode: args.schemaCode, sheetCode: args.sheetCode, face: "draftHtmlJson" });
|
|
557
|
+
}
|
|
558
|
+
// Caller-provided layouts get the distribution-grade shape gate (options
|
|
559
|
+
// objects, string viewJson cells, no child keys as rows) before any wire.
|
|
560
|
+
if (args.draftViewJson !== undefined || args.draftAttributesJson !== undefined)
|
|
561
|
+
assertCallerFormLayout(args);
|
|
562
|
+
// Ticket 74 — refuse CALLER-PROVIDED empty layouts before any wire call.
|
|
563
|
+
// The platform designer's Save clears draft faces to "[]"/"{}" (non-lossless);
|
|
564
|
+
// re-writing that shape explicitly is how the 13-control loss happened.
|
|
565
|
+
if (args.allowEmptyLayout !== true) {
|
|
566
|
+
const emptyFaces = [];
|
|
567
|
+
if (typeof args.draftViewJson === "string" && isEmptyLayoutJson(args.draftViewJson))
|
|
568
|
+
emptyFaces.push("draftViewJson");
|
|
569
|
+
if (typeof args.draftAttributesJson === "string" && isEmptyLayoutJson(args.draftAttributesJson))
|
|
570
|
+
emptyFaces.push("draftAttributesJson");
|
|
571
|
+
if (emptyFaces.length > 0) {
|
|
572
|
+
throw new ToolRefusalError({
|
|
573
|
+
status: "refused",
|
|
574
|
+
blocker: "form-empty-layout",
|
|
575
|
+
schemaCode: args.schemaCode,
|
|
576
|
+
sheetCode: args.sheetCode,
|
|
577
|
+
faces: emptyFaces,
|
|
578
|
+
message: `显式提供的 ${emptyFaces.join("、")} 是空布局("[]"/"{}")——平台设计器 Save 会把草稿清成该形态(非无损),照写会丢控件。若确要清空布局请显式传 allowEmptyLayout:true;若是设计器 Save 后的修复,先用表单版本历史(/api/api/app/bizsheet/history/list)取回完好版本原文再写回`,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
// Ticket 48: an explicit fieldCodes selection shapes the scaffold — exactly
|
|
583
|
+
// those fields, in the given order. Validate against the published face
|
|
584
|
+
// BEFORE creating anything so an unknown code never writes a form row.
|
|
585
|
+
const wanted = args.fieldCodes;
|
|
586
|
+
let selectionUnknown;
|
|
587
|
+
if (wanted && args.draftAttributesJson === undefined) {
|
|
588
|
+
const rows = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "true" } })).data);
|
|
589
|
+
const publishedCodes = new Set(rows.map((row) => String(row.code ?? row.propertyCode ?? "")));
|
|
590
|
+
selectionUnknown = wanted.filter((code) => !publishedCodes.has(code));
|
|
591
|
+
}
|
|
592
|
+
if (selectionUnknown && selectionUnknown.length > 0) {
|
|
593
|
+
throw new ToolRefusalError({
|
|
594
|
+
status: "refused",
|
|
595
|
+
blocker: "form-fieldcodes-unknown",
|
|
596
|
+
schemaCode: args.schemaCode,
|
|
597
|
+
fieldCodes: selectionUnknown,
|
|
598
|
+
message: `fieldCodes 引用了模型 ${args.schemaCode} 发布面上不存在的字段:${selectionUnknown.join("、")};先 field.create + field.publish 这些字段,或修正 fieldCodes`,
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
// The default form may not exist yet on a freshly created model
|
|
602
|
+
// (bizmodels/create seeds no form) — create it, then scaffold into it.
|
|
603
|
+
let pre = await readFormOrNull(session, args.schemaCode, args.sheetCode);
|
|
604
|
+
if (pre === null) {
|
|
605
|
+
await createDefaultForm(session, args.schemaCode, args.sheetCode);
|
|
606
|
+
// Creation must be immediately authoritative before composing an update
|
|
607
|
+
// body; do not continue against an assumed/asynchronously visible form.
|
|
608
|
+
pre = await readFormOrNull(session, args.schemaCode, args.sheetCode);
|
|
609
|
+
if (pre === null)
|
|
610
|
+
throw new WireError("business", `form ${args.sheetCode} was created but is not readable`, 302024);
|
|
611
|
+
}
|
|
612
|
+
// Field-level relation mappings are the source of truth; read the published
|
|
613
|
+
// fields so the scaffold (or an existing draft) can carry them into the
|
|
614
|
+
// matching form control, where the form renderer reads them at fill time.
|
|
615
|
+
const publishedFields = args.draftAttributesJson === undefined
|
|
616
|
+
? await Promise.all(wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "true" } })).data).map(async (row) => {
|
|
617
|
+
const type = Number(row.type ?? row.propertyType);
|
|
618
|
+
const relativeCode = row.relativeCode ? String(row.relativeCode) : undefined;
|
|
619
|
+
// fieldOptions face decode; policy: corrupt strings throw, objects accepted, missing → undefined.
|
|
620
|
+
const fieldOptions = decodeJsonObjectColumn(row.options, { invalid: "throw", array: "accept", object: "accept" });
|
|
621
|
+
let displayField;
|
|
622
|
+
if (relativeCode && (type === 9 || type === 11)) {
|
|
623
|
+
displayField = relationDisplayField(row.options);
|
|
624
|
+
if (!displayField) {
|
|
625
|
+
const targetRows = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: relativeCode, isPublish: "true" } })).data);
|
|
626
|
+
const title = targetRows.find((target) => String(target.code ?? target.propertyCode) === "name" || target.name === "数据标题");
|
|
627
|
+
displayField = title ? String(title.code ?? title.propertyCode) : "name";
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
const mappings = relationMappings(row.options);
|
|
631
|
+
// CHILD_TABLE(8): the child columns live on the row's subSchema; carry
|
|
632
|
+
// them onto the form control as Sheet201 columns.
|
|
633
|
+
const subSchema = type === 8 ? decodeJsonObjectColumn(row.subSchema, { invalid: "undefined", array: "reject", object: "accept" }) : undefined;
|
|
634
|
+
const rawChildren = subSchema && Array.isArray(subSchema.properties) ? subSchema.properties : undefined;
|
|
635
|
+
const childProperties = rawChildren
|
|
636
|
+
?.filter((child) => child.defaultProperty !== true && !SYSTEM_FIELD_CODES.has(String(child.code ?? "")))
|
|
637
|
+
.map((child) => {
|
|
638
|
+
const childOptions = decodeJsonObjectColumn(child.options, { invalid: "undefined", array: "accept", object: "accept" });
|
|
639
|
+
const childType = Number(child.type ?? child.propertyType);
|
|
640
|
+
const childRelativeCode = child.relativeCode ? String(child.relativeCode) : undefined;
|
|
641
|
+
// Relation child columns MUST keep their target schema + carried
|
|
642
|
+
// mappings on the WORK_SHEET control, or bizsheet/publish answers
|
|
643
|
+
// 301015 (see childTableColumns).
|
|
644
|
+
const childMappings = relationMappings(child.options);
|
|
645
|
+
const childDisplayField = childRelativeCode && (childType === 9 || childType === 11) ? relationDisplayField(child.options) : undefined;
|
|
646
|
+
return {
|
|
647
|
+
code: String(child.code ?? ""),
|
|
648
|
+
name: String(child.name ?? child.code ?? ""),
|
|
649
|
+
type: childType,
|
|
650
|
+
...(childRelativeCode ? { relativeCode: childRelativeCode } : {}),
|
|
651
|
+
...(childMappings ? { mappings: childMappings } : {}),
|
|
652
|
+
...(childDisplayField ? { displayField: childDisplayField } : {}),
|
|
653
|
+
...(childOptions ? { fieldOptions: childOptions } : {}),
|
|
654
|
+
};
|
|
655
|
+
})
|
|
656
|
+
.filter((child) => child.code.length > 0 && Number.isFinite(child.type));
|
|
657
|
+
return {
|
|
658
|
+
code: String(row.code ?? row.propertyCode),
|
|
659
|
+
name: String(row.name ?? row.code ?? ""),
|
|
660
|
+
type,
|
|
661
|
+
...(relativeCode ? { relativeCode } : {}),
|
|
662
|
+
...(mappings ? { mappings } : {}),
|
|
663
|
+
...(displayField ? { displayField } : {}),
|
|
664
|
+
...(fieldOptions ? { fieldOptions } : {}),
|
|
665
|
+
...(childProperties && childProperties.length > 0 ? { childProperties } : {}),
|
|
666
|
+
};
|
|
667
|
+
}))
|
|
668
|
+
: [];
|
|
669
|
+
let view = args.draftViewJson ?? (pre ? pre.draftViewJson : undefined);
|
|
670
|
+
let attributes = args.draftAttributesJson ?? (pre ? pre.draftAttributesJson : undefined);
|
|
671
|
+
// Designer edits that live ONLY on the form draft (relation carry mappings,
|
|
672
|
+
// control visibility/width, label tweaks) are not derivable from field
|
|
673
|
+
// metadata. A rebuild re-scaffolds, so capture the previous draft first and
|
|
674
|
+
// layer those edits back on (see preserveDesignerControlOptions).
|
|
675
|
+
const previousAttributes = !args.rebuild || typeof pre?.draftAttributesJson !== "string"
|
|
676
|
+
? undefined
|
|
677
|
+
: decodeJsonObjectColumn(pre.draftAttributesJson, { invalid: "undefined", array: "reject", object: "accept" });
|
|
678
|
+
// `rebuild: true` discards the existing draft layout and re-scaffolds from
|
|
679
|
+
// the published fields, so fields added after the form was first built (a
|
|
680
|
+
// child table, a new relation) land in the layout. The platform designer's
|
|
681
|
+
// draft is otherwise preserved verbatim.
|
|
682
|
+
if (args.rebuild === true && args.draftViewJson === undefined && args.draftAttributesJson === undefined) {
|
|
683
|
+
view = undefined;
|
|
684
|
+
attributes = undefined;
|
|
685
|
+
}
|
|
686
|
+
let rebuildPreservedControls = [];
|
|
687
|
+
if (typeof view !== "string" || typeof attributes !== "string") {
|
|
688
|
+
// Ticket 48: an explicit fieldCodes selection picks (and orders) the
|
|
689
|
+
// scaffolded business fields; omitted scaffolds every published field.
|
|
690
|
+
const scaffoldFields = wanted
|
|
691
|
+
? wanted.map((code) => publishedFields.find((field) => field.code === code)).filter((field) => field !== undefined)
|
|
692
|
+
: publishedFields;
|
|
693
|
+
if (scaffoldFields.length === 0)
|
|
694
|
+
throw new WireError("business", "form draft requires published fields to scaffold (none found); create and publish fields first", 302020);
|
|
695
|
+
// Ticket 66: redundant (carry-through) fields stay bound. When the model
|
|
696
|
+
// has NO workflow, hide them in the form design (option 1); with a
|
|
697
|
+
// workflow, leave them visible so per-node propertyPermissions govern
|
|
698
|
+
// (option 2, preferred — configured on the workflow template's activities).
|
|
699
|
+
const redundant = redundantFieldCodes(scaffoldFields);
|
|
700
|
+
let hidden;
|
|
701
|
+
if (redundant.size > 0) {
|
|
702
|
+
const hasWorkflow = (await wireCall(session, { path: ENDPOINTS.workflowIsWorkflow, query: { schemaCode: args.schemaCode } })).data === true;
|
|
703
|
+
if (!hasWorkflow)
|
|
704
|
+
hidden = redundant;
|
|
705
|
+
}
|
|
706
|
+
const scaffold = buildFormScaffold(scaffoldFields, pre?.name ?? String(args.sheetCode), hidden);
|
|
707
|
+
if (previousAttributes)
|
|
708
|
+
rebuildPreservedControls = preserveDesignerControlOptions(scaffold.attributes, previousAttributes);
|
|
709
|
+
view = JSON.stringify(scaffold.rows);
|
|
710
|
+
attributes = JSON.stringify(scaffold.attributes);
|
|
711
|
+
}
|
|
712
|
+
if (args.draftAttributesJson === undefined && typeof attributes === "string") {
|
|
713
|
+
attributes = propagateRelationMappings(attributes, publishedFields);
|
|
714
|
+
// Ticket 69: repair/densify org-selection controls on the existing draft.
|
|
715
|
+
attributes = normalizeOrgSelectorControls(attributes, publishedFields);
|
|
716
|
+
// Repair relation child columns written before the childProperties fix
|
|
717
|
+
// (301015 "[列名]未选择关联模型").
|
|
718
|
+
attributes = repairChildTableRelationColumns(attributes, publishedFields);
|
|
719
|
+
// Field-owned form conditions (required/readonly/display) must reach the
|
|
720
|
+
// control even when the form already existed — a non-rebuild draft keeps
|
|
721
|
+
// controls verbatim, so a later field.update never propagated (bench20).
|
|
722
|
+
attributes = syncFieldDerivedControlOptions(attributes, publishedFields);
|
|
723
|
+
}
|
|
724
|
+
const payload = {
|
|
725
|
+
...(pre ?? {}),
|
|
726
|
+
schemaCode: args.schemaCode,
|
|
727
|
+
code: args.sheetCode,
|
|
728
|
+
name: pre?.name ?? args.sheetCode,
|
|
729
|
+
sheetType: pre?.sheetType ?? 0,
|
|
730
|
+
published: false,
|
|
731
|
+
// Draft writes are full-row updates, but they must not destroy the
|
|
732
|
+
// currently published face. Publish promotion is a separate operation;
|
|
733
|
+
// preserving these columns keeps the published form usable if the draft
|
|
734
|
+
// write or a later publish fails.
|
|
735
|
+
publishedViewJson: pre?.publishedViewJson ?? null,
|
|
736
|
+
publishedAttributesJson: pre?.publishedAttributesJson ?? null,
|
|
737
|
+
publishedActionsJson: pre?.publishedActionsJson ?? null,
|
|
738
|
+
publishedHtmlJson: pre?.publishedHtmlJson ?? null,
|
|
739
|
+
draftViewJson: view,
|
|
740
|
+
draftAttributesJson: attributes,
|
|
741
|
+
...(args.draftHtmlJson ? { draftHtmlJson: args.draftHtmlJson } : {}),
|
|
742
|
+
};
|
|
743
|
+
const performed = await wireCall(session, { path: ENDPOINTS.sheetUpdate, method: "POST", body: payload });
|
|
744
|
+
const readback = await wireCall(session, { path: ENDPOINTS.sheetGet, query: { schemaCode: args.schemaCode, sheetCode: args.sheetCode } });
|
|
745
|
+
const sheet = (readback.data ?? {});
|
|
746
|
+
return { performed: { errcode: performed.errcode }, readback: { hasDraft: sheet.existDraft === true || typeof sheet.draftViewJson === "string", ...(args.rebuild === true && previousAttributes ? { rebuildPreservedControls } : {}) } };
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Shared form.publish write body — promotes the saved draft to published,
|
|
750
|
+
* promoting draft*Json → published*Json verbatim (including draftHtmlJson →
|
|
751
|
+
* publishedHtmlJson). Used by the form.publish write tool AND by configure_form
|
|
752
|
+
* AND by onlinejs.compose. Pass `force:true` to override the
|
|
753
|
+
* already-published skip (compose needs this because it may legitimately
|
|
754
|
+
* re-publish after the caller has already published once).
|
|
755
|
+
*/
|
|
756
|
+
export async function executeFormPublishWrite(session, args) {
|
|
757
|
+
// A publish promotes a saved draft. When the model has no form yet, or the
|
|
758
|
+
// form exists but holds no draft, materialise the draft first (form.draft
|
|
759
|
+
// creates the default form and scaffolds the published fields) so a single
|
|
760
|
+
// form.publish call is self-sufficient. A draft that already exists is left
|
|
761
|
+
// untouched — this only fills the gap.
|
|
762
|
+
let pre = await readFormOrNull(session, args.schemaCode, args.sheetCode);
|
|
763
|
+
const hasDraft = (sheet) => sheet !== null && typeof sheet.draftViewJson === "string" && typeof sheet.draftAttributesJson === "string";
|
|
764
|
+
if (!hasDraft(pre)) {
|
|
765
|
+
await executeFormDraftWrite(session, { schemaCode: args.schemaCode, sheetCode: args.sheetCode });
|
|
766
|
+
pre = await readFormOrNull(session, args.schemaCode, args.sheetCode);
|
|
767
|
+
}
|
|
768
|
+
if (!pre)
|
|
769
|
+
throw new WireError("business", `form ${args.sheetCode} not found`, 302024);
|
|
770
|
+
if (pre.draftHtmlJson !== "[]")
|
|
771
|
+
assertHtmlRender(pre.draftHtmlJson, { schemaCode: args.schemaCode, sheetCode: args.sheetCode, face: "draftHtmlJson" });
|
|
772
|
+
if (pre.published === true && args.force !== true)
|
|
773
|
+
return { performed: { skipped: "already-published" }, readback: { published: true } };
|
|
774
|
+
if (!hasDraft(pre))
|
|
775
|
+
throw new WireError("business", "form publish requires a saved draft (draftViewJson/draftAttributesJson)", 302020);
|
|
776
|
+
if (pre.draftHtmlJson !== "[]")
|
|
777
|
+
assertHtmlRender(pre.draftHtmlJson, { schemaCode: args.schemaCode, sheetCode: args.sheetCode, face: "draftHtmlJson" });
|
|
778
|
+
// Ticket 74 — degrade guard: refuse to promote a draft that carries FEWER
|
|
779
|
+
// controls than the already-published face. The platform designer's Save
|
|
780
|
+
// clears the draft faces to "[]"/"{}" (non-lossless); promoting that empty
|
|
781
|
+
// draft is exactly how the 13-control loss happened. Escape: allowDegrade:true.
|
|
782
|
+
if (args.allowDegrade !== true) {
|
|
783
|
+
const draftControls = controlCount(pre.draftAttributesJson);
|
|
784
|
+
const publishedControls = controlCount(pre.publishedAttributesJson);
|
|
785
|
+
if (draftControls < publishedControls) {
|
|
786
|
+
throw new ToolRefusalError({
|
|
787
|
+
status: "refused",
|
|
788
|
+
blocker: "form-publish-degrade",
|
|
789
|
+
schemaCode: args.schemaCode,
|
|
790
|
+
sheetCode: args.sheetCode,
|
|
791
|
+
draftControlCount: draftControls,
|
|
792
|
+
publishedControlCount: publishedControls,
|
|
793
|
+
message: `草稿面控件数(${draftControls})少于已发布面(${publishedControls})——拒绝降级提拔。平台设计器 Save 会清空草稿(非无损);恢复路径是表单版本历史 /api/api/app/bizsheet/history/list 取回完好版本的 view/attributes 原文重写草稿后再发布。确要减少控件请显式传 allowDegrade:true`,
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
const payload = {
|
|
798
|
+
...pre,
|
|
799
|
+
schemaCode: args.schemaCode,
|
|
800
|
+
code: args.sheetCode,
|
|
801
|
+
name: pre.name,
|
|
802
|
+
sheetType: pre.sheetType ?? 0,
|
|
803
|
+
published: false,
|
|
804
|
+
existDraft: false,
|
|
805
|
+
sendMessage: false,
|
|
806
|
+
publishRemarks: args.publishRemarks ?? "omc-mcp-form-publication",
|
|
807
|
+
publishedViewJson: pre.draftViewJson,
|
|
808
|
+
publishedAttributesJson: pre.draftAttributesJson,
|
|
809
|
+
publishedActionsJson: pre.draftActionsJson ?? "[]",
|
|
810
|
+
publishedHtmlJson: pre.draftHtmlJson ?? "{}",
|
|
811
|
+
draftViewJson: pre.draftViewJson,
|
|
812
|
+
draftAttributesJson: pre.draftAttributesJson,
|
|
813
|
+
...(pre.draftActionsJson !== null && pre.draftActionsJson !== undefined ? { draftActionsJson: pre.draftActionsJson } : {}),
|
|
814
|
+
...(pre.draftHtmlJson !== null && pre.draftHtmlJson !== undefined ? { draftHtmlJson: pre.draftHtmlJson } : {}),
|
|
815
|
+
};
|
|
816
|
+
const performed = await wireCall(session, { path: ENDPOINTS.sheetPublish, method: "POST", body: payload });
|
|
817
|
+
const readback = await wireCall(session, { path: ENDPOINTS.sheetGet, query: { schemaCode: args.schemaCode, sheetCode: args.sheetCode } });
|
|
818
|
+
const sheet = (readback.data ?? {});
|
|
819
|
+
return { performed: { errcode: performed.errcode }, readback: { published: sheet.published === true, publishedHtmlJsonPresent: typeof sheet.publishedHtmlJson === "string" } };
|
|
820
|
+
}
|
|
821
|
+
//# sourceMappingURL=form-support.js.map
|