Skip to content

Commit b159d26

Browse files
author
John van Leeuwen
committed
feat: focus in tree
1 parent 0f51904 commit b159d26

4 files changed

Lines changed: 99 additions & 14 deletions

File tree

apps/frontend/src/app/model/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export interface Config {
22
groups?: string[];
33
scopes: string[];
4+
focus?: string;
45
}
56

67
export const initConfig: Config = {

apps/frontend/src/app/shell/filter-tree/filter-tree.component.html

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
<mat-checkbox
99
[checked]="isChecked(node)"
1010
(change)="onCheckChange(node, $event)"
11+
(contextmenu)="noContextMenu($event)"
1112
>{{ node.name }}</mat-checkbox
1213
>
1314
</mat-tree-node>
@@ -22,10 +23,32 @@
2223
{{ tree.isExpanded(node) ? 'expand_more' : 'chevron_right' }}
2324
</mat-icon>
2425
</button>
26+
<div
27+
#contextMenuTrigger="matMenuTrigger"
28+
[matMenuTriggerFor]="folderMenu"
29+
></div>
2530
<mat-checkbox
2631
[checked]="isChecked(node)"
2732
(change)="onCheckChange(node, $event)"
33+
(contextmenu)="onContextMenu($event, contextMenuTrigger)"
2834
>{{ node.name }}</mat-checkbox
2935
>
36+
<mat-menu #folderMenu="matMenu">
37+
@if (hasFocus(node)) {
38+
<button mat-menu-item (click)="focusTree(undefined)">
39+
<mat-icon>open_in_full</mat-icon>
40+
<span>Unfocus</span>
41+
</button>
42+
} @else {
43+
<button mat-menu-item (click)="focusTree(node)">
44+
<mat-icon>start</mat-icon>
45+
<span>Focus</span>
46+
</button>
47+
}
48+
<button mat-menu-item (click)="selectChildren(node)">
49+
<mat-icon>checklist</mat-icon>
50+
<span>Select the children</span>
51+
</button>
52+
</mat-menu>
3053
</mat-tree-node>
3154
</mat-tree>

apps/frontend/src/app/shell/filter-tree/filter-tree.component.ts

Lines changed: 74 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
1+
import { CdkMenuModule } from '@angular/cdk/menu';
12
import { CdkTree } from '@angular/cdk/tree';
2-
import { Component, inject, OnInit, viewChild } from '@angular/core';
3+
import {
4+
ChangeDetectorRef,
5+
Component,
6+
inject,
7+
OnInit,
8+
viewChild,
9+
} from '@angular/core';
310
import { MatButtonModule } from '@angular/material/button';
411
import {
512
MatCheckboxChange,
613
MatCheckboxModule,
714
} from '@angular/material/checkbox';
815
import { MatIconModule } from '@angular/material/icon';
16+
import { MatMenuModule, MatMenuTrigger } from '@angular/material/menu';
917
import { MatTreeModule, MatTreeNestedDataSource } from '@angular/material/tree';
1018
import { combineLatest, of } from 'rxjs';
1119

@@ -20,14 +28,22 @@ const MIN_OPEN_LEVEL = 2;
2028
@Component({
2129
selector: 'app-filter-tree',
2230
standalone: true,
23-
imports: [MatTreeModule, MatIconModule, MatButtonModule, MatCheckboxModule],
31+
imports: [
32+
MatTreeModule,
33+
MatIconModule,
34+
MatButtonModule,
35+
MatCheckboxModule,
36+
MatMenuModule,
37+
CdkMenuModule,
38+
],
2439
templateUrl: './filter-tree.component.html',
2540
styleUrl: './filter-tree.component.css',
2641
})
2742
export class FilterTreeComponent implements OnInit {
2843
private folderService = inject(FolderService);
2944
private configService = inject(ConfigService);
3045
private eventService = inject(EventService);
46+
private cdr = inject(ChangeDetectorRef);
3147

3248
tree = viewChild.required<CdkTree<Folder>>(CdkTree);
3349
dataSource = new MatTreeNestedDataSource<Folder>();
@@ -36,27 +52,38 @@ export class FilterTreeComponent implements OnInit {
3652
selected = new Set<string>();
3753
folders: Folder[] = [];
3854

39-
childrenAccessor = (folder: Folder) => of(folder.folders);
40-
hasChild = (_: number, node: Folder) =>
41-
!!node.folders && node.folders.length > 0;
55+
childrenAccessor = ({ folders }: Folder) => of(folders);
56+
hasChild = (_: number, { folders }: Folder) => !!folders?.length;
4257

4358
ngOnInit(): void {
4459
const folders$ = this.folderService.load();
4560
const config$ = this.configService.load();
4661
combineLatest({
4762
folders: folders$,
4863
config: config$,
49-
}).subscribe((result) => {
50-
this.dataSource.data = result.folders;
51-
this.config = result.config;
52-
this.folders = result.folders;
64+
}).subscribe(({ folders, config }) => {
65+
const focusFolder = this.findFolder(folders, config.focus);
66+
this.dataSource.data = focusFolder ? [focusFolder] : folders;
67+
this.config = config;
68+
this.folders = folders;
5369
this.selected.clear();
5470
this.config.scopes.forEach((scope) => this.selected.add(scope));
55-
this.expandChecked(result.folders);
71+
this.expandChecked(this.dataSource.data);
5672
removeFocus();
5773
});
5874
}
5975

76+
private findFolder(folders: Folder[], focus?: string): Folder | undefined {
77+
if (!focus || !folders?.length) return undefined;
78+
return (
79+
folders.find((folder) => folder && folder.path === focus) ??
80+
this.findFolder(
81+
folders.flatMap(({ folders }) => folders),
82+
focus
83+
)
84+
);
85+
}
86+
6087
expandChecked(folders: Folder[], depth = 0): boolean {
6188
let open = depth <= MIN_OPEN_LEVEL;
6289
for (const folder of folders) {
@@ -71,8 +98,40 @@ export class FilterTreeComponent implements OnInit {
7198
return open;
7299
}
73100

74-
isChecked(folder: Folder): boolean {
75-
return this.selected.has(folder.path);
101+
noContextMenu(event: MouseEvent) {
102+
event.preventDefault();
103+
}
104+
105+
onContextMenu(event: MouseEvent, trigger: MatMenuTrigger) {
106+
event.preventDefault();
107+
trigger.openMenu();
108+
}
109+
110+
selectChildren(folder: Folder) {
111+
this.deselectParents(folder);
112+
this.selected.delete(folder.path);
113+
for (const child of folder.folders) {
114+
this.selected.add(child.path);
115+
}
116+
this.tree().expand(folder);
117+
this.updateConfig();
118+
}
119+
120+
focusTree(folder?: Folder) {
121+
this.dataSource = new MatTreeNestedDataSource<Folder>();
122+
this.dataSource.data = folder ? [{ ...folder }] : this.folders;
123+
this.config.focus = folder?.path;
124+
this.expandChecked(this.dataSource.data);
125+
this.tree().renderNodeChanges(this.dataSource.data);
126+
this.updateConfig();
127+
}
128+
129+
isChecked({ path }: Folder): boolean {
130+
return this.selected.has(path);
131+
}
132+
133+
hasFocus({ path }: Folder): boolean {
134+
return path === this.config.focus;
76135
}
77136

78137
onCheckChange(folder: Folder, $event: MatCheckboxChange) {
@@ -84,7 +143,10 @@ export class FilterTreeComponent implements OnInit {
84143

85144
this.deselectParents(folder);
86145
this.deselectSubtree(folder.folders);
146+
this.updateConfig();
147+
}
87148

149+
private updateConfig() {
88150
this.config.scopes = [...this.selected];
89151
this.config.groups = this.findParents();
90152

apps/frontend/src/app/ui/graph/graph.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ function createGraph(container: HTMLElement, graph: Graph): cytoscape.Core {
7575
'text-valign': 'center',
7676
'text-halign': 'center',
7777
height: '20px',
78-
width: 'label',
78+
width: '100px',
7979
padding: '10px',
8080
'background-color': '#60a3bc',
8181
'border-color': '#1e272e',
@@ -118,7 +118,6 @@ function createGraph(container: HTMLElement, graph: Graph): cytoscape.Core {
118118
nodes: graph.nodes,
119119
edges: graph.edges,
120120
},
121-
wheelSensitivity: 0.2,
122121
zoomingEnabled: true,
123122
userZoomingEnabled: true,
124123
panningEnabled: true,

0 commit comments

Comments
 (0)