Skip to content

Commit

Permalink
Merge pull request #30 from wamasimba/feature/focus-in-tree
Browse files Browse the repository at this point in the history
feat: focus in tree
  • Loading branch information
manfredsteyer authored Oct 15, 2024
2 parents 6670420 + f578116 commit b0000b6
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 14 deletions.
1 change: 1 addition & 0 deletions apps/frontend/src/app/model/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export interface Config {
groups?: string[];
scopes: string[];
focus?: string;
}

export const initConfig: Config = {
Expand Down
23 changes: 23 additions & 0 deletions apps/frontend/src/app/shell/filter-tree/filter-tree.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<mat-checkbox
[checked]="isChecked(node)"
(change)="onCheckChange(node, $event)"
(contextmenu)="noContextMenu($event)"
>{{ node.name }}</mat-checkbox
>
</mat-tree-node>
Expand All @@ -22,10 +23,32 @@
{{ tree.isExpanded(node) ? 'expand_more' : 'chevron_right' }}
</mat-icon>
</button>
<div
#contextMenuTrigger="matMenuTrigger"
[matMenuTriggerFor]="folderMenu"
></div>
<mat-checkbox
[checked]="isChecked(node)"
(change)="onCheckChange(node, $event)"
(contextmenu)="onContextMenu($event, contextMenuTrigger)"
>{{ node.name }}</mat-checkbox
>
<mat-menu #folderMenu="matMenu">
@if (hasFocus(node)) {
<button mat-menu-item (click)="focusTree(undefined)">
<mat-icon>open_in_full</mat-icon>
<span>Unfocus</span>
</button>
} @else {
<button mat-menu-item (click)="focusTree(node)">
<mat-icon>start</mat-icon>
<span>Focus</span>
</button>
}
<button mat-menu-item (click)="selectChildren(node)">
<mat-icon>checklist</mat-icon>
<span>Select the children</span>
</button>
</mat-menu>
</mat-tree-node>
</mat-tree>
91 changes: 79 additions & 12 deletions apps/frontend/src/app/shell/filter-tree/filter-tree.component.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { CdkMenuModule } from '@angular/cdk/menu';
import { CdkTree } from '@angular/cdk/tree';
import { Component, inject, OnInit, viewChild } from '@angular/core';
import {
ChangeDetectorRef,
Component,
inject,
OnInit,
viewChild,
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import {
MatCheckboxChange,
MatCheckboxModule,
} from '@angular/material/checkbox';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule, MatMenuTrigger } from '@angular/material/menu';
import { MatTreeModule, MatTreeNestedDataSource } from '@angular/material/tree';
import { combineLatest, of } from 'rxjs';

Expand All @@ -20,14 +28,22 @@ const MIN_OPEN_LEVEL = 2;
@Component({
selector: 'app-filter-tree',
standalone: true,
imports: [MatTreeModule, MatIconModule, MatButtonModule, MatCheckboxModule],
imports: [
MatTreeModule,
MatIconModule,
MatButtonModule,
MatCheckboxModule,
MatMenuModule,
CdkMenuModule,
],
templateUrl: './filter-tree.component.html',
styleUrl: './filter-tree.component.css',
})
export class FilterTreeComponent implements OnInit {
private folderService = inject(FolderService);
private configService = inject(ConfigService);
private eventService = inject(EventService);
private cdr = inject(ChangeDetectorRef);

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

childrenAccessor = (folder: Folder) => of(folder.folders);
hasChild = (_: number, node: Folder) =>
!!node.folders && node.folders.length > 0;
childrenAccessor = ({ folders }: Folder) => of(folders);
hasChild = (_: number, { folders }: Folder) => !!folders?.length;

ngOnInit(): void {
const folders$ = this.folderService.load();
const config$ = this.configService.load();
combineLatest({
folders: folders$,
config: config$,
}).subscribe((result) => {
this.dataSource.data = result.folders;
this.config = result.config;
this.folders = result.folders;
}).subscribe(({ folders, config }) => {
const focusFolder = this.findFolder(folders, config.focus);
this.dataSource.data = focusFolder ? [focusFolder] : folders;
this.config = config;
this.folders = folders;
this.selected.clear();
this.config.scopes.forEach((scope) => this.selected.add(scope));
this.expandChecked(result.folders);
this.expandChecked(this.dataSource.data);
removeFocus();
});
}

private findFolder(folders: Folder[], focus?: string): Folder | undefined {
if (!focus || !folders?.length) return undefined;
return (
folders.find((folder) => folder && folder.path === focus) ??
this.findFolder(
folders.flatMap(({ folders }) => folders),
focus
)
);
}

expandChecked(folders: Folder[], depth = 0): boolean {
let open = depth <= MIN_OPEN_LEVEL;
for (const folder of folders) {
Expand All @@ -71,8 +98,45 @@ export class FilterTreeComponent implements OnInit {
return open;
}

isChecked(folder: Folder): boolean {
return this.selected.has(folder.path);
noContextMenu(event: MouseEvent) {
event.preventDefault();
}

onContextMenu(event: MouseEvent, trigger: MatMenuTrigger) {
event.preventDefault();
trigger.openMenu();
document
.querySelector('div.cdk-overlay-backdrop')
?.addEventListener('mousedown', () => {
trigger.closeMenu();
});
}

selectChildren(folder: Folder) {
this.deselectParents(folder);
this.selected.delete(folder.path);
for (const child of folder.folders) {
this.selected.add(child.path);
}
this.tree().expand(folder);
this.updateConfig();
}

focusTree(folder?: Folder) {
this.dataSource = new MatTreeNestedDataSource<Folder>();
this.dataSource.data = folder ? [{ ...folder }] : this.folders;
this.config.focus = folder?.path;
this.expandChecked(this.dataSource.data);
this.tree().renderNodeChanges(this.dataSource.data);
this.updateConfig();
}

isChecked({ path }: Folder): boolean {
return this.selected.has(path);
}

hasFocus({ path }: Folder): boolean {
return path === this.config.focus;
}

onCheckChange(folder: Folder, $event: MatCheckboxChange) {
Expand All @@ -84,7 +148,10 @@ export class FilterTreeComponent implements OnInit {

this.deselectParents(folder);
this.deselectSubtree(folder.folders);
this.updateConfig();
}

private updateConfig() {
this.config.scopes = [...this.selected];
this.config.groups = this.findParents();

Expand Down
3 changes: 1 addition & 2 deletions apps/frontend/src/app/ui/graph/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ function createGraph(container: HTMLElement, graph: Graph): cytoscape.Core {
'text-valign': 'center',
'text-halign': 'center',
height: '20px',
width: 'label',
width: '100px',
padding: '10px',
'background-color': '#60a3bc',
'border-color': '#1e272e',
Expand Down Expand Up @@ -118,7 +118,6 @@ function createGraph(container: HTMLElement, graph: Graph): cytoscape.Core {
nodes: graph.nodes,
edges: graph.edges,
},
wheelSensitivity: 0.2,
zoomingEnabled: true,
userZoomingEnabled: true,
panningEnabled: true,
Expand Down

0 comments on commit b0000b6

Please sign in to comment.