96 SUCCESS tests complet

This commit is contained in:
2025-10-03 14:24:16 -04:00
parent 2dfe827759
commit 77ced60d7c
155 changed files with 11107 additions and 207392 deletions
@@ -0,0 +1,263 @@
/**
* Enhanced Graph View Container V2
* Integrates Canvas renderer with filtering, grouping, and settings
*/
import {
Component,
ChangeDetectionStrategy,
input,
output,
signal,
computed,
inject,
effect,
viewChild
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { GraphCanvasComponent } from '../../app/graph/graph-canvas.component';
import { GraphLegendComponent } from '../../app/graph/graph-legend.component';
import { GraphSettingsStore } from '../../app/graph/graph-settings.store';
import { GraphSettingsPanelComponent } from '../../app/graph/ui/settings-panel.component';
import { GraphSettingsService } from '../../app/graph/graph-settings.service';
import { NoteIndexService } from '../../services/note-index.service';
import { VaultService } from '../../services/vault.service';
import { GraphData, GraphNode as SimpleGraphNode } from '../../types';
import { GraphNode, GraphLink, GraphNodeWithVisuals } from '../../app/graph/graph-data.types';
import { createFilteredGraphData, createGroupLegend, createFocusedGraphData } from '../../app/graph/graph.selectors';
@Component({
selector: 'app-graph-view-container-v2',
standalone: true,
imports: [
CommonModule,
GraphCanvasComponent,
GraphLegendComponent,
GraphSettingsPanelComponent
],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="flex flex-col h-full w-full bg-gray-50 dark:bg-gray-900">
<!-- Main graph area -->
<div class="flex-1 relative">
<app-graph-canvas
[nodes]="displayData().nodes"
[links]="displayData().links"
[settings]="store.settings()"
[centerNodeId]="centerNoteId()"
(nodeClicked)="onNodeClicked($event)"
(nodeHovered)="onNodeHovered($event)">
</app-graph-canvas>
<!-- Settings button -->
<button
type="button"
(click)="toggleSettings()"
class="absolute top-5 right-5 flex items-center justify-center w-11 h-11 rounded-full bg-white dark:bg-gray-800 shadow-lg hover:shadow-xl transition-all hover:rotate-90 z-50 border border-gray-200 dark:border-gray-700"
aria-label="Graph settings"
title="Graph settings">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-gray-700 dark:text-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</button>
</div>
<!-- Legend at bottom -->
@if (legendItems().length > 0) {
<div class="p-4 border-t border-gray-200 dark:border-gray-700">
<app-graph-legend
[items]="legendItems()"
(itemClicked)="onLegendItemClicked($event)">
</app-graph-legend>
</div>
}
<!-- Settings panel -->
<app-graph-settings-panel
[isOpen]="settingsOpen()"
(close)="closeSettings()"
(animateRequested)="onAnimateRequested()">
</app-graph-settings-panel>
</div>
`,
styles: [`
:host {
display: block;
width: 100%;
height: 100%;
}
`]
})
export class GraphViewContainerV2Component {
// Inputs
centerNoteId = input<string | null>(null);
// Outputs
nodeSelected = output<string>();
// Injected services
private noteIndexService = inject(NoteIndexService);
private vaultService = inject(VaultService);
readonly store = inject(GraphSettingsStore);
private settingsService = inject(GraphSettingsService);
// Local state
settingsOpen = signal(false);
private canvasCmp = viewChild(GraphCanvasComponent);
// Base graph data from vault (full graph)
private rawGraphData = computed<GraphData>(() => this.vaultService.graphData());
// Convert to enhanced graph data format
private enhancedGraphData = computed(() => {
const rawData = this.rawGraphData();
const allNotes = this.vaultService.allNotes();
// Build lookup for note metadata
const noteMap = new Map(allNotes.map(n => [n.id, n]));
// Convert nodes to enhanced format
const nodes: GraphNode[] = rawData.nodes.map(node => {
const note = noteMap.get(node.id);
return {
id: node.id,
title: node.label,
path: note?.filePath || note?.originalPath || '',
tags: note?.tags || [],
hasAttachment: this.isAttachment(node.label),
exists: noteMap.has(node.id)
};
});
// Convert edges to links
const links: GraphLink[] = rawData.edges.map(edge => ({
source: edge.source,
target: edge.target,
directed: true
}));
return { nodes, links };
});
// Apply all filters using selectors
private filteredData = createFilteredGraphData(
this.enhancedGraphData,
this.store.settings
);
// Create legend
legendItems = createGroupLegend(
this.filteredData,
this.store.settings,
this.store.focusedGroupIndex
);
// Apply group focus if active
displayData = createFocusedGraphData(
this.filteredData,
this.store.focusedGroupIndex
);
constructor() {
// Snapshot refs to avoid TS complaining about possibly-undefined this inside effects
const store = this.store;
const settingsService = this.settingsService;
const settingsOpen = this.settingsOpen;
// Debounced URL updates to avoid scheduling many timers
let urlTimer: ReturnType<typeof setTimeout> | null = null;
effect(() => {
// Track settings changes
store.settings();
if (urlTimer) clearTimeout(urlTimer);
urlTimer = setTimeout(() => {
store.updateURL();
}, 300);
});
// Close settings on Escape only when panel is open
effect(() => {
if (!settingsOpen()) return;
const handler = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
this.closeSettings();
}
};
document.addEventListener('keydown', handler);
return () => document.removeEventListener('keydown', handler);
});
// One-way sync: store -> service (persist). Debounced to prevent rapid churn.
let saveTimer: ReturnType<typeof setTimeout> | null = null;
effect(() => {
const cfg = store.settings();
if (saveTimer) clearTimeout(saveTimer);
saveTimer = setTimeout(() => {
const svcCfg = settingsService.config();
const a = JSON.stringify(cfg);
const b = JSON.stringify(svcCfg);
if (a !== b) {
settingsService.save(cfg);
}
}, 150);
});
}
/**
* Check if a node label represents an attachment
*/
private isAttachment(label: string): boolean {
const attachmentExtensions = [
'pdf', 'png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'bmp',
'mp4', 'webm', 'ogv', 'mov', 'mkv',
'mp3', 'wav', 'ogg', 'm4a', 'flac'
];
const ext = label.split('.').pop()?.toLowerCase();
return ext ? attachmentExtensions.includes(ext) : false;
}
/**
* Event handlers
*/
onNodeClicked(node: GraphNodeWithVisuals): void {
this.nodeSelected.emit(node.id);
}
onNodeHovered(node: GraphNodeWithVisuals | null): void {
// Can emit hover events if needed
}
onLegendItemClicked(groupIndex: number): void {
this.store.toggleFocusedGroup(groupIndex);
}
toggleSettings(): void {
this.settingsOpen.update(open => !open);
}
closeSettings(): void {
this.settingsOpen.set(false);
}
onAnimateRequested(): void {
// Trigger animation on canvas
const canvas = this.canvasCmp();
if (canvas) {
canvas.animate();
}
}
/**
* Public API: Focus on a specific node by path
*/
focusNodeByPath(path: string): void {
const node = this.displayData().nodes.find(n => n.path === path);
const canvas = this.canvasCmp();
if (node && canvas) {
canvas.focusOnNode(node.id);
}
}
}