@syntax-syllogism/flow-delta 0.2.1 → 0.4.3
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 +18 -3
- package/dist/cli.js +733 -317
- package/dist/github-report.js +342 -0
- package/dist/gitlab-report.js +122 -40
- package/examples/cloudflare-worker/src/index.ts +63 -0
- package/examples/cloudflare-worker/wrangler.toml +8 -0
- package/examples/github-actions.yml +56 -0
- package/examples/gitlab-ci.yml +23 -0
- package/package.json +8 -4
- package/scripts/r2-publish.mjs +132 -0
package/dist/cli.js
CHANGED
|
@@ -628,6 +628,38 @@ function isPlainObject(value) {
|
|
|
628
628
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
629
629
|
}
|
|
630
630
|
|
|
631
|
+
// src/model/flow-header.ts
|
|
632
|
+
import { Parser as Parser2 } from "xml2js";
|
|
633
|
+
var FLOW_HEADER_KEYS = [
|
|
634
|
+
"status",
|
|
635
|
+
"processType",
|
|
636
|
+
"runInMode",
|
|
637
|
+
"apiVersion",
|
|
638
|
+
"triggerOrder",
|
|
639
|
+
"description",
|
|
640
|
+
"interviewLabel",
|
|
641
|
+
"isTemplate"
|
|
642
|
+
];
|
|
643
|
+
async function extractFlowHeader(xml) {
|
|
644
|
+
try {
|
|
645
|
+
const parsed = await new Parser2({ explicitArray: false }).parseStringPromise(xml);
|
|
646
|
+
const flow = parsed.Flow;
|
|
647
|
+
if (!flow) {
|
|
648
|
+
return {};
|
|
649
|
+
}
|
|
650
|
+
const header = {};
|
|
651
|
+
for (const key of FLOW_HEADER_KEYS) {
|
|
652
|
+
const value = flow[key];
|
|
653
|
+
if (value !== void 0 && (value === null || typeof value !== "object")) {
|
|
654
|
+
header[key] = String(value);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return header;
|
|
658
|
+
} catch {
|
|
659
|
+
return {};
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
631
663
|
// src/diff/deep-diff.ts
|
|
632
664
|
function deepDiff(before, after, path = "") {
|
|
633
665
|
if (Object.is(before, after)) {
|
|
@@ -685,21 +717,32 @@ function diffModel(oldModel, newModel) {
|
|
|
685
717
|
const edgeIds = [.../* @__PURE__ */ new Set([...oldEdges.keys(), ...newEdges.keys()])].sort();
|
|
686
718
|
const nodes = nodeIds.map((id) => classifyNode(oldNodes.get(id), newNodes.get(id)));
|
|
687
719
|
const edges = edgeIds.map((id) => classifyEdge(oldEdges.get(id), newEdges.get(id)));
|
|
720
|
+
const flowChanges = diffFlowHeaders(oldModel, newModel);
|
|
688
721
|
const summary = {
|
|
689
722
|
addedNodes: nodes.filter((node) => node.status === "added").length,
|
|
690
723
|
removedNodes: nodes.filter((node) => node.status === "deleted").length,
|
|
691
724
|
modifiedNodes: nodes.filter((node) => node.status === "modified").length,
|
|
692
725
|
unchangedNodes: nodes.filter((node) => node.status === "unchanged").length,
|
|
693
726
|
addedEdges: edges.filter((edge) => edge.status === "added").length,
|
|
694
|
-
removedEdges: edges.filter((edge) => edge.status === "deleted").length
|
|
727
|
+
removedEdges: edges.filter((edge) => edge.status === "deleted").length,
|
|
728
|
+
changedFlowAttributes: flowChanges.length
|
|
695
729
|
};
|
|
696
730
|
return {
|
|
697
731
|
flowName: selectFlowName(oldModel, newModel),
|
|
698
732
|
summary,
|
|
733
|
+
...flowChanges.length > 0 ? { flowChanges } : {},
|
|
699
734
|
nodes,
|
|
700
735
|
edges
|
|
701
736
|
};
|
|
702
737
|
}
|
|
738
|
+
function diffFlowHeaders(oldModel, newModel) {
|
|
739
|
+
if (!oldModel.header || !newModel.header) {
|
|
740
|
+
return [];
|
|
741
|
+
}
|
|
742
|
+
const changes = deepDiff(oldModel.header, newModel.header);
|
|
743
|
+
const order = new Map(FLOW_HEADER_KEYS.map((key, index) => [key, index]));
|
|
744
|
+
return changes.sort((left, right) => (order.get(left.path) ?? 999) - (order.get(right.path) ?? 999));
|
|
745
|
+
}
|
|
703
746
|
function selectFlowName(oldModel, newModel) {
|
|
704
747
|
if (newModel.nodes.length > 0 || newModel.edges.length > 0) {
|
|
705
748
|
return newModel.flowName;
|
|
@@ -714,10 +757,10 @@ function classifyNode(oldNode, newNode) {
|
|
|
714
757
|
throw new Error("Cannot classify absent node");
|
|
715
758
|
}
|
|
716
759
|
if (!oldNode) {
|
|
717
|
-
return { id: newNode.id, type: newNode.type, label: newNode.label, status: "added" };
|
|
760
|
+
return { id: newNode.id, type: newNode.type, label: newNode.label, status: "added", after: newNode.properties };
|
|
718
761
|
}
|
|
719
762
|
if (!newNode) {
|
|
720
|
-
return { id: oldNode.id, type: oldNode.type, label: oldNode.label, status: "deleted" };
|
|
763
|
+
return { id: oldNode.id, type: oldNode.type, label: oldNode.label, status: "deleted", before: oldNode.properties };
|
|
721
764
|
}
|
|
722
765
|
const changes = [
|
|
723
766
|
...deepDiff(oldNode.label, newNode.label, "label"),
|
|
@@ -880,7 +923,37 @@ var PARAM_COLUMNS = [
|
|
|
880
923
|
{ path: "value", label: "Value" },
|
|
881
924
|
{ path: "assignToReference", label: "Assign To" }
|
|
882
925
|
];
|
|
926
|
+
var SCREEN_SETTING_PATHS = [
|
|
927
|
+
"allowBack",
|
|
928
|
+
"allowFinish",
|
|
929
|
+
"allowPause",
|
|
930
|
+
"showFooter",
|
|
931
|
+
"showHeader"
|
|
932
|
+
];
|
|
883
933
|
var SECTION_SCHEMAS = {
|
|
934
|
+
screen: [
|
|
935
|
+
{ name: "Screen Settings", paths: SCREEN_SETTING_PATHS, render: "lines" }
|
|
936
|
+
],
|
|
937
|
+
start: [
|
|
938
|
+
{
|
|
939
|
+
name: "Trigger",
|
|
940
|
+
paths: ["object", "triggerType", "recordTriggerType", "schedule", "filterLogic"],
|
|
941
|
+
render: "lines"
|
|
942
|
+
},
|
|
943
|
+
{ name: "Entry Conditions", paths: ["filters"], render: "table", columns: FILTER_COLUMNS },
|
|
944
|
+
{
|
|
945
|
+
name: "Scheduled Paths",
|
|
946
|
+
paths: ["scheduledPaths"],
|
|
947
|
+
render: "table",
|
|
948
|
+
columns: [
|
|
949
|
+
{ path: "name", label: "Name" },
|
|
950
|
+
{ path: "label", label: "Label" },
|
|
951
|
+
{ path: "offsetNumber", label: "Offset" },
|
|
952
|
+
{ path: "offsetUnit", label: "Unit" },
|
|
953
|
+
{ path: "timeSource", label: "Time Source" }
|
|
954
|
+
]
|
|
955
|
+
}
|
|
956
|
+
],
|
|
884
957
|
decision: [{
|
|
885
958
|
name: "Outcomes",
|
|
886
959
|
paths: ["rules"],
|
|
@@ -907,6 +980,7 @@ var SECTION_SCHEMAS = {
|
|
|
907
980
|
recordUpdate: [{ name: "Field Mappings", paths: ["inputAssignments"], render: "table", columns: MAPPING_COLUMNS }],
|
|
908
981
|
recordLookup: [{ name: "Filters", paths: ["filters"], render: "table", columns: FILTER_COLUMNS }],
|
|
909
982
|
recordDelete: [{ name: "Filters", paths: ["filters"], render: "table", columns: FILTER_COLUMNS }],
|
|
983
|
+
subflow: [{ name: "Parameters", paths: ["inputAssignments", "outputAssignments"], render: "table", columns: PARAM_COLUMNS }],
|
|
910
984
|
actionCall: [{ name: "Parameters", paths: ["inputParameters", "outputParameters"], render: "table", columns: PARAM_COLUMNS }],
|
|
911
985
|
apexPluginCall: [{ name: "Parameters", paths: ["inputParameters", "outputParameters"], render: "table", columns: PARAM_COLUMNS }],
|
|
912
986
|
wait: [{
|
|
@@ -919,12 +993,571 @@ var SECTION_SCHEMAS = {
|
|
|
919
993
|
{ path: "offsetUnit", label: "Unit" },
|
|
920
994
|
{ path: "resumeDateReference", label: "Resume Date" }
|
|
921
995
|
]
|
|
922
|
-
}]
|
|
996
|
+
}],
|
|
997
|
+
customError: [{
|
|
998
|
+
name: "Error Messages",
|
|
999
|
+
paths: ["customErrorMessages"],
|
|
1000
|
+
render: "table",
|
|
1001
|
+
columns: [
|
|
1002
|
+
{ path: "name", label: "Name" },
|
|
1003
|
+
{ path: "errorMessage", label: "Message" }
|
|
1004
|
+
]
|
|
1005
|
+
}],
|
|
1006
|
+
transform: [
|
|
1007
|
+
{
|
|
1008
|
+
name: "Value Mappings",
|
|
1009
|
+
paths: ["transformValues", "valueMappings"],
|
|
1010
|
+
render: "table",
|
|
1011
|
+
columns: [
|
|
1012
|
+
{ path: "name", label: "Name" },
|
|
1013
|
+
{ path: "sourceDataType", label: "Source Type" },
|
|
1014
|
+
{ path: "targetDataType", label: "Target Type" },
|
|
1015
|
+
{ path: "value", label: "Value" }
|
|
1016
|
+
]
|
|
1017
|
+
},
|
|
1018
|
+
{
|
|
1019
|
+
name: "Data Type Mappings",
|
|
1020
|
+
paths: ["dataTypeMappings"],
|
|
1021
|
+
render: "table",
|
|
1022
|
+
columns: [
|
|
1023
|
+
{ path: "name", label: "Name" },
|
|
1024
|
+
{ path: "sourceDataType", label: "Source Type" },
|
|
1025
|
+
{ path: "targetDataType", label: "Target Type" }
|
|
1026
|
+
]
|
|
1027
|
+
}
|
|
1028
|
+
],
|
|
1029
|
+
collectionProcessor: [
|
|
1030
|
+
{ name: "Collection Settings", paths: ["collectionReference", "operation"], render: "lines" },
|
|
1031
|
+
{ name: "Filters", paths: ["filters"], render: "table", columns: FILTER_COLUMNS },
|
|
1032
|
+
{
|
|
1033
|
+
name: "Sort Options",
|
|
1034
|
+
paths: ["sortOptions"],
|
|
1035
|
+
render: "table",
|
|
1036
|
+
columns: [
|
|
1037
|
+
{ path: "field", label: "Field" },
|
|
1038
|
+
{ path: "sortOrder", label: "Order" }
|
|
1039
|
+
]
|
|
1040
|
+
}
|
|
1041
|
+
],
|
|
1042
|
+
orchestratedStage: [
|
|
1043
|
+
{ name: "Stage Settings", paths: ["stageLabel", "stageOrder"], render: "lines" }
|
|
1044
|
+
]
|
|
923
1045
|
};
|
|
924
1046
|
function getSectionSchemas(type) {
|
|
925
1047
|
return SECTION_SCHEMAS[type] ?? [];
|
|
926
1048
|
}
|
|
927
1049
|
|
|
1050
|
+
// src/render/snapshot-panel.ts
|
|
1051
|
+
var LONG_TEXT_THRESHOLD = 160;
|
|
1052
|
+
var COLLECTION_KEY_NAMES = [
|
|
1053
|
+
"assignmentItems",
|
|
1054
|
+
"choiceReferences",
|
|
1055
|
+
"choices",
|
|
1056
|
+
"conditions",
|
|
1057
|
+
"customErrorMessages",
|
|
1058
|
+
"dataTypeMappings",
|
|
1059
|
+
"fields",
|
|
1060
|
+
"filters",
|
|
1061
|
+
"inputAssignments",
|
|
1062
|
+
"inputParameters",
|
|
1063
|
+
"outputParameters",
|
|
1064
|
+
"outputAssignments",
|
|
1065
|
+
"processMetadataValues",
|
|
1066
|
+
"rules",
|
|
1067
|
+
"scheduledPaths",
|
|
1068
|
+
"sortOptions",
|
|
1069
|
+
"stageSteps",
|
|
1070
|
+
"steps",
|
|
1071
|
+
"storeOutputParameters",
|
|
1072
|
+
"transformValues",
|
|
1073
|
+
"valueMappings",
|
|
1074
|
+
"waitEvents"
|
|
1075
|
+
];
|
|
1076
|
+
var COLLECTION_KEYS = new Set(COLLECTION_KEY_NAMES);
|
|
1077
|
+
function renderNodePanelBody(node, schemas = []) {
|
|
1078
|
+
if (node.status === "unchanged") {
|
|
1079
|
+
return "<div class='empty'>No property changes.</div>";
|
|
1080
|
+
}
|
|
1081
|
+
const before = node.status === "added" ? void 0 : node.before;
|
|
1082
|
+
const after = node.status === "deleted" ? void 0 : node.after;
|
|
1083
|
+
const options = { showUnchanged: node.status !== "modified", nodeStatus: node.status };
|
|
1084
|
+
const sections = renderSections(before, after, schemas || [], options);
|
|
1085
|
+
if (!sections.length) {
|
|
1086
|
+
return "<div class='empty'>No configuration.</div>";
|
|
1087
|
+
}
|
|
1088
|
+
return sections.map((section, index) => {
|
|
1089
|
+
const open = node.status === "modified" || index === 0 ? " open" : "";
|
|
1090
|
+
return "<details class='detail-section snapshot-section " + node.status + "'" + open + "><summary>" + escapeHtml(section.name) + " <span class='section-count'>(" + section.count + ")</span></summary><div class='section-body'>" + section.html + "</div></details>";
|
|
1091
|
+
}).join("");
|
|
1092
|
+
}
|
|
1093
|
+
function renderSections(before, after, schemas, options) {
|
|
1094
|
+
const sections = [];
|
|
1095
|
+
const owned = /* @__PURE__ */ new Set();
|
|
1096
|
+
for (const schema of schemas) {
|
|
1097
|
+
for (const path of schema.paths) owned.add(path);
|
|
1098
|
+
const rendered = renderSchemaSection(before, after, schema, options);
|
|
1099
|
+
if (rendered) sections.push(rendered);
|
|
1100
|
+
}
|
|
1101
|
+
const keys = unionKeys(before, after).filter((key) => !ownsAny(owned, key));
|
|
1102
|
+
const settingsRows = [];
|
|
1103
|
+
let settingsCount = 0;
|
|
1104
|
+
const nestedSections = [];
|
|
1105
|
+
for (const key of keys) {
|
|
1106
|
+
const childBefore = getField(before, key);
|
|
1107
|
+
const childAfter = getField(after, key);
|
|
1108
|
+
const childChanged = hasChange(childBefore, childAfter, key);
|
|
1109
|
+
const scalar = scalarPair(childBefore, childAfter);
|
|
1110
|
+
if (scalar) {
|
|
1111
|
+
if (options.showUnchanged || childChanged) {
|
|
1112
|
+
settingsRows.push(scalarRow(key, scalar.before, scalar.after, statusOf(childBefore, childAfter), options.nodeStatus));
|
|
1113
|
+
}
|
|
1114
|
+
if (childChanged) settingsCount += 1;
|
|
1115
|
+
continue;
|
|
1116
|
+
}
|
|
1117
|
+
const rendered = renderValue(getField(before, key), getField(after, key), {
|
|
1118
|
+
...options,
|
|
1119
|
+
label: humanizePath(key),
|
|
1120
|
+
pathKey: key,
|
|
1121
|
+
depth: 0
|
|
1122
|
+
});
|
|
1123
|
+
if (rendered.html) {
|
|
1124
|
+
nestedSections.push({ name: humanizePath(key), html: rendered.html, count: rendered.count });
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
if (settingsRows.length) {
|
|
1128
|
+
sections.push({
|
|
1129
|
+
name: "Settings",
|
|
1130
|
+
html: "<table class='snapshot-kv'><tbody>" + settingsRows.join("") + "</tbody></table>",
|
|
1131
|
+
count: settingsCount
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
sections.push(...nestedSections);
|
|
1135
|
+
return sections;
|
|
1136
|
+
}
|
|
1137
|
+
function renderSchemaSection(before, after, schema, options) {
|
|
1138
|
+
let rendered;
|
|
1139
|
+
if (schema.render === "table" && schema.columns) {
|
|
1140
|
+
rendered = renderTable(before, after, schema.paths, schema.columns, options);
|
|
1141
|
+
} else if (schema.render === "grouped-table" && schema.innerArray && schema.innerColumns) {
|
|
1142
|
+
rendered = renderGroupedTable(before, after, schema, options);
|
|
1143
|
+
} else {
|
|
1144
|
+
const sectionBefore = pickSectionObject(before, schema.paths);
|
|
1145
|
+
const sectionAfter = pickSectionObject(after, schema.paths);
|
|
1146
|
+
rendered = renderValue(sectionBefore, sectionAfter, {
|
|
1147
|
+
...options,
|
|
1148
|
+
label: schema.name,
|
|
1149
|
+
pathKey: schema.paths[0] || schema.name,
|
|
1150
|
+
depth: 0
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
if (!rendered.html) return null;
|
|
1154
|
+
return { name: schema.name, html: rendered.html, count: rendered.count };
|
|
1155
|
+
}
|
|
1156
|
+
function renderValue(before, after, ctx) {
|
|
1157
|
+
const changed = hasChange(before, after, ctx.pathKey);
|
|
1158
|
+
if (!ctx.showUnchanged && !changed) return { html: "", changed: false, count: 0 };
|
|
1159
|
+
const scalar = scalarPair(before, after);
|
|
1160
|
+
if (scalar) {
|
|
1161
|
+
return {
|
|
1162
|
+
html: renderLine(ctx.label, scalar.before, scalar.after, statusOf(before, after), ctx.nodeStatus),
|
|
1163
|
+
changed,
|
|
1164
|
+
count: changed ? 1 : 0
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
if (isCollection(before, after, ctx.pathKey)) {
|
|
1168
|
+
const rendered2 = renderArray(before, after, ctx);
|
|
1169
|
+
return { html: rendered2.html, changed, count: rendered2.count || (changed ? 1 : 0) };
|
|
1170
|
+
}
|
|
1171
|
+
const rendered = renderObject(before, after, ctx);
|
|
1172
|
+
return { html: rendered.html, changed, count: rendered.count || (changed ? 1 : 0) };
|
|
1173
|
+
}
|
|
1174
|
+
function renderObject(before, after, ctx) {
|
|
1175
|
+
const beforeObj = isObj(before) ? before : {};
|
|
1176
|
+
const afterObj = isObj(after) ? after : {};
|
|
1177
|
+
const keys = unionKeys(beforeObj, afterObj);
|
|
1178
|
+
const scalarRows = [];
|
|
1179
|
+
const nested = [];
|
|
1180
|
+
let count = 0;
|
|
1181
|
+
for (const key of keys) {
|
|
1182
|
+
const childBefore = getField(beforeObj, key);
|
|
1183
|
+
const childAfter = getField(afterObj, key);
|
|
1184
|
+
const childChanged = hasChange(childBefore, childAfter, key);
|
|
1185
|
+
const scalar = scalarPair(childBefore, childAfter);
|
|
1186
|
+
if (scalar) {
|
|
1187
|
+
if (ctx.showUnchanged || childChanged || ctx.depth > 0) {
|
|
1188
|
+
scalarRows.push(scalarRow(key, scalar.before, scalar.after, statusOf(childBefore, childAfter), ctx.nodeStatus));
|
|
1189
|
+
}
|
|
1190
|
+
if (childChanged) count += 1;
|
|
1191
|
+
continue;
|
|
1192
|
+
}
|
|
1193
|
+
const rendered = renderValue(childBefore, childAfter, {
|
|
1194
|
+
...ctx,
|
|
1195
|
+
label: humanizePath(key),
|
|
1196
|
+
pathKey: key,
|
|
1197
|
+
depth: ctx.depth + 1
|
|
1198
|
+
});
|
|
1199
|
+
if (rendered.html) {
|
|
1200
|
+
nested.push("<div class='snapshot-nested'><div class='snapshot-nested-title'>" + escapeHtml(humanizePath(key)) + "</div>" + rendered.html + "</div>");
|
|
1201
|
+
count += rendered.count;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
const table = scalarRows.length ? "<table class='snapshot-kv'><tbody>" + scalarRows.join("") + "</tbody></table>" : "";
|
|
1205
|
+
return { html: table + nested.join(""), changed: count > 0, count };
|
|
1206
|
+
}
|
|
1207
|
+
function renderArray(before, after, ctx) {
|
|
1208
|
+
const beforeItems = arr(before);
|
|
1209
|
+
const afterItems = arr(after);
|
|
1210
|
+
if (looksLikeNameValueArray(beforeItems, afterItems)) {
|
|
1211
|
+
return renderTableFromItems(before, after, [""], [
|
|
1212
|
+
{ path: "name", label: "Name" },
|
|
1213
|
+
{ path: "value", label: "Value" }
|
|
1214
|
+
], ctx);
|
|
1215
|
+
}
|
|
1216
|
+
const max = Math.max(beforeItems.length, afterItems.length);
|
|
1217
|
+
const cards = [];
|
|
1218
|
+
let count = 0;
|
|
1219
|
+
for (let i = 0; i < max; i += 1) {
|
|
1220
|
+
const childBefore = beforeItems[i];
|
|
1221
|
+
const childAfter = afterItems[i];
|
|
1222
|
+
const childChanged = hasChange(childBefore, childAfter, "");
|
|
1223
|
+
if (!ctx.showUnchanged && !childChanged) continue;
|
|
1224
|
+
const title = itemTitle(childBefore, childAfter, i);
|
|
1225
|
+
const rendered = renderValue(childBefore, childAfter, {
|
|
1226
|
+
...ctx,
|
|
1227
|
+
label: title,
|
|
1228
|
+
pathKey: "",
|
|
1229
|
+
depth: ctx.depth + 1
|
|
1230
|
+
});
|
|
1231
|
+
if (!rendered.html) continue;
|
|
1232
|
+
count += rendered.count || (childChanged ? 1 : 0);
|
|
1233
|
+
cards.push("<div class='snapshot-card " + statusOf(childBefore, childAfter) + "'><div class='snapshot-card-title'>" + escapeHtml(title) + "</div>" + rendered.html + "</div>");
|
|
1234
|
+
}
|
|
1235
|
+
return { html: cards.join(""), changed: count > 0, count };
|
|
1236
|
+
}
|
|
1237
|
+
function renderTable(before, after, paths, columns, options) {
|
|
1238
|
+
return renderTableFromItems(before, after, paths, columns, { ...options, label: "", pathKey: paths[0] || "", depth: 0 });
|
|
1239
|
+
}
|
|
1240
|
+
function renderTableFromItems(before, after, paths, columns, options) {
|
|
1241
|
+
const rows = [];
|
|
1242
|
+
for (const path of paths) {
|
|
1243
|
+
const beforeItems = arr(resolvePath(before, path));
|
|
1244
|
+
const afterItems = arr(resolvePath(after, path));
|
|
1245
|
+
const max = Math.max(beforeItems.length, afterItems.length);
|
|
1246
|
+
for (let i = 0; i < max; i += 1) {
|
|
1247
|
+
const itemBefore = beforeItems[i];
|
|
1248
|
+
const itemAfter = afterItems[i];
|
|
1249
|
+
if (!options.showUnchanged && !hasChange(itemBefore, itemAfter, path)) continue;
|
|
1250
|
+
const cells = {};
|
|
1251
|
+
for (const col of columns) {
|
|
1252
|
+
const cellBefore = resolvePath(itemBefore, col.path);
|
|
1253
|
+
const cellAfter = resolvePath(itemAfter, col.path);
|
|
1254
|
+
cells[col.path] = {
|
|
1255
|
+
before: unwrapValue(cellBefore),
|
|
1256
|
+
after: unwrapValue(cellAfter),
|
|
1257
|
+
changed: hasChange(cellBefore, cellAfter, col.path)
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
rows.push({ idx: i, kind: statusOf(itemBefore, itemAfter), cells });
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
if (!rows.length) return { html: "", changed: false, count: 0 };
|
|
1264
|
+
const showChange = options.nodeStatus === "modified" && !options.suppressChangeColumn;
|
|
1265
|
+
const head = "<tr><th class='row-idx'>#</th>" + columns.map((col) => "<th>" + escapeHtml(col.label) + "</th>").join("") + (showChange ? "<th>Change</th>" : "") + "</tr>";
|
|
1266
|
+
const body = rows.map((row) => renderRow(row, columns, showChange, options.nodeStatus)).join("");
|
|
1267
|
+
return {
|
|
1268
|
+
html: "<table class='change-table'><thead>" + head + "</thead><tbody>" + body + "</tbody></table>",
|
|
1269
|
+
changed: rows.some((row) => row.kind !== "unchanged" || Object.values(row.cells).some((cell) => cell.changed)),
|
|
1270
|
+
count: rows.filter((row) => row.kind !== "unchanged" || Object.values(row.cells).some((cell) => cell.changed)).length
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
function renderRow(row, columns, showChange, nodeStatus) {
|
|
1274
|
+
const cells = columns.map((col) => {
|
|
1275
|
+
const cell = row.cells[col.path];
|
|
1276
|
+
return "<td>" + renderValueCell(cell.before, cell.after, cell.changed ? row.kind : "unchanged", nodeStatus) + "</td>";
|
|
1277
|
+
}).join("");
|
|
1278
|
+
const badge = showChange ? "<td><span class='change-kind " + row.kind + "'>" + capitalize(row.kind) + "</span></td>" : "";
|
|
1279
|
+
return "<tr class='row-" + row.kind + "'><td class='row-idx'>" + (row.idx + 1) + "</td>" + cells + badge + "</tr>";
|
|
1280
|
+
}
|
|
1281
|
+
function renderGroupedTable(before, after, schema, options) {
|
|
1282
|
+
const groups = [];
|
|
1283
|
+
let count = 0;
|
|
1284
|
+
for (const path of schema.paths) {
|
|
1285
|
+
const beforeGroups = arr(resolvePath(before, path));
|
|
1286
|
+
const afterGroups = arr(resolvePath(after, path));
|
|
1287
|
+
const max = Math.max(beforeGroups.length, afterGroups.length);
|
|
1288
|
+
for (let i = 0; i < max; i += 1) {
|
|
1289
|
+
const groupBefore = beforeGroups[i];
|
|
1290
|
+
const groupAfter = afterGroups[i];
|
|
1291
|
+
if (!options.showUnchanged && !hasChange(groupBefore, groupAfter, path)) continue;
|
|
1292
|
+
const kind = statusOf(groupBefore, groupAfter);
|
|
1293
|
+
const present = groupAfter !== void 0 ? groupAfter : groupBefore;
|
|
1294
|
+
const labelValue = schema.groupLabelPath && present ? resolvePath(present, schema.groupLabelPath) : void 0;
|
|
1295
|
+
const label = labelValue != null ? stripHtml(String(labelValue)) : "Outcome " + (i + 1);
|
|
1296
|
+
const scalarRows = renderGroupScalars(groupBefore, groupAfter, schema, options);
|
|
1297
|
+
const inner = renderTableFromItems(groupBefore, groupAfter, [schema.innerArray || ""], schema.innerColumns || [], {
|
|
1298
|
+
...options,
|
|
1299
|
+
suppressChangeColumn: kind !== "modified",
|
|
1300
|
+
label,
|
|
1301
|
+
pathKey: schema.innerArray || "",
|
|
1302
|
+
depth: 1
|
|
1303
|
+
}).html;
|
|
1304
|
+
const head = "<div class='group-head'><span class='change-kind " + kind + "'>" + capitalize(kind) + "</span><span class='group-label'>" + escapeHtml(label) + "</span></div>";
|
|
1305
|
+
groups.push("<div class='outcome-group'>" + head + scalarRows + inner + "</div>");
|
|
1306
|
+
count += 1;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
return { html: groups.join(""), changed: count > 0, count };
|
|
1310
|
+
}
|
|
1311
|
+
function renderGroupScalars(before, after, schema, options) {
|
|
1312
|
+
const beforeObj = isObj(before) ? before : {};
|
|
1313
|
+
const afterObj = isObj(after) ? after : {};
|
|
1314
|
+
const skip = new Set([schema.innerArray, schema.groupLabelPath].filter(Boolean));
|
|
1315
|
+
const rows = unionKeys(beforeObj, afterObj).filter((key) => !skip.has(key)).map((key) => {
|
|
1316
|
+
const cellBefore = getField(beforeObj, key);
|
|
1317
|
+
const cellAfter = getField(afterObj, key);
|
|
1318
|
+
const scalar = scalarPair(cellBefore, cellAfter);
|
|
1319
|
+
if (!scalar) return "";
|
|
1320
|
+
if (!options.showUnchanged && !hasChange(cellBefore, cellAfter, key)) return "";
|
|
1321
|
+
return scalarRow(key, scalar.before, scalar.after, statusOf(cellBefore, cellAfter), options.nodeStatus);
|
|
1322
|
+
}).filter(Boolean).join("");
|
|
1323
|
+
return rows ? "<table class='snapshot-kv'><tbody>" + rows + "</tbody></table>" : "";
|
|
1324
|
+
}
|
|
1325
|
+
function scalarRow(key, before, after, kind, nodeStatus) {
|
|
1326
|
+
return "<tr><th>" + escapeHtml(humanizePath(key)) + "</th><td>" + renderValueCell(before, after, kind, nodeStatus) + "</td></tr>";
|
|
1327
|
+
}
|
|
1328
|
+
function renderLine(label, before, after, kind, nodeStatus) {
|
|
1329
|
+
const badge = nodeStatus === "modified" ? "<span class='change-kind " + kind + "'>" + capitalize(kind) + "</span>" : "";
|
|
1330
|
+
return "<ul class='changes'><li><div class='change-line-head'>" + badge + "<span class='change-line-label'>" + escapeHtml(label) + "</span></div><div class='change-line-value'>" + renderValueCell(before, after, kind, nodeStatus) + "</div></li></ul>";
|
|
1331
|
+
}
|
|
1332
|
+
function renderValueCell(before, after, kind, nodeStatus) {
|
|
1333
|
+
if (nodeStatus === "added") return oneSidedVal(after !== void 0 ? after : before);
|
|
1334
|
+
if (nodeStatus === "deleted") return oneSidedVal(before !== void 0 ? before : after);
|
|
1335
|
+
if (kind === "added") return insVal(after !== void 0 ? after : before);
|
|
1336
|
+
if (kind === "removed") return delVal(before !== void 0 ? before : after);
|
|
1337
|
+
if (kind === "modified") return diffValue(before, after);
|
|
1338
|
+
return faintVal(after !== void 0 ? after : before);
|
|
1339
|
+
}
|
|
1340
|
+
function statusOf(before, after) {
|
|
1341
|
+
if (before === void 0) return "added";
|
|
1342
|
+
if (after === void 0) return "removed";
|
|
1343
|
+
return valuesEqual(before, after) ? "unchanged" : "modified";
|
|
1344
|
+
}
|
|
1345
|
+
function hasChange(before, after, pathKey = "") {
|
|
1346
|
+
if (before === void 0 || after === void 0) return before !== after;
|
|
1347
|
+
const scalar = scalarPair(before, after);
|
|
1348
|
+
if (scalar) return !valuesEqual(scalar.before, scalar.after);
|
|
1349
|
+
if (isCollection(before, after, pathKey)) {
|
|
1350
|
+
const beforeItems = arr(before);
|
|
1351
|
+
const afterItems = arr(after);
|
|
1352
|
+
const max = Math.max(beforeItems.length, afterItems.length);
|
|
1353
|
+
for (let i = 0; i < max; i += 1) {
|
|
1354
|
+
if (hasChange(beforeItems[i], afterItems[i], "")) return true;
|
|
1355
|
+
}
|
|
1356
|
+
return false;
|
|
1357
|
+
}
|
|
1358
|
+
const beforeObj = isObj(before) ? before : {};
|
|
1359
|
+
const afterObj = isObj(after) ? after : {};
|
|
1360
|
+
for (const key of unionKeys(beforeObj, afterObj)) {
|
|
1361
|
+
if (hasChange(beforeObj[key], afterObj[key], key)) return true;
|
|
1362
|
+
}
|
|
1363
|
+
return false;
|
|
1364
|
+
}
|
|
1365
|
+
function scalarPair(before, after) {
|
|
1366
|
+
const beforeUnwrapped = unwrapValue(before);
|
|
1367
|
+
const afterUnwrapped = unwrapValue(after);
|
|
1368
|
+
const beforeScalar = beforeUnwrapped == null || typeof beforeUnwrapped !== "object";
|
|
1369
|
+
const afterScalar = afterUnwrapped == null || typeof afterUnwrapped !== "object";
|
|
1370
|
+
return beforeScalar && afterScalar ? { before: beforeUnwrapped, after: afterUnwrapped } : null;
|
|
1371
|
+
}
|
|
1372
|
+
function arr(value) {
|
|
1373
|
+
if (value === void 0 || value === null) return [];
|
|
1374
|
+
return Array.isArray(value) ? value : [value];
|
|
1375
|
+
}
|
|
1376
|
+
function isObj(value) {
|
|
1377
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
1378
|
+
}
|
|
1379
|
+
function isCollection(before, after, pathKey) {
|
|
1380
|
+
return Array.isArray(before) || Array.isArray(after) || isCollectionKey(pathKey);
|
|
1381
|
+
}
|
|
1382
|
+
function isCollectionKey(key) {
|
|
1383
|
+
return COLLECTION_KEYS.has(key);
|
|
1384
|
+
}
|
|
1385
|
+
function unionKeys(before, after) {
|
|
1386
|
+
return [.../* @__PURE__ */ new Set([...Object.keys(before || {}), ...Object.keys(after || {})])].sort();
|
|
1387
|
+
}
|
|
1388
|
+
function ownsAny(paths, key) {
|
|
1389
|
+
for (const path of paths) {
|
|
1390
|
+
if (key === path || key.startsWith(path + ".") || key.startsWith(path + "[")) return true;
|
|
1391
|
+
}
|
|
1392
|
+
return false;
|
|
1393
|
+
}
|
|
1394
|
+
function pickSectionObject(obj, paths) {
|
|
1395
|
+
if (!obj) return void 0;
|
|
1396
|
+
const out = {};
|
|
1397
|
+
for (const path of paths) {
|
|
1398
|
+
const value = resolvePath(obj, path);
|
|
1399
|
+
if (value !== void 0) out[path] = value;
|
|
1400
|
+
}
|
|
1401
|
+
return Object.keys(out).length ? out : void 0;
|
|
1402
|
+
}
|
|
1403
|
+
function getField(obj, key) {
|
|
1404
|
+
return obj ? obj[key] : void 0;
|
|
1405
|
+
}
|
|
1406
|
+
function resolvePath(obj, path) {
|
|
1407
|
+
if (obj == null || !path) return obj;
|
|
1408
|
+
let cur = obj;
|
|
1409
|
+
const tokens = path.match(/[^.[\]]+|\[\d+\]/g) || [];
|
|
1410
|
+
for (const token of tokens) {
|
|
1411
|
+
if (cur == null || typeof cur !== "object") return void 0;
|
|
1412
|
+
cur = token[0] === "[" ? cur[Number(token.slice(1, -1))] : cur[token];
|
|
1413
|
+
}
|
|
1414
|
+
return cur;
|
|
1415
|
+
}
|
|
1416
|
+
function looksLikeNameValueArray(beforeItems, afterItems) {
|
|
1417
|
+
const items = [...beforeItems, ...afterItems].filter(isObj);
|
|
1418
|
+
return items.length > 0 && items.every((item) => "name" in item && "value" in item);
|
|
1419
|
+
}
|
|
1420
|
+
function itemTitle(before, after, index) {
|
|
1421
|
+
const item = isObj(after) ? after : before;
|
|
1422
|
+
if (item) {
|
|
1423
|
+
for (const key of ["fieldText", "label", "name", "fieldType", "field"]) {
|
|
1424
|
+
const value = unwrapValue(item[key]);
|
|
1425
|
+
if (value !== void 0 && value !== null && typeof value !== "object") {
|
|
1426
|
+
return stripHtml(String(value));
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
return "Item " + (index + 1);
|
|
1431
|
+
}
|
|
1432
|
+
function stripHtml(value) {
|
|
1433
|
+
return value.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
|
|
1434
|
+
}
|
|
1435
|
+
function capitalize(value) {
|
|
1436
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
1437
|
+
}
|
|
1438
|
+
function humanizePath(path) {
|
|
1439
|
+
const names = {
|
|
1440
|
+
assignmentItems: "Assignment item",
|
|
1441
|
+
choiceReferences: "Choice reference",
|
|
1442
|
+
choices: "Choice",
|
|
1443
|
+
conditions: "Condition",
|
|
1444
|
+
customErrorMessages: "Custom error message",
|
|
1445
|
+
dataTypeMappings: "Data type mapping",
|
|
1446
|
+
fields: "Field",
|
|
1447
|
+
filters: "Filter",
|
|
1448
|
+
inputAssignments: "Input assignment",
|
|
1449
|
+
inputParameters: "Input parameter",
|
|
1450
|
+
outputParameters: "Output parameter",
|
|
1451
|
+
processMetadataValues: "Process metadata value",
|
|
1452
|
+
rules: "Rule",
|
|
1453
|
+
scheduledPaths: "Scheduled path",
|
|
1454
|
+
storeOutputParameters: "Store output parameter",
|
|
1455
|
+
transformValues: "Transform value",
|
|
1456
|
+
valueMappings: "Value mapping",
|
|
1457
|
+
waitEvents: "Wait event"
|
|
1458
|
+
};
|
|
1459
|
+
return path.split(".").map((segment) => {
|
|
1460
|
+
const match = segment.match(/^([^[]+)(?:\[(\d+)\])?$/);
|
|
1461
|
+
if (!match) return segment;
|
|
1462
|
+
const name = names[match[1]] || match[1].replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (char) => char.toUpperCase());
|
|
1463
|
+
return match[2] === void 0 ? name : name + " " + (Number(match[2]) + 1);
|
|
1464
|
+
}).join(" > ");
|
|
1465
|
+
}
|
|
1466
|
+
function unwrapValue(value) {
|
|
1467
|
+
if (value === null || value === void 0 || typeof value !== "object" || Array.isArray(value)) return value;
|
|
1468
|
+
if ("elementReference" in value) return "{!" + value.elementReference + "}";
|
|
1469
|
+
const keys = Object.keys(value);
|
|
1470
|
+
if (keys.length === 1) {
|
|
1471
|
+
const key = keys[0];
|
|
1472
|
+
const obj = value;
|
|
1473
|
+
if (key === "stringValue" || key === "numberValue" || key === "dateValue" || key === "dateTimeValue") return obj[key];
|
|
1474
|
+
if (key === "booleanValue") return String(obj[key]) === "true" ? "true" : "false";
|
|
1475
|
+
}
|
|
1476
|
+
return value;
|
|
1477
|
+
}
|
|
1478
|
+
function valuesEqual(a, b) {
|
|
1479
|
+
if (a === b) return true;
|
|
1480
|
+
if (a && b && typeof a === "object" && typeof b === "object") return JSON.stringify(a) === JSON.stringify(b);
|
|
1481
|
+
return false;
|
|
1482
|
+
}
|
|
1483
|
+
function renderScalar(value) {
|
|
1484
|
+
if (value === void 0) return "<em>-</em>";
|
|
1485
|
+
if (value === null) return "<em>empty</em>";
|
|
1486
|
+
if (typeof value === "object") return "<pre>" + escapeHtml(JSON.stringify(value, null, 2)) + "</pre>";
|
|
1487
|
+
const text = String(value);
|
|
1488
|
+
if (text.length > LONG_TEXT_THRESHOLD || text.includes("\n")) {
|
|
1489
|
+
return "<pre>" + escapeHtml(text) + "</pre>";
|
|
1490
|
+
}
|
|
1491
|
+
return escapeHtml(text);
|
|
1492
|
+
}
|
|
1493
|
+
function insVal(value) {
|
|
1494
|
+
return "<span class='val ins'>" + renderScalar(value) + "</span>";
|
|
1495
|
+
}
|
|
1496
|
+
function delVal(value) {
|
|
1497
|
+
return "<span class='val del'>" + renderScalar(value) + "</span>";
|
|
1498
|
+
}
|
|
1499
|
+
function faintVal(value) {
|
|
1500
|
+
return "<span class='val faint'>" + renderScalar(value) + "</span>";
|
|
1501
|
+
}
|
|
1502
|
+
function oneSidedVal(value) {
|
|
1503
|
+
return "<span class='val one-sided'>" + renderScalar(value) + "</span>";
|
|
1504
|
+
}
|
|
1505
|
+
function diffValue(before, after) {
|
|
1506
|
+
before = unwrapValue(before);
|
|
1507
|
+
after = unwrapValue(after);
|
|
1508
|
+
if (before === void 0) return insVal(after);
|
|
1509
|
+
if (after === void 0) return delVal(before);
|
|
1510
|
+
return delVal(before) + " <span class='arrow'>-></span> " + insVal(after);
|
|
1511
|
+
}
|
|
1512
|
+
function escapeHtml(value) {
|
|
1513
|
+
return String(value).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1514
|
+
}
|
|
1515
|
+
function snapshotPanelClientScript() {
|
|
1516
|
+
const functions = [
|
|
1517
|
+
renderNodePanelBody,
|
|
1518
|
+
renderSections,
|
|
1519
|
+
renderSchemaSection,
|
|
1520
|
+
renderValue,
|
|
1521
|
+
renderObject,
|
|
1522
|
+
renderArray,
|
|
1523
|
+
renderTable,
|
|
1524
|
+
renderTableFromItems,
|
|
1525
|
+
renderRow,
|
|
1526
|
+
renderGroupedTable,
|
|
1527
|
+
renderGroupScalars,
|
|
1528
|
+
scalarRow,
|
|
1529
|
+
renderLine,
|
|
1530
|
+
renderValueCell,
|
|
1531
|
+
statusOf,
|
|
1532
|
+
hasChange,
|
|
1533
|
+
scalarPair,
|
|
1534
|
+
arr,
|
|
1535
|
+
isObj,
|
|
1536
|
+
isCollection,
|
|
1537
|
+
isCollectionKey,
|
|
1538
|
+
unionKeys,
|
|
1539
|
+
ownsAny,
|
|
1540
|
+
pickSectionObject,
|
|
1541
|
+
getField,
|
|
1542
|
+
resolvePath,
|
|
1543
|
+
looksLikeNameValueArray,
|
|
1544
|
+
itemTitle,
|
|
1545
|
+
stripHtml,
|
|
1546
|
+
capitalize,
|
|
1547
|
+
humanizePath,
|
|
1548
|
+
unwrapValue,
|
|
1549
|
+
valuesEqual,
|
|
1550
|
+
renderScalar,
|
|
1551
|
+
insVal,
|
|
1552
|
+
delVal,
|
|
1553
|
+
faintVal,
|
|
1554
|
+
oneSidedVal,
|
|
1555
|
+
diffValue,
|
|
1556
|
+
escapeHtml
|
|
1557
|
+
].map((fn) => fn.toString()).join("\n\n");
|
|
1558
|
+
return "const LONG_TEXT_THRESHOLD = " + LONG_TEXT_THRESHOLD + ";\nconst COLLECTION_KEYS = new Set(" + JSON.stringify(COLLECTION_KEY_NAMES) + ");\n\n" + functions;
|
|
1559
|
+
}
|
|
1560
|
+
|
|
928
1561
|
// src/render/render-html.ts
|
|
929
1562
|
function renderHtml(layout) {
|
|
930
1563
|
const data = {
|
|
@@ -942,12 +1575,13 @@ function renderHtml(layout) {
|
|
|
942
1575
|
const edgeStat = s.addedEdges + s.removedEdges > 0 ? `<span class="sep">\xB7</span><span class="stat edges">+${s.addedEdges}/\u2212${s.removedEdges} edges</span>` : "";
|
|
943
1576
|
const metaHtml = `${stat(s.addedNodes, "added", "added")}<span class="sep">\xB7</span>${stat(s.removedNodes, "deleted", "deleted")}<span class="sep">\xB7</span>${stat(s.modifiedNodes, "modified", "modified")}${edgeStat}`;
|
|
944
1577
|
const viewBox = fitViewBox(layout.width, layout.height);
|
|
1578
|
+
const flowBannerHtml = renderFlowBanner(layout.diff.flowChanges);
|
|
945
1579
|
return `<!doctype html>
|
|
946
1580
|
<html lang="en">
|
|
947
1581
|
<head>
|
|
948
1582
|
<meta charset="utf-8" />
|
|
949
1583
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
950
|
-
<title>${
|
|
1584
|
+
<title>${escapeHtml2(layout.diff.flowName)}</title>
|
|
951
1585
|
<style>
|
|
952
1586
|
:root {
|
|
953
1587
|
color-scheme: light;
|
|
@@ -1035,6 +1669,11 @@ function renderHtml(layout) {
|
|
|
1035
1669
|
.panel.modified .badge { background: var(--modified-fill); color: var(--modified); }
|
|
1036
1670
|
.panel .empty { color: var(--muted); font-size: 13px; }
|
|
1037
1671
|
.detail-section { margin: 0 0 12px; border: 1px solid var(--border); border-radius: 8px; background: #fbfcfe; overflow: hidden; }
|
|
1672
|
+
.detail-section.snapshot-section { border-left-width: 3px; }
|
|
1673
|
+
.detail-section.snapshot-section.added { border-left-color: rgba(22, 163, 74, 0.5); }
|
|
1674
|
+
.detail-section.snapshot-section.deleted { border-left-color: rgba(220, 38, 38, 0.45); }
|
|
1675
|
+
.detail-section.snapshot-section.added summary { background: rgba(220, 252, 231, 0.35); }
|
|
1676
|
+
.detail-section.snapshot-section.deleted summary { background: rgba(254, 226, 226, 0.35); }
|
|
1038
1677
|
.detail-section summary { cursor: pointer; padding: 11px 13px; font-weight: 650; }
|
|
1039
1678
|
.detail-section[open] summary { border-bottom: 1px solid var(--border); }
|
|
1040
1679
|
.section-count { color: var(--muted); font-size: 11px; font-weight: 500; }
|
|
@@ -1048,8 +1687,9 @@ function renderHtml(layout) {
|
|
|
1048
1687
|
.val.ins { color: #14532d; background: var(--added-fill); }
|
|
1049
1688
|
.val.del { color: #7f1d1d; background: var(--deleted-fill); text-decoration: line-through; text-decoration-color: rgba(127, 29, 29, 0.5); }
|
|
1050
1689
|
.val.faint { color: var(--muted); background: transparent; padding-left: 0; }
|
|
1690
|
+
.val.one-sided { color: var(--text); background: transparent; padding-left: 0; }
|
|
1051
1691
|
.val em { font-style: normal; color: var(--faint); }
|
|
1052
|
-
.val pre { margin: 4px 0 0; padding: 8px 10px; background: var(--surface); border: 1px solid var(--border); border-radius: 6px; white-space: pre-wrap; overflow-wrap: anywhere; font-size: 11.5px; line-height: 1.5; }
|
|
1692
|
+
.val pre { margin: 4px 0 0; padding: 8px 10px; background: var(--surface); border: 1px solid var(--border); border-radius: 6px; white-space: pre-wrap; overflow-wrap: anywhere; font-size: 11.5px; line-height: 1.5; max-height: 220px; overflow: auto; }
|
|
1053
1693
|
.arrow { color: var(--faint); margin: 0 2px; }
|
|
1054
1694
|
/* scalar / fallback change lines */
|
|
1055
1695
|
.changes { list-style: none; padding: 0; margin: 0; }
|
|
@@ -1066,12 +1706,31 @@ function renderHtml(layout) {
|
|
|
1066
1706
|
.change-table .row-idx { color: var(--faint); font-variant-numeric: tabular-nums; width: 1%; white-space: nowrap; }
|
|
1067
1707
|
.change-table tr.row-added { background: rgba(22, 163, 74, 0.06); }
|
|
1068
1708
|
.change-table tr.row-removed { background: rgba(220, 38, 38, 0.05); }
|
|
1709
|
+
.snapshot-kv { width: 100%; border-collapse: collapse; font-size: 12px; margin-bottom: 10px; }
|
|
1710
|
+
.snapshot-kv th, .snapshot-kv td { padding: 6px 7px; border-bottom: 1px solid var(--border); text-align: left; vertical-align: top; overflow-wrap: anywhere; }
|
|
1711
|
+
.snapshot-kv th { color: var(--muted); width: 34%; font-weight: 650; }
|
|
1712
|
+
.snapshot-kv tr:last-child th, .snapshot-kv tr:last-child td { border-bottom: 0; }
|
|
1713
|
+
.snapshot-nested { border-left: 2px solid var(--border); padding-left: 10px; margin: 10px 0; }
|
|
1714
|
+
.snapshot-nested-title { color: var(--muted); font-size: 11px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; margin-bottom: 7px; }
|
|
1715
|
+
.snapshot-card { border: 1px dashed var(--border); border-radius: 7px; padding: 9px 10px; margin: 8px 0; background: var(--surface); }
|
|
1716
|
+
.snapshot-card.added { border-left: 2px solid rgba(22, 163, 74, 0.5); }
|
|
1717
|
+
.snapshot-card.removed { border-left: 2px solid rgba(220, 38, 38, 0.45); }
|
|
1718
|
+
.snapshot-card-title { font-size: 12px; font-weight: 700; margin-bottom: 7px; overflow-wrap: anywhere; }
|
|
1069
1719
|
/* outcome groups (decisions) */
|
|
1070
1720
|
.outcome-group { margin-bottom: 12px; padding: 10px 11px; border: 1px solid var(--border); border-radius: 7px; background: var(--surface); }
|
|
1071
1721
|
.outcome-group:last-child { margin-bottom: 0; }
|
|
1072
1722
|
.group-head { display: flex; align-items: center; gap: 7px; margin-bottom: 8px; }
|
|
1073
1723
|
.group-label { font-weight: 700; font-size: 12.5px; }
|
|
1074
1724
|
.outcome-group .changes { margin-bottom: 8px; }
|
|
1725
|
+
.flow-banner { border-bottom: 1px solid var(--border); background: #fffaf0; padding: 12px 20px; }
|
|
1726
|
+
.flow-banner-callout { margin-bottom: 8px; font-weight: 750; }
|
|
1727
|
+
.flow-banner-callout.deactivated { color: var(--deleted); }
|
|
1728
|
+
.flow-banner-callout.activated { color: var(--added); }
|
|
1729
|
+
.flow-banner-callout.neutral { color: var(--modified); }
|
|
1730
|
+
.flow-banner-list { display: grid; gap: 7px; max-width: 960px; }
|
|
1731
|
+
.flow-change-row { display: grid; grid-template-columns: minmax(120px, 190px) minmax(0, 1fr); align-items: start; gap: 10px; }
|
|
1732
|
+
.flow-change-label { color: var(--muted); font-size: 12px; font-weight: 700; }
|
|
1733
|
+
.flow-change-value { min-width: 0; max-height: 180px; overflow: auto; }
|
|
1075
1734
|
.edge { fill: none; }
|
|
1076
1735
|
.edge.unchanged { stroke-width: 1.4; opacity: 0.55; }
|
|
1077
1736
|
.edge.normal.unchanged { stroke: var(--edge); marker-end: url(#arrow-normal); }
|
|
@@ -1110,7 +1769,7 @@ function renderHtml(layout) {
|
|
|
1110
1769
|
<body>
|
|
1111
1770
|
<header>
|
|
1112
1771
|
<div>
|
|
1113
|
-
<h1>${
|
|
1772
|
+
<h1>${escapeHtml2(layout.diff.flowName)}</h1>
|
|
1114
1773
|
<div class="meta">${metaHtml}</div>
|
|
1115
1774
|
</div>
|
|
1116
1775
|
<div class="header-actions">
|
|
@@ -1128,6 +1787,7 @@ function renderHtml(layout) {
|
|
|
1128
1787
|
</div>
|
|
1129
1788
|
</div>
|
|
1130
1789
|
</header>
|
|
1790
|
+
${flowBannerHtml}
|
|
1131
1791
|
<main>
|
|
1132
1792
|
<div class="canvas">
|
|
1133
1793
|
<svg id="flow-svg" viewBox="${viewBox}" preserveAspectRatio="xMidYMid meet" role="img" aria-label="Flow diff">
|
|
@@ -1178,14 +1838,6 @@ function renderHtml(layout) {
|
|
|
1178
1838
|
let dragStart = null;
|
|
1179
1839
|
let selectedNodeId = null;
|
|
1180
1840
|
|
|
1181
|
-
function escapeHtml(value) {
|
|
1182
|
-
return String(value)
|
|
1183
|
-
.replace(/&/g, "&")
|
|
1184
|
-
.replace(/</g, "<")
|
|
1185
|
-
.replace(/>/g, ">")
|
|
1186
|
-
.replace(/"/g, """);
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
1841
|
function isNodeVisible(node, mode) {
|
|
1190
1842
|
if (mode === "after") return node.status !== "deleted";
|
|
1191
1843
|
if (mode === "before") return node.status !== "added";
|
|
@@ -1311,237 +1963,10 @@ function renderHtml(layout) {
|
|
|
1311
1963
|
panel.className = "panel " + node.status;
|
|
1312
1964
|
panelTitle.textContent = node.label;
|
|
1313
1965
|
panelBadge.textContent = node.status.toUpperCase();
|
|
1314
|
-
|
|
1315
|
-
panelBody.innerHTML = changes.length
|
|
1316
|
-
? organizeChanges(node).map((section) => formatSection(section, node)).join("")
|
|
1317
|
-
: node.status === "added"
|
|
1318
|
-
? "<div class='empty'>This node was added in the new version.</div>"
|
|
1319
|
-
: node.status === "deleted"
|
|
1320
|
-
? "<div class='empty'>This node was removed in the new version.</div>"
|
|
1321
|
-
: "<div class='empty'>No property changes.</div>";
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
function owns(prefix, path) {
|
|
1325
|
-
return path === prefix || path.startsWith(prefix + ".") || path.startsWith(prefix + "[");
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
function changeKind(change) {
|
|
1329
|
-
return change.before === undefined ? "added" : change.after === undefined ? "removed" : "modified";
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
function capitalize(value) {
|
|
1333
|
-
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
// Parse "base[idx]rest" \u2014 returns the item index and trailing path, or null.
|
|
1337
|
-
function matchItem(path, base) {
|
|
1338
|
-
if (!path.startsWith(base + "[")) return null;
|
|
1339
|
-
const close = path.indexOf("]", base.length + 1);
|
|
1340
|
-
if (close === -1) return null;
|
|
1341
|
-
const idx = path.slice(base.length + 1, close);
|
|
1342
|
-
if (!/^\\d+$/.test(idx)) return null;
|
|
1343
|
-
return { idx, rest: path.slice(close + 1) };
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
// Group a node's flat change list into ordered sections: schema-driven first,
|
|
1347
|
-
// then a generic fallback (array-prefixed groups, then a Configuration bucket).
|
|
1348
|
-
function organizeChanges(node) {
|
|
1349
|
-
const schemas = DATA.sectionSchemas[node.type] || [];
|
|
1350
|
-
const sections = schemas.map((schema) => ({ schema, changes: [] }));
|
|
1351
|
-
const fallback = new Map();
|
|
1352
|
-
for (const change of node.changes || []) {
|
|
1353
|
-
const sec = sections.find((s) => s.schema.paths.some((path) => owns(path, change.path)));
|
|
1354
|
-
if (sec) { sec.changes.push(change); continue; }
|
|
1355
|
-
const match = change.path.match(/^([^.[]+)(\\[\\d+\\])?/);
|
|
1356
|
-
const isArray = !!(match && match[2] !== undefined);
|
|
1357
|
-
const key = isArray ? match[1] : "Configuration";
|
|
1358
|
-
if (!fallback.has(key)) fallback.set(key, []);
|
|
1359
|
-
fallback.get(key).push(change);
|
|
1360
|
-
}
|
|
1361
|
-
const out = [];
|
|
1362
|
-
for (const s of sections) {
|
|
1363
|
-
if (s.changes.length) out.push({ name: s.schema.name, schema: s.schema, changes: s.changes });
|
|
1364
|
-
}
|
|
1365
|
-
for (const [key, changes] of fallback) {
|
|
1366
|
-
out.push({
|
|
1367
|
-
name: key === "Configuration" ? "Configuration" : humanizePath(key),
|
|
1368
|
-
schema: { render: "lines", paths: [key] },
|
|
1369
|
-
changes,
|
|
1370
|
-
});
|
|
1371
|
-
}
|
|
1372
|
-
return out;
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
|
-
function formatSection(section, node) {
|
|
1376
|
-
const body = renderSectionBody(section, node);
|
|
1377
|
-
return "<details class='detail-section' open><summary>" + escapeHtml(section.name)
|
|
1378
|
-
+ " <span class='section-count'>(" + section.changes.length + ")</span></summary>"
|
|
1379
|
-
+ "<div class='section-body'>" + body + "</div></details>";
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
function renderSectionBody(section, node) {
|
|
1383
|
-
const render = section.schema.render;
|
|
1384
|
-
if (render === "grouped-table") return renderGroupedTable(section, node);
|
|
1385
|
-
if (render === "table" && section.schema.columns) {
|
|
1386
|
-
return renderTable(section.changes, section.schema.paths, section.schema.columns, { before: node.before, after: node.after, prefix: "" });
|
|
1387
|
-
}
|
|
1388
|
-
return renderLines(section.changes, {});
|
|
1966
|
+
panelBody.innerHTML = renderNodePanelBody(node, DATA.sectionSchemas[node.type] || []);
|
|
1389
1967
|
}
|
|
1390
1968
|
|
|
1391
|
-
|
|
1392
|
-
// The plain option drops per-line badges \u2014 used under a wholly added/removed
|
|
1393
|
-
// outcome, where the single group badge already states the change (Finding 3).
|
|
1394
|
-
function renderLines(changes, options) {
|
|
1395
|
-
const plain = options && options.plain;
|
|
1396
|
-
return "<ul class='changes'>" + changes.map((change) => {
|
|
1397
|
-
const kind = changeKind(change);
|
|
1398
|
-
const badge = plain ? "" : "<span class='change-kind " + kind + "'>" + capitalize(kind) + "</span>";
|
|
1399
|
-
return "<li><div class='change-line-head'>" + badge
|
|
1400
|
-
+ "<span class='change-line-label'>" + escapeHtml(humanizePath(change.path)) + "</span></div>"
|
|
1401
|
-
+ "<div class='change-line-value'>" + diffValue(change.before, change.after) + "</div></li>";
|
|
1402
|
-
}).join("") + "</ul>";
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
|
-
// Walk a path like "rules[0].conditions[2]" into an object.
|
|
1406
|
-
function resolvePath(obj, path) {
|
|
1407
|
-
if (obj == null || !path) return undefined;
|
|
1408
|
-
let cur = obj;
|
|
1409
|
-
const tokens = path.match(/[^.[\\]]+|\\[\\d+\\]/g) || [];
|
|
1410
|
-
for (const token of tokens) {
|
|
1411
|
-
if (cur == null || typeof cur !== "object") return undefined;
|
|
1412
|
-
cur = token[0] === "[" ? cur[Number(token.slice(1, -1))] : cur[token];
|
|
1413
|
-
}
|
|
1414
|
-
return cur;
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
// Collapse arr[i].* leaf changes (and whole-item changes) into one row per
|
|
1418
|
-
// item index, then fill any unchanged columns from the item snapshot so a
|
|
1419
|
-
// modified row still shows its sibling context (Resource/Operator/etc.).
|
|
1420
|
-
function buildRows(changes, basePaths, columns, ctx) {
|
|
1421
|
-
const rows = new Map();
|
|
1422
|
-
let order = 0;
|
|
1423
|
-
for (const change of changes) {
|
|
1424
|
-
let base = null, idx = null, rest = "";
|
|
1425
|
-
for (const p of basePaths) {
|
|
1426
|
-
const m = matchItem(change.path, p);
|
|
1427
|
-
if (m) { base = p; idx = m.idx; rest = m.rest; break; }
|
|
1428
|
-
}
|
|
1429
|
-
if (base === null) continue;
|
|
1430
|
-
const itemKey = base + "[" + idx + "]";
|
|
1431
|
-
if (!rows.has(itemKey)) rows.set(itemKey, { kind: "modified", order: order++, idx, base, cells: {} });
|
|
1432
|
-
const row = rows.get(itemKey);
|
|
1433
|
-
if (rest === "") {
|
|
1434
|
-
row.kind = changeKind(change);
|
|
1435
|
-
for (const col of columns) {
|
|
1436
|
-
const before = extractField(change.before, col.path);
|
|
1437
|
-
const after = extractField(change.after, col.path);
|
|
1438
|
-
row.cells[col.path] = { before, after, changed: !valuesEqual(before, after) };
|
|
1439
|
-
}
|
|
1440
|
-
continue;
|
|
1441
|
-
}
|
|
1442
|
-
const sub = rest.replace(/^\\./, "");
|
|
1443
|
-
const col = columns.find((c) => sub === c.path || sub.startsWith(c.path + ".") || sub.startsWith(c.path + "["));
|
|
1444
|
-
if (col) row.cells[col.path] = { before: unwrapValue(change.before), after: unwrapValue(change.after), changed: true };
|
|
1445
|
-
}
|
|
1446
|
-
const result = [...rows.values()].sort((a, b) => a.order - b.order);
|
|
1447
|
-
if (ctx) {
|
|
1448
|
-
for (const row of result) {
|
|
1449
|
-
const abs = ctx.prefix ? ctx.prefix + "." + row.base + "[" + row.idx + "]" : row.base + "[" + row.idx + "]";
|
|
1450
|
-
const snapshot = row.kind === "removed" ? resolvePath(ctx.before, abs) : resolvePath(ctx.after, abs);
|
|
1451
|
-
if (!snapshot || typeof snapshot !== "object") continue;
|
|
1452
|
-
for (const col of columns) {
|
|
1453
|
-
if (row.cells[col.path]) continue;
|
|
1454
|
-
const value = extractField(snapshot, col.path);
|
|
1455
|
-
if (value !== undefined) row.cells[col.path] = { before: value, after: value, changed: false };
|
|
1456
|
-
}
|
|
1457
|
-
}
|
|
1458
|
-
}
|
|
1459
|
-
return result;
|
|
1460
|
-
}
|
|
1461
|
-
|
|
1462
|
-
// options.uniformKind (a wholly added/removed group): drop the Change
|
|
1463
|
-
// column and per-row badges, colouring every cell by that single kind.
|
|
1464
|
-
function renderTable(changes, basePaths, columns, ctx, options) {
|
|
1465
|
-
const uniform = options && options.uniformKind;
|
|
1466
|
-
const rows = buildRows(changes, basePaths, columns, ctx);
|
|
1467
|
-
if (!rows.length) return renderLines(changes, {});
|
|
1468
|
-
const head = "<tr><th class='row-idx'>#</th>" + columns.map((c) => "<th>" + escapeHtml(c.label) + "</th>").join("")
|
|
1469
|
-
+ (uniform ? "" : "<th>Change</th>") + "</tr>";
|
|
1470
|
-
const body = rows.map((row) => renderRow(row, columns, uniform)).join("");
|
|
1471
|
-
return "<table class='change-table'><thead>" + head + "</thead><tbody>" + body + "</tbody></table>";
|
|
1472
|
-
}
|
|
1473
|
-
|
|
1474
|
-
function renderRow(row, columns, uniform) {
|
|
1475
|
-
const kind = uniform || row.kind;
|
|
1476
|
-
const cells = columns.map((col) => {
|
|
1477
|
-
const cell = row.cells[col.path];
|
|
1478
|
-
if (!cell) return "<td></td>";
|
|
1479
|
-
if (kind === "added") return "<td>" + insVal(cell.after !== undefined ? cell.after : cell.before) + "</td>";
|
|
1480
|
-
if (kind === "removed") return "<td>" + delVal(cell.before !== undefined ? cell.before : cell.after) + "</td>";
|
|
1481
|
-
return "<td>" + (cell.changed ? diffValue(cell.before, cell.after) : faintVal(cell.after)) + "</td>";
|
|
1482
|
-
}).join("");
|
|
1483
|
-
const badge = uniform ? "" : "<td><span class='change-kind " + row.kind + "'>" + capitalize(row.kind) + "</span></td>";
|
|
1484
|
-
return "<tr class='row-" + kind + "'><td class='row-idx'>" + (Number(row.idx) + 1) + "</td>" + cells + badge + "</tr>";
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
// Decisions et al.: group by the outer array (one group per outcome/rule).
|
|
1488
|
-
// A wholly added/removed outcome shows ONE group badge + plain context; a
|
|
1489
|
-
// modified outcome shows its changed conditions in a per-row-badged table.
|
|
1490
|
-
function renderGroupedTable(section, node) {
|
|
1491
|
-
const schema = section.schema;
|
|
1492
|
-
const groups = new Map();
|
|
1493
|
-
let order = 0;
|
|
1494
|
-
for (const change of section.changes) {
|
|
1495
|
-
let matched = null;
|
|
1496
|
-
for (const p of schema.paths) {
|
|
1497
|
-
const m = matchItem(change.path, p);
|
|
1498
|
-
if (m) { matched = { base: p, idx: m.idx, rest: m.rest }; break; }
|
|
1499
|
-
}
|
|
1500
|
-
if (!matched) continue;
|
|
1501
|
-
const key = matched.idx;
|
|
1502
|
-
if (!groups.has(key)) groups.set(key, { idx: matched.idx, base: matched.base, order: order++, label: null, kind: "modified", whole: false, inner: [], scalars: [] });
|
|
1503
|
-
const g = groups.get(key);
|
|
1504
|
-
if (matched.rest === "") {
|
|
1505
|
-
g.kind = changeKind(change);
|
|
1506
|
-
g.whole = true;
|
|
1507
|
-
const obj = change.after !== undefined ? change.after : change.before;
|
|
1508
|
-
if (obj && typeof obj === "object") {
|
|
1509
|
-
if (schema.groupLabelPath && obj[schema.groupLabelPath]) g.label = obj[schema.groupLabelPath];
|
|
1510
|
-
const inner = obj[schema.innerArray];
|
|
1511
|
-
if (Array.isArray(inner)) {
|
|
1512
|
-
inner.forEach((item, j) => g.inner.push({
|
|
1513
|
-
path: schema.innerArray + "[" + j + "]",
|
|
1514
|
-
before: g.kind === "removed" ? item : undefined,
|
|
1515
|
-
after: g.kind === "removed" ? undefined : item,
|
|
1516
|
-
}));
|
|
1517
|
-
}
|
|
1518
|
-
for (const k of Object.keys(obj)) {
|
|
1519
|
-
if (k === schema.innerArray || k === schema.groupLabelPath) continue;
|
|
1520
|
-
g.scalars.push({ path: k, before: g.kind === "removed" ? obj[k] : undefined, after: g.kind === "removed" ? undefined : obj[k] });
|
|
1521
|
-
}
|
|
1522
|
-
}
|
|
1523
|
-
continue;
|
|
1524
|
-
}
|
|
1525
|
-
const sub = matched.rest.replace(/^\\./, "");
|
|
1526
|
-
if (sub === schema.groupLabelPath) g.label = change.after !== undefined ? change.after : change.before;
|
|
1527
|
-
if (sub === schema.innerArray || sub.startsWith(schema.innerArray + "[")) {
|
|
1528
|
-
g.inner.push({ path: sub, before: change.before, after: change.after });
|
|
1529
|
-
} else {
|
|
1530
|
-
g.scalars.push({ path: sub, before: change.before, after: change.after });
|
|
1531
|
-
}
|
|
1532
|
-
}
|
|
1533
|
-
return [...groups.values()].sort((a, b) => a.order - b.order).map((g) => {
|
|
1534
|
-
const label = g.label != null ? String(g.label) : "Outcome " + (Number(g.idx) + 1);
|
|
1535
|
-
const head = "<div class='group-head'><span class='change-kind " + g.kind + "'>" + capitalize(g.kind) + "</span>"
|
|
1536
|
-
+ "<span class='group-label'>" + escapeHtml(label) + "</span></div>";
|
|
1537
|
-
const prefix = g.base + "[" + g.idx + "]";
|
|
1538
|
-
const scalars = g.scalars.length ? renderLines(g.scalars, { plain: g.whole }) : "";
|
|
1539
|
-
const inner = g.inner.length
|
|
1540
|
-
? renderTable(g.inner, [schema.innerArray], schema.innerColumns, { before: node.before, after: node.after, prefix }, g.whole ? { uniformKind: g.kind } : {})
|
|
1541
|
-
: "";
|
|
1542
|
-
return "<div class='outcome-group'>" + head + scalars + inner + "</div>";
|
|
1543
|
-
}).join("");
|
|
1544
|
-
}
|
|
1969
|
+
${snapshotPanelClientScript()}
|
|
1545
1970
|
|
|
1546
1971
|
function selectNode(id) {
|
|
1547
1972
|
selectedNodeId = id;
|
|
@@ -1552,70 +1977,6 @@ function renderHtml(layout) {
|
|
|
1552
1977
|
if (node) updatePanel(node);
|
|
1553
1978
|
}
|
|
1554
1979
|
|
|
1555
|
-
function humanizePath(path) {
|
|
1556
|
-
const names = {
|
|
1557
|
-
assignmentItems: "Assignment item",
|
|
1558
|
-
conditions: "Condition",
|
|
1559
|
-
rules: "Rule",
|
|
1560
|
-
};
|
|
1561
|
-
return path.split(".").map((segment) => {
|
|
1562
|
-
const match = segment.match(/^([^[]+)(?:\\[(\\d+)\\])?$/);
|
|
1563
|
-
if (!match) return segment;
|
|
1564
|
-
const name = names[match[1]] || match[1].replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (char) => char.toUpperCase());
|
|
1565
|
-
return match[2] === undefined ? name : name + " " + (Number(match[2]) + 1);
|
|
1566
|
-
}).join(" \u203A ");
|
|
1567
|
-
}
|
|
1568
|
-
|
|
1569
|
-
// Collapse Salesforce typed-value wrappers to their scalar so reviewers see
|
|
1570
|
-
// "true" / "Something Else" / "{!myVar}" instead of a JSON blob.
|
|
1571
|
-
function unwrapValue(value) {
|
|
1572
|
-
if (value === null || value === undefined || typeof value !== "object" || Array.isArray(value)) return value;
|
|
1573
|
-
if ("elementReference" in value) return "{!" + value.elementReference + "}";
|
|
1574
|
-
const keys = Object.keys(value);
|
|
1575
|
-
if (keys.length === 1) {
|
|
1576
|
-
const key = keys[0];
|
|
1577
|
-
if (key === "stringValue" || key === "numberValue" || key === "dateValue" || key === "dateTimeValue") return value[key];
|
|
1578
|
-
if (key === "booleanValue") return String(value[key]) === "true" ? "true" : "false";
|
|
1579
|
-
}
|
|
1580
|
-
return value;
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1583
|
-
function extractField(obj, path) {
|
|
1584
|
-
if (obj === undefined || obj === null) return undefined;
|
|
1585
|
-
let cur = obj;
|
|
1586
|
-
for (const segment of path.split(".")) {
|
|
1587
|
-
if (cur === null || typeof cur !== "object") return undefined;
|
|
1588
|
-
cur = cur[segment];
|
|
1589
|
-
}
|
|
1590
|
-
return unwrapValue(cur);
|
|
1591
|
-
}
|
|
1592
|
-
|
|
1593
|
-
function valuesEqual(a, b) {
|
|
1594
|
-
if (a === b) return true;
|
|
1595
|
-
if (a && b && typeof a === "object" && typeof b === "object") return JSON.stringify(a) === JSON.stringify(b);
|
|
1596
|
-
return false;
|
|
1597
|
-
}
|
|
1598
|
-
|
|
1599
|
-
function renderScalar(value) {
|
|
1600
|
-
if (value === undefined) return "<em>\u2014</em>";
|
|
1601
|
-
if (value === null) return "<em>empty</em>";
|
|
1602
|
-
if (typeof value === "object") return "<pre>" + escapeHtml(JSON.stringify(value, null, 2)) + "</pre>";
|
|
1603
|
-
return escapeHtml(String(value));
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
|
-
function insVal(value) { return "<span class='val ins'>" + renderScalar(value) + "</span>"; }
|
|
1607
|
-
function delVal(value) { return "<span class='val del'>" + renderScalar(value) + "</span>"; }
|
|
1608
|
-
function faintVal(value) { return "<span class='val faint'>" + renderScalar(value) + "</span>"; }
|
|
1609
|
-
|
|
1610
|
-
// git-diff value grammar: removed (red strike) \u2192 added (green).
|
|
1611
|
-
function diffValue(before, after) {
|
|
1612
|
-
before = unwrapValue(before);
|
|
1613
|
-
after = unwrapValue(after);
|
|
1614
|
-
if (before === undefined) return insVal(after);
|
|
1615
|
-
if (after === undefined) return delVal(before);
|
|
1616
|
-
return delVal(before) + " <span class='arrow'>\u2192</span> " + insVal(after);
|
|
1617
|
-
}
|
|
1618
|
-
|
|
1619
1980
|
function pickInitialNode(visibleNodeIds) {
|
|
1620
1981
|
return DATA.nodes.find((node) => visibleNodeIds.has(node.id) && node.status === "modified")
|
|
1621
1982
|
|| DATA.nodes.find((node) => visibleNodeIds.has(node.id) && node.status !== "unchanged")
|
|
@@ -1766,6 +2127,48 @@ function renderHtml(layout) {
|
|
|
1766
2127
|
</body>
|
|
1767
2128
|
</html>`;
|
|
1768
2129
|
}
|
|
2130
|
+
function renderFlowBanner(changes) {
|
|
2131
|
+
if (!changes || changes.length === 0) {
|
|
2132
|
+
return "";
|
|
2133
|
+
}
|
|
2134
|
+
const statusChange = changes.find((change) => change.path === "status");
|
|
2135
|
+
const callout = statusChange ? renderStatusCallout(statusChange.before, statusChange.after) : "";
|
|
2136
|
+
return `<section class="flow-banner" aria-label="Flow-level changes">
|
|
2137
|
+
${callout}
|
|
2138
|
+
<div class="flow-banner-list">
|
|
2139
|
+
${changes.map((change) => `<div class="flow-change-row">
|
|
2140
|
+
<div class="flow-change-label">${escapeHtml2(humanizePath(change.path))}</div>
|
|
2141
|
+
<div class="flow-change-value">${renderValueDelta(change.before, change.after)}</div>
|
|
2142
|
+
</div>`).join("")}
|
|
2143
|
+
</div>
|
|
2144
|
+
</section>`;
|
|
2145
|
+
}
|
|
2146
|
+
function renderStatusCallout(before, after) {
|
|
2147
|
+
const beforeText = String(before);
|
|
2148
|
+
const afterText = String(after);
|
|
2149
|
+
if (beforeText === "Active" && afterText !== "Active") {
|
|
2150
|
+
return `<div class="flow-banner-callout deactivated">Deactivated (${escapeHtml2(beforeText)} -> ${escapeHtml2(afterText)})</div>`;
|
|
2151
|
+
}
|
|
2152
|
+
if (beforeText !== "Active" && afterText === "Active") {
|
|
2153
|
+
return `<div class="flow-banner-callout activated">Activated (${escapeHtml2(beforeText)} -> ${escapeHtml2(afterText)})</div>`;
|
|
2154
|
+
}
|
|
2155
|
+
return `<div class="flow-banner-callout neutral">Status: ${escapeHtml2(beforeText)} -> ${escapeHtml2(afterText)}</div>`;
|
|
2156
|
+
}
|
|
2157
|
+
function renderValueDelta(before, after) {
|
|
2158
|
+
if (before === void 0) {
|
|
2159
|
+
return `<span class="val ins">${escapeHtml2(formatValue(after))}</span>`;
|
|
2160
|
+
}
|
|
2161
|
+
if (after === void 0) {
|
|
2162
|
+
return `<span class="val del">${escapeHtml2(formatValue(before))}</span>`;
|
|
2163
|
+
}
|
|
2164
|
+
return `<span class="val del">${escapeHtml2(formatValue(before))}</span><span class="arrow">-></span><span class="val ins">${escapeHtml2(formatValue(after))}</span>`;
|
|
2165
|
+
}
|
|
2166
|
+
function formatValue(value) {
|
|
2167
|
+
if (value === void 0) {
|
|
2168
|
+
return "(missing)";
|
|
2169
|
+
}
|
|
2170
|
+
return String(value);
|
|
2171
|
+
}
|
|
1769
2172
|
function fitViewBox(contentWidth, contentHeight) {
|
|
1770
2173
|
const minWidth = 720;
|
|
1771
2174
|
const minHeight = 540;
|
|
@@ -1789,16 +2192,16 @@ function renderEdge(edge) {
|
|
|
1789
2192
|
return "";
|
|
1790
2193
|
}
|
|
1791
2194
|
const d = points.map((point, index) => `${index === 0 ? "M" : "L"} ${point.x} ${point.y}`).join(" ");
|
|
1792
|
-
return `<path class="edge ${edge.kind} ${edge.status}" d="${d}" data-edge-id="${
|
|
2195
|
+
return `<path class="edge ${edge.kind} ${edge.status}" d="${d}" data-edge-id="${escapeHtml2(edge.id)}"></path>`;
|
|
1793
2196
|
}
|
|
1794
2197
|
function renderNode(node) {
|
|
1795
2198
|
const lines = wrapLabel(node.label, 22);
|
|
1796
2199
|
const lineHeight = 14;
|
|
1797
2200
|
const textY = node.height / 2 - (lines.length - 1) * lineHeight / 2 + 5;
|
|
1798
|
-
return `<g class="node ${node.status} type-${node.type}" data-node-id="${
|
|
2201
|
+
return `<g class="node ${node.status} type-${node.type}" data-node-id="${escapeHtml2(node.id)}" transform="translate(${node.x},${node.y})">
|
|
1799
2202
|
<rect width="${node.width}" height="${node.height}"></rect>
|
|
1800
2203
|
<text x="${node.width / 2}" y="${textY}" text-anchor="middle" class="node-label">
|
|
1801
|
-
${lines.map((line, index) => `<tspan x="${node.width / 2}" dy="${index === 0 ? 0 : lineHeight}">${
|
|
2204
|
+
${lines.map((line, index) => `<tspan x="${node.width / 2}" dy="${index === 0 ? 0 : lineHeight}">${escapeHtml2(line)}</tspan>`).join("")}
|
|
1802
2205
|
</text>
|
|
1803
2206
|
</g>`;
|
|
1804
2207
|
}
|
|
@@ -1820,7 +2223,7 @@ function wrapLabel(label, maxChars) {
|
|
|
1820
2223
|
}
|
|
1821
2224
|
return lines.length ? lines.slice(0, 3) : [label];
|
|
1822
2225
|
}
|
|
1823
|
-
function
|
|
2226
|
+
function escapeHtml2(value) {
|
|
1824
2227
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1825
2228
|
}
|
|
1826
2229
|
|
|
@@ -1929,8 +2332,10 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
1929
2332
|
}
|
|
1930
2333
|
}
|
|
1931
2334
|
async function runFileMode(oldPath, newPath, outDir, writeJson) {
|
|
1932
|
-
const
|
|
1933
|
-
const
|
|
2335
|
+
const oldXml = readFlowFromFile(oldPath);
|
|
2336
|
+
const newXml = readFlowFromFile(newPath);
|
|
2337
|
+
const oldModel = await buildModelWithHeader(oldXml);
|
|
2338
|
+
const newModel = await buildModelWithHeader(newXml);
|
|
1934
2339
|
await writeArtifacts(oldModel, newModel, outDir, writeJson);
|
|
1935
2340
|
}
|
|
1936
2341
|
async function runGitMode(repo, from, to, pattern, outDir, writeJson, changedOnly) {
|
|
@@ -1940,8 +2345,8 @@ async function runGitMode(repo, from, to, pattern, outDir, writeJson, changedOnl
|
|
|
1940
2345
|
try {
|
|
1941
2346
|
const oldXml = readFlowFromGit(repo, from, filePath);
|
|
1942
2347
|
const newXml = readFlowFromGit(repo, to, filePath);
|
|
1943
|
-
const oldModel = oldXml ?
|
|
1944
|
-
const newModel = newXml ?
|
|
2348
|
+
const oldModel = oldXml ? await buildModelWithHeader(oldXml) : buildEmptyModel();
|
|
2349
|
+
const newModel = newXml ? await buildModelWithHeader(newXml) : buildEmptyModel();
|
|
1945
2350
|
await writeArtifacts(oldModel, newModel, outDir, writeJson, filePath);
|
|
1946
2351
|
} catch (error) {
|
|
1947
2352
|
hadFailure = true;
|
|
@@ -1956,6 +2361,11 @@ async function parseXml(xml) {
|
|
|
1956
2361
|
const parser = new FlowParser(xml);
|
|
1957
2362
|
return parser.generateFlowDefinition();
|
|
1958
2363
|
}
|
|
2364
|
+
async function buildModelWithHeader(xml) {
|
|
2365
|
+
const model = buildModel(await parseXml(xml));
|
|
2366
|
+
model.header = await extractFlowHeader(xml);
|
|
2367
|
+
return model;
|
|
2368
|
+
}
|
|
1959
2369
|
function buildEmptyModel() {
|
|
1960
2370
|
return {
|
|
1961
2371
|
flowName: "(unknown)",
|
|
@@ -1974,9 +2384,15 @@ async function writeArtifacts(oldModel, newModel, outDir, writeJson, sourcePath)
|
|
|
1974
2384
|
writeFileSync(join(outDir, `${fileStem}.diff.json`), JSON.stringify(diff, null, 2), "utf8");
|
|
1975
2385
|
}
|
|
1976
2386
|
console.log(
|
|
1977
|
-
`${diff.flowName}: nodes ${diff.summary.addedNodes} added, ${diff.summary.removedNodes} deleted, ${diff.summary.modifiedNodes} modified; edges ${diff.summary.addedEdges} added, ${diff.summary.removedEdges} deleted`
|
|
2387
|
+
`${diff.flowName}: nodes ${diff.summary.addedNodes} added, ${diff.summary.removedNodes} deleted, ${diff.summary.modifiedNodes} modified; edges ${diff.summary.addedEdges} added, ${diff.summary.removedEdges} deleted${formatFlowAttributeSummary(diff.flowChanges)}`
|
|
1978
2388
|
);
|
|
1979
2389
|
}
|
|
2390
|
+
function formatFlowAttributeSummary(flowChanges) {
|
|
2391
|
+
if (!flowChanges || flowChanges.length === 0) {
|
|
2392
|
+
return "";
|
|
2393
|
+
}
|
|
2394
|
+
return `; flow attributes: ${flowChanges.length} changed (${flowChanges.map((change) => change.path).join(", ")})`;
|
|
2395
|
+
}
|
|
1980
2396
|
function discoverGitFiles(repo, from, to, pattern, changedOnly) {
|
|
1981
2397
|
const matcher = createPathMatcher(pattern);
|
|
1982
2398
|
if (!changedOnly) {
|