@stalfh233/omc-cli 0.4.74 → 0.4.76
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 +9 -3
- package/dist/artifacts.js +2 -2
- package/dist/capability-overview.js +12 -3
- package/dist/cli.js +105 -39
- package/dist/command-transport.js +1 -1
- package/dist/compute.js +2 -1
- package/dist/discovery.js +18 -0
- package/dist/endpoints.js +24 -0
- package/dist/form-default.js +97 -10
- package/dist/form-layout.js +25 -2
- package/dist/form-support.js +361 -10
- package/dist/human-summary.js +4 -0
- package/dist/lanes/businessrule.js +7 -0
- package/dist/lanes/datarule.js +33 -9
- package/dist/lanes/form-contract.js +14 -4
- package/dist/lanes/form-coverage.js +2 -1
- package/dist/lanes/form.js +44 -1
- package/dist/lanes/organization.js +37 -3
- package/dist/lanes/rule-lifecycle-contract.js +17 -0
- package/dist/lanes/rule-lifecycle-coverage.js +2 -0
- package/dist/lanes/rule-lifecycle.js +103 -19
- package/dist/lanes/workflow-contract.js +57 -10
- package/dist/lanes/workflow-coverage.js +4 -0
- package/dist/lanes/workflow.js +458 -40
- package/dist/path-projection.js +129 -0
- package/dist/rollback-spec.js +32 -1
- package/dist/usage-log.js +11 -2
- package/dist/write.js +9 -3
- package/package.json +3 -3
- package/skills/omc-business-configuration.md +20 -2
- package/skills/omc-capabilities.md +8 -7
- package/skills/omc-write-hazards.md +4 -4
package/dist/form-support.js
CHANGED
|
@@ -4,7 +4,7 @@ import { decodeJsonObjectColumn } from "./json-column.js";
|
|
|
4
4
|
import { ToolRefusalError } from "./tool-types.js";
|
|
5
5
|
import { FIELD_CONTROL_TYPES, SYSTEM_FIELD_CODES, fieldControlOptions, relationDisplayField, relationMappings, } from "./field-families.js";
|
|
6
6
|
import { deriveControlOptions } from "./capability-shape.js";
|
|
7
|
-
import { validateFormLayoutShape } from "./form-layout.js";
|
|
7
|
+
import { hardLayoutViolations, validateFormLayoutShape } from "./form-layout.js";
|
|
8
8
|
/**
|
|
9
9
|
* Propagate each relation field's `options.mappings` into its form control so
|
|
10
10
|
* the form-fill carry is configured where the form renderer reads it. The
|
|
@@ -602,9 +602,7 @@ export function isEmptyLayoutJson(value) {
|
|
|
602
602
|
* face (one-sided included), covering configure_form/apply_changes paths that
|
|
603
603
|
* do not pass through the `form.draft` write permission gate.
|
|
604
604
|
*/
|
|
605
|
-
export function
|
|
606
|
-
if (args.draftViewJson === undefined && args.draftAttributesJson === undefined)
|
|
607
|
-
return;
|
|
605
|
+
export function formLayoutViolations(args) {
|
|
608
606
|
const parse = (value) => {
|
|
609
607
|
if (typeof value !== "string")
|
|
610
608
|
return undefined;
|
|
@@ -615,7 +613,12 @@ export function assertCallerFormLayout(args) {
|
|
|
615
613
|
return undefined;
|
|
616
614
|
}
|
|
617
615
|
};
|
|
618
|
-
|
|
616
|
+
return validateFormLayoutShape(parse(args.draftViewJson), parse(args.draftAttributesJson));
|
|
617
|
+
}
|
|
618
|
+
export function assertCallerFormLayout(args) {
|
|
619
|
+
if (args.draftViewJson === undefined && args.draftAttributesJson === undefined)
|
|
620
|
+
return;
|
|
621
|
+
const violations = hardLayoutViolations(formLayoutViolations(args));
|
|
619
622
|
if (violations.length > 0) {
|
|
620
623
|
throw new ToolRefusalError({
|
|
621
624
|
status: "refused",
|
|
@@ -665,14 +668,177 @@ export function refuseEmptyCallerLayout(args) {
|
|
|
665
668
|
message: `显式提供的 ${emptyFaces.join("、")} 是空布局("[]"/"{}")——平台设计器 Save 会把草稿清成该形态(非无损),照写会丢控件。若确要清空布局请显式传 allowEmptyLayout:true;若是设计器 Save 后的修复,先用表单版本历史(/api/api/app/bizsheet/history/list)取回完好版本原文再写回`,
|
|
666
669
|
});
|
|
667
670
|
}
|
|
671
|
+
/**
|
|
672
|
+
* Form-toolbar buttons are a THREE-LAYER configuration (live 2026-10-23, 1.1.25
|
|
673
|
+
* + backend source):
|
|
674
|
+
* 1. `bizsheet.options.customButtonJson` (a STRINGIFIED JSON array) is the
|
|
675
|
+
* authoring face — `ModelManagementFacadeImpl.publishBizForm:1022` turns it
|
|
676
|
+
* into `h_biz_button` rows with `triggerType=FORM`, `triggerCode=<formCode>`
|
|
677
|
+
* at publish time.
|
|
678
|
+
* 2. Those rows are what `ActionPermissionHandler` reads
|
|
679
|
+
* (`getByTriggerTypeAndCode(FORM, bizCode)`).
|
|
680
|
+
* 3. Whether a button shows at a given node is filtered by the workflow
|
|
681
|
+
* activity's `buttonPermission.customBtns[].enabled` (the designer's
|
|
682
|
+
* 「表单操作」 checkboxes) plus `showPermCode`.
|
|
683
|
+
*
|
|
684
|
+
* `customButtonJson` MUST stay a string inside the options JSON — passing the
|
|
685
|
+
* array inline makes the platform answer 50000 (fastjson
|
|
686
|
+
* "syntax error, position at 0, name customButtonJson").
|
|
687
|
+
*/
|
|
688
|
+
export const FORM_BUTTON_LOCATIONS = Object.freeze(["FORM", "ROW", "LIST"]);
|
|
689
|
+
const FORM_BUTTON_BIND_ACTIONS = new Set([
|
|
690
|
+
"LIST",
|
|
691
|
+
"FORM",
|
|
692
|
+
"ADD_DATA",
|
|
693
|
+
"FLOW",
|
|
694
|
+
"BUSINESS_RULE",
|
|
695
|
+
"SCRIPT",
|
|
696
|
+
"CUSTOM_PAGE",
|
|
697
|
+
"WORKFLOW_CENTER_PAGE",
|
|
698
|
+
"FRONT_EVENT",
|
|
699
|
+
"AGENT",
|
|
700
|
+
]);
|
|
701
|
+
const FORM_BUTTON_OPERATE_TYPES = new Set(["WITH_VIEW", "WITH_OUT_VIEW"]);
|
|
702
|
+
/** Validate the caller's form-button list before any wire (same shape as listview buttons, FORM location). */
|
|
703
|
+
export function validateFormButtons(buttons, tool) {
|
|
704
|
+
if (!Array.isArray(buttons))
|
|
705
|
+
throw new ToolRefusalError({ status: "refused", blocker: "form-buttons-invalid", message: `${tool}: buttons 必须是数组(BizButtonModel 数组)` });
|
|
706
|
+
const seen = new Set();
|
|
707
|
+
return buttons.map((raw, index) => {
|
|
708
|
+
const button = (raw ?? {});
|
|
709
|
+
const code = typeof button.code === "string" ? button.code : "";
|
|
710
|
+
const name = typeof button.name === "string" ? button.name : "";
|
|
711
|
+
if (code === "")
|
|
712
|
+
throw new ToolRefusalError({ status: "refused", blocker: "form-buttons-invalid", message: `${tool}: buttons[${index}].code 必填(表单按钮的唯一标识,流程节点按它勾选)` });
|
|
713
|
+
if (name === "")
|
|
714
|
+
throw new ToolRefusalError({ status: "refused", blocker: "form-buttons-invalid", message: `${tool}: buttons[${index}].name 必填(设计器/表单页显示名)` });
|
|
715
|
+
if (seen.has(code))
|
|
716
|
+
throw new ToolRefusalError({ status: "refused", blocker: "form-buttons-duplicate", message: `${tool}: buttons 里 code ${code} 重复` });
|
|
717
|
+
seen.add(code);
|
|
718
|
+
const useLocation = typeof button.useLocation === "string" ? button.useLocation : "FORM";
|
|
719
|
+
if (!FORM_BUTTON_LOCATIONS.includes(useLocation))
|
|
720
|
+
throw new ToolRefusalError({ status: "refused", blocker: "form-buttons-invalid", message: `${tool}: buttons[${index}].useLocation 只能是 ${FORM_BUTTON_LOCATIONS.join("|")}(表单工具栏按钮用 FORM)` });
|
|
721
|
+
const bindAction = typeof button.bindAction === "string" ? button.bindAction : "";
|
|
722
|
+
if (!FORM_BUTTON_BIND_ACTIONS.has(bindAction))
|
|
723
|
+
throw new ToolRefusalError({ status: "refused", blocker: "form-buttons-invalid", message: `${tool}: buttons[${index}].bindAction 必须是 ButtonBindAction 名(SCRIPT/BUSINESS_RULE/FLOW/FORM/ADD_DATA/CUSTOM_PAGE/…)` });
|
|
724
|
+
const operateType = typeof button.operateType === "string" ? button.operateType : "";
|
|
725
|
+
if (!FORM_BUTTON_OPERATE_TYPES.has(operateType))
|
|
726
|
+
throw new ToolRefusalError({ status: "refused", blocker: "form-buttons-invalid", message: `${tool}: buttons[${index}].operateType 必须是 WITH_VIEW 或 WITH_OUT_VIEW` });
|
|
727
|
+
if (bindAction === "SCRIPT" || bindAction === "BUSINESS_RULE" || bindAction === "FLOW" || bindAction === "ADD_DATA" || bindAction === "FORM") {
|
|
728
|
+
const actionConfig = button.actionConfig;
|
|
729
|
+
const record = typeof actionConfig === "string" ? (() => { try {
|
|
730
|
+
return JSON.parse(actionConfig);
|
|
731
|
+
}
|
|
732
|
+
catch {
|
|
733
|
+
return undefined;
|
|
734
|
+
} })() : actionConfig;
|
|
735
|
+
const config = record && typeof record === "object" && !Array.isArray(record) ? record : undefined;
|
|
736
|
+
if (bindAction === "SCRIPT" && typeof config?.scriptData !== "string")
|
|
737
|
+
throw new ToolRefusalError({ status: "refused", blocker: "form-buttons-invalid", message: `${tool}: buttons[${index}] SCRIPT 需要 actionConfig.scriptData(前端脚本源码字符串)` });
|
|
738
|
+
if ((bindAction === "BUSINESS_RULE" || bindAction === "FLOW") && (typeof button.targetCode !== "string" || button.targetCode === ""))
|
|
739
|
+
throw new ToolRefusalError({ status: "refused", blocker: "form-buttons-invalid", message: `${tool}: buttons[${index}] ${bindAction} 需要 targetCode` });
|
|
740
|
+
}
|
|
741
|
+
return {
|
|
742
|
+
...button,
|
|
743
|
+
code,
|
|
744
|
+
name,
|
|
745
|
+
useLocation,
|
|
746
|
+
bindAction,
|
|
747
|
+
operateType,
|
|
748
|
+
// The platform stores actionConfig as a string; a caller's object is
|
|
749
|
+
// serialized once here (double-encoding breaks the renderer).
|
|
750
|
+
...(button.actionConfig !== undefined && typeof button.actionConfig !== "string" ? { actionConfig: JSON.stringify(button.actionConfig) } : {}),
|
|
751
|
+
sortKey: typeof button.sortKey === "number" ? button.sortKey : index,
|
|
752
|
+
};
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
function decodeOptionsColumn(value) {
|
|
756
|
+
if (value === null || value === undefined || value === "")
|
|
757
|
+
return {};
|
|
758
|
+
if (typeof value === "object" && !Array.isArray(value))
|
|
759
|
+
return { ...value };
|
|
760
|
+
if (typeof value === "string") {
|
|
761
|
+
try {
|
|
762
|
+
const parsed = JSON.parse(value);
|
|
763
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
|
|
764
|
+
return { ...parsed };
|
|
765
|
+
}
|
|
766
|
+
catch {
|
|
767
|
+
// A corrupt options column must not silently drop other options.
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return {};
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Merge the caller's form buttons into the sheet `options` column. Returns the
|
|
774
|
+
* options JSON string to POST (undefined = leave the column untouched).
|
|
775
|
+
*/
|
|
776
|
+
export function mergeFormOptionsWithButtons(existingOptions, input, tool) {
|
|
777
|
+
const hasButtons = input.buttons !== undefined;
|
|
778
|
+
const hasJson = input.customButtonJson !== undefined;
|
|
779
|
+
const removeCodes = input.removeButtonCodes ?? [];
|
|
780
|
+
if (hasButtons && hasJson)
|
|
781
|
+
throw new ToolRefusalError({ status: "refused", blocker: "form-buttons-ambiguous", message: `${tool}: buttons 与 customButtonJson 只能给一种(buttons 数组更好读;customButtonJson 用于原样透传读回值)` });
|
|
782
|
+
if (!hasButtons && !hasJson && removeCodes.length === 0)
|
|
783
|
+
return undefined;
|
|
784
|
+
let incoming = [];
|
|
785
|
+
if (hasButtons) {
|
|
786
|
+
incoming = validateFormButtons(input.buttons, tool);
|
|
787
|
+
}
|
|
788
|
+
else if (hasJson) {
|
|
789
|
+
const raw = String(input.customButtonJson);
|
|
790
|
+
let parsed;
|
|
791
|
+
try {
|
|
792
|
+
parsed = JSON.parse(raw);
|
|
793
|
+
}
|
|
794
|
+
catch {
|
|
795
|
+
throw new ToolRefusalError({ status: "refused", blocker: "form-buttons-invalid", message: `${tool}: customButtonJson 必须是 JSON 数组字符串` });
|
|
796
|
+
}
|
|
797
|
+
incoming = validateFormButtons(parsed, tool);
|
|
798
|
+
}
|
|
799
|
+
const options = decodeOptionsColumn(existingOptions);
|
|
800
|
+
// Default MERGE: an upsert by `code` so a caller can add or edit ONE button
|
|
801
|
+
// without re-sending the whole list (and without a read-modify-write race).
|
|
802
|
+
// `replace` restores the whole-list semantics.
|
|
803
|
+
const mode = input.buttonsMode ?? "merge";
|
|
804
|
+
const existing = readFormButtons(options) ?? [];
|
|
805
|
+
let final;
|
|
806
|
+
if (mode === "replace") {
|
|
807
|
+
final = incoming;
|
|
808
|
+
}
|
|
809
|
+
else {
|
|
810
|
+
const byCode = new Map(existing.map((button) => [String(button.code ?? ""), button]));
|
|
811
|
+
for (const button of incoming)
|
|
812
|
+
byCode.set(String(button.code), { ...(byCode.get(String(button.code)) ?? {}), ...button });
|
|
813
|
+
for (const code of removeCodes)
|
|
814
|
+
byCode.delete(String(code));
|
|
815
|
+
final = [...byCode.values()].map((button, index) => ({ ...button, sortKey: typeof button.sortKey === "number" ? button.sortKey : index }));
|
|
816
|
+
}
|
|
817
|
+
if (removeCodes.length > 0 && mode === "replace")
|
|
818
|
+
throw new ToolRefusalError({ status: "refused", blocker: "form-buttons-invalid", message: `${tool}: removeButtonCodes 只用于 buttonsMode:"merge"(replace 已用新列表整体替换)` });
|
|
819
|
+
options.customButtonJson = JSON.stringify(final);
|
|
820
|
+
return JSON.stringify(options);
|
|
821
|
+
}
|
|
822
|
+
/** The caller's buttons from a READ face (parsed array, or null when absent). */
|
|
823
|
+
export function readFormButtons(options) {
|
|
824
|
+
const record = decodeOptionsColumn(options);
|
|
825
|
+
const raw = record.customButtonJson;
|
|
826
|
+
if (typeof raw !== "string" || raw === "")
|
|
827
|
+
return null;
|
|
828
|
+
try {
|
|
829
|
+
const parsed = JSON.parse(raw);
|
|
830
|
+
return Array.isArray(parsed) ? parsed : null;
|
|
831
|
+
}
|
|
832
|
+
catch {
|
|
833
|
+
return null;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
668
836
|
export async function executeFormDraftWrite(session, args) {
|
|
669
837
|
if (args.draftHtmlJson !== undefined) {
|
|
670
838
|
assertHtmlRender(args.draftHtmlJson, { schemaCode: args.schemaCode, sheetCode: args.sheetCode, face: "draftHtmlJson" });
|
|
671
839
|
}
|
|
672
840
|
// Caller-provided layouts get the distribution-grade shape gate (options
|
|
673
841
|
// objects, string viewJson cells, no child keys as rows) before any wire.
|
|
674
|
-
if (args.draftViewJson !== undefined || args.draftAttributesJson !== undefined)
|
|
675
|
-
assertCallerFormLayout(args);
|
|
676
842
|
// Ticket 74 — refuse CALLER-PROVIDED empty layouts before any wire call.
|
|
677
843
|
// The platform designer's Save clears draft faces to "[]"/"{}" (non-lossless);
|
|
678
844
|
// re-writing that shape explicitly is how the 13-control loss happened.
|
|
@@ -723,6 +889,42 @@ export async function executeFormDraftWrite(session, args) {
|
|
|
723
889
|
if (pre === null)
|
|
724
890
|
throw new WireError("business", `form ${args.sheetCode} was created but is not readable`, 302024);
|
|
725
891
|
}
|
|
892
|
+
//
|
|
893
|
+
// 1.3 of issue 2026-10-23: the platform's OWN readback can carry shapes this
|
|
894
|
+
// gate rejects (`child_column_in_rows` when a published layout lists child
|
|
895
|
+
// columns as rows; `layout_row_overflow` on the auto system row). Refusing a
|
|
896
|
+
// byte-identical round-trip makes "read → write back" impossible, so a face
|
|
897
|
+
// that matches the sheet's own published/draft face is allowed through with
|
|
898
|
+
// the violations reported as warnings. Caller-authored layouts stay strict.
|
|
899
|
+
const roundTripFaces = new Set();
|
|
900
|
+
let layoutWarnings = [];
|
|
901
|
+
if (args.draftViewJson !== undefined || args.draftAttributesJson !== undefined) {
|
|
902
|
+
const pre2 = pre;
|
|
903
|
+
const same = (provided, stored) => typeof provided === "string" && typeof stored === "string" && provided.trim() === stored.trim();
|
|
904
|
+
const viewRoundTrip = same(args.draftViewJson, pre2?.publishedViewJson) || same(args.draftViewJson, pre2?.draftViewJson);
|
|
905
|
+
const attributesRoundTrip = same(args.draftAttributesJson, pre2?.publishedAttributesJson) || same(args.draftAttributesJson, pre2?.draftAttributesJson);
|
|
906
|
+
const allProvidedAreRoundTrip = (args.draftViewJson === undefined || viewRoundTrip) && (args.draftAttributesJson === undefined || attributesRoundTrip);
|
|
907
|
+
if (!allProvidedAreRoundTrip && args.fromReadback === true) {
|
|
908
|
+
throw new ToolRefusalError({
|
|
909
|
+
status: "refused",
|
|
910
|
+
blocker: "form-layout-invalid",
|
|
911
|
+
schemaCode: args.schemaCode,
|
|
912
|
+
sheetCode: args.sheetCode,
|
|
913
|
+
message: "fromReadback:true 但本次布局与已存草稿/发布面不一致——读过之后又改过就必须按普通写面校验(去掉 fromReadback,或按 validation 修正)",
|
|
914
|
+
validation: formLayoutViolations(args),
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
if (allProvidedAreRoundTrip) {
|
|
918
|
+
layoutWarnings = formLayoutViolations(args);
|
|
919
|
+
if (args.draftViewJson !== undefined)
|
|
920
|
+
roundTripFaces.add("draftViewJson");
|
|
921
|
+
if (args.draftAttributesJson !== undefined)
|
|
922
|
+
roundTripFaces.add("draftAttributesJson");
|
|
923
|
+
}
|
|
924
|
+
else {
|
|
925
|
+
assertCallerFormLayout(args);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
726
928
|
// Field-level relation mappings are the source of truth; read the published
|
|
727
929
|
// fields so the scaffold (or an existing draft) can carry them into the
|
|
728
930
|
// matching form control, where the form renderer reads them at fill time.
|
|
@@ -856,6 +1058,11 @@ export async function executeFormDraftWrite(session, args) {
|
|
|
856
1058
|
if (composed !== undefined)
|
|
857
1059
|
assertFormulasSurvivedRebuild(composed, previousAttributes, { schemaCode: args.schemaCode, sheetCode: args.sheetCode });
|
|
858
1060
|
}
|
|
1061
|
+
// Form toolbar buttons live on `options.customButtonJson`; the publish step
|
|
1062
|
+
// turns them into h_biz_button rows (triggerType=FORM). Merge rather than
|
|
1063
|
+
// replace so unrelated options (presetButtonManage, sheetPresetButtonManage)
|
|
1064
|
+
// survive.
|
|
1065
|
+
const mergedOptions = mergeFormOptionsWithButtons(pre?.options, { buttons: args.buttons, customButtonJson: args.customButtonJson, buttonsMode: args.buttonsMode, removeButtonCodes: args.removeButtonCodes }, "form.draft");
|
|
859
1066
|
const payload = {
|
|
860
1067
|
...(pre ?? {}),
|
|
861
1068
|
schemaCode: args.schemaCode,
|
|
@@ -863,6 +1070,7 @@ export async function executeFormDraftWrite(session, args) {
|
|
|
863
1070
|
name: pre?.name ?? args.sheetCode,
|
|
864
1071
|
sheetType: pre?.sheetType ?? 0,
|
|
865
1072
|
published: false,
|
|
1073
|
+
...(mergedOptions !== undefined ? { options: mergedOptions } : {}),
|
|
866
1074
|
// Draft writes are full-row updates, but they must not destroy the
|
|
867
1075
|
// currently published face. Publish promotion is a separate operation;
|
|
868
1076
|
// preserving these columns keeps the published form usable if the draft
|
|
@@ -878,7 +1086,123 @@ export async function executeFormDraftWrite(session, args) {
|
|
|
878
1086
|
const performed = await wireCall(session, { path: ENDPOINTS.sheetUpdate, method: "POST", body: payload });
|
|
879
1087
|
const readback = await wireCall(session, { path: ENDPOINTS.sheetGet, query: { schemaCode: args.schemaCode, sheetCode: args.sheetCode } });
|
|
880
1088
|
const sheet = (readback.data ?? {});
|
|
881
|
-
|
|
1089
|
+
const formButtons = readFormButtons(sheet.options);
|
|
1090
|
+
return {
|
|
1091
|
+
performed: { errcode: performed.errcode },
|
|
1092
|
+
readback: {
|
|
1093
|
+
hasDraft: sheet.existDraft === true || typeof sheet.draftViewJson === "string",
|
|
1094
|
+
...(formButtons !== null ? { formButtons, formButtonCodes: formButtons.map((button) => String(button.code ?? "")) } : {}),
|
|
1095
|
+
...(mergedOptions !== undefined ? { customButtonJsonWritten: true } : {}),
|
|
1096
|
+
...(roundTripFaces.size > 0
|
|
1097
|
+
? {
|
|
1098
|
+
layoutGate: "platform-roundtrip",
|
|
1099
|
+
layoutRoundTripFaces: [...roundTripFaces],
|
|
1100
|
+
layoutNote: "本次布局与平台读回的原样布局一致,形状告警只提示不拒绝(平台自身产出可能含子表列行/系统行超宽)",
|
|
1101
|
+
...(layoutWarnings.length > 0 ? { layoutWarnings } : {}),
|
|
1102
|
+
}
|
|
1103
|
+
: {}),
|
|
1104
|
+
...(args.rebuild === true && previousAttributes ? { rebuildPreservedControls } : {}),
|
|
1105
|
+
},
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
/** Schema-wide FORM buttons (code -> row) — the set a workflow node can tick. */
|
|
1109
|
+
export async function readSchemaFormButtons(session, schemaCode) {
|
|
1110
|
+
const inventory = (await wireCall(session, { path: ENDPOINTS.systemGetCustomButtons, query: { schemaCode } })).data;
|
|
1111
|
+
const triggers = Array.isArray(inventory?.formCustomButtons) ? inventory.formCustomButtons : [];
|
|
1112
|
+
const byCode = {};
|
|
1113
|
+
for (const trigger of triggers) {
|
|
1114
|
+
const buttons = Array.isArray(trigger.buttonModels) ? trigger.buttonModels : [];
|
|
1115
|
+
for (const button of buttons) {
|
|
1116
|
+
const code = String(button.code ?? "");
|
|
1117
|
+
if (code !== "" && byCode[code] === undefined)
|
|
1118
|
+
byCode[code] = button;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
return byCode;
|
|
1122
|
+
}
|
|
1123
|
+
/**
|
|
1124
|
+
* Execute ONE form-toolbar button on ONE record — the exact request the portal
|
|
1125
|
+
* posts when that button is clicked (live 2026-10-23, Playwright capture,
|
|
1126
|
+
* AI-Platform 1.1.25):
|
|
1127
|
+
* POST /api/api/runtime/form/executeCustomButton
|
|
1128
|
+
* {bizButtonModel:<the h_biz_button row>, bizObjectList:[<full bizObject>],
|
|
1129
|
+
* reacquireDataFromDb:true}
|
|
1130
|
+
* Distinct from the list-row button face (`launcher/domain_service_runtime`
|
|
1131
|
+
* with `ids[]`): form buttons carry the whole record.
|
|
1132
|
+
*/
|
|
1133
|
+
export async function executeFormRunButtonWrite(session, args) {
|
|
1134
|
+
const inventory = (await wireCall(session, { path: ENDPOINTS.systemGetCustomButtons, query: { schemaCode: args.schemaCode } })).data;
|
|
1135
|
+
const triggers = Array.isArray(inventory?.formCustomButtons) ? inventory.formCustomButtons : [];
|
|
1136
|
+
const trigger = triggers.find((row) => String(row.triggerCode ?? "") === args.sheetCode) ?? triggers[0];
|
|
1137
|
+
const buttons = Array.isArray(trigger?.buttonModels) ? trigger.buttonModels : [];
|
|
1138
|
+
if (buttons.length === 0)
|
|
1139
|
+
throw new ToolRefusalError({
|
|
1140
|
+
status: "refused",
|
|
1141
|
+
blocker: "form-button-absent",
|
|
1142
|
+
schemaCode: args.schemaCode,
|
|
1143
|
+
sheetCode: args.sheetCode,
|
|
1144
|
+
message: `表单 ${args.sheetCode} 没有自定义按钮(getCustomButtons 的 buttonModels 为空)——先用 form.publish 传 buttons(写 options.customButtonJson)生成 h_biz_button(triggerType=FORM)`,
|
|
1145
|
+
});
|
|
1146
|
+
const button = buttons.find((row) => String(row.code ?? "") === args.buttonCode);
|
|
1147
|
+
if (!button)
|
|
1148
|
+
throw new ToolRefusalError({
|
|
1149
|
+
status: "refused",
|
|
1150
|
+
blocker: "form-button-absent",
|
|
1151
|
+
schemaCode: args.schemaCode,
|
|
1152
|
+
sheetCode: args.sheetCode,
|
|
1153
|
+
buttonCode: args.buttonCode,
|
|
1154
|
+
available: buttons.map((row) => String(row.code ?? "")),
|
|
1155
|
+
message: `表单 ${args.sheetCode} 没有按钮 ${args.buttonCode};可用:${buttons.map((row) => String(row.code ?? "")).join("、")}`,
|
|
1156
|
+
});
|
|
1157
|
+
const loaded = (await wireCall(session, { path: ENDPOINTS.runtimeFormLoad, query: { schemaCode: args.schemaCode, sheetCode: args.sheetCode, objectId: args.bizObjectId, isMobile: "false" } })).data;
|
|
1158
|
+
const bizObject = (loaded?.bizObject ?? null);
|
|
1159
|
+
if (!bizObject)
|
|
1160
|
+
throw new ToolRefusalError({
|
|
1161
|
+
status: "refused",
|
|
1162
|
+
blocker: "form-object-absent",
|
|
1163
|
+
schemaCode: args.schemaCode,
|
|
1164
|
+
bizObjectId: args.bizObjectId,
|
|
1165
|
+
message: `读不到记录 ${args.bizObjectId} 的 bizObject(runtime/form/load 无 bizObject)——先 data.load 核对 bizObjectId`,
|
|
1166
|
+
});
|
|
1167
|
+
// The portal flattens the org/person fields to id strings before posting
|
|
1168
|
+
// (form-detail's customButtonRule: `c.owner=(c.owner?.id)||""` etc.). Sending
|
|
1169
|
+
// the loaded objects makes the platform answer 10009
|
|
1170
|
+
// "Cannot deserialize value of type java.lang.String from Object value".
|
|
1171
|
+
const idOf = (value) => {
|
|
1172
|
+
if (typeof value === "string")
|
|
1173
|
+
return value;
|
|
1174
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
1175
|
+
const id = value.id;
|
|
1176
|
+
return typeof id === "string" ? id : "";
|
|
1177
|
+
}
|
|
1178
|
+
return "";
|
|
1179
|
+
};
|
|
1180
|
+
const submitted = { ...bizObject };
|
|
1181
|
+
for (const key of ["owner", "creater", "modifier", "ownerDeptId", "createdDeptId"]) {
|
|
1182
|
+
if (submitted[key] !== undefined)
|
|
1183
|
+
submitted[key] = idOf(submitted[key]);
|
|
1184
|
+
}
|
|
1185
|
+
submitted.formMode = loaded?.formMode ?? "isRead";
|
|
1186
|
+
submitted.activityCode = loaded?.activityCode ?? null;
|
|
1187
|
+
const performed = await wireCall(session, {
|
|
1188
|
+
path: ENDPOINTS.runtimeFormExecuteCustomButton,
|
|
1189
|
+
method: "POST",
|
|
1190
|
+
body: { bizButtonModel: button, bizObjectList: [submitted], reacquireDataFromDb: true },
|
|
1191
|
+
});
|
|
1192
|
+
return {
|
|
1193
|
+
performed: { errcode: performed.errcode, buttonCode: args.buttonCode, bizObjectId: args.bizObjectId },
|
|
1194
|
+
readback: {
|
|
1195
|
+
schemaCode: args.schemaCode,
|
|
1196
|
+
sheetCode: args.sheetCode,
|
|
1197
|
+
buttonCode: args.buttonCode,
|
|
1198
|
+
buttonName: String(button.name ?? ""),
|
|
1199
|
+
bindAction: String(button.bindAction ?? ""),
|
|
1200
|
+
targetCode: button.targetCode ?? null,
|
|
1201
|
+
errmsg: performed.data?.errmsg ?? null,
|
|
1202
|
+
...(performed.errcode === 302045 ? { dataInUse: true, hint: "302045:数据被占用(有人正在编辑该行),稍后重试" } : {}),
|
|
1203
|
+
effectHint: "平台只回执行结果;按钮绑定的规则副作用请另读 data.load。表单按钮走 executeCustomButton(带整条记录),与列表按钮的 launcher 路径不同",
|
|
1204
|
+
},
|
|
1205
|
+
};
|
|
882
1206
|
}
|
|
883
1207
|
/**
|
|
884
1208
|
* Shared form.publish write body — promotes the saved draft to published,
|
|
@@ -913,7 +1237,14 @@ export async function executeFormPublishWrite(session, args) {
|
|
|
913
1237
|
throw new WireError("business", `form ${args.sheetCode} not found`, 302024);
|
|
914
1238
|
if (pre.draftHtmlJson !== "[]")
|
|
915
1239
|
assertHtmlRender(pre.draftHtmlJson, { schemaCode: args.schemaCode, sheetCode: args.sheetCode, face: "draftHtmlJson" });
|
|
916
|
-
|
|
1240
|
+
// A button-only publish on an ALREADY published form must still promote: the
|
|
1241
|
+
// platform materialises h_biz_button rows from options.customButtonJson during
|
|
1242
|
+
// the publish call, so short-circuiting "already-published" would silently
|
|
1243
|
+
// drop the buttons (live 2026-10-23).
|
|
1244
|
+
const hasButtonDelta = args.buttons !== undefined ||
|
|
1245
|
+
args.customButtonJson !== undefined ||
|
|
1246
|
+
(Array.isArray(args.removeButtonCodes) && args.removeButtonCodes.length > 0);
|
|
1247
|
+
const forcePromote = args.force === true || args.rebuild === true || hasButtonDelta;
|
|
917
1248
|
if (pre.published === true && !forcePromote)
|
|
918
1249
|
return { performed: { skipped: "already-published" }, readback: { published: true } };
|
|
919
1250
|
if (!hasDraft(pre))
|
|
@@ -939,8 +1270,14 @@ export async function executeFormPublishWrite(session, args) {
|
|
|
939
1270
|
});
|
|
940
1271
|
}
|
|
941
1272
|
}
|
|
1273
|
+
// Buttons ride `options.customButtonJson`; the platform converts them into
|
|
1274
|
+
// h_biz_button(triggerType=FORM) rows during THIS publish call
|
|
1275
|
+
// (ModelManagementFacadeImpl.publishBizForm:1022). Passing them here is the
|
|
1276
|
+
// only way to create form buttons without the designer.
|
|
1277
|
+
const mergedOptions = mergeFormOptionsWithButtons(pre.options, { buttons: args.buttons, customButtonJson: args.customButtonJson, buttonsMode: args.buttonsMode, removeButtonCodes: args.removeButtonCodes }, "form.publish");
|
|
942
1278
|
const payload = {
|
|
943
1279
|
...pre,
|
|
1280
|
+
...(mergedOptions !== undefined ? { options: mergedOptions } : {}),
|
|
944
1281
|
schemaCode: args.schemaCode,
|
|
945
1282
|
code: args.sheetCode,
|
|
946
1283
|
name: pre.name,
|
|
@@ -961,6 +1298,20 @@ export async function executeFormPublishWrite(session, args) {
|
|
|
961
1298
|
const performed = await wireCall(session, { path: ENDPOINTS.sheetPublish, method: "POST", body: payload });
|
|
962
1299
|
const readback = await wireCall(session, { path: ENDPOINTS.sheetGet, query: { schemaCode: args.schemaCode, sheetCode: args.sheetCode } });
|
|
963
1300
|
const sheet = (readback.data ?? {});
|
|
964
|
-
|
|
1301
|
+
const formButtons = readFormButtons(sheet.options);
|
|
1302
|
+
return {
|
|
1303
|
+
performed: { errcode: performed.errcode },
|
|
1304
|
+
readback: {
|
|
1305
|
+
published: sheet.published === true,
|
|
1306
|
+
publishedHtmlJsonPresent: typeof sheet.publishedHtmlJson === "string",
|
|
1307
|
+
...(formButtons !== null
|
|
1308
|
+
? {
|
|
1309
|
+
formButtons,
|
|
1310
|
+
formButtonCodes: formButtons.map((button) => String(button.code ?? "")),
|
|
1311
|
+
formButtonNote: "按钮已写入 options.customButtonJson,本次 publish 已生成 h_biz_button(triggerType=FORM);表单页是否显示还取决于流程节点 buttonPermission.customBtns[].enabled(设计器「表单操作」勾选)",
|
|
1312
|
+
}
|
|
1313
|
+
: {}),
|
|
1314
|
+
},
|
|
1315
|
+
};
|
|
965
1316
|
}
|
|
966
1317
|
//# sourceMappingURL=form-support.js.map
|
package/dist/human-summary.js
CHANGED
|
@@ -168,6 +168,10 @@ function phraseOf(tool, args, outcome) {
|
|
|
168
168
|
return `发布流程 ${stringArg(args, "code") ?? "?"}(${schemaCode ?? "?"})`;
|
|
169
169
|
case "workflow.bindRule": {
|
|
170
170
|
const codes = Array.isArray(args.ruleCodes) ? args.ruleCodes.join("、") : stringArg(args, "ruleCode") ?? "?";
|
|
171
|
+
const activityCode = stringArg(args, "activityCode");
|
|
172
|
+
const nodeEvent = stringArg(args, "nodeEvent");
|
|
173
|
+
if (activityCode !== undefined && nodeEvent !== undefined)
|
|
174
|
+
return `绑定流程节点事件规则:${stringArg(args, "workflowCode") ?? "?"} 的节点 ${activityCode}.${nodeEvent} ← ${codes}`;
|
|
171
175
|
return `绑定流程事件规则:${stringArg(args, "workflowCode") ?? "?"} 的 ${stringArg(args, "handler") ?? "end"} 处理器 ← ${codes}`;
|
|
172
176
|
}
|
|
173
177
|
case "workflow.delete":
|
|
@@ -220,7 +220,14 @@ export function registerBusinessRuleLane(tools) {
|
|
|
220
220
|
schemaCode: record.schemaCode,
|
|
221
221
|
bizRuleType: record.bizRuleType,
|
|
222
222
|
enabled: record.enabled,
|
|
223
|
+
// The platform's `status` column lags behind the enable switch: r10
|
|
224
|
+
// saw `enabled:true` with `ruleStatus:"DISABLED"` while rule.list said
|
|
225
|
+
// ENABLED. `enabled` is the dispatch truth; flag the disagreement
|
|
226
|
+
// instead of letting two faces contradict each other silently.
|
|
223
227
|
ruleStatus: record.status,
|
|
228
|
+
...(record.enabled === true && String(record.status ?? "") !== "ENABLED"
|
|
229
|
+
? { statusLag: true, statusNote: "平台 status 列滞后:enabled=true 才是分发真相(列表面/日志可复核);不要据 ruleStatus=DISABLED 判定未启用" }
|
|
230
|
+
: {}),
|
|
224
231
|
defaultRule: record.defaultRule,
|
|
225
232
|
developmentMode: record.developmentMode,
|
|
226
233
|
ruleVersion: record.ruleVersion,
|
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
|
|
@@ -15,11 +15,13 @@ export const FORM_WIRE = Object.freeze({
|
|
|
15
15
|
publishAlreadyPublished: "published:true with force!==true skips as already-published",
|
|
16
16
|
forceRepublish: "onlinejs.compose passes force:true so it can re-promote after a prior publish",
|
|
17
17
|
emptyLayoutGuard: "ticket 74: the platform designer's Save is NOT lossless — it clears draftViewJson to \"[]\" and draftAttributesJson to \"{}\"; form.draft refuses CALLER-PROVIDED empty layouts (blocker form-empty-layout, zero wire, escape allowEmptyLayout:true) and form.publish refuses to promote a draft with fewer controls than the published face (blocker form-publish-degrade, escape allowDegrade:true); the recovery path is the form version history (/api/api/app/bizsheet/history/list) — fetch the intact version's view/attributes originals and rewrite the draft before publishing",
|
|
18
|
+
formButtons: "form toolbar buttons are a three-layer configuration (live 2026-10-23, 1.1.25): (1) bizsheet.options.customButtonJson — a STRINGIFIED JSON array — is the authoring face; ModelManagementFacadeImpl.publishBizForm:1022 turns it into h_biz_button rows with triggerType=FORM, triggerCode=<formCode> at publish; (2) those rows are what ActionPermissionHandler.getByTriggerTypeAndCode(FORM, bizCode) reads; (3) whether a button shows at a node is filtered by the workflow activity's buttonPermission.customBtns[].enabled (the designer 表单操作 checkboxes) plus showPermCode; the execute face is POST /api/api/runtime/form/executeCustomButton {bizButtonModel, bizObjectList:[<full bizObject>], reacquireDataFromDb:true} — NOT the list-button launcher path with ids[]",
|
|
18
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",
|
|
19
20
|
});
|
|
20
|
-
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`
|
|
21
|
-
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). **表单控件属性(含关联填充 mappings)从字段行派生**:改关联映射的事实源在字段 options.mappings(field.update → field.publish),直接改这里的控件映射会被平台按字段重算覆盖(真机 2026-10-14 实证);字段发布后如需刷新已发布表单的控件映射,用本工具 rebuild:false 重存(保留现有布局与在线JS)再 form.publish。";
|
|
22
|
-
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.";
|
|
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). **读回原样回写**:从 `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
|
+
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
|
+
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` = 数据被占用。";
|
|
23
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).";
|
|
24
26
|
export const FORM_LANE_CONTRACT = Object.freeze({
|
|
25
27
|
lane: LANES.form,
|
|
@@ -42,6 +44,7 @@ export const FORM_LANE_CONTRACT = Object.freeze({
|
|
|
42
44
|
forceRepublish: FORM_WIRE.forceRepublish,
|
|
43
45
|
emptyLayoutGuard: FORM_WIRE.emptyLayoutGuard,
|
|
44
46
|
actionsColumns: FORM_WIRE.actionsColumns,
|
|
47
|
+
formButtons: FORM_WIRE.formButtons,
|
|
45
48
|
}),
|
|
46
49
|
wireBindings: Object.freeze({
|
|
47
50
|
draftBody: Object.freeze([behaviorBinding("test/form-scaffold.test.ts#form.draft scaffolds an empty default form from published fields", assertFormContract)]),
|
|
@@ -54,6 +57,7 @@ export const FORM_LANE_CONTRACT = Object.freeze({
|
|
|
54
57
|
forceRepublish: Object.freeze([behaviorBinding("test/issue-11-online-js.test.ts#onlinejs.compose merges customScript into the existing draftHtmlJson layout", assertFormContract)]),
|
|
55
58
|
emptyLayoutGuard: Object.freeze([behaviorBinding("test/ticket-74-form-empty-layout.test.ts#ticket 74: form.draft refuses an explicitly empty draftViewJson", assertFormContract)]),
|
|
56
59
|
actionsColumns: Object.freeze([behaviorBinding("test/ticket-100-form-actions-columns.test.ts#form.get full:true surfaces draftActionsJson/publishedActionsJson", assertFormContract)]),
|
|
60
|
+
formButtons: Object.freeze([behaviorBinding("test/form-buttons.test.ts#form.publish with buttons materialises FORM-trigger buttons and form.get surfaces them", assertFormContract)]),
|
|
57
61
|
}),
|
|
58
62
|
evidence: Object.freeze([
|
|
59
63
|
".scratch/omc-route-wayfinder/leg2-closeout.md",
|
|
@@ -65,6 +69,7 @@ export const FORM_LANE_CONTRACT = Object.freeze({
|
|
|
65
69
|
"form.draft": Object.freeze({ scope: "draft publish publishedHtmlJson readback", description: FORM_DRAFT_DESCRIPTION }),
|
|
66
70
|
"form.publish": Object.freeze({ scope: "draft publish publishedHtmlJson readback", description: FORM_PUBLISH_DESCRIPTION }),
|
|
67
71
|
configure_form: Object.freeze({ scope: "draft publish publishedHtmlJson readback", description: CONFIGURE_FORM_DESCRIPTION }),
|
|
72
|
+
"form.runButton": Object.freeze({ scope: "form toolbar button execution", description: FORM_RUN_BUTTON_DESCRIPTION }),
|
|
68
73
|
}),
|
|
69
74
|
});
|
|
70
75
|
export function assertFormContract() {
|
|
@@ -74,7 +79,7 @@ export function assertFormContract() {
|
|
|
74
79
|
assertDescriptionCarries(FORM_LANE_CONTRACT, "form.get", ["bizsheet/get", "draft/published", "mappings", "`full:true`", "actions"]);
|
|
75
80
|
if (!FORM_WIRE.actionsColumns.includes("BizForm.java:95-100"))
|
|
76
81
|
throw new Error("form contract must pin the actions-columns evidence (ticket 100)");
|
|
77
|
-
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"]);
|
|
78
83
|
if (!FORM_WIRE.redundantFieldVisibility.includes("propertyPermissions"))
|
|
79
84
|
throw new Error("form contract must carry the redundant-field visibility (propertyPermissions) fact");
|
|
80
85
|
assertDescriptionCarries(FORM_LANE_CONTRACT, "form.publish", ["bizsheet/publish", "draft*Json", "published*Json", "verbatim", "form-publish-degrade", "history/list", "rebuild:true", "force-promote"]);
|
|
@@ -93,5 +98,10 @@ export function assertFormContract() {
|
|
|
93
98
|
throw new Error("form contract must pin the force-republish override");
|
|
94
99
|
if (!FORM_WIRE.emptyLayoutGuard.includes("form-empty-layout") || !FORM_WIRE.emptyLayoutGuard.includes("form-publish-degrade"))
|
|
95
100
|
throw new Error("form contract must pin the empty-layout/degrade guards");
|
|
101
|
+
if (!FORM_WIRE.formButtons.includes("customButtonJson") || !FORM_WIRE.formButtons.includes("executeCustomButton") || !FORM_WIRE.formButtons.includes("publishBizForm:1022"))
|
|
102
|
+
throw new Error("form contract must pin the form-button three-layer mechanism");
|
|
103
|
+
assertDescriptionCarries(FORM_LANE_CONTRACT, "form.draft", ["buttons", "customButtonJson", "h_biz_button", "buttonPermission"]);
|
|
104
|
+
assertDescriptionCarries(FORM_LANE_CONTRACT, "form.publish", ["buttons", "customButtonJson", "triggerType=FORM", "buttonPermission"]);
|
|
105
|
+
assertDescriptionCarries(FORM_LANE_CONTRACT, "form.runButton", ["executeCustomButton", "bizObjectList", "getCustomButtons", "rule.run", "302045"]);
|
|
96
106
|
}
|
|
97
107
|
//# sourceMappingURL=form-contract.js.map
|