@syntax-syllogism/flow-delta 0.2.1 → 0.3.0
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/dist/cli.js +618 -311
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -714,10 +714,10 @@ function classifyNode(oldNode, newNode) {
|
|
|
714
714
|
throw new Error("Cannot classify absent node");
|
|
715
715
|
}
|
|
716
716
|
if (!oldNode) {
|
|
717
|
-
return { id: newNode.id, type: newNode.type, label: newNode.label, status: "added" };
|
|
717
|
+
return { id: newNode.id, type: newNode.type, label: newNode.label, status: "added", after: newNode.properties };
|
|
718
718
|
}
|
|
719
719
|
if (!newNode) {
|
|
720
|
-
return { id: oldNode.id, type: oldNode.type, label: oldNode.label, status: "deleted" };
|
|
720
|
+
return { id: oldNode.id, type: oldNode.type, label: oldNode.label, status: "deleted", before: oldNode.properties };
|
|
721
721
|
}
|
|
722
722
|
const changes = [
|
|
723
723
|
...deepDiff(oldNode.label, newNode.label, "label"),
|
|
@@ -880,7 +880,37 @@ var PARAM_COLUMNS = [
|
|
|
880
880
|
{ path: "value", label: "Value" },
|
|
881
881
|
{ path: "assignToReference", label: "Assign To" }
|
|
882
882
|
];
|
|
883
|
+
var SCREEN_SETTING_PATHS = [
|
|
884
|
+
"allowBack",
|
|
885
|
+
"allowFinish",
|
|
886
|
+
"allowPause",
|
|
887
|
+
"showFooter",
|
|
888
|
+
"showHeader"
|
|
889
|
+
];
|
|
883
890
|
var SECTION_SCHEMAS = {
|
|
891
|
+
screen: [
|
|
892
|
+
{ name: "Screen Settings", paths: SCREEN_SETTING_PATHS, render: "lines" }
|
|
893
|
+
],
|
|
894
|
+
start: [
|
|
895
|
+
{
|
|
896
|
+
name: "Trigger",
|
|
897
|
+
paths: ["object", "triggerType", "recordTriggerType", "schedule", "filterLogic"],
|
|
898
|
+
render: "lines"
|
|
899
|
+
},
|
|
900
|
+
{ name: "Entry Conditions", paths: ["filters"], render: "table", columns: FILTER_COLUMNS },
|
|
901
|
+
{
|
|
902
|
+
name: "Scheduled Paths",
|
|
903
|
+
paths: ["scheduledPaths"],
|
|
904
|
+
render: "table",
|
|
905
|
+
columns: [
|
|
906
|
+
{ path: "name", label: "Name" },
|
|
907
|
+
{ path: "label", label: "Label" },
|
|
908
|
+
{ path: "offsetNumber", label: "Offset" },
|
|
909
|
+
{ path: "offsetUnit", label: "Unit" },
|
|
910
|
+
{ path: "timeSource", label: "Time Source" }
|
|
911
|
+
]
|
|
912
|
+
}
|
|
913
|
+
],
|
|
884
914
|
decision: [{
|
|
885
915
|
name: "Outcomes",
|
|
886
916
|
paths: ["rules"],
|
|
@@ -907,6 +937,7 @@ var SECTION_SCHEMAS = {
|
|
|
907
937
|
recordUpdate: [{ name: "Field Mappings", paths: ["inputAssignments"], render: "table", columns: MAPPING_COLUMNS }],
|
|
908
938
|
recordLookup: [{ name: "Filters", paths: ["filters"], render: "table", columns: FILTER_COLUMNS }],
|
|
909
939
|
recordDelete: [{ name: "Filters", paths: ["filters"], render: "table", columns: FILTER_COLUMNS }],
|
|
940
|
+
subflow: [{ name: "Parameters", paths: ["inputAssignments", "outputAssignments"], render: "table", columns: PARAM_COLUMNS }],
|
|
910
941
|
actionCall: [{ name: "Parameters", paths: ["inputParameters", "outputParameters"], render: "table", columns: PARAM_COLUMNS }],
|
|
911
942
|
apexPluginCall: [{ name: "Parameters", paths: ["inputParameters", "outputParameters"], render: "table", columns: PARAM_COLUMNS }],
|
|
912
943
|
wait: [{
|
|
@@ -919,12 +950,571 @@ var SECTION_SCHEMAS = {
|
|
|
919
950
|
{ path: "offsetUnit", label: "Unit" },
|
|
920
951
|
{ path: "resumeDateReference", label: "Resume Date" }
|
|
921
952
|
]
|
|
922
|
-
}]
|
|
953
|
+
}],
|
|
954
|
+
customError: [{
|
|
955
|
+
name: "Error Messages",
|
|
956
|
+
paths: ["customErrorMessages"],
|
|
957
|
+
render: "table",
|
|
958
|
+
columns: [
|
|
959
|
+
{ path: "name", label: "Name" },
|
|
960
|
+
{ path: "errorMessage", label: "Message" }
|
|
961
|
+
]
|
|
962
|
+
}],
|
|
963
|
+
transform: [
|
|
964
|
+
{
|
|
965
|
+
name: "Value Mappings",
|
|
966
|
+
paths: ["transformValues", "valueMappings"],
|
|
967
|
+
render: "table",
|
|
968
|
+
columns: [
|
|
969
|
+
{ path: "name", label: "Name" },
|
|
970
|
+
{ path: "sourceDataType", label: "Source Type" },
|
|
971
|
+
{ path: "targetDataType", label: "Target Type" },
|
|
972
|
+
{ path: "value", label: "Value" }
|
|
973
|
+
]
|
|
974
|
+
},
|
|
975
|
+
{
|
|
976
|
+
name: "Data Type Mappings",
|
|
977
|
+
paths: ["dataTypeMappings"],
|
|
978
|
+
render: "table",
|
|
979
|
+
columns: [
|
|
980
|
+
{ path: "name", label: "Name" },
|
|
981
|
+
{ path: "sourceDataType", label: "Source Type" },
|
|
982
|
+
{ path: "targetDataType", label: "Target Type" }
|
|
983
|
+
]
|
|
984
|
+
}
|
|
985
|
+
],
|
|
986
|
+
collectionProcessor: [
|
|
987
|
+
{ name: "Collection Settings", paths: ["collectionReference", "operation"], render: "lines" },
|
|
988
|
+
{ name: "Filters", paths: ["filters"], render: "table", columns: FILTER_COLUMNS },
|
|
989
|
+
{
|
|
990
|
+
name: "Sort Options",
|
|
991
|
+
paths: ["sortOptions"],
|
|
992
|
+
render: "table",
|
|
993
|
+
columns: [
|
|
994
|
+
{ path: "field", label: "Field" },
|
|
995
|
+
{ path: "sortOrder", label: "Order" }
|
|
996
|
+
]
|
|
997
|
+
}
|
|
998
|
+
],
|
|
999
|
+
orchestratedStage: [
|
|
1000
|
+
{ name: "Stage Settings", paths: ["stageLabel", "stageOrder"], render: "lines" }
|
|
1001
|
+
]
|
|
923
1002
|
};
|
|
924
1003
|
function getSectionSchemas(type) {
|
|
925
1004
|
return SECTION_SCHEMAS[type] ?? [];
|
|
926
1005
|
}
|
|
927
1006
|
|
|
1007
|
+
// src/render/snapshot-panel.ts
|
|
1008
|
+
var LONG_TEXT_THRESHOLD = 160;
|
|
1009
|
+
var COLLECTION_KEY_NAMES = [
|
|
1010
|
+
"assignmentItems",
|
|
1011
|
+
"choiceReferences",
|
|
1012
|
+
"choices",
|
|
1013
|
+
"conditions",
|
|
1014
|
+
"customErrorMessages",
|
|
1015
|
+
"dataTypeMappings",
|
|
1016
|
+
"fields",
|
|
1017
|
+
"filters",
|
|
1018
|
+
"inputAssignments",
|
|
1019
|
+
"inputParameters",
|
|
1020
|
+
"outputParameters",
|
|
1021
|
+
"outputAssignments",
|
|
1022
|
+
"processMetadataValues",
|
|
1023
|
+
"rules",
|
|
1024
|
+
"scheduledPaths",
|
|
1025
|
+
"sortOptions",
|
|
1026
|
+
"stageSteps",
|
|
1027
|
+
"steps",
|
|
1028
|
+
"storeOutputParameters",
|
|
1029
|
+
"transformValues",
|
|
1030
|
+
"valueMappings",
|
|
1031
|
+
"waitEvents"
|
|
1032
|
+
];
|
|
1033
|
+
var COLLECTION_KEYS = new Set(COLLECTION_KEY_NAMES);
|
|
1034
|
+
function renderNodePanelBody(node, schemas = []) {
|
|
1035
|
+
if (node.status === "unchanged") {
|
|
1036
|
+
return "<div class='empty'>No property changes.</div>";
|
|
1037
|
+
}
|
|
1038
|
+
const before = node.status === "added" ? void 0 : node.before;
|
|
1039
|
+
const after = node.status === "deleted" ? void 0 : node.after;
|
|
1040
|
+
const options = { showUnchanged: node.status !== "modified", nodeStatus: node.status };
|
|
1041
|
+
const sections = renderSections(before, after, schemas || [], options);
|
|
1042
|
+
if (!sections.length) {
|
|
1043
|
+
return "<div class='empty'>No configuration.</div>";
|
|
1044
|
+
}
|
|
1045
|
+
return sections.map((section, index) => {
|
|
1046
|
+
const open = node.status === "modified" || index === 0 ? " open" : "";
|
|
1047
|
+
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>";
|
|
1048
|
+
}).join("");
|
|
1049
|
+
}
|
|
1050
|
+
function renderSections(before, after, schemas, options) {
|
|
1051
|
+
const sections = [];
|
|
1052
|
+
const owned = /* @__PURE__ */ new Set();
|
|
1053
|
+
for (const schema of schemas) {
|
|
1054
|
+
for (const path of schema.paths) owned.add(path);
|
|
1055
|
+
const rendered = renderSchemaSection(before, after, schema, options);
|
|
1056
|
+
if (rendered) sections.push(rendered);
|
|
1057
|
+
}
|
|
1058
|
+
const keys = unionKeys(before, after).filter((key) => !ownsAny(owned, key));
|
|
1059
|
+
const settingsRows = [];
|
|
1060
|
+
let settingsCount = 0;
|
|
1061
|
+
const nestedSections = [];
|
|
1062
|
+
for (const key of keys) {
|
|
1063
|
+
const childBefore = getField(before, key);
|
|
1064
|
+
const childAfter = getField(after, key);
|
|
1065
|
+
const childChanged = hasChange(childBefore, childAfter, key);
|
|
1066
|
+
const scalar = scalarPair(childBefore, childAfter);
|
|
1067
|
+
if (scalar) {
|
|
1068
|
+
if (options.showUnchanged || childChanged) {
|
|
1069
|
+
settingsRows.push(scalarRow(key, scalar.before, scalar.after, statusOf(childBefore, childAfter), options.nodeStatus));
|
|
1070
|
+
}
|
|
1071
|
+
if (childChanged) settingsCount += 1;
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
const rendered = renderValue(getField(before, key), getField(after, key), {
|
|
1075
|
+
...options,
|
|
1076
|
+
label: humanizePath(key),
|
|
1077
|
+
pathKey: key,
|
|
1078
|
+
depth: 0
|
|
1079
|
+
});
|
|
1080
|
+
if (rendered.html) {
|
|
1081
|
+
nestedSections.push({ name: humanizePath(key), html: rendered.html, count: rendered.count });
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
if (settingsRows.length) {
|
|
1085
|
+
sections.push({
|
|
1086
|
+
name: "Settings",
|
|
1087
|
+
html: "<table class='snapshot-kv'><tbody>" + settingsRows.join("") + "</tbody></table>",
|
|
1088
|
+
count: settingsCount
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
sections.push(...nestedSections);
|
|
1092
|
+
return sections;
|
|
1093
|
+
}
|
|
1094
|
+
function renderSchemaSection(before, after, schema, options) {
|
|
1095
|
+
let rendered;
|
|
1096
|
+
if (schema.render === "table" && schema.columns) {
|
|
1097
|
+
rendered = renderTable(before, after, schema.paths, schema.columns, options);
|
|
1098
|
+
} else if (schema.render === "grouped-table" && schema.innerArray && schema.innerColumns) {
|
|
1099
|
+
rendered = renderGroupedTable(before, after, schema, options);
|
|
1100
|
+
} else {
|
|
1101
|
+
const sectionBefore = pickSectionObject(before, schema.paths);
|
|
1102
|
+
const sectionAfter = pickSectionObject(after, schema.paths);
|
|
1103
|
+
rendered = renderValue(sectionBefore, sectionAfter, {
|
|
1104
|
+
...options,
|
|
1105
|
+
label: schema.name,
|
|
1106
|
+
pathKey: schema.paths[0] || schema.name,
|
|
1107
|
+
depth: 0
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
if (!rendered.html) return null;
|
|
1111
|
+
return { name: schema.name, html: rendered.html, count: rendered.count };
|
|
1112
|
+
}
|
|
1113
|
+
function renderValue(before, after, ctx) {
|
|
1114
|
+
const changed = hasChange(before, after, ctx.pathKey);
|
|
1115
|
+
if (!ctx.showUnchanged && !changed) return { html: "", changed: false, count: 0 };
|
|
1116
|
+
const scalar = scalarPair(before, after);
|
|
1117
|
+
if (scalar) {
|
|
1118
|
+
return {
|
|
1119
|
+
html: renderLine(ctx.label, scalar.before, scalar.after, statusOf(before, after), ctx.nodeStatus),
|
|
1120
|
+
changed,
|
|
1121
|
+
count: changed ? 1 : 0
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
if (isCollection(before, after, ctx.pathKey)) {
|
|
1125
|
+
const rendered2 = renderArray(before, after, ctx);
|
|
1126
|
+
return { html: rendered2.html, changed, count: rendered2.count || (changed ? 1 : 0) };
|
|
1127
|
+
}
|
|
1128
|
+
const rendered = renderObject(before, after, ctx);
|
|
1129
|
+
return { html: rendered.html, changed, count: rendered.count || (changed ? 1 : 0) };
|
|
1130
|
+
}
|
|
1131
|
+
function renderObject(before, after, ctx) {
|
|
1132
|
+
const beforeObj = isObj(before) ? before : {};
|
|
1133
|
+
const afterObj = isObj(after) ? after : {};
|
|
1134
|
+
const keys = unionKeys(beforeObj, afterObj);
|
|
1135
|
+
const scalarRows = [];
|
|
1136
|
+
const nested = [];
|
|
1137
|
+
let count = 0;
|
|
1138
|
+
for (const key of keys) {
|
|
1139
|
+
const childBefore = getField(beforeObj, key);
|
|
1140
|
+
const childAfter = getField(afterObj, key);
|
|
1141
|
+
const childChanged = hasChange(childBefore, childAfter, key);
|
|
1142
|
+
const scalar = scalarPair(childBefore, childAfter);
|
|
1143
|
+
if (scalar) {
|
|
1144
|
+
if (ctx.showUnchanged || childChanged || ctx.depth > 0) {
|
|
1145
|
+
scalarRows.push(scalarRow(key, scalar.before, scalar.after, statusOf(childBefore, childAfter), ctx.nodeStatus));
|
|
1146
|
+
}
|
|
1147
|
+
if (childChanged) count += 1;
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1150
|
+
const rendered = renderValue(childBefore, childAfter, {
|
|
1151
|
+
...ctx,
|
|
1152
|
+
label: humanizePath(key),
|
|
1153
|
+
pathKey: key,
|
|
1154
|
+
depth: ctx.depth + 1
|
|
1155
|
+
});
|
|
1156
|
+
if (rendered.html) {
|
|
1157
|
+
nested.push("<div class='snapshot-nested'><div class='snapshot-nested-title'>" + escapeHtml(humanizePath(key)) + "</div>" + rendered.html + "</div>");
|
|
1158
|
+
count += rendered.count;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
const table = scalarRows.length ? "<table class='snapshot-kv'><tbody>" + scalarRows.join("") + "</tbody></table>" : "";
|
|
1162
|
+
return { html: table + nested.join(""), changed: count > 0, count };
|
|
1163
|
+
}
|
|
1164
|
+
function renderArray(before, after, ctx) {
|
|
1165
|
+
const beforeItems = arr(before);
|
|
1166
|
+
const afterItems = arr(after);
|
|
1167
|
+
if (looksLikeNameValueArray(beforeItems, afterItems)) {
|
|
1168
|
+
return renderTableFromItems(before, after, [""], [
|
|
1169
|
+
{ path: "name", label: "Name" },
|
|
1170
|
+
{ path: "value", label: "Value" }
|
|
1171
|
+
], ctx);
|
|
1172
|
+
}
|
|
1173
|
+
const max = Math.max(beforeItems.length, afterItems.length);
|
|
1174
|
+
const cards = [];
|
|
1175
|
+
let count = 0;
|
|
1176
|
+
for (let i = 0; i < max; i += 1) {
|
|
1177
|
+
const childBefore = beforeItems[i];
|
|
1178
|
+
const childAfter = afterItems[i];
|
|
1179
|
+
const childChanged = hasChange(childBefore, childAfter, "");
|
|
1180
|
+
if (!ctx.showUnchanged && !childChanged) continue;
|
|
1181
|
+
const title = itemTitle(childBefore, childAfter, i);
|
|
1182
|
+
const rendered = renderValue(childBefore, childAfter, {
|
|
1183
|
+
...ctx,
|
|
1184
|
+
label: title,
|
|
1185
|
+
pathKey: "",
|
|
1186
|
+
depth: ctx.depth + 1
|
|
1187
|
+
});
|
|
1188
|
+
if (!rendered.html) continue;
|
|
1189
|
+
count += rendered.count || (childChanged ? 1 : 0);
|
|
1190
|
+
cards.push("<div class='snapshot-card " + statusOf(childBefore, childAfter) + "'><div class='snapshot-card-title'>" + escapeHtml(title) + "</div>" + rendered.html + "</div>");
|
|
1191
|
+
}
|
|
1192
|
+
return { html: cards.join(""), changed: count > 0, count };
|
|
1193
|
+
}
|
|
1194
|
+
function renderTable(before, after, paths, columns, options) {
|
|
1195
|
+
return renderTableFromItems(before, after, paths, columns, { ...options, label: "", pathKey: paths[0] || "", depth: 0 });
|
|
1196
|
+
}
|
|
1197
|
+
function renderTableFromItems(before, after, paths, columns, options) {
|
|
1198
|
+
const rows = [];
|
|
1199
|
+
for (const path of paths) {
|
|
1200
|
+
const beforeItems = arr(resolvePath(before, path));
|
|
1201
|
+
const afterItems = arr(resolvePath(after, path));
|
|
1202
|
+
const max = Math.max(beforeItems.length, afterItems.length);
|
|
1203
|
+
for (let i = 0; i < max; i += 1) {
|
|
1204
|
+
const itemBefore = beforeItems[i];
|
|
1205
|
+
const itemAfter = afterItems[i];
|
|
1206
|
+
if (!options.showUnchanged && !hasChange(itemBefore, itemAfter, path)) continue;
|
|
1207
|
+
const cells = {};
|
|
1208
|
+
for (const col of columns) {
|
|
1209
|
+
const cellBefore = resolvePath(itemBefore, col.path);
|
|
1210
|
+
const cellAfter = resolvePath(itemAfter, col.path);
|
|
1211
|
+
cells[col.path] = {
|
|
1212
|
+
before: unwrapValue(cellBefore),
|
|
1213
|
+
after: unwrapValue(cellAfter),
|
|
1214
|
+
changed: hasChange(cellBefore, cellAfter, col.path)
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
rows.push({ idx: i, kind: statusOf(itemBefore, itemAfter), cells });
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
if (!rows.length) return { html: "", changed: false, count: 0 };
|
|
1221
|
+
const showChange = options.nodeStatus === "modified" && !options.suppressChangeColumn;
|
|
1222
|
+
const head = "<tr><th class='row-idx'>#</th>" + columns.map((col) => "<th>" + escapeHtml(col.label) + "</th>").join("") + (showChange ? "<th>Change</th>" : "") + "</tr>";
|
|
1223
|
+
const body = rows.map((row) => renderRow(row, columns, showChange, options.nodeStatus)).join("");
|
|
1224
|
+
return {
|
|
1225
|
+
html: "<table class='change-table'><thead>" + head + "</thead><tbody>" + body + "</tbody></table>",
|
|
1226
|
+
changed: rows.some((row) => row.kind !== "unchanged" || Object.values(row.cells).some((cell) => cell.changed)),
|
|
1227
|
+
count: rows.filter((row) => row.kind !== "unchanged" || Object.values(row.cells).some((cell) => cell.changed)).length
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
function renderRow(row, columns, showChange, nodeStatus) {
|
|
1231
|
+
const cells = columns.map((col) => {
|
|
1232
|
+
const cell = row.cells[col.path];
|
|
1233
|
+
return "<td>" + renderValueCell(cell.before, cell.after, cell.changed ? row.kind : "unchanged", nodeStatus) + "</td>";
|
|
1234
|
+
}).join("");
|
|
1235
|
+
const badge = showChange ? "<td><span class='change-kind " + row.kind + "'>" + capitalize(row.kind) + "</span></td>" : "";
|
|
1236
|
+
return "<tr class='row-" + row.kind + "'><td class='row-idx'>" + (row.idx + 1) + "</td>" + cells + badge + "</tr>";
|
|
1237
|
+
}
|
|
1238
|
+
function renderGroupedTable(before, after, schema, options) {
|
|
1239
|
+
const groups = [];
|
|
1240
|
+
let count = 0;
|
|
1241
|
+
for (const path of schema.paths) {
|
|
1242
|
+
const beforeGroups = arr(resolvePath(before, path));
|
|
1243
|
+
const afterGroups = arr(resolvePath(after, path));
|
|
1244
|
+
const max = Math.max(beforeGroups.length, afterGroups.length);
|
|
1245
|
+
for (let i = 0; i < max; i += 1) {
|
|
1246
|
+
const groupBefore = beforeGroups[i];
|
|
1247
|
+
const groupAfter = afterGroups[i];
|
|
1248
|
+
if (!options.showUnchanged && !hasChange(groupBefore, groupAfter, path)) continue;
|
|
1249
|
+
const kind = statusOf(groupBefore, groupAfter);
|
|
1250
|
+
const present = groupAfter !== void 0 ? groupAfter : groupBefore;
|
|
1251
|
+
const labelValue = schema.groupLabelPath && present ? resolvePath(present, schema.groupLabelPath) : void 0;
|
|
1252
|
+
const label = labelValue != null ? stripHtml(String(labelValue)) : "Outcome " + (i + 1);
|
|
1253
|
+
const scalarRows = renderGroupScalars(groupBefore, groupAfter, schema, options);
|
|
1254
|
+
const inner = renderTableFromItems(groupBefore, groupAfter, [schema.innerArray || ""], schema.innerColumns || [], {
|
|
1255
|
+
...options,
|
|
1256
|
+
suppressChangeColumn: kind !== "modified",
|
|
1257
|
+
label,
|
|
1258
|
+
pathKey: schema.innerArray || "",
|
|
1259
|
+
depth: 1
|
|
1260
|
+
}).html;
|
|
1261
|
+
const head = "<div class='group-head'><span class='change-kind " + kind + "'>" + capitalize(kind) + "</span><span class='group-label'>" + escapeHtml(label) + "</span></div>";
|
|
1262
|
+
groups.push("<div class='outcome-group'>" + head + scalarRows + inner + "</div>");
|
|
1263
|
+
count += 1;
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
return { html: groups.join(""), changed: count > 0, count };
|
|
1267
|
+
}
|
|
1268
|
+
function renderGroupScalars(before, after, schema, options) {
|
|
1269
|
+
const beforeObj = isObj(before) ? before : {};
|
|
1270
|
+
const afterObj = isObj(after) ? after : {};
|
|
1271
|
+
const skip = new Set([schema.innerArray, schema.groupLabelPath].filter(Boolean));
|
|
1272
|
+
const rows = unionKeys(beforeObj, afterObj).filter((key) => !skip.has(key)).map((key) => {
|
|
1273
|
+
const cellBefore = getField(beforeObj, key);
|
|
1274
|
+
const cellAfter = getField(afterObj, key);
|
|
1275
|
+
const scalar = scalarPair(cellBefore, cellAfter);
|
|
1276
|
+
if (!scalar) return "";
|
|
1277
|
+
if (!options.showUnchanged && !hasChange(cellBefore, cellAfter, key)) return "";
|
|
1278
|
+
return scalarRow(key, scalar.before, scalar.after, statusOf(cellBefore, cellAfter), options.nodeStatus);
|
|
1279
|
+
}).filter(Boolean).join("");
|
|
1280
|
+
return rows ? "<table class='snapshot-kv'><tbody>" + rows + "</tbody></table>" : "";
|
|
1281
|
+
}
|
|
1282
|
+
function scalarRow(key, before, after, kind, nodeStatus) {
|
|
1283
|
+
return "<tr><th>" + escapeHtml(humanizePath(key)) + "</th><td>" + renderValueCell(before, after, kind, nodeStatus) + "</td></tr>";
|
|
1284
|
+
}
|
|
1285
|
+
function renderLine(label, before, after, kind, nodeStatus) {
|
|
1286
|
+
const badge = nodeStatus === "modified" ? "<span class='change-kind " + kind + "'>" + capitalize(kind) + "</span>" : "";
|
|
1287
|
+
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>";
|
|
1288
|
+
}
|
|
1289
|
+
function renderValueCell(before, after, kind, nodeStatus) {
|
|
1290
|
+
if (nodeStatus === "added") return oneSidedVal(after !== void 0 ? after : before);
|
|
1291
|
+
if (nodeStatus === "deleted") return oneSidedVal(before !== void 0 ? before : after);
|
|
1292
|
+
if (kind === "added") return insVal(after !== void 0 ? after : before);
|
|
1293
|
+
if (kind === "removed") return delVal(before !== void 0 ? before : after);
|
|
1294
|
+
if (kind === "modified") return diffValue(before, after);
|
|
1295
|
+
return faintVal(after !== void 0 ? after : before);
|
|
1296
|
+
}
|
|
1297
|
+
function statusOf(before, after) {
|
|
1298
|
+
if (before === void 0) return "added";
|
|
1299
|
+
if (after === void 0) return "removed";
|
|
1300
|
+
return valuesEqual(before, after) ? "unchanged" : "modified";
|
|
1301
|
+
}
|
|
1302
|
+
function hasChange(before, after, pathKey = "") {
|
|
1303
|
+
if (before === void 0 || after === void 0) return before !== after;
|
|
1304
|
+
const scalar = scalarPair(before, after);
|
|
1305
|
+
if (scalar) return !valuesEqual(scalar.before, scalar.after);
|
|
1306
|
+
if (isCollection(before, after, pathKey)) {
|
|
1307
|
+
const beforeItems = arr(before);
|
|
1308
|
+
const afterItems = arr(after);
|
|
1309
|
+
const max = Math.max(beforeItems.length, afterItems.length);
|
|
1310
|
+
for (let i = 0; i < max; i += 1) {
|
|
1311
|
+
if (hasChange(beforeItems[i], afterItems[i], "")) return true;
|
|
1312
|
+
}
|
|
1313
|
+
return false;
|
|
1314
|
+
}
|
|
1315
|
+
const beforeObj = isObj(before) ? before : {};
|
|
1316
|
+
const afterObj = isObj(after) ? after : {};
|
|
1317
|
+
for (const key of unionKeys(beforeObj, afterObj)) {
|
|
1318
|
+
if (hasChange(beforeObj[key], afterObj[key], key)) return true;
|
|
1319
|
+
}
|
|
1320
|
+
return false;
|
|
1321
|
+
}
|
|
1322
|
+
function scalarPair(before, after) {
|
|
1323
|
+
const beforeUnwrapped = unwrapValue(before);
|
|
1324
|
+
const afterUnwrapped = unwrapValue(after);
|
|
1325
|
+
const beforeScalar = beforeUnwrapped == null || typeof beforeUnwrapped !== "object";
|
|
1326
|
+
const afterScalar = afterUnwrapped == null || typeof afterUnwrapped !== "object";
|
|
1327
|
+
return beforeScalar && afterScalar ? { before: beforeUnwrapped, after: afterUnwrapped } : null;
|
|
1328
|
+
}
|
|
1329
|
+
function arr(value) {
|
|
1330
|
+
if (value === void 0 || value === null) return [];
|
|
1331
|
+
return Array.isArray(value) ? value : [value];
|
|
1332
|
+
}
|
|
1333
|
+
function isObj(value) {
|
|
1334
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
1335
|
+
}
|
|
1336
|
+
function isCollection(before, after, pathKey) {
|
|
1337
|
+
return Array.isArray(before) || Array.isArray(after) || isCollectionKey(pathKey);
|
|
1338
|
+
}
|
|
1339
|
+
function isCollectionKey(key) {
|
|
1340
|
+
return COLLECTION_KEYS.has(key);
|
|
1341
|
+
}
|
|
1342
|
+
function unionKeys(before, after) {
|
|
1343
|
+
return [.../* @__PURE__ */ new Set([...Object.keys(before || {}), ...Object.keys(after || {})])].sort();
|
|
1344
|
+
}
|
|
1345
|
+
function ownsAny(paths, key) {
|
|
1346
|
+
for (const path of paths) {
|
|
1347
|
+
if (key === path || key.startsWith(path + ".") || key.startsWith(path + "[")) return true;
|
|
1348
|
+
}
|
|
1349
|
+
return false;
|
|
1350
|
+
}
|
|
1351
|
+
function pickSectionObject(obj, paths) {
|
|
1352
|
+
if (!obj) return void 0;
|
|
1353
|
+
const out = {};
|
|
1354
|
+
for (const path of paths) {
|
|
1355
|
+
const value = resolvePath(obj, path);
|
|
1356
|
+
if (value !== void 0) out[path] = value;
|
|
1357
|
+
}
|
|
1358
|
+
return Object.keys(out).length ? out : void 0;
|
|
1359
|
+
}
|
|
1360
|
+
function getField(obj, key) {
|
|
1361
|
+
return obj ? obj[key] : void 0;
|
|
1362
|
+
}
|
|
1363
|
+
function resolvePath(obj, path) {
|
|
1364
|
+
if (obj == null || !path) return obj;
|
|
1365
|
+
let cur = obj;
|
|
1366
|
+
const tokens = path.match(/[^.[\]]+|\[\d+\]/g) || [];
|
|
1367
|
+
for (const token of tokens) {
|
|
1368
|
+
if (cur == null || typeof cur !== "object") return void 0;
|
|
1369
|
+
cur = token[0] === "[" ? cur[Number(token.slice(1, -1))] : cur[token];
|
|
1370
|
+
}
|
|
1371
|
+
return cur;
|
|
1372
|
+
}
|
|
1373
|
+
function looksLikeNameValueArray(beforeItems, afterItems) {
|
|
1374
|
+
const items = [...beforeItems, ...afterItems].filter(isObj);
|
|
1375
|
+
return items.length > 0 && items.every((item) => "name" in item && "value" in item);
|
|
1376
|
+
}
|
|
1377
|
+
function itemTitle(before, after, index) {
|
|
1378
|
+
const item = isObj(after) ? after : before;
|
|
1379
|
+
if (item) {
|
|
1380
|
+
for (const key of ["fieldText", "label", "name", "fieldType", "field"]) {
|
|
1381
|
+
const value = unwrapValue(item[key]);
|
|
1382
|
+
if (value !== void 0 && value !== null && typeof value !== "object") {
|
|
1383
|
+
return stripHtml(String(value));
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
return "Item " + (index + 1);
|
|
1388
|
+
}
|
|
1389
|
+
function stripHtml(value) {
|
|
1390
|
+
return value.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
|
|
1391
|
+
}
|
|
1392
|
+
function capitalize(value) {
|
|
1393
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
1394
|
+
}
|
|
1395
|
+
function humanizePath(path) {
|
|
1396
|
+
const names = {
|
|
1397
|
+
assignmentItems: "Assignment item",
|
|
1398
|
+
choiceReferences: "Choice reference",
|
|
1399
|
+
choices: "Choice",
|
|
1400
|
+
conditions: "Condition",
|
|
1401
|
+
customErrorMessages: "Custom error message",
|
|
1402
|
+
dataTypeMappings: "Data type mapping",
|
|
1403
|
+
fields: "Field",
|
|
1404
|
+
filters: "Filter",
|
|
1405
|
+
inputAssignments: "Input assignment",
|
|
1406
|
+
inputParameters: "Input parameter",
|
|
1407
|
+
outputParameters: "Output parameter",
|
|
1408
|
+
processMetadataValues: "Process metadata value",
|
|
1409
|
+
rules: "Rule",
|
|
1410
|
+
scheduledPaths: "Scheduled path",
|
|
1411
|
+
storeOutputParameters: "Store output parameter",
|
|
1412
|
+
transformValues: "Transform value",
|
|
1413
|
+
valueMappings: "Value mapping",
|
|
1414
|
+
waitEvents: "Wait event"
|
|
1415
|
+
};
|
|
1416
|
+
return path.split(".").map((segment) => {
|
|
1417
|
+
const match = segment.match(/^([^[]+)(?:\[(\d+)\])?$/);
|
|
1418
|
+
if (!match) return segment;
|
|
1419
|
+
const name = names[match[1]] || match[1].replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (char) => char.toUpperCase());
|
|
1420
|
+
return match[2] === void 0 ? name : name + " " + (Number(match[2]) + 1);
|
|
1421
|
+
}).join(" > ");
|
|
1422
|
+
}
|
|
1423
|
+
function unwrapValue(value) {
|
|
1424
|
+
if (value === null || value === void 0 || typeof value !== "object" || Array.isArray(value)) return value;
|
|
1425
|
+
if ("elementReference" in value) return "{!" + value.elementReference + "}";
|
|
1426
|
+
const keys = Object.keys(value);
|
|
1427
|
+
if (keys.length === 1) {
|
|
1428
|
+
const key = keys[0];
|
|
1429
|
+
const obj = value;
|
|
1430
|
+
if (key === "stringValue" || key === "numberValue" || key === "dateValue" || key === "dateTimeValue") return obj[key];
|
|
1431
|
+
if (key === "booleanValue") return String(obj[key]) === "true" ? "true" : "false";
|
|
1432
|
+
}
|
|
1433
|
+
return value;
|
|
1434
|
+
}
|
|
1435
|
+
function valuesEqual(a, b) {
|
|
1436
|
+
if (a === b) return true;
|
|
1437
|
+
if (a && b && typeof a === "object" && typeof b === "object") return JSON.stringify(a) === JSON.stringify(b);
|
|
1438
|
+
return false;
|
|
1439
|
+
}
|
|
1440
|
+
function renderScalar(value) {
|
|
1441
|
+
if (value === void 0) return "<em>-</em>";
|
|
1442
|
+
if (value === null) return "<em>empty</em>";
|
|
1443
|
+
if (typeof value === "object") return "<pre>" + escapeHtml(JSON.stringify(value, null, 2)) + "</pre>";
|
|
1444
|
+
const text = String(value);
|
|
1445
|
+
if (text.length > LONG_TEXT_THRESHOLD || text.includes("\n")) {
|
|
1446
|
+
return "<pre>" + escapeHtml(text) + "</pre>";
|
|
1447
|
+
}
|
|
1448
|
+
return escapeHtml(text);
|
|
1449
|
+
}
|
|
1450
|
+
function insVal(value) {
|
|
1451
|
+
return "<span class='val ins'>" + renderScalar(value) + "</span>";
|
|
1452
|
+
}
|
|
1453
|
+
function delVal(value) {
|
|
1454
|
+
return "<span class='val del'>" + renderScalar(value) + "</span>";
|
|
1455
|
+
}
|
|
1456
|
+
function faintVal(value) {
|
|
1457
|
+
return "<span class='val faint'>" + renderScalar(value) + "</span>";
|
|
1458
|
+
}
|
|
1459
|
+
function oneSidedVal(value) {
|
|
1460
|
+
return "<span class='val one-sided'>" + renderScalar(value) + "</span>";
|
|
1461
|
+
}
|
|
1462
|
+
function diffValue(before, after) {
|
|
1463
|
+
before = unwrapValue(before);
|
|
1464
|
+
after = unwrapValue(after);
|
|
1465
|
+
if (before === void 0) return insVal(after);
|
|
1466
|
+
if (after === void 0) return delVal(before);
|
|
1467
|
+
return delVal(before) + " <span class='arrow'>-></span> " + insVal(after);
|
|
1468
|
+
}
|
|
1469
|
+
function escapeHtml(value) {
|
|
1470
|
+
return String(value).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1471
|
+
}
|
|
1472
|
+
function snapshotPanelClientScript() {
|
|
1473
|
+
const functions = [
|
|
1474
|
+
renderNodePanelBody,
|
|
1475
|
+
renderSections,
|
|
1476
|
+
renderSchemaSection,
|
|
1477
|
+
renderValue,
|
|
1478
|
+
renderObject,
|
|
1479
|
+
renderArray,
|
|
1480
|
+
renderTable,
|
|
1481
|
+
renderTableFromItems,
|
|
1482
|
+
renderRow,
|
|
1483
|
+
renderGroupedTable,
|
|
1484
|
+
renderGroupScalars,
|
|
1485
|
+
scalarRow,
|
|
1486
|
+
renderLine,
|
|
1487
|
+
renderValueCell,
|
|
1488
|
+
statusOf,
|
|
1489
|
+
hasChange,
|
|
1490
|
+
scalarPair,
|
|
1491
|
+
arr,
|
|
1492
|
+
isObj,
|
|
1493
|
+
isCollection,
|
|
1494
|
+
isCollectionKey,
|
|
1495
|
+
unionKeys,
|
|
1496
|
+
ownsAny,
|
|
1497
|
+
pickSectionObject,
|
|
1498
|
+
getField,
|
|
1499
|
+
resolvePath,
|
|
1500
|
+
looksLikeNameValueArray,
|
|
1501
|
+
itemTitle,
|
|
1502
|
+
stripHtml,
|
|
1503
|
+
capitalize,
|
|
1504
|
+
humanizePath,
|
|
1505
|
+
unwrapValue,
|
|
1506
|
+
valuesEqual,
|
|
1507
|
+
renderScalar,
|
|
1508
|
+
insVal,
|
|
1509
|
+
delVal,
|
|
1510
|
+
faintVal,
|
|
1511
|
+
oneSidedVal,
|
|
1512
|
+
diffValue,
|
|
1513
|
+
escapeHtml
|
|
1514
|
+
].map((fn) => fn.toString()).join("\n\n");
|
|
1515
|
+
return "const LONG_TEXT_THRESHOLD = " + LONG_TEXT_THRESHOLD + ";\nconst COLLECTION_KEYS = new Set(" + JSON.stringify(COLLECTION_KEY_NAMES) + ");\n\n" + functions;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
928
1518
|
// src/render/render-html.ts
|
|
929
1519
|
function renderHtml(layout) {
|
|
930
1520
|
const data = {
|
|
@@ -947,7 +1537,7 @@ function renderHtml(layout) {
|
|
|
947
1537
|
<head>
|
|
948
1538
|
<meta charset="utf-8" />
|
|
949
1539
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
950
|
-
<title>${
|
|
1540
|
+
<title>${escapeHtml2(layout.diff.flowName)}</title>
|
|
951
1541
|
<style>
|
|
952
1542
|
:root {
|
|
953
1543
|
color-scheme: light;
|
|
@@ -1035,6 +1625,11 @@ function renderHtml(layout) {
|
|
|
1035
1625
|
.panel.modified .badge { background: var(--modified-fill); color: var(--modified); }
|
|
1036
1626
|
.panel .empty { color: var(--muted); font-size: 13px; }
|
|
1037
1627
|
.detail-section { margin: 0 0 12px; border: 1px solid var(--border); border-radius: 8px; background: #fbfcfe; overflow: hidden; }
|
|
1628
|
+
.detail-section.snapshot-section { border-left-width: 3px; }
|
|
1629
|
+
.detail-section.snapshot-section.added { border-left-color: rgba(22, 163, 74, 0.5); }
|
|
1630
|
+
.detail-section.snapshot-section.deleted { border-left-color: rgba(220, 38, 38, 0.45); }
|
|
1631
|
+
.detail-section.snapshot-section.added summary { background: rgba(220, 252, 231, 0.35); }
|
|
1632
|
+
.detail-section.snapshot-section.deleted summary { background: rgba(254, 226, 226, 0.35); }
|
|
1038
1633
|
.detail-section summary { cursor: pointer; padding: 11px 13px; font-weight: 650; }
|
|
1039
1634
|
.detail-section[open] summary { border-bottom: 1px solid var(--border); }
|
|
1040
1635
|
.section-count { color: var(--muted); font-size: 11px; font-weight: 500; }
|
|
@@ -1048,8 +1643,9 @@ function renderHtml(layout) {
|
|
|
1048
1643
|
.val.ins { color: #14532d; background: var(--added-fill); }
|
|
1049
1644
|
.val.del { color: #7f1d1d; background: var(--deleted-fill); text-decoration: line-through; text-decoration-color: rgba(127, 29, 29, 0.5); }
|
|
1050
1645
|
.val.faint { color: var(--muted); background: transparent; padding-left: 0; }
|
|
1646
|
+
.val.one-sided { color: var(--text); background: transparent; padding-left: 0; }
|
|
1051
1647
|
.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; }
|
|
1648
|
+
.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
1649
|
.arrow { color: var(--faint); margin: 0 2px; }
|
|
1054
1650
|
/* scalar / fallback change lines */
|
|
1055
1651
|
.changes { list-style: none; padding: 0; margin: 0; }
|
|
@@ -1066,6 +1662,16 @@ function renderHtml(layout) {
|
|
|
1066
1662
|
.change-table .row-idx { color: var(--faint); font-variant-numeric: tabular-nums; width: 1%; white-space: nowrap; }
|
|
1067
1663
|
.change-table tr.row-added { background: rgba(22, 163, 74, 0.06); }
|
|
1068
1664
|
.change-table tr.row-removed { background: rgba(220, 38, 38, 0.05); }
|
|
1665
|
+
.snapshot-kv { width: 100%; border-collapse: collapse; font-size: 12px; margin-bottom: 10px; }
|
|
1666
|
+
.snapshot-kv th, .snapshot-kv td { padding: 6px 7px; border-bottom: 1px solid var(--border); text-align: left; vertical-align: top; overflow-wrap: anywhere; }
|
|
1667
|
+
.snapshot-kv th { color: var(--muted); width: 34%; font-weight: 650; }
|
|
1668
|
+
.snapshot-kv tr:last-child th, .snapshot-kv tr:last-child td { border-bottom: 0; }
|
|
1669
|
+
.snapshot-nested { border-left: 2px solid var(--border); padding-left: 10px; margin: 10px 0; }
|
|
1670
|
+
.snapshot-nested-title { color: var(--muted); font-size: 11px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; margin-bottom: 7px; }
|
|
1671
|
+
.snapshot-card { border: 1px dashed var(--border); border-radius: 7px; padding: 9px 10px; margin: 8px 0; background: var(--surface); }
|
|
1672
|
+
.snapshot-card.added { border-left: 2px solid rgba(22, 163, 74, 0.5); }
|
|
1673
|
+
.snapshot-card.removed { border-left: 2px solid rgba(220, 38, 38, 0.45); }
|
|
1674
|
+
.snapshot-card-title { font-size: 12px; font-weight: 700; margin-bottom: 7px; overflow-wrap: anywhere; }
|
|
1069
1675
|
/* outcome groups (decisions) */
|
|
1070
1676
|
.outcome-group { margin-bottom: 12px; padding: 10px 11px; border: 1px solid var(--border); border-radius: 7px; background: var(--surface); }
|
|
1071
1677
|
.outcome-group:last-child { margin-bottom: 0; }
|
|
@@ -1110,7 +1716,7 @@ function renderHtml(layout) {
|
|
|
1110
1716
|
<body>
|
|
1111
1717
|
<header>
|
|
1112
1718
|
<div>
|
|
1113
|
-
<h1>${
|
|
1719
|
+
<h1>${escapeHtml2(layout.diff.flowName)}</h1>
|
|
1114
1720
|
<div class="meta">${metaHtml}</div>
|
|
1115
1721
|
</div>
|
|
1116
1722
|
<div class="header-actions">
|
|
@@ -1178,14 +1784,6 @@ function renderHtml(layout) {
|
|
|
1178
1784
|
let dragStart = null;
|
|
1179
1785
|
let selectedNodeId = null;
|
|
1180
1786
|
|
|
1181
|
-
function escapeHtml(value) {
|
|
1182
|
-
return String(value)
|
|
1183
|
-
.replace(/&/g, "&")
|
|
1184
|
-
.replace(/</g, "<")
|
|
1185
|
-
.replace(/>/g, ">")
|
|
1186
|
-
.replace(/"/g, """);
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
1787
|
function isNodeVisible(node, mode) {
|
|
1190
1788
|
if (mode === "after") return node.status !== "deleted";
|
|
1191
1789
|
if (mode === "before") return node.status !== "added";
|
|
@@ -1311,237 +1909,10 @@ function renderHtml(layout) {
|
|
|
1311
1909
|
panel.className = "panel " + node.status;
|
|
1312
1910
|
panelTitle.textContent = node.label;
|
|
1313
1911
|
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, {});
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1391
|
-
// Labeled before \u2192 after lines, for scalar changes and the generic fallback.
|
|
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>";
|
|
1912
|
+
panelBody.innerHTML = renderNodePanelBody(node, DATA.sectionSchemas[node.type] || []);
|
|
1485
1913
|
}
|
|
1486
1914
|
|
|
1487
|
-
|
|
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
|
-
}
|
|
1915
|
+
${snapshotPanelClientScript()}
|
|
1545
1916
|
|
|
1546
1917
|
function selectNode(id) {
|
|
1547
1918
|
selectedNodeId = id;
|
|
@@ -1552,70 +1923,6 @@ function renderHtml(layout) {
|
|
|
1552
1923
|
if (node) updatePanel(node);
|
|
1553
1924
|
}
|
|
1554
1925
|
|
|
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
1926
|
function pickInitialNode(visibleNodeIds) {
|
|
1620
1927
|
return DATA.nodes.find((node) => visibleNodeIds.has(node.id) && node.status === "modified")
|
|
1621
1928
|
|| DATA.nodes.find((node) => visibleNodeIds.has(node.id) && node.status !== "unchanged")
|
|
@@ -1789,16 +2096,16 @@ function renderEdge(edge) {
|
|
|
1789
2096
|
return "";
|
|
1790
2097
|
}
|
|
1791
2098
|
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="${
|
|
2099
|
+
return `<path class="edge ${edge.kind} ${edge.status}" d="${d}" data-edge-id="${escapeHtml2(edge.id)}"></path>`;
|
|
1793
2100
|
}
|
|
1794
2101
|
function renderNode(node) {
|
|
1795
2102
|
const lines = wrapLabel(node.label, 22);
|
|
1796
2103
|
const lineHeight = 14;
|
|
1797
2104
|
const textY = node.height / 2 - (lines.length - 1) * lineHeight / 2 + 5;
|
|
1798
|
-
return `<g class="node ${node.status} type-${node.type}" data-node-id="${
|
|
2105
|
+
return `<g class="node ${node.status} type-${node.type}" data-node-id="${escapeHtml2(node.id)}" transform="translate(${node.x},${node.y})">
|
|
1799
2106
|
<rect width="${node.width}" height="${node.height}"></rect>
|
|
1800
2107
|
<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}">${
|
|
2108
|
+
${lines.map((line, index) => `<tspan x="${node.width / 2}" dy="${index === 0 ? 0 : lineHeight}">${escapeHtml2(line)}</tspan>`).join("")}
|
|
1802
2109
|
</text>
|
|
1803
2110
|
</g>`;
|
|
1804
2111
|
}
|
|
@@ -1820,7 +2127,7 @@ function wrapLabel(label, maxChars) {
|
|
|
1820
2127
|
}
|
|
1821
2128
|
return lines.length ? lines.slice(0, 3) : [label];
|
|
1822
2129
|
}
|
|
1823
|
-
function
|
|
2130
|
+
function escapeHtml2(value) {
|
|
1824
2131
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1825
2132
|
}
|
|
1826
2133
|
|