@stalfh233/omc-cli 0.6.6 → 0.6.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/README.md +2 -1
- package/dist/approval-flow.js +5 -3
- package/dist/bizservice-config.js +26 -10
- package/dist/call.js +47 -0
- package/dist/capability-overview.js +47 -24
- package/dist/cli-contract.js +90 -17
- package/dist/cli-output.js +41 -2
- package/dist/cli-stdout.js +74 -0
- package/dist/cli.js +162 -54
- package/dist/command-transport.js +8 -1
- package/dist/config-store.js +17 -2
- package/dist/discovery.js +62 -3
- package/dist/field-families.js +35 -6
- package/dist/goal-routes.js +118 -11
- package/dist/human-summary.js +11 -0
- package/dist/lanes/app-contract.js +12 -5
- package/dist/lanes/app.js +221 -134
- package/dist/lanes/apply-changes.js +2 -1
- package/dist/lanes/b2-registration-contract.js +3 -3
- package/dist/lanes/b2-registration.js +890 -693
- package/dist/lanes/businessrule.js +75 -51
- package/dist/lanes/custom-page.js +166 -115
- package/dist/lanes/datarule-contract.js +2 -2
- package/dist/lanes/datarule.js +238 -154
- package/dist/lanes/dictionary.js +253 -110
- package/dist/lanes/environment.js +57 -9
- package/dist/lanes/field.js +160 -107
- package/dist/lanes/folder-contract.js +7 -3
- package/dist/lanes/folder.js +249 -118
- package/dist/lanes/form-contract.js +1 -1
- package/dist/lanes/form.js +188 -64
- package/dist/lanes/list-view.js +405 -369
- package/dist/lanes/meta.js +31 -6
- package/dist/lanes/model-contract.js +1 -1
- package/dist/lanes/model.js +610 -512
- package/dist/lanes/online-js-contract.js +1 -1
- package/dist/lanes/online-js.js +160 -146
- package/dist/lanes/organization.js +905 -640
- package/dist/lanes/programming.js +274 -136
- package/dist/lanes/report-contract.js +2 -2
- package/dist/lanes/report.js +354 -198
- package/dist/lanes/rule-lifecycle.js +294 -140
- package/dist/lanes/runtime-data-contract.js +4 -4
- package/dist/lanes/runtime-data.js +869 -745
- package/dist/lanes/workflow-contract.js +3 -3
- package/dist/lanes/workflow.js +732 -605
- package/dist/model-scaffold.js +26 -5
- package/dist/path-projection.js +196 -14
- package/dist/plan.js +25 -0
- package/dist/programming-workflow.js +55 -3
- package/dist/read-shapes.js +146 -0
- package/dist/read.js +40 -9
- package/dist/recipes/businessrule.js +74 -24
- package/dist/recipes/dictionary.js +7 -4
- package/dist/recipes/form.js +46 -0
- package/dist/recipes/runtime-data.js +3 -2
- package/dist/refusal-guidance.js +29 -5
- package/dist/rule-support.js +14 -1
- package/dist/rule-when.js +2 -2
- package/dist/tool-types.js +9 -3
- package/dist/version-family-gaps.js +101 -0
- package/dist/write.js +2 -1
- package/package.json +6 -4
- package/skills/omc-glossary.md +13 -0
- package/skills/omc-refusals.md +13 -5
- package/skills/omc-start-here.md +2 -2
- package/skills/omc-tool-selection.md +2 -1
- package/skills/omc-write-hazards.md +5 -2
package/dist/lanes/list-view.js
CHANGED
|
@@ -289,10 +289,13 @@ export function registerListViewLane(tools) {
|
|
|
289
289
|
code,
|
|
290
290
|
present: true,
|
|
291
291
|
name: model.name ?? null,
|
|
292
|
-
// r18
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
published:
|
|
292
|
+
// r27 F5 / r18 G5: list views have no draft/publish lifecycle —
|
|
293
|
+
// save IS live. Previously `published:null` meant both "unpublished"
|
|
294
|
+
// and "N/A", so probes had to infer from `lifecycle`. Give an
|
|
295
|
+
// explicit published:true (save == effective; no separate publish
|
|
296
|
+
// step) plus effective:true so "does this view open?" is one field.
|
|
297
|
+
published: true, // 视图保存即生效,无需单独发布;不再用 null 兼表「未发布/不适用」
|
|
298
|
+
effective: true,
|
|
296
299
|
lifecycle: "always-live",
|
|
297
300
|
presentationType: model.queryPresentationType ?? null,
|
|
298
301
|
columnCount: columns.length,
|
|
@@ -445,381 +448,414 @@ export function registerListViewLane(tools) {
|
|
|
445
448
|
};
|
|
446
449
|
}),
|
|
447
450
|
}));
|
|
448
|
-
writeTool(
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
// the create — the platform's 303002 is absence of work, not failure.
|
|
468
|
-
if (!(error instanceof WireError && error.kind === "business" && error.errcode === 303002))
|
|
469
|
-
throw error;
|
|
470
|
-
const header = await wireCall(session, { path: ENDPOINTS.listQueryGetHeader, query: { schemaCode: args.schemaCode, code: args.code ?? "" } });
|
|
471
|
-
if (!header.data)
|
|
472
|
-
throw error;
|
|
473
|
-
return { performed: { skipped: "already-present" }, readback: { alreadyPresent: true, header: header.data } };
|
|
474
|
-
}
|
|
475
|
-
}, undefined, undefined, listviewRollbackSpec("listview.create"));
|
|
476
|
-
writeTool(tools, "listview.configure", LIST_VIEW_LANE, LIST_VIEW_TOOLS["listview.configure"].scope, LIST_VIEW_TOOLS["listview.configure"].description, {
|
|
477
|
-
schemaCode: z.string().min(1),
|
|
478
|
-
code: z.string().optional(),
|
|
479
|
-
fieldCodes: z.array(z.string()).min(1).describe("要展示的字段:可给 propertyCode 或字段显示名(唯一匹配时自动解析,歧义或不存在则拒绝,不静默跳过)"),
|
|
480
|
-
hideDefaultSystemFields: z.boolean().optional().describe("是否隐藏平台默认系统列(createdTime/creater/name/sequenceNo 等);平台 update 按提交列集合决定,不能靠 visible:false"),
|
|
481
|
-
replaceColumns: z.boolean().optional().describe("把 fieldCodes 当作列集合的权威全集:未列出的已有列(含系统列)全部移除,最终列 = fieldCodes(按给定顺序)。默认 false 为增量(保留已有列)。系统列与业务列一视同仁——想要 createdTime/sequenceNo 就把它写进 fieldCodes"),
|
|
482
|
-
sorts: z.array(listViewSortArg).optional().describe("querySorts; omitted leaves the existing sorts untouched, provided replaces them"),
|
|
483
|
-
conditions: z.array(listViewConditionArg).optional().describe("queryConditions; omitted leaves the existing conditions untouched, provided replaces them"),
|
|
484
|
-
actions: z.array(listViewActionArg).optional().describe("queryActions; omitted leaves the existing actions untouched, provided MERGES into the effective set by actionCode (existing platform defaults like 新增/删除/导入/导出 are preserved, a matching actionCode is updated). Use replaceActions:true for the authoritative full set"),
|
|
485
|
-
replaceActions: z.boolean().optional().describe("把 actions 当作权威全集:未列出的已有动作(含平台内置的 add/删除/导入/导出/修改拥有者)全部移除。默认 false 为合并(保留内置动作,按 actionCode 更新/追加)"),
|
|
486
|
-
defaultActions: z.boolean().optional().describe("先并入平台对 PC LIST 视图的默认动作集(add/delete/batch_update/batch_submit/import/export/batch_print/qr_code/editowner/import_attachment/export_attachment,常用于把被替换掉的内置按钮找回),再叠加 actions"),
|
|
487
|
-
presentation: listViewPresentationArg.optional(),
|
|
488
|
-
buttons: z
|
|
489
|
-
.array(listViewButtonArg)
|
|
490
|
-
.optional()
|
|
491
|
-
.describe("customButtons; sent as the customButtonJson JSON array string; omitted leaves the existing buttons untouched, provided replaces every button under the QUERY trigger (schema + child tables)"),
|
|
492
|
-
buttonScripts: z
|
|
493
|
-
.array(z.object({ code: z.string().min(1), script: z.string() }))
|
|
494
|
-
.optional()
|
|
495
|
-
.describe("直接改按钮脚本:按 code 把 actionConfig.scriptData 换成 script(不改其它按钮/字段)。内部读回现有按钮再回写,因此不必先 listview.get 再整表重发;与 buttons 互斥"),
|
|
496
|
-
options: listViewOptionsArg.optional(),
|
|
497
|
-
presentationType: presentationTypeArg.optional(),
|
|
498
|
-
gantt: listViewGanttArg.optional(),
|
|
499
|
-
queryLayer: listViewQueryLayerArg.optional(),
|
|
500
|
-
multiTableHeaderConfig: listViewMultiTableHeaderArg.optional(),
|
|
501
|
-
}, async (session, args) => {
|
|
502
|
-
const code = args.code ?? args.schemaCode;
|
|
503
|
-
const got = await wireCall(session, { path: ENDPOINTS.listQueryGet, query: { schemaCode: args.schemaCode, code, clientType: "PC" } });
|
|
504
|
-
const model = (got.data ?? {});
|
|
505
|
-
if (!model.id)
|
|
506
|
-
throw new WireError("business", `list view ${code} not found on ${args.schemaCode}`, 302025);
|
|
507
|
-
const columns = Array.isArray(model.queryColumns) ? ([...model.queryColumns]) : [];
|
|
508
|
-
const fieldRows = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "true" } })).data);
|
|
509
|
-
let sortKey = columns.reduce((max, c) => Math.max(max, Number(c.sortKey) || 0), 0);
|
|
510
|
-
const added = [];
|
|
511
|
-
const updated = [];
|
|
512
|
-
// A `fieldCodes` entry may be a propertyCode OR a display name (cpdocs
|
|
513
|
-
// base3.0 parity); an unresolvable reference is REFUSED, never silently
|
|
514
|
-
// skipped — a silently dropped column reported as success is the
|
|
515
|
-
// "declared a column, nothing appeared" trap.
|
|
516
|
-
const unknownRefs = args.fieldCodes.filter((ref) => resolveFieldRef(fieldRows, ref) === undefined);
|
|
517
|
-
if (unknownRefs.length > 0)
|
|
518
|
-
throw new ToolRefusalError({
|
|
519
|
-
status: "refused",
|
|
520
|
-
blocker: "listview-columns-unknown-field",
|
|
521
|
-
unknown: unknownRefs,
|
|
522
|
-
message: `fieldCodes 引用了模型 ${args.schemaCode} 发布面上不存在的字段(可给 propertyCode 或字段名):${unknownRefs.join("、")};先 field.create + field.publish,或修正引用`,
|
|
523
|
-
});
|
|
524
|
-
// replaceColumns: the caller's fieldCodes ARE the view. Start from an
|
|
525
|
-
// empty set so system columns fall away unless listed — a system field
|
|
526
|
-
// can still be shown by naming its code in fieldCodes (createdTime etc.
|
|
527
|
-
// are published fields too), so the caller controls the exact set.
|
|
528
|
-
if (args.replaceColumns === true)
|
|
529
|
-
columns.length = 0;
|
|
530
|
-
for (const ref of args.fieldCodes) {
|
|
531
|
-
const fieldCode = resolveFieldRef(fieldRows, ref).code;
|
|
532
|
-
const field = fieldRows.find((row) => (row.code ?? row.propertyCode) === fieldCode);
|
|
533
|
-
const type = Number(field.type ?? field.propertyType);
|
|
534
|
-
const displayFormat = columnDisplayFormat(type, field.options);
|
|
535
|
-
const existing = columns.find((c) => c.propertyCode === fieldCode);
|
|
536
|
-
if (existing) {
|
|
537
|
-
existing.name = field.name;
|
|
538
|
-
existing.propertyType = type;
|
|
539
|
-
existing.displayFormat = displayFormat;
|
|
540
|
-
existing.visible = true;
|
|
541
|
-
updated.push(fieldCode);
|
|
542
|
-
continue;
|
|
451
|
+
writeTool({
|
|
452
|
+
tools,
|
|
453
|
+
name: "listview.create",
|
|
454
|
+
lane: LIST_VIEW_LANE,
|
|
455
|
+
scope: LIST_VIEW_TOOLS["listview.create"].scope,
|
|
456
|
+
description: LIST_VIEW_TOOLS["listview.create"].description,
|
|
457
|
+
inputSchema: { schemaCode: z.string().min(1), name: z.string().min(1), code: z.string().optional().describe("视图 code;省略时**默认取 schemaCode**(与 listview.get / listview.list / rollback 的默认视图口径一致)。平台侧 code 必填,本工具会替你填,不要再依赖不回执的猜测(r26 真机:不传直接被 errcode 10005「编码不能为空」拒绝,白跑一轮线上往返)") },
|
|
458
|
+
execute: async (session, args) => {
|
|
459
|
+
// r26 真机:`code` 在 schema 里可选,但三处读面/rollback 都用
|
|
460
|
+
// `args.code ?? schemaCode`,唯独 create 把 undefined 原样发给平台 →
|
|
461
|
+
// `errcode 10005 编码不能为空`,探针白跑一轮。默认视图 code 就是
|
|
462
|
+
// schemaCode(listview.list 的 hint 也这么写),这里对齐。
|
|
463
|
+
const code = args.code ?? args.schemaCode;
|
|
464
|
+
// Ticket 81: read the header FIRST — a same-code retry short-circuits
|
|
465
|
+
// with the existing view and sends ZERO create wires (the live
|
|
466
|
+
// post-timeout retry otherwise answers 303002 编码重复).
|
|
467
|
+
const existing = await wireCall(session, { path: ENDPOINTS.listQueryGetHeader, query: { schemaCode: args.schemaCode, code } });
|
|
468
|
+
if (existing.data != null) {
|
|
469
|
+
return { performed: { skipped: "already-present" }, readback: { alreadyPresent: true, header: existing.data } };
|
|
543
470
|
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
// the platform forces every submitted column visible=true
|
|
553
|
-
// (BizQueryController.reSetQueryColumns:1000-1027), so visible:false is
|
|
554
|
-
// not an option.
|
|
555
|
-
const configuredColumns = filterDefaultSystemColumns(columns, args.hideDefaultSystemFields === true);
|
|
556
|
-
const body = { ...model, queryColumns: configuredColumns };
|
|
557
|
-
if (args.presentation !== undefined)
|
|
558
|
-
body.queryPresentation = normalizePresentation(args.presentation);
|
|
559
|
-
if (args.sorts !== undefined)
|
|
560
|
-
body.querySorts = normalizeSorts(args.sorts, fieldRows, args.schemaCode);
|
|
561
|
-
// Keep the normalized requested conditions so the post-write readback can
|
|
562
|
-
// compare what was ASKED for against what the platform stored, rather than
|
|
563
|
-
// reporting a write-side `ok` that silently lost fields (dogfood R2).
|
|
564
|
-
let requestedConditionKeys;
|
|
565
|
-
let requestedConditionSpec;
|
|
566
|
-
if (args.conditions !== undefined) {
|
|
567
|
-
const normalizedConditions = normalizeConditions(args.conditions, fieldRows, args.schemaCode);
|
|
568
|
-
body.queryConditions = normalizedConditions;
|
|
569
|
-
requestedConditionKeys = normalizedConditions.map((row) => String(row.propertyCode ?? ""));
|
|
570
|
-
requestedConditionSpec = normalizedConditions.map((row) => ({ propertyCode: String(row.propertyCode ?? ""), filterType: row.filterType }));
|
|
571
|
-
}
|
|
572
|
-
// saveBizQueryDetail removes every action whose clientType matches the
|
|
573
|
-
// update and re-adds the submitted list, so an omitted `actions` must echo
|
|
574
|
-
// the read-face effective set (stored + platform-injected) rather than
|
|
575
|
-
// sending an empty list, which would delete the view's actions.
|
|
576
|
-
//
|
|
577
|
-
// Live 2026-09-17: because it is replace-then-re-add, passing ONE action
|
|
578
|
-
// used to delete the built-in 新增/删除/导入/导出 and the list rendered
|
|
579
|
-
// with no Add button. `actions` now MERGES into the effective set by
|
|
580
|
-
// actionCode; replaceActions:true restores the authoritative-set
|
|
581
|
-
// behaviour, and defaultActions:true re-seeds the platform's own default
|
|
582
|
-
// set (the repair path for a view that already lost its buttons).
|
|
583
|
-
if (args.actions === undefined) {
|
|
584
|
-
body.queryActions = readArray(model, "queryActions");
|
|
585
|
-
}
|
|
586
|
-
else {
|
|
587
|
-
const normalized = normalizeActions(args.actions, args.schemaCode);
|
|
588
|
-
if (args.replaceActions === true) {
|
|
589
|
-
body.queryActions = normalized;
|
|
471
|
+
try {
|
|
472
|
+
const performed = await wireCall(session, {
|
|
473
|
+
path: ENDPOINTS.listQueryCreate,
|
|
474
|
+
method: "POST",
|
|
475
|
+
body: { schemaCode: args.schemaCode, name: args.name, code, type: "LIST", showOnPc: false, published: false },
|
|
476
|
+
});
|
|
477
|
+
const readback = await wireCall(session, { path: ENDPOINTS.listQueryGetHeader, query: { schemaCode: args.schemaCode, code } });
|
|
478
|
+
return { performed: { errcode: performed.errcode }, readback: { header: readback.data } };
|
|
590
479
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
480
|
+
catch (error) {
|
|
481
|
+
// Race path: another writer created the view between the pre-check and
|
|
482
|
+
// the create — the platform's 303002 is absence of work, not failure.
|
|
483
|
+
if (!(error instanceof WireError && error.kind === "business" && error.errcode === 303002))
|
|
484
|
+
throw error;
|
|
485
|
+
const header = await wireCall(session, { path: ENDPOINTS.listQueryGetHeader, query: { schemaCode: args.schemaCode, code } });
|
|
486
|
+
if (!header.data)
|
|
487
|
+
throw error;
|
|
488
|
+
return { performed: { skipped: "already-present" }, readback: { alreadyPresent: true, header: header.data } };
|
|
596
489
|
}
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
490
|
+
},
|
|
491
|
+
rollback: listviewRollbackSpec("listview.create"),
|
|
492
|
+
});
|
|
493
|
+
writeTool({
|
|
494
|
+
tools,
|
|
495
|
+
name: "listview.configure",
|
|
496
|
+
lane: LIST_VIEW_LANE,
|
|
497
|
+
scope: LIST_VIEW_TOOLS["listview.configure"].scope,
|
|
498
|
+
description: LIST_VIEW_TOOLS["listview.configure"].description,
|
|
499
|
+
inputSchema: {
|
|
500
|
+
schemaCode: z.string().min(1),
|
|
501
|
+
code: z.string().optional(),
|
|
502
|
+
fieldCodes: z.array(z.string()).min(1).describe("要展示的字段:可给 propertyCode 或字段显示名(唯一匹配时自动解析,歧义或不存在则拒绝,不静默跳过)"),
|
|
503
|
+
hideDefaultSystemFields: z.boolean().optional().describe("是否隐藏平台默认系统列(createdTime/creater/name/sequenceNo 等);平台 update 按提交列集合决定,不能靠 visible:false"),
|
|
504
|
+
replaceColumns: z.boolean().optional().describe("把 fieldCodes 当作列集合的权威全集:未列出的已有列(含系统列)全部移除,最终列 = fieldCodes(按给定顺序)。默认 false 为增量(保留已有列)。系统列与业务列一视同仁——想要 createdTime/sequenceNo 就把它写进 fieldCodes"),
|
|
505
|
+
sorts: z.array(listViewSortArg).optional().describe("querySorts; omitted leaves the existing sorts untouched, provided replaces them"),
|
|
506
|
+
conditions: z.array(listViewConditionArg).optional().describe("queryConditions; omitted leaves the existing conditions untouched, provided replaces them"),
|
|
507
|
+
actions: z.array(listViewActionArg).optional().describe("queryActions; omitted leaves the existing actions untouched, provided MERGES into the effective set by actionCode (existing platform defaults like 新增/删除/导入/导出 are preserved, a matching actionCode is updated). Use replaceActions:true for the authoritative full set"),
|
|
508
|
+
replaceActions: z.boolean().optional().describe("把 actions 当作权威全集:未列出的已有动作(含平台内置的 add/删除/导入/导出/修改拥有者)全部移除。默认 false 为合并(保留内置动作,按 actionCode 更新/追加)"),
|
|
509
|
+
defaultActions: z.boolean().optional().describe("先并入平台对 PC LIST 视图的默认动作集(add/delete/batch_update/batch_submit/import/export/batch_print/qr_code/editowner/import_attachment/export_attachment,常用于把被替换掉的内置按钮找回),再叠加 actions"),
|
|
510
|
+
presentation: listViewPresentationArg.optional(),
|
|
511
|
+
buttons: z
|
|
512
|
+
.array(listViewButtonArg)
|
|
513
|
+
.optional()
|
|
514
|
+
.describe("customButtons; sent as the customButtonJson JSON array string; omitted leaves the existing buttons untouched, provided replaces every button under the QUERY trigger (schema + child tables)"),
|
|
515
|
+
buttonScripts: z
|
|
516
|
+
.array(z.object({ code: z.string().min(1), script: z.string() }))
|
|
517
|
+
.optional()
|
|
518
|
+
.describe("直接改按钮脚本:按 code 把 actionConfig.scriptData 换成 script(不改其它按钮/字段)。内部读回现有按钮再回写,因此不必先 listview.get 再整表重发;与 buttons 互斥"),
|
|
519
|
+
options: listViewOptionsArg.optional(),
|
|
520
|
+
presentationType: presentationTypeArg.optional(),
|
|
521
|
+
gantt: listViewGanttArg.optional(),
|
|
522
|
+
queryLayer: listViewQueryLayerArg.optional(),
|
|
523
|
+
multiTableHeaderConfig: listViewMultiTableHeaderArg.optional(),
|
|
524
|
+
},
|
|
525
|
+
execute: async (session, args) => {
|
|
526
|
+
const code = args.code ?? args.schemaCode;
|
|
527
|
+
const got = await wireCall(session, { path: ENDPOINTS.listQueryGet, query: { schemaCode: args.schemaCode, code, clientType: "PC" } });
|
|
528
|
+
const model = (got.data ?? {});
|
|
529
|
+
if (!model.id)
|
|
530
|
+
throw new WireError("business", `list view ${code} not found on ${args.schemaCode}`, 302025);
|
|
531
|
+
const columns = Array.isArray(model.queryColumns) ? ([...model.queryColumns]) : [];
|
|
532
|
+
const fieldRows = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "true" } })).data);
|
|
533
|
+
let sortKey = columns.reduce((max, c) => Math.max(max, Number(c.sortKey) || 0), 0);
|
|
534
|
+
const added = [];
|
|
535
|
+
const updated = [];
|
|
536
|
+
// A `fieldCodes` entry may be a propertyCode OR a display name (cpdocs
|
|
537
|
+
// base3.0 parity); an unresolvable reference is REFUSED, never silently
|
|
538
|
+
// skipped — a silently dropped column reported as success is the
|
|
539
|
+
// "declared a column, nothing appeared" trap.
|
|
540
|
+
const unknownRefs = args.fieldCodes.filter((ref) => resolveFieldRef(fieldRows, ref) === undefined);
|
|
541
|
+
if (unknownRefs.length > 0)
|
|
619
542
|
throw new ToolRefusalError({
|
|
620
543
|
status: "refused",
|
|
621
|
-
blocker: "listview-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
available: existing.map((button) => String(button.code ?? "")),
|
|
625
|
-
message: `视图 ${code} 没有按钮 ${missing.join("、")};可用:${existing.map((button) => String(button.code ?? "")).join("、")}`,
|
|
544
|
+
blocker: "listview-columns-unknown-field",
|
|
545
|
+
unknown: unknownRefs,
|
|
546
|
+
message: `fieldCodes 引用了模型 ${args.schemaCode} 发布面上不存在的字段(可给 propertyCode 或字段名):${unknownRefs.join("、")};先 field.create + field.publish,或修正引用`,
|
|
626
547
|
});
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
548
|
+
// replaceColumns: the caller's fieldCodes ARE the view. Start from an
|
|
549
|
+
// empty set so system columns fall away unless listed — a system field
|
|
550
|
+
// can still be shown by naming its code in fieldCodes (createdTime etc.
|
|
551
|
+
// are published fields too), so the caller controls the exact set.
|
|
552
|
+
if (args.replaceColumns === true)
|
|
553
|
+
columns.length = 0;
|
|
554
|
+
for (const ref of args.fieldCodes) {
|
|
555
|
+
const fieldCode = resolveFieldRef(fieldRows, ref).code;
|
|
556
|
+
const field = fieldRows.find((row) => (row.code ?? row.propertyCode) === fieldCode);
|
|
557
|
+
const type = Number(field.type ?? field.propertyType);
|
|
558
|
+
const displayFormat = columnDisplayFormat(type, field.options);
|
|
559
|
+
const existing = columns.find((c) => c.propertyCode === fieldCode);
|
|
560
|
+
if (existing) {
|
|
561
|
+
existing.name = field.name;
|
|
562
|
+
existing.propertyType = type;
|
|
563
|
+
existing.displayFormat = displayFormat;
|
|
564
|
+
existing.visible = true;
|
|
565
|
+
updated.push(fieldCode);
|
|
566
|
+
continue;
|
|
642
567
|
}
|
|
643
|
-
|
|
644
|
-
|
|
568
|
+
sortKey += 1;
|
|
569
|
+
columns.push({ name: field.name, propertyCode: fieldCode, propertyType: type, isDefaultProperty: false, displayFormat, width: "160", sortKey, sumType: null, relativeSchemaCode: null, childColumns: null, clientType: "PC", schemaAlias: "a_1", propertyAlias: fieldCode, syncDefaultFormat: false, displayFields: null, queryLevel: 1, options: null, visible: true, virtualField: false, enableEncrypt: false, autoFilled: false });
|
|
570
|
+
added.push(fieldCode);
|
|
571
|
+
}
|
|
572
|
+
// replaceColumns already reduced the set to fieldCodes; hideDefaultSystemFields
|
|
573
|
+
// additionally drops any system column that slipped in (an echoed system
|
|
574
|
+
// column in incremental mode, or one the caller listed only to name its
|
|
575
|
+
// display). Both act by OMITTING columns from the submitted PC collection:
|
|
576
|
+
// the platform forces every submitted column visible=true
|
|
577
|
+
// (BizQueryController.reSetQueryColumns:1000-1027), so visible:false is
|
|
578
|
+
// not an option.
|
|
579
|
+
const configuredColumns = filterDefaultSystemColumns(columns, args.hideDefaultSystemFields === true);
|
|
580
|
+
const body = { ...model, queryColumns: configuredColumns };
|
|
581
|
+
if (args.presentation !== undefined)
|
|
582
|
+
body.queryPresentation = normalizePresentation(args.presentation);
|
|
583
|
+
if (args.sorts !== undefined)
|
|
584
|
+
body.querySorts = normalizeSorts(args.sorts, fieldRows, args.schemaCode);
|
|
585
|
+
// Keep the normalized requested conditions so the post-write readback can
|
|
586
|
+
// compare what was ASKED for against what the platform stored, rather than
|
|
587
|
+
// reporting a write-side `ok` that silently lost fields (dogfood R2).
|
|
588
|
+
let requestedConditionKeys;
|
|
589
|
+
let requestedConditionSpec;
|
|
590
|
+
if (args.conditions !== undefined) {
|
|
591
|
+
const normalizedConditions = normalizeConditions(args.conditions, fieldRows, args.schemaCode);
|
|
592
|
+
body.queryConditions = normalizedConditions;
|
|
593
|
+
requestedConditionKeys = normalizedConditions.map((row) => String(row.propertyCode ?? ""));
|
|
594
|
+
requestedConditionSpec = normalizedConditions.map((row) => ({ propertyCode: String(row.propertyCode ?? ""), filterType: row.filterType }));
|
|
595
|
+
}
|
|
596
|
+
// saveBizQueryDetail removes every action whose clientType matches the
|
|
597
|
+
// update and re-adds the submitted list, so an omitted `actions` must echo
|
|
598
|
+
// the read-face effective set (stored + platform-injected) rather than
|
|
599
|
+
// sending an empty list, which would delete the view's actions.
|
|
600
|
+
//
|
|
601
|
+
// Live 2026-09-17: because it is replace-then-re-add, passing ONE action
|
|
602
|
+
// used to delete the built-in 新增/删除/导入/导出 and the list rendered
|
|
603
|
+
// with no Add button. `actions` now MERGES into the effective set by
|
|
604
|
+
// actionCode; replaceActions:true restores the authoritative-set
|
|
605
|
+
// behaviour, and defaultActions:true re-seeds the platform's own default
|
|
606
|
+
// set (the repair path for a view that already lost its buttons).
|
|
607
|
+
if (args.actions === undefined) {
|
|
608
|
+
body.queryActions = readArray(model, "queryActions");
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
const normalized = normalizeActions(args.actions, args.schemaCode);
|
|
612
|
+
if (args.replaceActions === true) {
|
|
613
|
+
body.queryActions = normalized;
|
|
645
614
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
body.options = encodeJsonColumn(args.options);
|
|
665
|
-
// QueryPresentationType is a header enum always assignable by
|
|
666
|
-
// updateBizQuery (:1955, not gated), so an omitted body key (null) would
|
|
667
|
-
// overwrite a stored type; `{...model}` echoes the read-face value. GANTT
|
|
668
|
-
// requires queryGanttModel (controller validateNotEmptyGantt :1121-1136).
|
|
669
|
-
if (args.presentationType !== undefined)
|
|
670
|
-
body.queryPresentationType = args.presentationType;
|
|
671
|
-
// saveBizQueryDetail only swaps queryGantt when non-null (:178-183), so an
|
|
672
|
-
// omitted gantt leaves the stored one untouched.
|
|
673
|
-
if (args.gantt !== undefined)
|
|
674
|
-
body.queryGanttModel = normalizeGantt(args.gantt, args.schemaCode);
|
|
675
|
-
// saveBizQueryDetail assigns queryLayer and multiTableHeaderConfig
|
|
676
|
-
// unconditionally (:184-186); the read face always carries them
|
|
677
|
-
// (BizQueryFacadeImpl:344-345), so an omitted arg relies on the
|
|
678
|
-
// `{...model}` echo while a provided value replaces it.
|
|
679
|
-
if (args.queryLayer !== undefined)
|
|
680
|
-
body.queryLayer = args.queryLayer;
|
|
681
|
-
if (args.multiTableHeaderConfig !== undefined) {
|
|
682
|
-
body.multiTableHeaderConfig = encodeJsonColumn(args.multiTableHeaderConfig);
|
|
683
|
-
}
|
|
684
|
-
const performed = await wireCall(session, { path: ENDPOINTS.listQueryUpdate, method: "POST", body });
|
|
685
|
-
// BUG-11 (live, docker+source verified): the controller's /query/update
|
|
686
|
-
// forces publish=TRUE and the engine's updateBizQuery SKIPS the entity
|
|
687
|
-
// update block once the STORED view is published
|
|
688
|
-
// (ModelManagementFacadeImpl:1998-2010), so a body-driven
|
|
689
|
-
// queryPresentationType is silently dropped for a published view (the
|
|
690
|
-
// default view is published). The designer switches view type through
|
|
691
|
-
// /query/update_header (a plain header update, no publish gate) — drive
|
|
692
|
-
// the same path whenever presentationType is provided.
|
|
693
|
-
if (args.presentationType !== undefined) {
|
|
694
|
-
const header = await wireCall(session, { path: ENDPOINTS.listQueryGetHeader, query: { schemaCode: args.schemaCode, code } });
|
|
695
|
-
const headerModel = (header.data ?? {});
|
|
696
|
-
await wireCall(session, {
|
|
697
|
-
path: ENDPOINTS.listQueryUpdateHeader,
|
|
698
|
-
method: "POST",
|
|
699
|
-
body: { ...headerModel, code, schemaCode: args.schemaCode, queryPresentationType: args.presentationType },
|
|
700
|
-
});
|
|
701
|
-
}
|
|
702
|
-
const after = await wireCall(session, { path: ENDPOINTS.listQueryGet, query: { schemaCode: args.schemaCode, code, clientType: "PC" } });
|
|
703
|
-
const afterModel = (after.data ?? {});
|
|
704
|
-
const afterCols = readArray(afterModel, "queryColumns").map((c) => ({
|
|
705
|
-
propertyCode: c.propertyCode,
|
|
706
|
-
propertyType: c.propertyType,
|
|
707
|
-
displayFormat: c.displayFormat ?? null,
|
|
708
|
-
visible: c.visible ?? null,
|
|
709
|
-
sumType: c.sumType ?? null,
|
|
710
|
-
}));
|
|
711
|
-
const sorts = readArray(afterModel, "querySorts")
|
|
712
|
-
.map(decodeSortRow)
|
|
713
|
-
.sort((a, b) => Number(a.sortKey ?? 0) - Number(b.sortKey ?? 0));
|
|
714
|
-
const conditions = readArray(afterModel, "queryConditions")
|
|
715
|
-
.map(decodeConditionRow)
|
|
716
|
-
.sort((a, b) => String(a.propertyCode ?? "").localeCompare(String(b.propertyCode ?? "")));
|
|
717
|
-
const readback = { code, added, updated, columns: afterCols, sorts, conditions };
|
|
718
|
-
// r35(2026-10-31 真机):视图固定条件读回正确,但 `data.list --queryCode <视图>`
|
|
719
|
-
// 不应用该过滤(4 条未过滤返回)——视图条件的运行时过滤语义未取证(门户 vs
|
|
720
|
-
// REST queryCode),coverage 已声明 "filtering … not probed"。前置声明省 3 轮排查。
|
|
721
|
-
if (conditions.length > 0)
|
|
722
|
-
readback.conditionRuntimeNote =
|
|
723
|
-
"视图条件的运行时过滤语义未取证:REST `data.list --queryCode <本视图>` 实测不应用固定条件(r35:读回 Gte/5000 正确、返回未过滤 4 条)。别用 queryCode 断言过滤生效;REST 侧过滤用 `data.list` 显式 `filter`。";
|
|
724
|
-
// Post-write semantic readback (dogfood R2): the platform MODELS a list
|
|
725
|
-
// condition as a filter CONTROL definition, so a requested condition whose
|
|
726
|
-
// propertyCode does not survive the round-trip was lost. Surface that as
|
|
727
|
-
// `conditionReadbackStatus` (not a business `ok`) so a caller never
|
|
728
|
-
// mistakes the success envelope for the filter having landed.
|
|
729
|
-
if (requestedConditionKeys !== undefined) {
|
|
730
|
-
const storedKeys = new Set(conditions.map((condition) => String(condition.propertyCode ?? "")));
|
|
731
|
-
const missingConditions = requestedConditionKeys.filter((key) => !storedKeys.has(key));
|
|
732
|
-
// Also verify the OPERATOR (filterType) survived — a propertyCode that
|
|
733
|
-
// lands with the WRONG comparison is just as wrong as one that is
|
|
734
|
-
// dropped (dogfood R2 follow-up).
|
|
735
|
-
const storedByCode = new Map(conditions.map((condition) => [String(condition.propertyCode ?? ""), condition]));
|
|
736
|
-
const filterTypeMismatches = (requestedConditionSpec ?? [])
|
|
737
|
-
.filter((spec) => storedByCode.has(spec.propertyCode))
|
|
738
|
-
.filter((spec) => String(storedByCode.get(spec.propertyCode)?.filterType ?? "") !== String(spec.filterType ?? ""))
|
|
739
|
-
.map((spec) => ({ propertyCode: spec.propertyCode, requested: spec.filterType ?? null, stored: storedByCode.get(spec.propertyCode)?.filterType ?? null }));
|
|
740
|
-
const equivalent = missingConditions.length === 0 && filterTypeMismatches.length === 0;
|
|
741
|
-
readback.conditionReadback = {
|
|
742
|
-
status: equivalent ? "equivalent" : "listview-condition-readback-mismatch",
|
|
743
|
-
requested: requestedConditionKeys,
|
|
744
|
-
stored: [...storedKeys],
|
|
745
|
-
// Operator-level summary so "operator + value" acceptance needs no
|
|
746
|
-
// `--full`/artifact fetch (dogfood R2 round17b).
|
|
747
|
-
operators: (requestedConditionSpec ?? []).map((spec) => ({
|
|
748
|
-
propertyCode: spec.propertyCode,
|
|
749
|
-
requested: spec.filterType ?? null,
|
|
750
|
-
stored: storedByCode.get(spec.propertyCode)?.filterType ?? null,
|
|
751
|
-
defaultValue: storedByCode.get(spec.propertyCode)?.defaultValue ?? null,
|
|
752
|
-
})),
|
|
753
|
-
...(missingConditions.length > 0 ? { missing: missingConditions } : {}),
|
|
754
|
-
...(filterTypeMismatches.length > 0 ? { filterTypeMismatches } : {}),
|
|
755
|
-
};
|
|
756
|
-
}
|
|
757
|
-
readback.columnsRenderCompat = columnsRenderCompat(readArray(afterModel, "queryColumns"));
|
|
758
|
-
if (args.actions !== undefined) {
|
|
759
|
-
readback.actions = readArray(afterModel, "queryActions")
|
|
760
|
-
.map(decodeActionRow)
|
|
761
|
-
.sort((a, b) => Number(a.sortKey ?? 0) - Number(b.sortKey ?? 0));
|
|
762
|
-
}
|
|
763
|
-
if (afterModel.queryPresentation && typeof afterModel.queryPresentation === "object") {
|
|
764
|
-
readback.presentation = decodePresentation(afterModel.queryPresentation);
|
|
765
|
-
}
|
|
766
|
-
if (Array.isArray(afterModel.customButtons)) {
|
|
767
|
-
const buttons = readArray(afterModel, "customButtons");
|
|
768
|
-
readback.buttons = buttons
|
|
769
|
-
.map(decodeButtonRow)
|
|
770
|
-
.sort((a, b) => Number(a.sortKey ?? 0) - Number(b.sortKey ?? 0));
|
|
615
|
+
else {
|
|
616
|
+
const seeded = args.defaultActions === true
|
|
617
|
+
? normalizeActions(PLATFORM_DEFAULT_LIST_ACTIONS.map((action) => ({ ...action })), args.schemaCode)
|
|
618
|
+
: [];
|
|
619
|
+
body.queryActions = mergeActions([...seeded, ...readArray(model, "queryActions")], normalized);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
// BizButtonFacadeImpl.addBizButtonList runs only when the update body
|
|
623
|
+
// carries a JSON array in customButtonJson; it then deletes every button
|
|
624
|
+
// under the QUERY trigger for the schema (+ its child tables) and re-adds
|
|
625
|
+
// the submitted list. An omitted `buttons` therefore leaves the stored
|
|
626
|
+
// buttons untouched (no customButtonJson key), while `buttons: []` clears.
|
|
627
|
+
// Ticket 75: high-level targetModel/targetForm/configureList params are
|
|
628
|
+
// expanded into the authoritative actionConfig shape from readback
|
|
629
|
+
// BEFORE normalizeButtons (targetCode/targetObjCode never emitted for
|
|
630
|
+
// ADD_DATA/FORM — measured ineffective, see dry-lint).
|
|
631
|
+
if (args.buttons !== undefined && args.buttonScripts !== undefined)
|
|
632
|
+
throw new ToolRefusalError({ status: "refused", blocker: "listview-buttons-ambiguous", message: "buttons 与 buttonScripts 互斥:整表重配用 buttons,只改脚本用 buttonScripts" });
|
|
771
633
|
if (args.buttonScripts !== undefined) {
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
634
|
+
// Patch ONE field of ONE button in place: read the stored rows, rewrite
|
|
635
|
+
// actionConfig.scriptData by code, and send the full list back. This is
|
|
636
|
+
// the write counterpart of `listview.get --buttons`.
|
|
637
|
+
const existing = readArray(model, "customButtons");
|
|
638
|
+
if (existing.length === 0)
|
|
639
|
+
throw new ToolRefusalError({ status: "refused", blocker: "listview-button-absent", schemaCode: args.schemaCode, message: `视图 ${code} 没有自定义按钮——先用 listview.configure 的 buttons 建按钮(bindAction:SCRIPT)` });
|
|
640
|
+
const byCode = new Map(existing.map((button) => [String(button.code ?? ""), button]));
|
|
641
|
+
const missing = args.buttonScripts.map((entry) => entry.code).filter((buttonCode) => !byCode.has(buttonCode));
|
|
642
|
+
if (missing.length > 0)
|
|
643
|
+
throw new ToolRefusalError({
|
|
644
|
+
status: "refused",
|
|
645
|
+
blocker: "listview-button-absent",
|
|
646
|
+
schemaCode: args.schemaCode,
|
|
647
|
+
buttonCodes: missing,
|
|
648
|
+
available: existing.map((button) => String(button.code ?? "")),
|
|
649
|
+
message: `视图 ${code} 没有按钮 ${missing.join("、")};可用:${existing.map((button) => String(button.code ?? "")).join("、")}`,
|
|
650
|
+
});
|
|
651
|
+
const patched = existing.map((button) => {
|
|
652
|
+
const entry = args.buttonScripts.find((candidate) => candidate.code === String(button.code ?? ""));
|
|
653
|
+
if (!entry)
|
|
654
|
+
return button;
|
|
655
|
+
const rawConfig = button.actionConfig;
|
|
656
|
+
let config = {};
|
|
657
|
+
if (typeof rawConfig === "string" && rawConfig.trim() !== "") {
|
|
658
|
+
try {
|
|
659
|
+
const parsed = JSON.parse(rawConfig);
|
|
660
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed))
|
|
661
|
+
config = parsed;
|
|
662
|
+
}
|
|
663
|
+
catch {
|
|
664
|
+
config = {};
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
else if (rawConfig !== null && typeof rawConfig === "object" && !Array.isArray(rawConfig)) {
|
|
668
|
+
config = { ...rawConfig };
|
|
669
|
+
}
|
|
670
|
+
return { ...button, actionConfig: encodeJsonColumn({ ...config, scriptData: entry.script }) };
|
|
671
|
+
});
|
|
672
|
+
body.customButtonJson = JSON.stringify(normalizeButtons(patched, args.schemaCode, code));
|
|
776
673
|
}
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
//
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
if (
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
674
|
+
else if (args.buttons !== undefined) {
|
|
675
|
+
const expandedButtons = await expandButtonActionConfigs(session, args.schemaCode, args.buttons);
|
|
676
|
+
body.customButtonJson = JSON.stringify(normalizeButtons(expandedButtons, args.schemaCode, code));
|
|
677
|
+
}
|
|
678
|
+
else {
|
|
679
|
+
delete body.customButtonJson;
|
|
680
|
+
}
|
|
681
|
+
// BizQueryModel.options is a single String column. BizQueryController.update
|
|
682
|
+
// sets publish=TRUE before updateBizQuery, so after the first configure the
|
|
683
|
+
// store's publish is TRUE and only the name/options-diff branch
|
|
684
|
+
// (ModelManagementFacadeImpl:1966-1973) persists options — an omitted body
|
|
685
|
+
// key (null) would overwrite a non-null stored value. `{...model}` already
|
|
686
|
+
// echoes the read-face options, so only a provided value replaces it.
|
|
687
|
+
if (args.options !== undefined)
|
|
688
|
+
body.options = encodeJsonColumn(args.options);
|
|
689
|
+
// QueryPresentationType is a header enum always assignable by
|
|
690
|
+
// updateBizQuery (:1955, not gated), so an omitted body key (null) would
|
|
691
|
+
// overwrite a stored type; `{...model}` echoes the read-face value. GANTT
|
|
692
|
+
// requires queryGanttModel (controller validateNotEmptyGantt :1121-1136).
|
|
693
|
+
if (args.presentationType !== undefined)
|
|
694
|
+
body.queryPresentationType = args.presentationType;
|
|
695
|
+
// saveBizQueryDetail only swaps queryGantt when non-null (:178-183), so an
|
|
696
|
+
// omitted gantt leaves the stored one untouched.
|
|
697
|
+
if (args.gantt !== undefined)
|
|
698
|
+
body.queryGanttModel = normalizeGantt(args.gantt, args.schemaCode);
|
|
699
|
+
// saveBizQueryDetail assigns queryLayer and multiTableHeaderConfig
|
|
700
|
+
// unconditionally (:184-186); the read face always carries them
|
|
701
|
+
// (BizQueryFacadeImpl:344-345), so an omitted arg relies on the
|
|
702
|
+
// `{...model}` echo while a provided value replaces it.
|
|
703
|
+
if (args.queryLayer !== undefined)
|
|
704
|
+
body.queryLayer = args.queryLayer;
|
|
705
|
+
if (args.multiTableHeaderConfig !== undefined) {
|
|
706
|
+
body.multiTableHeaderConfig = encodeJsonColumn(args.multiTableHeaderConfig);
|
|
707
|
+
}
|
|
708
|
+
const performed = await wireCall(session, { path: ENDPOINTS.listQueryUpdate, method: "POST", body });
|
|
709
|
+
// BUG-11 (live, docker+source verified): the controller's /query/update
|
|
710
|
+
// forces publish=TRUE and the engine's updateBizQuery SKIPS the entity
|
|
711
|
+
// update block once the STORED view is published
|
|
712
|
+
// (ModelManagementFacadeImpl:1998-2010), so a body-driven
|
|
713
|
+
// queryPresentationType is silently dropped for a published view (the
|
|
714
|
+
// default view is published). The designer switches view type through
|
|
715
|
+
// /query/update_header (a plain header update, no publish gate) — drive
|
|
716
|
+
// the same path whenever presentationType is provided.
|
|
717
|
+
if (args.presentationType !== undefined) {
|
|
718
|
+
const header = await wireCall(session, { path: ENDPOINTS.listQueryGetHeader, query: { schemaCode: args.schemaCode, code } });
|
|
719
|
+
const headerModel = (header.data ?? {});
|
|
720
|
+
await wireCall(session, {
|
|
721
|
+
path: ENDPOINTS.listQueryUpdateHeader,
|
|
722
|
+
method: "POST",
|
|
723
|
+
body: { ...headerModel, code, schemaCode: args.schemaCode, queryPresentationType: args.presentationType },
|
|
818
724
|
});
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
725
|
+
}
|
|
726
|
+
const after = await wireCall(session, { path: ENDPOINTS.listQueryGet, query: { schemaCode: args.schemaCode, code, clientType: "PC" } });
|
|
727
|
+
const afterModel = (after.data ?? {});
|
|
728
|
+
const afterCols = readArray(afterModel, "queryColumns").map((c) => ({
|
|
729
|
+
propertyCode: c.propertyCode,
|
|
730
|
+
propertyType: c.propertyType,
|
|
731
|
+
displayFormat: c.displayFormat ?? null,
|
|
732
|
+
visible: c.visible ?? null,
|
|
733
|
+
sumType: c.sumType ?? null,
|
|
734
|
+
}));
|
|
735
|
+
const sorts = readArray(afterModel, "querySorts")
|
|
736
|
+
.map(decodeSortRow)
|
|
737
|
+
.sort((a, b) => Number(a.sortKey ?? 0) - Number(b.sortKey ?? 0));
|
|
738
|
+
const conditions = readArray(afterModel, "queryConditions")
|
|
739
|
+
.map(decodeConditionRow)
|
|
740
|
+
.sort((a, b) => String(a.propertyCode ?? "").localeCompare(String(b.propertyCode ?? "")));
|
|
741
|
+
const readback = { code, added, updated, columns: afterCols, sorts, conditions };
|
|
742
|
+
// r35(2026-10-31 真机):视图固定条件读回正确,但 `data.list --queryCode <视图>`
|
|
743
|
+
// 不应用该过滤(4 条未过滤返回)——视图条件的运行时过滤语义未取证(门户 vs
|
|
744
|
+
// REST queryCode),coverage 已声明 "filtering … not probed"。前置声明省 3 轮排查。
|
|
745
|
+
if (conditions.length > 0)
|
|
746
|
+
readback.conditionRuntimeNote =
|
|
747
|
+
"视图条件的运行时过滤语义未取证:REST `data.list --queryCode <本视图>` 实测不应用固定条件(r35:读回 Gte/5000 正确、返回未过滤 4 条)。别用 queryCode 断言过滤生效;REST 侧过滤用 `data.list` 显式 `filter`。";
|
|
748
|
+
// Post-write semantic readback (dogfood R2): the platform MODELS a list
|
|
749
|
+
// condition as a filter CONTROL definition, so a requested condition whose
|
|
750
|
+
// propertyCode does not survive the round-trip was lost. Surface that as
|
|
751
|
+
// `conditionReadbackStatus` (not a business `ok`) so a caller never
|
|
752
|
+
// mistakes the success envelope for the filter having landed.
|
|
753
|
+
if (requestedConditionKeys !== undefined) {
|
|
754
|
+
const storedKeys = new Set(conditions.map((condition) => String(condition.propertyCode ?? "")));
|
|
755
|
+
const missingConditions = requestedConditionKeys.filter((key) => !storedKeys.has(key));
|
|
756
|
+
// Also verify the OPERATOR (filterType) survived — a propertyCode that
|
|
757
|
+
// lands with the WRONG comparison is just as wrong as one that is
|
|
758
|
+
// dropped (dogfood R2 follow-up).
|
|
759
|
+
const storedByCode = new Map(conditions.map((condition) => [String(condition.propertyCode ?? ""), condition]));
|
|
760
|
+
const filterTypeMismatches = (requestedConditionSpec ?? [])
|
|
761
|
+
.filter((spec) => storedByCode.has(spec.propertyCode))
|
|
762
|
+
.filter((spec) => String(storedByCode.get(spec.propertyCode)?.filterType ?? "") !== String(spec.filterType ?? ""))
|
|
763
|
+
.map((spec) => ({ propertyCode: spec.propertyCode, requested: spec.filterType ?? null, stored: storedByCode.get(spec.propertyCode)?.filterType ?? null }));
|
|
764
|
+
const equivalent = missingConditions.length === 0 && filterTypeMismatches.length === 0;
|
|
765
|
+
readback.conditionReadback = {
|
|
766
|
+
status: equivalent ? "equivalent" : "listview-condition-readback-mismatch",
|
|
767
|
+
requested: requestedConditionKeys,
|
|
768
|
+
stored: [...storedKeys],
|
|
769
|
+
// Operator-level summary so "operator + value" acceptance needs no
|
|
770
|
+
// `--full`/artifact fetch (dogfood R2 round17b).
|
|
771
|
+
operators: (requestedConditionSpec ?? []).map((spec) => ({
|
|
772
|
+
propertyCode: spec.propertyCode,
|
|
773
|
+
requested: spec.filterType ?? null,
|
|
774
|
+
stored: storedByCode.get(spec.propertyCode)?.filterType ?? null,
|
|
775
|
+
defaultValue: storedByCode.get(spec.propertyCode)?.defaultValue ?? null,
|
|
776
|
+
})),
|
|
777
|
+
...(missingConditions.length > 0 ? { missing: missingConditions } : {}),
|
|
778
|
+
...(filterTypeMismatches.length > 0 ? { filterTypeMismatches } : {}),
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
readback.columnsRenderCompat = columnsRenderCompat(readArray(afterModel, "queryColumns"));
|
|
782
|
+
if (args.actions !== undefined) {
|
|
783
|
+
readback.actions = readArray(afterModel, "queryActions")
|
|
784
|
+
.map(decodeActionRow)
|
|
785
|
+
.sort((a, b) => Number(a.sortKey ?? 0) - Number(b.sortKey ?? 0));
|
|
786
|
+
}
|
|
787
|
+
if (afterModel.queryPresentation && typeof afterModel.queryPresentation === "object") {
|
|
788
|
+
readback.presentation = decodePresentation(afterModel.queryPresentation);
|
|
789
|
+
}
|
|
790
|
+
if (Array.isArray(afterModel.customButtons)) {
|
|
791
|
+
const buttons = readArray(afterModel, "customButtons");
|
|
792
|
+
readback.buttons = buttons
|
|
793
|
+
.map(decodeButtonRow)
|
|
794
|
+
.sort((a, b) => Number(a.sortKey ?? 0) - Number(b.sortKey ?? 0));
|
|
795
|
+
if (args.buttonScripts !== undefined) {
|
|
796
|
+
const decoded = buttons.map((button) => decodeButtonScript(button));
|
|
797
|
+
readback.buttonScripts = decoded
|
|
798
|
+
.filter((button) => (args.buttonScripts ?? []).some((entry) => entry.code === button.code))
|
|
799
|
+
.map((button) => ({ code: button.code, bindAction: button.bindAction, scriptBytes: button.scriptBytes ?? 0, scriptSha256: button.scriptSha256 ?? null, scriptData: button.scriptData ?? null }));
|
|
800
|
+
}
|
|
801
|
+
readback.buttonsRenderCompat = buttonsRenderCompat(buttons);
|
|
802
|
+
}
|
|
803
|
+
if (afterModel.options !== undefined && afterModel.options !== null) {
|
|
804
|
+
readback.options = parseConditionOptions(afterModel.options);
|
|
805
|
+
}
|
|
806
|
+
if (afterModel.queryPresentationType !== undefined && afterModel.queryPresentationType !== null) {
|
|
807
|
+
readback.presentationType = afterModel.queryPresentationType;
|
|
808
|
+
}
|
|
809
|
+
if (afterModel.queryGanttModel && typeof afterModel.queryGanttModel === "object") {
|
|
810
|
+
readback.gantt = decodeGantt(afterModel.queryGanttModel);
|
|
811
|
+
}
|
|
812
|
+
if (afterModel.queryLayer !== undefined && afterModel.queryLayer !== null) {
|
|
813
|
+
readback.queryLayer = afterModel.queryLayer;
|
|
814
|
+
}
|
|
815
|
+
if (afterModel.multiTableHeaderConfig !== undefined && afterModel.multiTableHeaderConfig !== null && afterModel.multiTableHeaderConfig !== "") {
|
|
816
|
+
readback.multiTableHeaderConfig = parseConditionOptions(afterModel.multiTableHeaderConfig);
|
|
817
|
+
}
|
|
818
|
+
return { performed: { errcode: performed.errcode }, readback };
|
|
819
|
+
},
|
|
820
|
+
preflight: assertListViewConfigureShape,
|
|
821
|
+
rollback: listviewRollbackSpec("listview.configure"),
|
|
822
|
+
});
|
|
823
|
+
writeTool({
|
|
824
|
+
tools,
|
|
825
|
+
name: "listview.delete",
|
|
826
|
+
lane: LIST_VIEW_LANE,
|
|
827
|
+
scope: LIST_VIEW_TOOLS["listview.delete"].scope,
|
|
828
|
+
description: LIST_VIEW_TOOLS["listview.delete"].description,
|
|
829
|
+
inputSchema: { schemaCode: z.string().min(1), code: z.string().min(1) },
|
|
830
|
+
execute: async (session, args) => {
|
|
831
|
+
const before = await wireCall(session, { path: ENDPOINTS.listQueryGetHeader, query: { schemaCode: args.schemaCode, code: args.code } });
|
|
832
|
+
if (before.data == null)
|
|
833
|
+
return { performed: { skipped: "already-absent" }, readback: { absent: true, header: null } };
|
|
834
|
+
let performed;
|
|
835
|
+
try {
|
|
836
|
+
performed = await wireCall(session, { path: ENDPOINTS.listQueryDelete, method: "POST", query: { schemaCode: args.schemaCode, code: args.code } });
|
|
837
|
+
}
|
|
838
|
+
catch (error) {
|
|
839
|
+
// 306103 删除后将无可用视图 — the platform requires at least one visible
|
|
840
|
+
// list view per model. r15's `nextCommand` fell back to the generic
|
|
841
|
+
// refusal router; name the constraint and the only real deletion path.
|
|
842
|
+
if (error instanceof WireError && error.kind === "business" && (error.errcode === 306103 || /无可用视图/.test(error.message)))
|
|
843
|
+
throw new ToolRefusalError({
|
|
844
|
+
status: "refused",
|
|
845
|
+
blocker: "listview-last-visible",
|
|
846
|
+
schemaCode: args.schemaCode,
|
|
847
|
+
code: args.code,
|
|
848
|
+
nextCommand: `omc model-purge --code ${args.schemaCode}`,
|
|
849
|
+
message: `不能删除视图 ${args.code}:平台要求每个模型至少保留一条可见列表视图(306103)。` +
|
|
850
|
+
`单独删最后一条视图没有意义——视图会随模型一起级联删除。要继续用这个模型就**就地改**(listview.configure 省略 code 即改默认视图),` +
|
|
851
|
+
`要整体清理就用 model-purge(默认 dry-run,先看计划)。`,
|
|
852
|
+
});
|
|
853
|
+
throw error;
|
|
854
|
+
}
|
|
855
|
+
const readback = await wireCall(session, { path: ENDPOINTS.listQueryGetHeader, query: { schemaCode: args.schemaCode, code: args.code } });
|
|
856
|
+
return { performed: { errcode: performed.errcode }, readback: { absent: readback.data == null, header: readback.data ?? null } };
|
|
857
|
+
},
|
|
858
|
+
rollback: listviewRollbackSpec("listview.delete"),
|
|
859
|
+
});
|
|
824
860
|
}
|
|
825
861
|
//# sourceMappingURL=list-view.js.map
|