storymaps 1.2.2 → 1.2.4
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 +1 -1
- package/lib/core/yaml.js +6 -2
- package/lib/util/summary.js +178 -46
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -265,7 +265,7 @@ The CLI works with any storymaps.io instance:
|
|
|
265
265
|
storymaps init --site storymaps.yourcompany.com
|
|
266
266
|
```
|
|
267
267
|
|
|
268
|
-
See the [main project README](https://github.com/jackgleeson/
|
|
268
|
+
See the [main project README](https://github.com/jackgleeson/storymaps.io#self-hosting) for setup instructions.
|
|
269
269
|
|
|
270
270
|
## Licence
|
|
271
271
|
|
package/lib/core/yaml.js
CHANGED
|
@@ -175,13 +175,15 @@ export function jsonToYamlObj(data) {
|
|
|
175
175
|
if (stepTypes[i] === 'partial') return { partial: partialSlugMap[step.partialMapId] };
|
|
176
176
|
const name = resolvedNames[i];
|
|
177
177
|
const hasExtra = (step.color && step.color !== DEFAULT_STEP_COLOR)
|
|
178
|
-
|| step.url || step.
|
|
178
|
+
|| step.body || step.url || step.status || step.points != null || step.tags?.length;
|
|
179
179
|
if (!hasExtra) return name;
|
|
180
180
|
const obj = { name };
|
|
181
181
|
if (step.color && step.color !== DEFAULT_STEP_COLOR) obj.color = step.color;
|
|
182
182
|
if (step.body) obj.body = step.body;
|
|
183
183
|
if (step.url) obj.url = step.url;
|
|
184
|
+
if (step.status) obj.status = step.status;
|
|
184
185
|
if (step.points != null) obj.points = step.points;
|
|
186
|
+
if (step.tags?.length) obj.tags = [...step.tags];
|
|
185
187
|
return obj;
|
|
186
188
|
});
|
|
187
189
|
|
|
@@ -330,9 +332,11 @@ export function yamlObjToJson(obj) {
|
|
|
330
332
|
stepNameToIndex[rawName] = jsonSteps.length;
|
|
331
333
|
const s = { name: displayName, color };
|
|
332
334
|
if (typeof step === 'object' && step !== null) {
|
|
333
|
-
if (step.url) s.url = step.url;
|
|
334
335
|
if (step.body) s.body = step.body;
|
|
336
|
+
if (step.url) s.url = step.url;
|
|
337
|
+
if (step.status) s.status = step.status;
|
|
335
338
|
if (step.points != null) s.points = step.points;
|
|
339
|
+
if (step.tags?.length) s.tags = [...step.tags];
|
|
336
340
|
}
|
|
337
341
|
jsonSteps.push(s);
|
|
338
342
|
});
|
package/lib/util/summary.js
CHANGED
|
@@ -4,6 +4,114 @@
|
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import jsyaml from 'js-yaml';
|
|
6
6
|
|
|
7
|
+
// Fields compared on cards and steps that exist on both sides
|
|
8
|
+
const FIELDS = ['body', 'color', 'url', 'status', 'points', 'tags'];
|
|
9
|
+
|
|
10
|
+
const plural = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`;
|
|
11
|
+
|
|
12
|
+
// Slice names can span lines (e.g. "V1:\nBasic House"); keep each summary entry on one line
|
|
13
|
+
const oneLine = (s) => String(s ?? '').replace(/\s*\n\s*/g, ' ');
|
|
14
|
+
|
|
15
|
+
const changedFields = (a, b) =>
|
|
16
|
+
FIELDS.filter(f => JSON.stringify(a?.[f] ?? null) !== JSON.stringify(b?.[f] ?? null));
|
|
17
|
+
|
|
18
|
+
// True when the names both lists share appear in a different order
|
|
19
|
+
const reordered = (oldNames, newNames) => {
|
|
20
|
+
const common = oldNames.filter(n => newNames.includes(n));
|
|
21
|
+
const commonNew = newNames.filter(n => oldNames.includes(n));
|
|
22
|
+
return common.join('\n') !== commonNew.join('\n');
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Compare located cards ({ card, group, at }) across the whole map. Cards are
|
|
26
|
+
// paired by name in order, so a card that changes slice or step counts as moved
|
|
27
|
+
// rather than removed and added. Returns per-group summary lines.
|
|
28
|
+
function compareCards(oldItems, newItems) {
|
|
29
|
+
const stats = new Map();
|
|
30
|
+
const statsFor = (group) => {
|
|
31
|
+
if (!stats.has(group)) stats.set(group, { added: 0, removed: 0, moved: 0, updated: 0, fields: new Set() });
|
|
32
|
+
return stats.get(group);
|
|
33
|
+
};
|
|
34
|
+
// Report groups in their new order, then any that only exist on the old side
|
|
35
|
+
newItems.forEach(i => statsFor(i.group));
|
|
36
|
+
oldItems.forEach(i => statsFor(i.group));
|
|
37
|
+
|
|
38
|
+
const byName = (items) => {
|
|
39
|
+
const map = new Map();
|
|
40
|
+
for (const item of items) {
|
|
41
|
+
const name = item.card?.name ?? '';
|
|
42
|
+
if (!map.has(name)) map.set(name, []);
|
|
43
|
+
map.get(name).push(item);
|
|
44
|
+
}
|
|
45
|
+
return map;
|
|
46
|
+
};
|
|
47
|
+
const oldByName = byName(oldItems);
|
|
48
|
+
const newByName = byName(newItems);
|
|
49
|
+
|
|
50
|
+
for (const name of new Set([...oldByName.keys(), ...newByName.keys()])) {
|
|
51
|
+
const o = oldByName.get(name) || [];
|
|
52
|
+
const c = newByName.get(name) || [];
|
|
53
|
+
const paired = Math.min(o.length, c.length);
|
|
54
|
+
for (let i = 0; i < paired; i++) {
|
|
55
|
+
const s = statsFor(c[i].group);
|
|
56
|
+
if (o[i].group !== c[i].group || o[i].at !== c[i].at) s.moved++;
|
|
57
|
+
const fields = changedFields(o[i].card, c[i].card);
|
|
58
|
+
if (fields.length) {
|
|
59
|
+
s.updated++;
|
|
60
|
+
fields.forEach(f => s.fields.add(f));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
c.slice(paired).forEach(item => statsFor(item.group).added++);
|
|
64
|
+
o.slice(paired).forEach(item => statsFor(item.group).removed++);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const lines = [];
|
|
68
|
+
for (const [group, s] of stats) {
|
|
69
|
+
// Equal numbers of unmatched adds and removes means the cards were renamed
|
|
70
|
+
if (s.added && s.added === s.removed) {
|
|
71
|
+
lines.push(chalk.yellow(` ~ ${oneLine(group)}: ${plural(s.added, 'card')} renamed`));
|
|
72
|
+
} else {
|
|
73
|
+
if (s.added) lines.push(chalk.green(` + ${oneLine(group)}: ${plural(s.added, 'card')}`));
|
|
74
|
+
if (s.removed) lines.push(chalk.red(` - ${oneLine(group)}: ${plural(s.removed, 'card')}`));
|
|
75
|
+
}
|
|
76
|
+
if (s.moved) lines.push(chalk.yellow(` ~ ${oneLine(group)}: ${plural(s.moved, 'card')} moved`));
|
|
77
|
+
if (s.updated) {
|
|
78
|
+
lines.push(chalk.yellow(` ~ ${oneLine(group)}: ${plural(s.updated, 'card')} updated (${[...s.fields].join(', ')})`));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return lines;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function compareLegend(oldLegend, newLegend) {
|
|
85
|
+
const lines = [];
|
|
86
|
+
const key = e => `${e?.color ?? ''}\n${e?.label ?? ''}`;
|
|
87
|
+
const removed = [...oldLegend];
|
|
88
|
+
const added = [];
|
|
89
|
+
for (const entry of newLegend) {
|
|
90
|
+
const i = removed.findIndex(e => key(e) === key(entry));
|
|
91
|
+
if (i >= 0) removed.splice(i, 1);
|
|
92
|
+
else added.push(entry);
|
|
93
|
+
}
|
|
94
|
+
// Same color with a new label is a rename; same label with a new color is a recolor
|
|
95
|
+
for (const entry of [...added]) {
|
|
96
|
+
let i = removed.findIndex(e => e.color === entry.color);
|
|
97
|
+
if (i >= 0) {
|
|
98
|
+
lines.push(chalk.yellow(` ~ legend: ${removed[i].label} → ${entry.label}`));
|
|
99
|
+
} else if ((i = removed.findIndex(e => e.label === entry.label)) >= 0) {
|
|
100
|
+
lines.push(chalk.yellow(` ~ legend: ${entry.label} color changed`));
|
|
101
|
+
} else {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
removed.splice(i, 1);
|
|
105
|
+
added.splice(added.indexOf(entry), 1);
|
|
106
|
+
}
|
|
107
|
+
for (const e of added) lines.push(chalk.green(` + legend: ${e.label}`));
|
|
108
|
+
for (const e of removed) lines.push(chalk.red(` - legend: ${e.label}`));
|
|
109
|
+
if (!lines.length && reordered(oldLegend.map(key), newLegend.map(key))) {
|
|
110
|
+
lines.push(chalk.yellow(' ~ legend reordered'));
|
|
111
|
+
}
|
|
112
|
+
return lines;
|
|
113
|
+
}
|
|
114
|
+
|
|
7
115
|
export function changeSummary(oldYaml, newYaml) {
|
|
8
116
|
const old = jsyaml.load(oldYaml);
|
|
9
117
|
const cur = jsyaml.load(newYaml);
|
|
@@ -15,13 +123,15 @@ export function changeSummary(oldYaml, newYaml) {
|
|
|
15
123
|
}
|
|
16
124
|
|
|
17
125
|
// Steps (skip spacers and partial refs)
|
|
18
|
-
const
|
|
19
|
-
if (typeof s === 'string' && s !== 'spacer') return s;
|
|
20
|
-
if (typeof s === 'object' && s?.name) return s
|
|
126
|
+
const toStep = s => {
|
|
127
|
+
if (typeof s === 'string' && s !== 'spacer') return { name: s };
|
|
128
|
+
if (typeof s === 'object' && s?.name) return s;
|
|
21
129
|
return null;
|
|
22
130
|
};
|
|
23
|
-
const
|
|
24
|
-
const
|
|
131
|
+
const oldStepObjs = (old.steps || []).map(toStep).filter(Boolean);
|
|
132
|
+
const newStepObjs = (cur.steps || []).map(toStep).filter(Boolean);
|
|
133
|
+
const oldSteps = oldStepObjs.map(s => s.name);
|
|
134
|
+
const newSteps = newStepObjs.map(s => s.name);
|
|
25
135
|
const addedSteps = newSteps.filter(s => !oldSteps.includes(s));
|
|
26
136
|
const removedSteps = oldSteps.filter(s => !newSteps.includes(s));
|
|
27
137
|
if (addedSteps.length || removedSteps.length) {
|
|
@@ -31,55 +141,74 @@ export function changeSummary(oldYaml, newYaml) {
|
|
|
31
141
|
for (const s of addedSteps) lines.push(chalk.green(` + ${s}`));
|
|
32
142
|
for (const s of removedSteps) lines.push(chalk.red(` - ${s}`));
|
|
33
143
|
}
|
|
144
|
+
if (reordered(oldSteps, newSteps)) lines.push(chalk.yellow(' ~ steps reordered'));
|
|
145
|
+
const stepFields = new Set();
|
|
146
|
+
let updatedSteps = 0;
|
|
147
|
+
for (const step of newStepObjs) {
|
|
148
|
+
const before = oldStepObjs.find(s => s.name === step.name);
|
|
149
|
+
if (!before) continue;
|
|
150
|
+
const fields = changedFields(before, step);
|
|
151
|
+
if (fields.length) {
|
|
152
|
+
updatedSteps++;
|
|
153
|
+
fields.forEach(f => stepFields.add(f));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (updatedSteps) {
|
|
157
|
+
lines.push(chalk.yellow(` ~ steps: ${plural(updatedSteps, 'step')} updated (${[...stepFields].join(', ')})`));
|
|
158
|
+
}
|
|
34
159
|
|
|
35
160
|
// Slices
|
|
36
161
|
const oldSlices = (old.slices || []).map(s => s.name);
|
|
37
162
|
const newSlices = (cur.slices || []).map(s => s.name);
|
|
38
|
-
|
|
39
|
-
|
|
163
|
+
// A slice whose name changed in the same position is a rename, so its cards aren't reported as moved
|
|
164
|
+
const sliceRenames = new Map();
|
|
165
|
+
oldSlices.forEach((name, i) => {
|
|
166
|
+
const newName = newSlices[i];
|
|
167
|
+
if (newName !== undefined && !newSlices.includes(name) && !oldSlices.includes(newName)) {
|
|
168
|
+
sliceRenames.set(name, newName);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
const currentName = name => sliceRenames.get(name) ?? name;
|
|
172
|
+
for (const [from, to] of sliceRenames) {
|
|
173
|
+
lines.push(chalk.yellow(` ~ slice: ${oneLine(from)} → ${oneLine(to)}`));
|
|
40
174
|
}
|
|
41
|
-
|
|
42
|
-
|
|
175
|
+
const renamedTo = new Set(sliceRenames.values());
|
|
176
|
+
for (const s of newSlices.filter(s => !oldSlices.includes(s) && !renamedTo.has(s))) {
|
|
177
|
+
lines.push(chalk.green(` + slice: ${oneLine(s)}`));
|
|
178
|
+
}
|
|
179
|
+
for (const s of oldSlices.filter(s => !newSlices.includes(s) && !sliceRenames.has(s))) {
|
|
180
|
+
lines.push(chalk.red(` - slice: ${oneLine(s)}`));
|
|
181
|
+
}
|
|
182
|
+
if (reordered(oldSlices.map(currentName), newSlices)) lines.push(chalk.yellow(' ~ slices reordered'));
|
|
183
|
+
for (const slice of cur.slices || []) {
|
|
184
|
+
const before = (old.slices || []).find(s => currentName(s.name) === slice.name);
|
|
185
|
+
if (!before) continue;
|
|
186
|
+
if (!!before.collapsed !== !!slice.collapsed || before.closedReason !== slice.closedReason) {
|
|
187
|
+
const state = !slice.collapsed ? 'reopened' : slice.closedReason === 'closed' ? 'closed' : 'completed';
|
|
188
|
+
lines.push(chalk.yellow(` ~ slice: ${oneLine(slice.name)} ${state}`));
|
|
189
|
+
}
|
|
43
190
|
}
|
|
44
191
|
|
|
45
|
-
//
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
for (const
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
192
|
+
// Cards: users, activities and stories in every slice
|
|
193
|
+
const collectCards = (map, sliceName) => {
|
|
194
|
+
const items = [];
|
|
195
|
+
for (const group of ['users', 'activities']) {
|
|
196
|
+
for (const card of map[group] || []) items.push({ card, group, at: card.step });
|
|
197
|
+
}
|
|
198
|
+
for (const slice of map.slices || []) {
|
|
199
|
+
const group = sliceName(slice.name);
|
|
200
|
+
for (const [step, cards] of Object.entries(slice.stories || {})) {
|
|
201
|
+
if (Array.isArray(cards)) cards.forEach(card => items.push({ card, group, at: step }));
|
|
52
202
|
}
|
|
53
|
-
result[slice.name] = names;
|
|
54
203
|
}
|
|
55
|
-
return
|
|
204
|
+
return items;
|
|
56
205
|
};
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const newCounts = new Map();
|
|
64
|
-
for (const n of oldNames) oldCounts.set(n, (oldCounts.get(n) || 0) + 1);
|
|
65
|
-
for (const n of newNames) newCounts.set(n, (newCounts.get(n) || 0) + 1);
|
|
66
|
-
const allNames = new Set([...oldCounts.keys(), ...newCounts.keys()]);
|
|
67
|
-
let added = 0, removed = 0;
|
|
68
|
-
for (const n of allNames) {
|
|
69
|
-
const o = oldCounts.get(n) || 0;
|
|
70
|
-
const c = newCounts.get(n) || 0;
|
|
71
|
-
if (c > o) added += c - o;
|
|
72
|
-
if (o > c) removed += o - c;
|
|
73
|
-
}
|
|
74
|
-
if (added === 0 && removed === 0) continue;
|
|
75
|
-
const net = newNames.length - oldNames.length;
|
|
76
|
-
if (net === 0) {
|
|
77
|
-
const word = added === 1 ? 'card' : 'cards';
|
|
78
|
-
lines.push(chalk.yellow(` ~ ${sliceName}: ${added} ${word} renamed`));
|
|
79
|
-
} else {
|
|
80
|
-
if (added) lines.push(chalk.green(` + ${sliceName}: ${added} ${added === 1 ? 'card' : 'cards'}`));
|
|
81
|
-
if (removed) lines.push(chalk.red(` - ${sliceName}: ${removed} ${removed === 1 ? 'card' : 'cards'}`));
|
|
82
|
-
}
|
|
206
|
+
lines.push(...compareCards(collectCards(old, currentName), collectCards(cur, name => name)));
|
|
207
|
+
|
|
208
|
+
lines.push(...compareLegend(old.legend || [], cur.legend || []));
|
|
209
|
+
|
|
210
|
+
if (JSON.stringify(old.partials ?? null) !== JSON.stringify(cur.partials ?? null)) {
|
|
211
|
+
lines.push(chalk.yellow(' ~ partials updated'));
|
|
83
212
|
}
|
|
84
213
|
|
|
85
214
|
// Notes
|
|
@@ -96,9 +225,12 @@ export function changeSummary(oldYaml, newYaml) {
|
|
|
96
225
|
|
|
97
226
|
export function printChangeSummary(oldYaml, newYaml) {
|
|
98
227
|
const lines = changeSummary(oldYaml, newYaml);
|
|
99
|
-
if (lines.length
|
|
228
|
+
if (lines.length) {
|
|
229
|
+
lines.forEach(l => console.log(l));
|
|
230
|
+
} else if (oldYaml === newYaml) {
|
|
100
231
|
console.log(chalk.dim('No changes.'));
|
|
101
232
|
} else {
|
|
102
|
-
|
|
233
|
+
// Something differs that the summary doesn't describe (e.g. card order within a step)
|
|
234
|
+
console.log(chalk.yellow(` ~ changes not shown in summary, run ${chalk.cyan('storymaps diff --full')} to see them`));
|
|
103
235
|
}
|
|
104
236
|
}
|