@webpieces/nx-webpieces-rules 0.4.978 → 0.4.980
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 +32 -21
- package/package.json +7 -7
- package/src/lib/graph-filter.client.d.ts +25 -11
- package/src/lib/graph-filter.client.js +65 -29
- package/src/lib/graph-filter.client.js.map +1 -1
- package/src/lib/graph-multi-select.client.d.ts +57 -0
- package/src/lib/graph-multi-select.client.js +222 -0
- package/src/lib/graph-multi-select.client.js.map +1 -0
- package/src/lib/graph-node-menu.d.ts +1 -1
- package/src/lib/graph-node-menu.js +1 -1
- package/src/lib/graph-node-menu.js.map +1 -1
- package/src/lib/graph-page-shell.d.ts +23 -2
- package/src/lib/graph-page-shell.js +63 -13
- package/src/lib/graph-page-shell.js.map +1 -1
- package/src/lib/graph-page-styles.d.ts +6 -0
- package/src/lib/graph-page-styles.js +38 -10
- package/src/lib/graph-page-styles.js.map +1 -1
- package/src/lib/graph-render-model.js +2 -2
- package/src/lib/graph-render-model.js.map +1 -1
- package/src/lib/graph-visualizer.client.d.ts +39 -19
- package/src/lib/graph-visualizer.client.js +96 -110
- package/src/lib/graph-visualizer.client.js.map +1 -1
- package/src/lib/graph-visualizer.d.ts +7 -11
- package/src/lib/graph-visualizer.js +14 -32
- package/src/lib/graph-visualizer.js.map +1 -1
- package/src/lib/runtime-html-page.d.ts +6 -1
- package/src/lib/runtime-html-page.js +13 -6
- package/src/lib/runtime-html-page.js.map +1 -1
- package/src/lib/runtime-visualizer.client.js +3 -1
- package/src/lib/runtime-visualizer.client.js.map +1 -1
- package/src/lib/runtime-visualizer.js +5 -3
- package/src/lib/runtime-visualizer.js.map +1 -1
|
@@ -98,7 +98,7 @@ class WpProjectFilter {
|
|
|
98
98
|
return selected.size === 0 || carried.some((value) => selected.has(value));
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
|
-
/** Architecture interactions bind to each new SVG; logical
|
|
101
|
+
/** Architecture interactions bind to each new SVG; logical Focus/Keep state belongs to the page. */
|
|
102
102
|
class GraphHighlighter {
|
|
103
103
|
svg;
|
|
104
104
|
chain;
|
|
@@ -130,13 +130,16 @@ class GraphHighlighter {
|
|
|
130
130
|
el.classList.remove('wp-focus', 'wp-neighbor', 'wp-hl', 'wp-locked');
|
|
131
131
|
});
|
|
132
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Light the UNION of every focused box's chains, plus the hovered box's. A focused box that is not
|
|
135
|
+
* drawn right now is suspended, its otherwise visible relatives included, until it is drawn again.
|
|
136
|
+
*/
|
|
133
137
|
relight() {
|
|
134
138
|
this.clear();
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
this.nodeByName.get(
|
|
138
|
-
|
|
139
|
-
const anchors = [locked, this.hovered].filter((name) => name !== null && this.nodeByName.has(name));
|
|
139
|
+
const focused = [...this.page.focusSelection()].filter((name) => this.nodeByName.has(name));
|
|
140
|
+
for (const name of focused)
|
|
141
|
+
this.nodeByName.get(name)?.classList.add('wp-locked');
|
|
142
|
+
const anchors = this.hovered !== null && this.nodeByName.has(this.hovered) ? [...focused, this.hovered] : focused;
|
|
140
143
|
if (anchors.length === 0)
|
|
141
144
|
return;
|
|
142
145
|
const lit = new Set();
|
|
@@ -177,9 +180,9 @@ class GraphHighlighter {
|
|
|
177
180
|
const href = designs.get(name);
|
|
178
181
|
if (href !== undefined)
|
|
179
182
|
items.push(new WpNodeMenuItem('View Design', () => window.open(href, '_blank')));
|
|
180
|
-
const
|
|
181
|
-
items.push(new WpNodeMenuItem(
|
|
182
|
-
items.push(this.page.
|
|
183
|
+
const focused = this.page.focusSelection().has(name);
|
|
184
|
+
items.push(new WpNodeMenuItem(focused ? 'Remove from Focus' : 'Add to Focus', () => this.page.toggleFocus(name)));
|
|
185
|
+
items.push(this.page.keepItem(name));
|
|
183
186
|
items.unshift(...this.page.impactItems(name, node));
|
|
184
187
|
const implemented = window.__WP_RUNTIME__?.implementsItem(name, node);
|
|
185
188
|
if (implemented)
|
|
@@ -301,17 +304,21 @@ class GraphDrawer {
|
|
|
301
304
|
page;
|
|
302
305
|
popover;
|
|
303
306
|
openers = new Map();
|
|
307
|
+
/** The two multi-selects (#1385): one implementation, two instances. */
|
|
308
|
+
keep;
|
|
309
|
+
focus;
|
|
304
310
|
constructor(page) {
|
|
305
311
|
this.page = page;
|
|
306
312
|
this.popover = new GraphFilterPopover(page);
|
|
313
|
+
this.keep = new WpMultiSelect('keep', new WpKeepSource(page));
|
|
314
|
+
this.focus = new WpMultiSelect('focus', new WpFocusSource(page));
|
|
307
315
|
}
|
|
308
316
|
wire() {
|
|
309
317
|
this.wireModeMenu();
|
|
310
318
|
this.popover.wire();
|
|
311
319
|
this.wireImpactKinds();
|
|
312
|
-
this.
|
|
313
|
-
this.
|
|
314
|
-
this.byId('wp-filter-toggle')?.addEventListener('click', () => this.page.toggleFilter());
|
|
320
|
+
this.keep.wire();
|
|
321
|
+
this.focus.wire();
|
|
315
322
|
this.toggles('wp-collapse', 'wp-shell', true);
|
|
316
323
|
this.toggles('wp-help-btn', 'wp-help', false);
|
|
317
324
|
this.toggles('wp-help-close', 'wp-help', false);
|
|
@@ -323,6 +330,10 @@ class GraphDrawer {
|
|
|
323
330
|
window.addEventListener('keydown', (ev) => this.onKey(ev), true);
|
|
324
331
|
window.addEventListener('hashchange', () => this.page.setMode(location.hash.slice(1)));
|
|
325
332
|
document.addEventListener('click', (ev) => {
|
|
333
|
+
for (const multi of [this.keep, this.focus]) {
|
|
334
|
+
if (multi.isOpen() && !multi.clicked(ev))
|
|
335
|
+
multi.close(false);
|
|
336
|
+
}
|
|
326
337
|
const target = ev.target;
|
|
327
338
|
if (target instanceof Node && this.byId('wp-mode-menu')?.parentElement?.contains(target))
|
|
328
339
|
return;
|
|
@@ -390,38 +401,11 @@ class GraphDrawer {
|
|
|
390
401
|
if (returnFocus)
|
|
391
402
|
trigger?.focus({ preventScroll: true });
|
|
392
403
|
}
|
|
393
|
-
/** Type-to-search lock: an exact project id (or a unique short name) locks, empty or Esc unlocks. */
|
|
394
|
-
wireLock() {
|
|
395
|
-
const input = this.lockInput();
|
|
396
|
-
if (input === null)
|
|
397
|
-
return;
|
|
398
|
-
input.addEventListener('input', () => {
|
|
399
|
-
const value = input.value.trim();
|
|
400
|
-
if (value === '')
|
|
401
|
-
this.page.clearSelection();
|
|
402
|
-
else if (this.page.hasNode(value))
|
|
403
|
-
this.page.setLock(value);
|
|
404
|
-
});
|
|
405
|
-
input.addEventListener('change', () => {
|
|
406
|
-
const match = this.page.resolveName(input.value.trim());
|
|
407
|
-
if (match !== null)
|
|
408
|
-
this.page.setLock(match);
|
|
409
|
-
else
|
|
410
|
-
input.value = this.page.lockSelection() ?? '';
|
|
411
|
-
});
|
|
412
|
-
input.addEventListener('keydown', (ev) => {
|
|
413
|
-
if (ev.key !== 'Escape')
|
|
414
|
-
return;
|
|
415
|
-
ev.preventDefault();
|
|
416
|
-
input.value = '';
|
|
417
|
-
this.page.clearSelection();
|
|
418
|
-
input.blur();
|
|
419
|
-
});
|
|
420
|
-
}
|
|
421
404
|
/**
|
|
422
405
|
* Page keys, in the CAPTURE phase so the open/closed state of the node menu is read before the
|
|
423
406
|
* menu's own Escape handler closes it: Esc with a menu open only closes the menu. Esc closes an
|
|
424
|
-
* open Color-by menu
|
|
407
|
+
* open Color-by menu, Filter popover or Keep/Focus pulldown first, too; with nothing open it clears
|
|
408
|
+
* Focus. `/` opens Focus with the cursor in its search.
|
|
425
409
|
*/
|
|
426
410
|
onKey(ev) {
|
|
427
411
|
if (ev.altKey || ev.ctrlKey || ev.metaKey)
|
|
@@ -434,6 +418,12 @@ class GraphDrawer {
|
|
|
434
418
|
this.popover.close();
|
|
435
419
|
return;
|
|
436
420
|
}
|
|
421
|
+
const pulldown = [this.keep, this.focus].find((multi) => multi.isOpen());
|
|
422
|
+
if (ev.key === 'Escape' && pulldown !== undefined) {
|
|
423
|
+
ev.preventDefault();
|
|
424
|
+
pulldown.close(true);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
437
427
|
if (ev.key === 'Escape' && this.dismissOverlay())
|
|
438
428
|
return;
|
|
439
429
|
const target = ev.target;
|
|
@@ -444,7 +434,8 @@ class GraphDrawer {
|
|
|
444
434
|
this.byId('wp-shell')?.classList.remove('wp-collapsed');
|
|
445
435
|
this.byId('wp-collapse')?.setAttribute('aria-expanded', 'true');
|
|
446
436
|
ev.preventDefault();
|
|
447
|
-
this.
|
|
437
|
+
this.keep.close(false);
|
|
438
|
+
this.focus.open();
|
|
448
439
|
return;
|
|
449
440
|
}
|
|
450
441
|
const index = ['1', '2', '3', '4', '5'].indexOf(ev.key);
|
|
@@ -452,8 +443,8 @@ class GraphDrawer {
|
|
|
452
443
|
this.page.setMode(WpModeState.MODES[index]);
|
|
453
444
|
return;
|
|
454
445
|
}
|
|
455
|
-
if (ev.key === 'Escape' && !menuOpen && this.page.
|
|
456
|
-
this.page.
|
|
446
|
+
if (ev.key === 'Escape' && !menuOpen && this.page.focusSelection().size > 0)
|
|
447
|
+
this.page.clearFocus();
|
|
457
448
|
}
|
|
458
449
|
/** `button` shows/hides `target`; for the drawer itself it toggles the collapsed class instead. */
|
|
459
450
|
toggles(buttonId, targetId, collapse) {
|
|
@@ -507,7 +498,7 @@ class GraphDrawer {
|
|
|
507
498
|
}
|
|
508
499
|
}
|
|
509
500
|
/** Repaint every control from the page's state. */
|
|
510
|
-
sync(modes,
|
|
501
|
+
sync(modes, filter) {
|
|
511
502
|
const reason = modes.impactReason();
|
|
512
503
|
document.querySelectorAll('.wp-mode[data-wp-mode]').forEach((button) => {
|
|
513
504
|
const mode = button.dataset['wpMode'] ?? '';
|
|
@@ -546,26 +537,10 @@ class GraphDrawer {
|
|
|
546
537
|
const title = this.byId('wp-legend-pop-title');
|
|
547
538
|
if (title !== null)
|
|
548
539
|
title.textContent = `Legend · ${modes.name(modes.mode)}`;
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
input.value = this.page.focusSelection() ?? '';
|
|
552
|
-
const lockToggle = this.byId('wp-lock-toggle');
|
|
553
|
-
if (lockToggle !== null) {
|
|
554
|
-
lockToggle.setAttribute('aria-pressed', String(locked !== null));
|
|
555
|
-
lockToggle.disabled = locked === null && this.page.focusSelection() === null;
|
|
556
|
-
lockToggle.title = lockToggle.disabled ? 'Select a node in Focus first' : '';
|
|
557
|
-
}
|
|
558
|
-
this.syncFilterToggle(locked, filtering);
|
|
540
|
+
this.keep.sync();
|
|
541
|
+
this.focus.sync();
|
|
559
542
|
this.syncFilter(filter);
|
|
560
|
-
this.crumb(modes.name(modes.mode),
|
|
561
|
-
}
|
|
562
|
-
syncFilterToggle(locked, filtering) {
|
|
563
|
-
const toggle = this.byId('wp-filter-toggle');
|
|
564
|
-
if (toggle === null)
|
|
565
|
-
return;
|
|
566
|
-
toggle.setAttribute('aria-pressed', String(filtering));
|
|
567
|
-
toggle.disabled = !filtering && locked === null;
|
|
568
|
-
toggle.title = toggle.disabled ? 'Lock a project first' : '';
|
|
543
|
+
this.crumb(modes.name(modes.mode), this.page.focusSelection());
|
|
569
544
|
}
|
|
570
545
|
/** The drawer button's count badge, and one removable pill per active filter in the top bar. */
|
|
571
546
|
syncFilter(filter) {
|
|
@@ -635,8 +610,8 @@ class GraphDrawer {
|
|
|
635
610
|
const named = files.slice(0, 2).join(', ') + (files.length > 2 ? ` and ${files.length - 2} more` : '');
|
|
636
611
|
return `${who}: ${named} changed (workspace-global input${files.length > 1 ? 's' : ''})`;
|
|
637
612
|
}
|
|
638
|
-
/** "Mode: <b>Runtime</b> ·
|
|
639
|
-
crumb(mode,
|
|
613
|
+
/** "Mode: <b>Runtime</b> · Focus: <b>a</b>, <b>b</b> +N", built with text nodes so no name is parsed as HTML. */
|
|
614
|
+
crumb(mode, focused) {
|
|
640
615
|
const crumb = this.byId('wp-crumb');
|
|
641
616
|
if (crumb === null)
|
|
642
617
|
return;
|
|
@@ -646,8 +621,14 @@ class GraphDrawer {
|
|
|
646
621
|
return b;
|
|
647
622
|
};
|
|
648
623
|
const parts = ['Mode: ', bold(mode)];
|
|
649
|
-
|
|
650
|
-
|
|
624
|
+
const names = [...focused].sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
|
625
|
+
if (names.length > 0)
|
|
626
|
+
parts.push(' · Focus: ');
|
|
627
|
+
names.slice(0, 2).forEach((name, index) => {
|
|
628
|
+
parts.push(...(index === 0 ? [] : [', ']), bold(name));
|
|
629
|
+
});
|
|
630
|
+
if (names.length > 2)
|
|
631
|
+
parts.push(` +${names.length - 2}`);
|
|
651
632
|
crumb.replaceChildren(...parts);
|
|
652
633
|
}
|
|
653
634
|
text(id, value) {
|
|
@@ -655,16 +636,34 @@ class GraphDrawer {
|
|
|
655
636
|
if (el !== null)
|
|
656
637
|
el.textContent = value;
|
|
657
638
|
}
|
|
658
|
-
lockInput() {
|
|
659
|
-
return document.getElementById('wp-lock');
|
|
660
|
-
}
|
|
661
639
|
byId(id) {
|
|
662
640
|
return document.getElementById(id);
|
|
663
641
|
}
|
|
664
642
|
}
|
|
643
|
+
/** "Focus": lights the picks' chains; offers only the boxes drawn after Filter and Keep connected to. */
|
|
644
|
+
class WpFocusSource {
|
|
645
|
+
page;
|
|
646
|
+
constructor(page) {
|
|
647
|
+
this.page = page;
|
|
648
|
+
}
|
|
649
|
+
selected() { return this.page.focusSelection(); }
|
|
650
|
+
available(id) { return this.page.isDrawn(id); }
|
|
651
|
+
toggle(id) { this.page.toggleFocus(id); }
|
|
652
|
+
clear() { this.page.clearFocus(); }
|
|
653
|
+
}
|
|
654
|
+
/** "Keep connected to": redraws with the union of the picks' chains; offers every box of the graph. */
|
|
655
|
+
class WpKeepSource {
|
|
656
|
+
page;
|
|
657
|
+
constructor(page) {
|
|
658
|
+
this.page = page;
|
|
659
|
+
}
|
|
660
|
+
selected() { return this.page.keepSelection(); }
|
|
661
|
+
available(id) { return this.page.hasNode(id); }
|
|
662
|
+
toggle(id) { this.page.toggleKeep(id); }
|
|
663
|
+
clear() { this.page.clearKeep(); }
|
|
664
|
+
}
|
|
665
665
|
class GraphPage extends WpFilterPage {
|
|
666
666
|
highlighter = null;
|
|
667
|
-
selected = null;
|
|
668
667
|
/** The page's own graph; Goal vs Current swaps in its overlay and back (#1239). */
|
|
669
668
|
base = this.model;
|
|
670
669
|
modes = new WpModeState(this.model.nodes.some((node) => node.products.length > 0), this.model.viewer ?? 'architecture', this.goalUnavailable(), this.model.comparison?.menuNote || this.goalUnavailable());
|
|
@@ -770,43 +769,39 @@ class GraphPage extends WpFilterPage {
|
|
|
770
769
|
this.filterCards();
|
|
771
770
|
this.syncControls();
|
|
772
771
|
svg.querySelectorAll('g.node').forEach((node) => {
|
|
773
|
-
if (node.querySelector('title')?.textContent
|
|
772
|
+
if (this.keep.has(node.querySelector('title')?.textContent ?? ''))
|
|
774
773
|
node.classList.add('wp-filter-anchor');
|
|
775
774
|
});
|
|
776
775
|
}
|
|
777
|
-
|
|
778
|
-
return this.
|
|
776
|
+
focusSelection() {
|
|
777
|
+
return this.focused;
|
|
778
|
+
}
|
|
779
|
+
/** Whether `id` is drawn right now: what Focus may offer, and a focused box's chip is not "hidden". */
|
|
780
|
+
isDrawn(id) {
|
|
781
|
+
return this.retained.has(id) && this.hasNode(id);
|
|
782
|
+
}
|
|
783
|
+
toggleFocus(id) {
|
|
784
|
+
if (this.focused.has(id))
|
|
785
|
+
this.focused.delete(id);
|
|
786
|
+
else
|
|
787
|
+
this.focused.add(id);
|
|
788
|
+
this.refocus();
|
|
789
|
+
}
|
|
790
|
+
clearFocus() {
|
|
791
|
+
this.focused.clear();
|
|
792
|
+
this.refocus();
|
|
779
793
|
}
|
|
780
794
|
hasNode(id) {
|
|
781
795
|
return this.model.nodes.some((node) => node.id === id);
|
|
782
796
|
}
|
|
783
797
|
noun() { return this.model.viewer === 'runtime' ? 'service' : 'project'; }
|
|
784
798
|
projectCount() { return this.model.nodes.filter(node => this.projects.isService(node)).length; }
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
if (this.locked !== null)
|
|
788
|
-
this.setLock(null);
|
|
789
|
-
else if (this.selected !== null)
|
|
790
|
-
this.setLock(this.selected);
|
|
791
|
-
}
|
|
792
|
-
/** An exact id, else the one id whose short name (after any scope) is `text`; null if none or several. */
|
|
793
|
-
resolveName(text) {
|
|
794
|
-
if (text === '')
|
|
795
|
-
return null;
|
|
796
|
-
if (this.hasNode(text))
|
|
797
|
-
return text;
|
|
798
|
-
const matches = this.model.nodes.filter((node) => node.id.split('/').pop() === text);
|
|
799
|
-
return matches.length === 1 ? matches[0].id : null;
|
|
800
|
-
}
|
|
801
|
-
setLock(name) {
|
|
802
|
-
this.locked = name;
|
|
803
|
-
if (name !== null)
|
|
804
|
-
this.selected = name;
|
|
799
|
+
/** Focus changed: relight, refilter the cards, and repaint the drawer — no relayout. */
|
|
800
|
+
refocus() {
|
|
805
801
|
this.highlighter?.relight();
|
|
806
802
|
this.filterCards();
|
|
807
803
|
this.syncControls();
|
|
808
804
|
}
|
|
809
|
-
clearSelection() { this.selected = null; this.setLock(null); }
|
|
810
805
|
setMode(mode) {
|
|
811
806
|
if (mode === this.modes.mode || !this.modes.set(mode))
|
|
812
807
|
return;
|
|
@@ -832,13 +827,6 @@ class GraphPage extends WpFilterPage {
|
|
|
832
827
|
this.syncControls();
|
|
833
828
|
this.redraw();
|
|
834
829
|
}
|
|
835
|
-
/** The drawer's "Hide unconnected": filter to the locked box's chain, or turn the filter off. */
|
|
836
|
-
toggleFilter() {
|
|
837
|
-
if (this.anchor !== null)
|
|
838
|
-
this.filter(null);
|
|
839
|
-
else if (this.locked !== null)
|
|
840
|
-
this.filter(this.locked);
|
|
841
|
-
}
|
|
842
830
|
appliedFilter() {
|
|
843
831
|
return this.applied;
|
|
844
832
|
}
|
|
@@ -853,12 +841,9 @@ class GraphPage extends WpFilterPage {
|
|
|
853
841
|
choice.scope = scope;
|
|
854
842
|
return this.preview(choice);
|
|
855
843
|
}
|
|
856
|
-
/** How many boxes `choice` would show, with the current
|
|
844
|
+
/** How many boxes `choice` would show, with the current "Keep connected to" picks. */
|
|
857
845
|
preview(choice) {
|
|
858
|
-
const
|
|
859
|
-
? new Set(this.model.nodes.map((node) => node.id))
|
|
860
|
-
: this.chain.nodes(this.anchor);
|
|
861
|
-
const kept = this.projects.apply(base, choice);
|
|
846
|
+
const kept = this.projects.apply(this.kept(this.keep), choice);
|
|
862
847
|
return this.model.nodes.filter(node => kept.has(node.id) && this.projects.isService(node)).length;
|
|
863
848
|
}
|
|
864
849
|
applyFilter(choice) {
|
|
@@ -879,7 +864,7 @@ class GraphPage extends WpFilterPage {
|
|
|
879
864
|
}
|
|
880
865
|
syncControls() {
|
|
881
866
|
document.body.dataset['wpMode'] = this.modes.mode;
|
|
882
|
-
this.drawer.sync(this.modes, this.
|
|
867
|
+
this.drawer.sync(this.modes, this.applied);
|
|
883
868
|
this.toggles.sync(this.productsOn);
|
|
884
869
|
const count = this.model.nodes.filter(node => this.retained.has(node.id) && this.projects.isService(node)).length;
|
|
885
870
|
const empty = document.getElementById('wp-empty');
|
|
@@ -898,8 +883,9 @@ class GraphPage extends WpFilterPage {
|
|
|
898
883
|
(group === 'runtime' ? (value === 'unknown' ? node.tags !== null && node.tags.frameworks.length === 0 : node.tags?.frameworks.includes(value)) : group === 'role' ? node.tags?.role === value : node.products.includes(value)));
|
|
899
884
|
});
|
|
900
885
|
}
|
|
886
|
+
/** The cards of the drawn boxes, narrowed by Focus to the union of the focused boxes' chains. */
|
|
901
887
|
filterCards() {
|
|
902
|
-
const lit = this.
|
|
888
|
+
const lit = this.focused.size === 0 ? this.retained : this.kept(this.focused);
|
|
903
889
|
document.querySelectorAll('.wp-resp-card').forEach((card) => {
|
|
904
890
|
const name = card.getAttribute('data-node');
|
|
905
891
|
card.classList.toggle('wp-hidden', name === null || !this.retained.has(name) || !lit.has(name));
|