96 SUCCESS tests complet
This commit is contained in:
+204
-3
@@ -1,19 +1,23 @@
|
||||
# Graph Settings Feature
|
||||
|
||||
Complete implementation of Obsidian-compatible graph settings panel for ObsiViewer.
|
||||
Complete implementation of Obsidian-compatible graph settings panel for ObsiViewer with high-performance Canvas rendering.
|
||||
|
||||
## Overview
|
||||
|
||||
This feature adds a comprehensive settings panel for the graph view, matching Obsidian's interface and functionality. Settings are stored in `.obsidian/graph.json` and synchronized in real-time with the graph rendering.
|
||||
This feature adds a comprehensive settings panel for the graph view, matching Obsidian's interface and functionality. Settings are stored in `.obsidian/graph.json`, persisted to localStorage, and can be shared via URL parameters. The graph is rendered using HTML5 Canvas with d3-force for optimal performance.
|
||||
|
||||
## Features
|
||||
|
||||
- ⚙️ **Settings Button**: Gear icon in top-right of graph view
|
||||
- 🎨 **Settings Panel**: Slide-over panel with four collapsible sections
|
||||
- 💾 **Persistence**: Auto-save to `.obsidian/graph.json` with 250ms debounce
|
||||
- 💾 **Triple Persistence**: `.obsidian/graph.json` + localStorage + URL query parameters
|
||||
- 🔗 **URL Sharing**: Settings encoded in URL (`?gs=base64url(...)`) for shareable views
|
||||
- 🚀 **Canvas Rendering**: High-performance rendering for 2-3k nodes
|
||||
- 🔄 **Live Sync**: Watch for external file changes and reload
|
||||
- 🎯 **Real-time Filtering**: All controls update graph within 100ms
|
||||
- 📱 **Responsive**: Desktop slide-over, mobile full-screen
|
||||
- ♿ **Accessible**: Keyboard navigation, ARIA labels, focus management
|
||||
- 🏷️ **Interactive Legend**: Click legend items to filter by group
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -384,6 +388,203 @@ this.settings.resetSection('display');
|
||||
- Increase debounce delay
|
||||
- Disable external change detection
|
||||
|
||||
## Settings Persistence
|
||||
|
||||
### Three-Layer Persistence Strategy
|
||||
|
||||
Settings are persisted across three mechanisms for maximum flexibility:
|
||||
|
||||
#### 1. File Storage (.obsidian/graph.json)
|
||||
|
||||
Primary storage matching Obsidian's format. Changes are debounced (250ms) and saved atomically.
|
||||
|
||||
```typescript
|
||||
// Auto-saved when settings change
|
||||
graphSettingsService.save({ showArrow: true });
|
||||
```
|
||||
|
||||
#### 2. localStorage
|
||||
|
||||
Cached locally for instant loading on page reload. Updated automatically whenever settings change.
|
||||
|
||||
```typescript
|
||||
// Key: 'obsi-graph-settings'
|
||||
localStorage.setItem('obsi-graph-settings', JSON.stringify(config));
|
||||
```
|
||||
|
||||
#### 3. URL Query Parameters
|
||||
|
||||
Settings can be encoded in the URL for sharing specific graph views:
|
||||
|
||||
```
|
||||
https://your-app.com/graph?gs=eyJzaG93QXJyb3ciOnRydWV9
|
||||
```
|
||||
|
||||
**Encoding:**
|
||||
```typescript
|
||||
const encoded = btoa(JSON.stringify(config))
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=/g, '');
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```typescript
|
||||
// Update URL with current settings
|
||||
graphSettingsStore.updateURL();
|
||||
|
||||
// Settings automatically loaded from URL on page load
|
||||
// Priority: URL > localStorage > .obsidian/graph.json > defaults
|
||||
```
|
||||
|
||||
### Loading Priority
|
||||
|
||||
On initialization, settings are loaded in this order:
|
||||
|
||||
1. **URL parameter** (`?gs=...`) - Highest priority (shareable state)
|
||||
2. **localStorage** - Fast local cache
|
||||
3. **graph.json** - Persistent vault settings
|
||||
4. **Defaults** - Fallback if nothing else available
|
||||
|
||||
### Sharing Graph Views
|
||||
|
||||
To share a specific graph configuration:
|
||||
|
||||
1. Configure your graph settings as desired
|
||||
2. Settings are automatically encoded in the URL
|
||||
3. Share the URL - recipients see the same view
|
||||
4. URL settings override their local settings temporarily
|
||||
|
||||
## Group Query Grammar
|
||||
|
||||
The group query system supports three query types for coloring nodes:
|
||||
|
||||
### Query Syntax
|
||||
|
||||
#### Tag Query: `tag:#tagname` or `tag:tagname`
|
||||
|
||||
Matches nodes that have the specified tag.
|
||||
|
||||
```typescript
|
||||
// Match all notes with #markdown tag
|
||||
"tag:#markdown"
|
||||
|
||||
// Hash is optional
|
||||
"tag:markdown"
|
||||
|
||||
// Case-insensitive matching
|
||||
"tag:TEST" // matches #test, #Test, #TEST
|
||||
```
|
||||
|
||||
**Behavior:**
|
||||
- Checks node's `tags` array
|
||||
- Handles tags with or without `#` prefix
|
||||
- Case-insensitive comparison
|
||||
|
||||
#### File Query: `file:searchtext`
|
||||
|
||||
Matches nodes where title or path contains the search text.
|
||||
|
||||
```typescript
|
||||
// Match notes with "test" in title or path
|
||||
"file:test"
|
||||
|
||||
// Partial matches work
|
||||
"file:meeting" // matches "Meeting Notes.md", "meetings/summary.md"
|
||||
```
|
||||
|
||||
**Behavior:**
|
||||
- Searches in both `title` and `path` fields
|
||||
- Case-insensitive substring matching
|
||||
- Useful for grouping by naming conventions
|
||||
|
||||
#### Path Query: `path:folder` or `path:folder/subfolder`
|
||||
|
||||
Matches nodes in a specific folder path.
|
||||
|
||||
```typescript
|
||||
// Match notes in "projects" folder
|
||||
"path:projects"
|
||||
|
||||
// Match notes in nested folder
|
||||
"path:projects/2024"
|
||||
|
||||
// Trailing slash optional
|
||||
"path:projects/"
|
||||
```
|
||||
|
||||
**Behavior:**
|
||||
- Checks if node's `path` starts with the specified folder
|
||||
- Case-insensitive comparison
|
||||
- Supports nested paths with `/` separator
|
||||
- Exact folder boundary matching (doesn't partially match folder names)
|
||||
|
||||
### Parsing Implementation
|
||||
|
||||
```typescript
|
||||
import { parseGroupQuery, nodeMatchesQuery } from './graph/group-query.parser';
|
||||
|
||||
// Parse a query
|
||||
const query = parseGroupQuery('tag:#markdown');
|
||||
// { type: 'tag', value: '#markdown', raw: 'tag:#markdown' }
|
||||
|
||||
// Check if node matches
|
||||
const matches = nodeMatchesQuery(node, query);
|
||||
// true or false
|
||||
```
|
||||
|
||||
### Query Validation
|
||||
|
||||
```typescript
|
||||
import { isValidQuery } from './graph/group-query.parser';
|
||||
|
||||
isValidQuery('tag:#test'); // true
|
||||
isValidQuery('file:meeting'); // true
|
||||
isValidQuery('path:folder'); // true
|
||||
isValidQuery('invalid'); // false
|
||||
isValidQuery('tag:'); // false
|
||||
```
|
||||
|
||||
### Color Group Application
|
||||
|
||||
Groups are applied in order with **first-match-wins** logic:
|
||||
|
||||
```json
|
||||
{
|
||||
"colorGroups": [
|
||||
{ "query": "tag:#important", "color": { "a": 1, "rgb": 16711680 } },
|
||||
{ "query": "tag:#markdown", "color": { "a": 1, "rgb": 65280 } },
|
||||
{ "query": "file:test", "color": { "a": 1, "rgb": 255 } }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
A node with both `#important` and `#markdown` tags will be colored with the **first matching group** (#important - red).
|
||||
|
||||
### Examples
|
||||
|
||||
```typescript
|
||||
// Group by project folder
|
||||
"path:projects"
|
||||
|
||||
// Group by status tags
|
||||
"tag:#todo"
|
||||
"tag:#in-progress"
|
||||
"tag:#done"
|
||||
|
||||
// Group by file type
|
||||
"file:.excalidraw"
|
||||
"file:.canvas"
|
||||
|
||||
// Group by area
|
||||
"path:work"
|
||||
"path:personal"
|
||||
|
||||
// Group by naming convention
|
||||
"file:meeting"
|
||||
"file:daily-note"
|
||||
```
|
||||
|
||||
## Credits
|
||||
|
||||
Designed to match [Obsidian](https://obsidian.md/) graph view interface and behavior.
|
||||
|
||||
Reference in New Issue
Block a user