@syntax-syllogism/flow-delta 0.1.2 → 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/README.md +5 -0
- package/dist/cli.js +790 -78
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,6 +5,11 @@ A semantic, visual diff for Salesforce Flows: parse two versions of a
|
|
|
5
5
|
interactive HTML artifact (plus `diff.json`) that shows added / deleted /
|
|
6
6
|
modified / unchanged nodes and edges with per-property deltas.
|
|
7
7
|
|
|
8
|
+
Inspired by Google's [Flow Lens](https://github.com/google/flow-lens), with
|
|
9
|
+
a focus on making it work with GitLab's pipelines. We also opted for our own HTML output over plantuml, graphviz, or mermaid.
|
|
10
|
+
|
|
11
|
+
[Sample Gitlab Project with artifacts](https://gitlab.com/j.p.richter/flow-delta-example/-/merge_requests/1)
|
|
12
|
+
|
|
8
13
|
## Usage
|
|
9
14
|
|
|
10
15
|
Runs as a TypeScript CLI via `tsx` (no build step). See [docs/cli.md](docs/cli.md)
|
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"),
|
|
@@ -737,7 +737,9 @@ function classifyNode(oldNode, newNode) {
|
|
|
737
737
|
type: newNode.type,
|
|
738
738
|
label: newNode.label,
|
|
739
739
|
status: "modified",
|
|
740
|
-
changes
|
|
740
|
+
changes,
|
|
741
|
+
before: oldNode.properties,
|
|
742
|
+
after: newNode.properties
|
|
741
743
|
};
|
|
742
744
|
}
|
|
743
745
|
function classifyEdge(oldEdge, newEdge) {
|
|
@@ -863,6 +865,656 @@ function measureBounds(nodes) {
|
|
|
863
865
|
return { width: maxX + 40, height: maxY + 40 };
|
|
864
866
|
}
|
|
865
867
|
|
|
868
|
+
// src/render/section-schemas.ts
|
|
869
|
+
var MAPPING_COLUMNS = [
|
|
870
|
+
{ path: "field", label: "Field" },
|
|
871
|
+
{ path: "value", label: "Value" }
|
|
872
|
+
];
|
|
873
|
+
var FILTER_COLUMNS = [
|
|
874
|
+
{ path: "field", label: "Field" },
|
|
875
|
+
{ path: "operator", label: "Operator" },
|
|
876
|
+
{ path: "value", label: "Value" }
|
|
877
|
+
];
|
|
878
|
+
var PARAM_COLUMNS = [
|
|
879
|
+
{ path: "name", label: "Name" },
|
|
880
|
+
{ path: "value", label: "Value" },
|
|
881
|
+
{ path: "assignToReference", label: "Assign To" }
|
|
882
|
+
];
|
|
883
|
+
var SCREEN_SETTING_PATHS = [
|
|
884
|
+
"allowBack",
|
|
885
|
+
"allowFinish",
|
|
886
|
+
"allowPause",
|
|
887
|
+
"showFooter",
|
|
888
|
+
"showHeader"
|
|
889
|
+
];
|
|
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
|
+
],
|
|
914
|
+
decision: [{
|
|
915
|
+
name: "Outcomes",
|
|
916
|
+
paths: ["rules"],
|
|
917
|
+
render: "grouped-table",
|
|
918
|
+
groupLabelPath: "label",
|
|
919
|
+
innerArray: "conditions",
|
|
920
|
+
innerColumns: [
|
|
921
|
+
{ path: "leftValueReference", label: "Resource" },
|
|
922
|
+
{ path: "operator", label: "Operator" },
|
|
923
|
+
{ path: "rightValue", label: "Value" }
|
|
924
|
+
]
|
|
925
|
+
}],
|
|
926
|
+
assignment: [{
|
|
927
|
+
name: "Assignment Items",
|
|
928
|
+
paths: ["assignmentItems"],
|
|
929
|
+
render: "table",
|
|
930
|
+
columns: [
|
|
931
|
+
{ path: "assignToReference", label: "Variable" },
|
|
932
|
+
{ path: "operator", label: "Operator" },
|
|
933
|
+
{ path: "value", label: "Value" }
|
|
934
|
+
]
|
|
935
|
+
}],
|
|
936
|
+
recordCreate: [{ name: "Field Mappings", paths: ["inputAssignments"], render: "table", columns: MAPPING_COLUMNS }],
|
|
937
|
+
recordUpdate: [{ name: "Field Mappings", paths: ["inputAssignments"], render: "table", columns: MAPPING_COLUMNS }],
|
|
938
|
+
recordLookup: [{ name: "Filters", paths: ["filters"], render: "table", columns: FILTER_COLUMNS }],
|
|
939
|
+
recordDelete: [{ name: "Filters", paths: ["filters"], render: "table", columns: FILTER_COLUMNS }],
|
|
940
|
+
subflow: [{ name: "Parameters", paths: ["inputAssignments", "outputAssignments"], render: "table", columns: PARAM_COLUMNS }],
|
|
941
|
+
actionCall: [{ name: "Parameters", paths: ["inputParameters", "outputParameters"], render: "table", columns: PARAM_COLUMNS }],
|
|
942
|
+
apexPluginCall: [{ name: "Parameters", paths: ["inputParameters", "outputParameters"], render: "table", columns: PARAM_COLUMNS }],
|
|
943
|
+
wait: [{
|
|
944
|
+
name: "Wait Events",
|
|
945
|
+
paths: ["waitEvents"],
|
|
946
|
+
render: "table",
|
|
947
|
+
columns: [
|
|
948
|
+
{ path: "label", label: "Label" },
|
|
949
|
+
{ path: "offset", label: "Offset" },
|
|
950
|
+
{ path: "offsetUnit", label: "Unit" },
|
|
951
|
+
{ path: "resumeDateReference", label: "Resume Date" }
|
|
952
|
+
]
|
|
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
|
+
]
|
|
1002
|
+
};
|
|
1003
|
+
function getSectionSchemas(type) {
|
|
1004
|
+
return SECTION_SCHEMAS[type] ?? [];
|
|
1005
|
+
}
|
|
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
|
+
|
|
866
1518
|
// src/render/render-html.ts
|
|
867
1519
|
function renderHtml(layout) {
|
|
868
1520
|
const data = {
|
|
@@ -871,7 +1523,8 @@ function renderHtml(layout) {
|
|
|
871
1523
|
edges: layout.edges,
|
|
872
1524
|
width: layout.width,
|
|
873
1525
|
height: layout.height,
|
|
874
|
-
layouts: layout.views
|
|
1526
|
+
layouts: layout.views,
|
|
1527
|
+
sectionSchemas: Object.fromEntries(layout.diff.nodes.map((node) => [node.type, getSectionSchemas(node.type)]))
|
|
875
1528
|
};
|
|
876
1529
|
const json = JSON.stringify(data).replace(/</g, "\\u003c");
|
|
877
1530
|
const s = layout.diff.summary;
|
|
@@ -884,7 +1537,7 @@ function renderHtml(layout) {
|
|
|
884
1537
|
<head>
|
|
885
1538
|
<meta charset="utf-8" />
|
|
886
1539
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
887
|
-
<title>${
|
|
1540
|
+
<title>${escapeHtml2(layout.diff.flowName)}</title>
|
|
888
1541
|
<style>
|
|
889
1542
|
:root {
|
|
890
1543
|
color-scheme: light;
|
|
@@ -896,6 +1549,7 @@ function renderHtml(layout) {
|
|
|
896
1549
|
--modified: #d97706; --modified-fill: #fef3c7;
|
|
897
1550
|
--unchanged: #94a3b8; --unchanged-fill: #e9edf3;
|
|
898
1551
|
--edge: #64748b; --fault: #7c3aed;
|
|
1552
|
+
--panel-width: 34vw;
|
|
899
1553
|
}
|
|
900
1554
|
* { box-sizing: border-box; }
|
|
901
1555
|
body { margin: 0; height: 100vh; display: grid; grid-template-rows: auto 1fr; background: var(--bg); color: var(--text); font-size: 14px; line-height: 1.45; }
|
|
@@ -938,7 +1592,8 @@ function renderHtml(layout) {
|
|
|
938
1592
|
.legend .deleted::before { background: var(--deleted); }
|
|
939
1593
|
.legend .modified::before { background: var(--modified); }
|
|
940
1594
|
.legend .unchanged::before { background: var(--unchanged); }
|
|
941
|
-
main { display: grid; grid-template-columns: minmax(0, 1fr) clamp(
|
|
1595
|
+
main { position: relative; display: grid; grid-template-columns: minmax(0, 1fr) clamp(300px, var(--panel-width), 70vw); min-height: 0; }
|
|
1596
|
+
body.panel-collapsed main { grid-template-columns: minmax(0, 1fr) 0; }
|
|
942
1597
|
.canvas { position: relative; overflow: hidden; }
|
|
943
1598
|
svg {
|
|
944
1599
|
width: 100%; height: 100%; display: block;
|
|
@@ -946,22 +1601,83 @@ function renderHtml(layout) {
|
|
|
946
1601
|
radial-gradient(circle, #dfe5ee 1px, transparent 1.4px) -11px -11px / 22px 22px,
|
|
947
1602
|
linear-gradient(#ffffff, #f7f9fc);
|
|
948
1603
|
}
|
|
949
|
-
.panel { min-width: 0; border-left: 1px solid var(--border); background: var(--surface); padding: 20px; overflow-y: auto; overflow-x: hidden; }
|
|
1604
|
+
.panel { position: relative; min-width: 0; border-left: 1px solid var(--border); background: var(--surface); padding: 20px; overflow-y: auto; overflow-x: hidden; }
|
|
1605
|
+
body.panel-collapsed .panel { padding: 0; border-left: 0; overflow: hidden; }
|
|
1606
|
+
body.panel-collapsed .panel > *:not(.panel-resizer) { display: none; }
|
|
1607
|
+
/* drag-to-resize handle on the panel's left edge */
|
|
1608
|
+
.panel-resizer { position: absolute; left: 0; top: 0; bottom: 0; width: 9px; transform: translateX(-50%); cursor: col-resize; z-index: 6; touch-action: none; }
|
|
1609
|
+
.panel-resizer::before { content: ""; position: absolute; left: 50%; top: 0; bottom: 0; width: 1px; background: var(--border); transform: translateX(-50%); transition: background-color 0.12s ease, width 0.12s ease; }
|
|
1610
|
+
.panel-resizer:hover::before, .panel-resizer.dragging::before { background: #93c5fd; width: 3px; }
|
|
1611
|
+
body.panel-collapsed .panel-resizer { display: none; }
|
|
1612
|
+
.panel-head { display: flex; align-items: flex-start; gap: 8px; }
|
|
1613
|
+
.panel-head h2 { flex: 1; min-width: 0; }
|
|
1614
|
+
.panel-toggle, .panel-reopen { border: 1px solid var(--border); background: #f8fafc; color: var(--muted); border-radius: 7px; cursor: pointer; font: inherit; line-height: 1; }
|
|
1615
|
+
.panel-toggle { flex: none; width: 26px; height: 26px; font-size: 16px; display: grid; place-items: center; }
|
|
1616
|
+
.panel-toggle:hover, .panel-reopen:hover { color: var(--text); border-color: #93c5fd; background: #eff6ff; }
|
|
1617
|
+
.panel-toggle:focus-visible, .panel-reopen:focus-visible { outline: 2px solid #93c5fd; outline-offset: 2px; }
|
|
1618
|
+
/* floating tab to reopen the panel once collapsed */
|
|
1619
|
+
.panel-reopen { display: none; position: absolute; top: 14px; right: 14px; z-index: 7; padding: 7px 12px; font-size: 12px; font-weight: 600; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.12); }
|
|
1620
|
+
body.panel-collapsed .panel-reopen { display: inline-flex; align-items: center; gap: 6px; }
|
|
950
1621
|
.panel h2 { margin: 0 0 8px; font-size: 15px; font-weight: 650; letter-spacing: -0.01em; }
|
|
951
1622
|
.panel .badge { display: inline-block; padding: 3px 9px; border-radius: 999px; font-size: 11px; font-weight: 600; letter-spacing: 0.03em; background: #eef1f6; color: var(--muted); margin-bottom: 16px; }
|
|
952
1623
|
.panel.added .badge { background: var(--added-fill); color: var(--added); }
|
|
953
1624
|
.panel.deleted .badge { background: var(--deleted-fill); color: var(--deleted); }
|
|
954
1625
|
.panel.modified .badge { background: var(--modified-fill); color: var(--modified); }
|
|
955
1626
|
.panel .empty { color: var(--muted); font-size: 13px; }
|
|
956
|
-
.
|
|
957
|
-
.
|
|
958
|
-
.
|
|
959
|
-
.
|
|
960
|
-
.
|
|
961
|
-
.
|
|
962
|
-
.
|
|
963
|
-
.
|
|
964
|
-
.
|
|
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); }
|
|
1633
|
+
.detail-section summary { cursor: pointer; padding: 11px 13px; font-weight: 650; }
|
|
1634
|
+
.detail-section[open] summary { border-bottom: 1px solid var(--border); }
|
|
1635
|
+
.section-count { color: var(--muted); font-size: 11px; font-weight: 500; }
|
|
1636
|
+
.section-body { padding: 12px; overflow-x: auto; }
|
|
1637
|
+
.change-kind { display: inline-block; padding: 2px 7px; border-radius: 999px; font-size: 10px; font-weight: 700; letter-spacing: 0.02em; }
|
|
1638
|
+
.change-kind.added { color: var(--added); background: var(--added-fill); }
|
|
1639
|
+
.change-kind.modified { color: var(--modified); background: var(--modified-fill); }
|
|
1640
|
+
.change-kind.removed { color: var(--deleted); background: var(--deleted-fill); }
|
|
1641
|
+
/* git-diff value grammar */
|
|
1642
|
+
.val { font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-size: 12px; border-radius: 4px; padding: 1px 5px; overflow-wrap: anywhere; }
|
|
1643
|
+
.val.ins { color: #14532d; background: var(--added-fill); }
|
|
1644
|
+
.val.del { color: #7f1d1d; background: var(--deleted-fill); text-decoration: line-through; text-decoration-color: rgba(127, 29, 29, 0.5); }
|
|
1645
|
+
.val.faint { color: var(--muted); background: transparent; padding-left: 0; }
|
|
1646
|
+
.val.one-sided { color: var(--text); background: transparent; padding-left: 0; }
|
|
1647
|
+
.val em { font-style: normal; color: var(--faint); }
|
|
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; }
|
|
1649
|
+
.arrow { color: var(--faint); margin: 0 2px; }
|
|
1650
|
+
/* scalar / fallback change lines */
|
|
1651
|
+
.changes { list-style: none; padding: 0; margin: 0; }
|
|
1652
|
+
.changes li { min-width: 0; margin-bottom: 9px; }
|
|
1653
|
+
.changes li:last-child { margin-bottom: 0; }
|
|
1654
|
+
.change-line-head { display: flex; align-items: center; gap: 7px; margin-bottom: 4px; }
|
|
1655
|
+
.change-line-label { font-weight: 600; font-size: 12px; }
|
|
1656
|
+
.change-line-value { padding-left: 2px; }
|
|
1657
|
+
/* item tables (field mappings, conditions, filters, parameters) */
|
|
1658
|
+
.change-table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
|
1659
|
+
.change-table th, .change-table td { padding: 7px 8px; border-bottom: 1px solid var(--border); text-align: left; vertical-align: top; overflow-wrap: anywhere; }
|
|
1660
|
+
.change-table th { color: var(--muted); font-size: 10.5px; letter-spacing: 0.04em; text-transform: uppercase; }
|
|
1661
|
+
.change-table tr:last-child td { border-bottom: 0; }
|
|
1662
|
+
.change-table .row-idx { color: var(--faint); font-variant-numeric: tabular-nums; width: 1%; white-space: nowrap; }
|
|
1663
|
+
.change-table tr.row-added { background: rgba(22, 163, 74, 0.06); }
|
|
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; }
|
|
1675
|
+
/* outcome groups (decisions) */
|
|
1676
|
+
.outcome-group { margin-bottom: 12px; padding: 10px 11px; border: 1px solid var(--border); border-radius: 7px; background: var(--surface); }
|
|
1677
|
+
.outcome-group:last-child { margin-bottom: 0; }
|
|
1678
|
+
.group-head { display: flex; align-items: center; gap: 7px; margin-bottom: 8px; }
|
|
1679
|
+
.group-label { font-weight: 700; font-size: 12.5px; }
|
|
1680
|
+
.outcome-group .changes { margin-bottom: 8px; }
|
|
965
1681
|
.edge { fill: none; }
|
|
966
1682
|
.edge.unchanged { stroke-width: 1.4; opacity: 0.55; }
|
|
967
1683
|
.edge.normal.unchanged { stroke: var(--edge); marker-end: url(#arrow-normal); }
|
|
@@ -981,8 +1697,18 @@ function renderHtml(layout) {
|
|
|
981
1697
|
.node.unchanged rect { fill: var(--unchanged-fill); stroke: var(--unchanged); }
|
|
982
1698
|
.node.selected rect { stroke-width: 3; filter: drop-shadow(0 2px 6px rgba(15, 23, 42, 0.18)); }
|
|
983
1699
|
.node-label { white-space: pre; }
|
|
1700
|
+
/* Type-based styling \u2014 SHAPE & stroke pattern only. Status (added/deleted/
|
|
1701
|
+
modified/unchanged) remains the sole authority on fill/stroke COLOR, so
|
|
1702
|
+
the legend stays reliable. Start/end stand out via a pill shape, heavier
|
|
1703
|
+
stroke, and a neutral emphasis halo \u2014 never via a status-like color. */
|
|
1704
|
+
.node.type-start rect, .node.type-end rect { rx: 24; ry: 24; stroke-width: 2.5; filter: drop-shadow(0 0 0 3px rgba(15, 23, 42, 0.16)) drop-shadow(0 1px 3px rgba(15, 23, 42, 0.18)); }
|
|
1705
|
+
.node.type-recordLookup rect, .node.type-recordCreate rect, .node.type-recordUpdate rect, .node.type-recordDelete rect { rx: 4; ry: 4; }
|
|
1706
|
+
.node.type-screen rect { rx: 6; ry: 6; }
|
|
1707
|
+
.node.type-subflow rect { stroke-width: 2.5; }
|
|
1708
|
+
.node.type-loop rect { stroke-dasharray: 2 3; }
|
|
1709
|
+
.node.type-wait rect { stroke-dasharray: 4 4; }
|
|
1710
|
+
.node.selected.type-start rect, .node.selected.type-end rect { stroke-width: 3.5; filter: drop-shadow(0 0 0 5px rgba(15, 23, 42, 0.22)) drop-shadow(0 2px 6px rgba(15, 23, 42, 0.2)); }
|
|
984
1711
|
@media (max-width: 1000px) {
|
|
985
|
-
main { grid-template-columns: minmax(0, 1fr) minmax(320px, 42vw); }
|
|
986
1712
|
.panel { padding: 16px; }
|
|
987
1713
|
}
|
|
988
1714
|
</style>
|
|
@@ -990,7 +1716,7 @@ function renderHtml(layout) {
|
|
|
990
1716
|
<body>
|
|
991
1717
|
<header>
|
|
992
1718
|
<div>
|
|
993
|
-
<h1>${
|
|
1719
|
+
<h1>${escapeHtml2(layout.diff.flowName)}</h1>
|
|
994
1720
|
<div class="meta">${metaHtml}</div>
|
|
995
1721
|
</div>
|
|
996
1722
|
<div class="header-actions">
|
|
@@ -1032,10 +1758,15 @@ function renderHtml(layout) {
|
|
|
1032
1758
|
</svg>
|
|
1033
1759
|
</div>
|
|
1034
1760
|
<aside class="panel">
|
|
1035
|
-
<
|
|
1761
|
+
<div class="panel-resizer" id="panel-resizer" role="separator" aria-orientation="vertical" aria-label="Resize panel"></div>
|
|
1762
|
+
<div class="panel-head">
|
|
1763
|
+
<h2 id="panel-title">Select a node</h2>
|
|
1764
|
+
<button type="button" id="panel-toggle" class="panel-toggle" aria-label="Collapse panel" title="Collapse panel">\u203A</button>
|
|
1765
|
+
</div>
|
|
1036
1766
|
<div id="panel-badge" class="badge">No selection</div>
|
|
1037
1767
|
<div id="panel-body">Click a node to inspect its properties.</div>
|
|
1038
1768
|
</aside>
|
|
1769
|
+
<button type="button" id="panel-reopen" class="panel-reopen" aria-label="Show details panel" title="Show details">\u2039 Details</button>
|
|
1039
1770
|
</main>
|
|
1040
1771
|
<script>
|
|
1041
1772
|
const DATA = ${json};
|
|
@@ -1053,14 +1784,6 @@ function renderHtml(layout) {
|
|
|
1053
1784
|
let dragStart = null;
|
|
1054
1785
|
let selectedNodeId = null;
|
|
1055
1786
|
|
|
1056
|
-
function escapeHtml(value) {
|
|
1057
|
-
return String(value)
|
|
1058
|
-
.replace(/&/g, "&")
|
|
1059
|
-
.replace(/</g, "<")
|
|
1060
|
-
.replace(/>/g, ">")
|
|
1061
|
-
.replace(/"/g, """);
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
1787
|
function isNodeVisible(node, mode) {
|
|
1065
1788
|
if (mode === "after") return node.status !== "deleted";
|
|
1066
1789
|
if (mode === "before") return node.status !== "added";
|
|
@@ -1186,16 +1909,11 @@ function renderHtml(layout) {
|
|
|
1186
1909
|
panel.className = "panel " + node.status;
|
|
1187
1910
|
panelTitle.textContent = node.label;
|
|
1188
1911
|
panelBadge.textContent = node.status.toUpperCase();
|
|
1189
|
-
|
|
1190
|
-
panelBody.innerHTML = changes.length
|
|
1191
|
-
? "<ul class='changes'>" + changes.map(formatChange).join("") + "</ul>"
|
|
1192
|
-
: node.status === "added"
|
|
1193
|
-
? "<div class='empty'>This node was added in the new version.</div>"
|
|
1194
|
-
: node.status === "deleted"
|
|
1195
|
-
? "<div class='empty'>This node was removed in the new version.</div>"
|
|
1196
|
-
: "<div class='empty'>No property changes.</div>";
|
|
1912
|
+
panelBody.innerHTML = renderNodePanelBody(node, DATA.sectionSchemas[node.type] || []);
|
|
1197
1913
|
}
|
|
1198
1914
|
|
|
1915
|
+
${snapshotPanelClientScript()}
|
|
1916
|
+
|
|
1199
1917
|
function selectNode(id) {
|
|
1200
1918
|
selectedNodeId = id;
|
|
1201
1919
|
document.querySelectorAll(".node").forEach((el) => el.classList.remove("selected"));
|
|
@@ -1205,45 +1923,6 @@ function renderHtml(layout) {
|
|
|
1205
1923
|
if (node) updatePanel(node);
|
|
1206
1924
|
}
|
|
1207
1925
|
|
|
1208
|
-
function formatChange(change) {
|
|
1209
|
-
const beforeMissing = change.before === undefined;
|
|
1210
|
-
const afterMissing = change.after === undefined;
|
|
1211
|
-
const action = beforeMissing ? "Added" : afterMissing ? "Removed" : "Changed";
|
|
1212
|
-
const values = beforeMissing
|
|
1213
|
-
? formatLabeledValue("New value", change.after)
|
|
1214
|
-
: afterMissing
|
|
1215
|
-
? formatLabeledValue("Previous value", change.before)
|
|
1216
|
-
: formatLabeledValue("Before", change.before) + formatLabeledValue("After", change.after);
|
|
1217
|
-
return "<li><div class='change-title'>" + action + ": " + escapeHtml(humanizePath(change.path)) + "</div>"
|
|
1218
|
-
+ "<div class='change-path'>Metadata path: <code>" + escapeHtml(change.path) + "</code></div>"
|
|
1219
|
-
+ "<div class='change-values'>" + values + "</div></li>";
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
function formatLabeledValue(label, value) {
|
|
1223
|
-
return "<div class='change-value'><div class='change-value-label'>" + label + "</div>" + formatValue(value) + "</div>";
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
function humanizePath(path) {
|
|
1227
|
-
const names = {
|
|
1228
|
-
assignmentItems: "Assignment item",
|
|
1229
|
-
conditions: "Condition",
|
|
1230
|
-
rules: "Rule",
|
|
1231
|
-
};
|
|
1232
|
-
return path.split(".").map((segment) => {
|
|
1233
|
-
const match = segment.match(/^([^[]+)(?:\\[(\\d+)\\])?$/);
|
|
1234
|
-
if (!match) return segment;
|
|
1235
|
-
const name = names[match[1]] || match[1].replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (char) => char.toUpperCase());
|
|
1236
|
-
return match[2] === undefined ? name : name + " " + (Number(match[2]) + 1);
|
|
1237
|
-
}).join(" \u203A ");
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
function formatValue(value) {
|
|
1241
|
-
if (value === undefined) return "<em>Not present</em>";
|
|
1242
|
-
if (value === null) return "<em>Empty value</em>";
|
|
1243
|
-
if (typeof value === "object") return "<pre>" + escapeHtml(JSON.stringify(value, null, 2)) + "</pre>";
|
|
1244
|
-
return "<code>" + escapeHtml(String(value)) + "</code>";
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
1926
|
function pickInitialNode(visibleNodeIds) {
|
|
1248
1927
|
return DATA.nodes.find((node) => visibleNodeIds.has(node.id) && node.status === "modified")
|
|
1249
1928
|
|| DATA.nodes.find((node) => visibleNodeIds.has(node.id) && node.status !== "unchanged")
|
|
@@ -1356,6 +2035,39 @@ function renderHtml(layout) {
|
|
|
1356
2035
|
});
|
|
1357
2036
|
});
|
|
1358
2037
|
|
|
2038
|
+
// Panel: collapse to focus on the canvas, and drag the left edge to resize.
|
|
2039
|
+
const panelToggle = document.getElementById("panel-toggle");
|
|
2040
|
+
const panelReopen = document.getElementById("panel-reopen");
|
|
2041
|
+
const panelResizer = document.getElementById("panel-resizer");
|
|
2042
|
+
|
|
2043
|
+
function setPanelCollapsed(collapsed) {
|
|
2044
|
+
document.body.classList.toggle("panel-collapsed", collapsed);
|
|
2045
|
+
if (panelToggle) panelToggle.setAttribute("aria-expanded", collapsed ? "false" : "true");
|
|
2046
|
+
}
|
|
2047
|
+
if (panelToggle) panelToggle.addEventListener("click", () => setPanelCollapsed(true));
|
|
2048
|
+
if (panelReopen) panelReopen.addEventListener("click", () => setPanelCollapsed(false));
|
|
2049
|
+
|
|
2050
|
+
if (panelResizer) {
|
|
2051
|
+
let resizeStart = null;
|
|
2052
|
+
panelResizer.addEventListener("pointerdown", (event) => {
|
|
2053
|
+
resizeStart = { x: event.clientX, width: panel.getBoundingClientRect().width };
|
|
2054
|
+
panelResizer.classList.add("dragging");
|
|
2055
|
+
panelResizer.setPointerCapture(event.pointerId);
|
|
2056
|
+
event.preventDefault();
|
|
2057
|
+
});
|
|
2058
|
+
panelResizer.addEventListener("pointermove", (event) => {
|
|
2059
|
+
if (!resizeStart) return;
|
|
2060
|
+
// The handle is on the panel's left edge, so dragging left widens it.
|
|
2061
|
+
const next = resizeStart.width - (event.clientX - resizeStart.x);
|
|
2062
|
+
const max = Math.max(320, window.innerWidth - 360);
|
|
2063
|
+
const clamped = Math.max(300, Math.min(max, next));
|
|
2064
|
+
document.documentElement.style.setProperty("--panel-width", clamped + "px");
|
|
2065
|
+
});
|
|
2066
|
+
const endResize = () => { resizeStart = null; panelResizer.classList.remove("dragging"); };
|
|
2067
|
+
panelResizer.addEventListener("pointerup", endResize);
|
|
2068
|
+
panelResizer.addEventListener("pointercancel", endResize);
|
|
2069
|
+
}
|
|
2070
|
+
|
|
1359
2071
|
applyView("all");
|
|
1360
2072
|
</script>
|
|
1361
2073
|
</body>
|
|
@@ -1384,16 +2096,16 @@ function renderEdge(edge) {
|
|
|
1384
2096
|
return "";
|
|
1385
2097
|
}
|
|
1386
2098
|
const d = points.map((point, index) => `${index === 0 ? "M" : "L"} ${point.x} ${point.y}`).join(" ");
|
|
1387
|
-
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>`;
|
|
1388
2100
|
}
|
|
1389
2101
|
function renderNode(node) {
|
|
1390
2102
|
const lines = wrapLabel(node.label, 22);
|
|
1391
2103
|
const lineHeight = 14;
|
|
1392
2104
|
const textY = node.height / 2 - (lines.length - 1) * lineHeight / 2 + 5;
|
|
1393
|
-
return `<g class="node ${node.status}" 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})">
|
|
1394
2106
|
<rect width="${node.width}" height="${node.height}"></rect>
|
|
1395
2107
|
<text x="${node.width / 2}" y="${textY}" text-anchor="middle" class="node-label">
|
|
1396
|
-
${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("")}
|
|
1397
2109
|
</text>
|
|
1398
2110
|
</g>`;
|
|
1399
2111
|
}
|
|
@@ -1415,7 +2127,7 @@ function wrapLabel(label, maxChars) {
|
|
|
1415
2127
|
}
|
|
1416
2128
|
return lines.length ? lines.slice(0, 3) : [label];
|
|
1417
2129
|
}
|
|
1418
|
-
function
|
|
2130
|
+
function escapeHtml2(value) {
|
|
1419
2131
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1420
2132
|
}
|
|
1421
2133
|
|