diff --git a/apps/frontend/src/app/model/config.ts b/apps/frontend/src/app/model/config.ts index 9eaf5f6..24bc67c 100644 --- a/apps/frontend/src/app/model/config.ts +++ b/apps/frontend/src/app/model/config.ts @@ -1,6 +1,7 @@ export interface Config { groups?: string[]; scopes: string[]; + focus?: string; } export const initConfig: Config = { diff --git a/apps/frontend/src/app/shell/filter-tree/filter-tree.component.html b/apps/frontend/src/app/shell/filter-tree/filter-tree.component.html index d72cf01..d0fa394 100644 --- a/apps/frontend/src/app/shell/filter-tree/filter-tree.component.html +++ b/apps/frontend/src/app/shell/filter-tree/filter-tree.component.html @@ -8,6 +8,7 @@ {{ node.name }} @@ -22,10 +23,32 @@ {{ tree.isExpanded(node) ? 'expand_more' : 'chevron_right' }} +
{{ node.name }} + + @if (hasFocus(node)) { + + } @else { + + } + + diff --git a/apps/frontend/src/app/shell/filter-tree/filter-tree.component.ts b/apps/frontend/src/app/shell/filter-tree/filter-tree.component.ts index 121c5d5..8cf797e 100644 --- a/apps/frontend/src/app/shell/filter-tree/filter-tree.component.ts +++ b/apps/frontend/src/app/shell/filter-tree/filter-tree.component.ts @@ -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'; @@ -20,7 +28,14 @@ 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', }) @@ -28,6 +43,7 @@ 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); dataSource = new MatTreeNestedDataSource(); @@ -36,9 +52,8 @@ export class FilterTreeComponent implements OnInit { selected = new Set(); 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(); @@ -46,17 +61,29 @@ export class FilterTreeComponent implements OnInit { 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) { @@ -71,8 +98,40 @@ 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(); + } + + 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(); + 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) { @@ -84,7 +143,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(); diff --git a/apps/frontend/src/app/ui/graph/graph.ts b/apps/frontend/src/app/ui/graph/graph.ts index 56afa3b..78050b4 100644 --- a/apps/frontend/src/app/ui/graph/graph.ts +++ b/apps/frontend/src/app/ui/graph/graph.ts @@ -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', @@ -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,