revert: remove pinned properties feature
Reverts commitsd263e43,ae2260c,473239b,aaa4691,cd7a570,3cd097d. Removes PinnedPropertiesBar, usePinnedProperties hook, Rust backend for _pinned_properties, and all related tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -39,9 +39,8 @@ Any frontmatter field whose name starts with `_` is a **system property**:
|
||||
Examples:
|
||||
```yaml
|
||||
_pinned_properties: # which properties appear in the editor inline bar (per-type)
|
||||
- "Status:circle-dot" # format: "key:icon" (icon is optional)
|
||||
- "Belongs to:arrow-up-right"
|
||||
- "Due date:calendar"
|
||||
- key: status
|
||||
icon: circle-dot
|
||||
_icon: shapes # icon assigned to a type
|
||||
_color: blue # color assigned to a type
|
||||
_order: 10 # sort order in the sidebar
|
||||
@@ -179,7 +178,6 @@ Each entity type can have a corresponding **type document** in the `type/` folde
|
||||
| `sort` | string | Default sort: "modified:desc", "title:asc", "property:Priority:asc" |
|
||||
| `view` | string | Default view mode: "all", "editor-list", "editor-only" |
|
||||
| `visible` | bool | Whether type appears in sidebar (default: true) |
|
||||
| `_pinned_properties` | list | Pinned properties shown in editor bar + note list. Each item: `"key:icon"` |
|
||||
|
||||
**Type relationship**: When any entry has an `isA` value (e.g., "Project"), the Rust backend automatically adds a `"Type"` entry to its `relationships` map pointing to `[[type/project]]`. This makes the type navigable from the Inspector panel.
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ use super::{parse_md_file, scan_vault, VaultEntry};
|
||||
// --- Vault Cache ---
|
||||
|
||||
/// Bump this when VaultEntry fields change to force a full rescan.
|
||||
const CACHE_VERSION: u32 = 9;
|
||||
const CACHE_VERSION: u32 = 8;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct VaultCache {
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// A single pinned-property config item stored in the type's frontmatter.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
pub struct PinnedPropertyConfig {
|
||||
pub key: String,
|
||||
pub icon: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
|
||||
pub struct VaultEntry {
|
||||
pub path: String,
|
||||
@@ -66,7 +59,4 @@ pub struct VaultEntry {
|
||||
/// Only includes strings, numbers, and booleans — arrays/objects are excluded.
|
||||
#[serde(default)]
|
||||
pub properties: HashMap<String, serde_json::Value>,
|
||||
/// Pinned properties configuration for Type entries.
|
||||
#[serde(rename = "pinnedProperties", default)]
|
||||
pub pinned_properties: Vec<PinnedPropertyConfig>,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::vault::entry::PinnedPropertyConfig;
|
||||
use crate::vault::parsing::contains_wikilink;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
@@ -194,9 +193,6 @@ pub(crate) fn extract_relationships(
|
||||
let mut relationships = HashMap::new();
|
||||
|
||||
for (key, value) in data {
|
||||
if key.starts_with('_') {
|
||||
continue;
|
||||
}
|
||||
if SKIP_KEYS.iter().any(|k| k.eq_ignore_ascii_case(key)) {
|
||||
continue;
|
||||
}
|
||||
@@ -232,9 +228,6 @@ pub(crate) fn extract_properties(
|
||||
let mut properties = HashMap::new();
|
||||
|
||||
for (key, value) in data {
|
||||
if key.starts_with('_') {
|
||||
continue;
|
||||
}
|
||||
let lower = key.to_ascii_lowercase();
|
||||
if SKIP_KEYS.iter().any(|k| k.eq_ignore_ascii_case(&lower)) {
|
||||
continue;
|
||||
@@ -272,38 +265,6 @@ pub(crate) fn resolve_is_a(fm_is_a: Option<StringOrList>) -> Option<String> {
|
||||
fm_is_a.and_then(|a| a.into_vec().into_iter().next())
|
||||
}
|
||||
|
||||
/// Parse a single pinned-property entry from "key:icon" format.
|
||||
fn parse_pinned_entry(s: &str) -> PinnedPropertyConfig {
|
||||
match s.split_once(':') {
|
||||
Some((key, icon)) if !icon.is_empty() => PinnedPropertyConfig {
|
||||
key: key.trim().to_string(),
|
||||
icon: Some(icon.trim().to_string()),
|
||||
},
|
||||
_ => PinnedPropertyConfig {
|
||||
key: s.trim().to_string(),
|
||||
icon: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract `_pinned_properties` from raw YAML frontmatter.
|
||||
pub(crate) fn extract_pinned_properties(
|
||||
data: &HashMap<String, serde_json::Value>,
|
||||
) -> Vec<PinnedPropertyConfig> {
|
||||
let value = match data.get("_pinned_properties") {
|
||||
Some(v) => v,
|
||||
None => return Vec::new(),
|
||||
};
|
||||
match value {
|
||||
serde_json::Value::Array(arr) => arr
|
||||
.iter()
|
||||
.filter_map(|v| v.as_str())
|
||||
.map(parse_pinned_entry)
|
||||
.collect(),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert gray_matter::Pod to serde_json::Value
|
||||
fn pod_to_json(pod: gray_matter::Pod) -> serde_json::Value {
|
||||
match pod {
|
||||
@@ -323,26 +284,17 @@ fn pod_to_json(pod: gray_matter::Pod) -> serde_json::Value {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return type for `extract_fm_and_rels`: (frontmatter, relationships, properties, pinned).
|
||||
type FrontmatterBundle = (
|
||||
/// Extract frontmatter, relationships, and custom properties from parsed gray_matter data.
|
||||
pub(crate) fn extract_fm_and_rels(
|
||||
data: Option<gray_matter::Pod>,
|
||||
) -> (
|
||||
Frontmatter,
|
||||
HashMap<String, Vec<String>>,
|
||||
HashMap<String, serde_json::Value>,
|
||||
Vec<PinnedPropertyConfig>,
|
||||
);
|
||||
|
||||
/// Extract frontmatter, relationships, custom properties, and pinned-property config.
|
||||
pub(crate) fn extract_fm_and_rels(data: Option<gray_matter::Pod>) -> FrontmatterBundle {
|
||||
) {
|
||||
let hash = match data {
|
||||
Some(gray_matter::Pod::Hash(map)) => map,
|
||||
_ => {
|
||||
return (
|
||||
Frontmatter::default(),
|
||||
HashMap::new(),
|
||||
HashMap::new(),
|
||||
Vec::new(),
|
||||
)
|
||||
}
|
||||
_ => return (Frontmatter::default(), HashMap::new(), HashMap::new()),
|
||||
};
|
||||
let json_map: HashMap<String, serde_json::Value> =
|
||||
hash.into_iter().map(|(k, v)| (k, pod_to_json(v))).collect();
|
||||
@@ -350,70 +302,5 @@ pub(crate) fn extract_fm_and_rels(data: Option<gray_matter::Pod>) -> Frontmatter
|
||||
parse_frontmatter(&json_map),
|
||||
extract_relationships(&json_map),
|
||||
extract_properties(&json_map),
|
||||
extract_pinned_properties(&json_map),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_pinned_entry_with_icon() {
|
||||
let result = parse_pinned_entry("Status:circle-dot");
|
||||
assert_eq!(result.key, "Status");
|
||||
assert_eq!(result.icon, Some("circle-dot".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_pinned_entry_without_icon() {
|
||||
let result = parse_pinned_entry("Priority");
|
||||
assert_eq!(result.key, "Priority");
|
||||
assert_eq!(result.icon, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_pinned_properties_array() {
|
||||
let mut data = HashMap::new();
|
||||
data.insert(
|
||||
"_pinned_properties".to_string(),
|
||||
serde_json::json!(["Status:circle-dot", "Belongs to:arrow-up-right", "date"]),
|
||||
);
|
||||
let result = extract_pinned_properties(&data);
|
||||
assert_eq!(result.len(), 3);
|
||||
assert_eq!(result[0].key, "Status");
|
||||
assert_eq!(result[0].icon, Some("circle-dot".to_string()));
|
||||
assert_eq!(result[1].key, "Belongs to");
|
||||
assert_eq!(result[1].icon, Some("arrow-up-right".to_string()));
|
||||
assert_eq!(result[2].key, "date");
|
||||
assert_eq!(result[2].icon, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_pinned_properties_missing() {
|
||||
let data = HashMap::new();
|
||||
assert!(extract_pinned_properties(&data).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_underscore_keys_excluded_from_properties() {
|
||||
let mut data = HashMap::new();
|
||||
data.insert("_pinned_properties".to_string(), serde_json::json!(["a:b"]));
|
||||
data.insert("_hidden".to_string(), serde_json::json!("secret"));
|
||||
data.insert("visible_key".to_string(), serde_json::json!("value"));
|
||||
let props = extract_properties(&data);
|
||||
assert!(!props.contains_key("_pinned_properties"));
|
||||
assert!(!props.contains_key("_hidden"));
|
||||
assert!(props.contains_key("visible_key"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_underscore_keys_excluded_from_relationships() {
|
||||
let mut data = HashMap::new();
|
||||
data.insert("_refs".to_string(), serde_json::json!("[[note]]"));
|
||||
data.insert("Topics".to_string(), serde_json::json!("[[topic]]"));
|
||||
let rels = extract_relationships(&data);
|
||||
assert!(!rels.contains_key("_refs"));
|
||||
assert!(rels.contains_key("Topics"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,8 +43,7 @@ pub fn parse_md_file(path: &Path) -> Result<VaultEntry, String> {
|
||||
|
||||
let matter = Matter::<YAML>::new();
|
||||
let parsed = matter.parse(&content);
|
||||
let (frontmatter, mut relationships, properties, pinned_properties) =
|
||||
extract_fm_and_rels(parsed.data);
|
||||
let (frontmatter, mut relationships, properties) = extract_fm_and_rels(parsed.data);
|
||||
|
||||
let title = extract_title(frontmatter.title.as_deref(), &content, &filename);
|
||||
let snippet = extract_snippet(&content);
|
||||
@@ -104,7 +103,6 @@ pub fn parse_md_file(path: &Path) -> Result<VaultEntry, String> {
|
||||
word_count,
|
||||
outgoing_links,
|
||||
properties,
|
||||
pinned_properties,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useState, useCallback, useRef, useEffect } from 'react'
|
||||
import type { VaultEntry } from '../types'
|
||||
import type { FrontmatterValue } from './Inspector'
|
||||
import type { ParsedFrontmatter } from '../utils/frontmatter'
|
||||
@@ -9,7 +8,6 @@ import { TypeSelector } from './TypeSelector'
|
||||
import { AddPropertyForm } from './AddPropertyForm'
|
||||
import { countWords } from '../utils/wikilinks'
|
||||
import type { PropertyDisplayMode } from '../utils/propertyTypes'
|
||||
import { PushPin } from '@phosphor-icons/react'
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components -- utility co-located with component
|
||||
export function containsWikilinks(value: FrontmatterValue): boolean {
|
||||
@@ -32,51 +30,15 @@ function formatFileSize(bytes: number): string {
|
||||
return `${mb.toFixed(1)} MB`
|
||||
}
|
||||
|
||||
function PropertyPinMenu({ x, y, isPinned, onPin, onUnpin, onClose }: {
|
||||
x: number; y: number; isPinned: boolean
|
||||
onPin: () => void; onUnpin: () => void; onClose: () => void
|
||||
}) {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
useEffect(() => {
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) onClose()
|
||||
}
|
||||
document.addEventListener('mousedown', handler)
|
||||
return () => document.removeEventListener('mousedown', handler)
|
||||
}, [onClose])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="fixed z-50 flex flex-col rounded-lg border border-border bg-popover shadow-lg"
|
||||
style={{ left: x, top: y, minWidth: 160, padding: 4 }}
|
||||
data-testid="property-context-menu"
|
||||
>
|
||||
<button
|
||||
className="flex w-full items-center gap-2 rounded px-2.5 py-1.5 text-left text-[13px] text-foreground transition-colors hover:bg-accent"
|
||||
style={{ border: 'none', background: 'transparent', cursor: 'pointer' }}
|
||||
onClick={() => { if (isPinned) { onUnpin() } else { onPin() } onClose() }}
|
||||
>
|
||||
<PushPin size={14} />
|
||||
{isPinned ? 'Unpin from editor' : 'Pin to editor'}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PropertyRow({ propKey, value, editingKey, displayMode, autoMode, vaultStatuses, vaultTags, isPinned, onStartEdit, onSave, onSaveList, onUpdate, onDelete, onDisplayModeChange, onPin, onUnpin }: {
|
||||
function PropertyRow({ propKey, value, editingKey, displayMode, autoMode, vaultStatuses, vaultTags, onStartEdit, onSave, onSaveList, onUpdate, onDelete, onDisplayModeChange }: {
|
||||
propKey: string; value: FrontmatterValue; editingKey: string | null
|
||||
displayMode: PropertyDisplayMode; autoMode: PropertyDisplayMode
|
||||
vaultStatuses: string[]; vaultTags: string[]
|
||||
isPinned: boolean
|
||||
onStartEdit: (key: string | null) => void; onSave: (key: string, value: string) => void
|
||||
onSaveList: (key: string, items: string[]) => void
|
||||
onUpdate?: (key: string, value: FrontmatterValue) => void; onDelete?: (key: string) => void
|
||||
onDisplayModeChange: (key: string, mode: PropertyDisplayMode | null) => void
|
||||
onPin?: (key: string) => void; onUnpin?: (key: string) => void
|
||||
}) {
|
||||
const [ctxMenu, setCtxMenu] = useState<{ x: number; y: number } | null>(null)
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && editingKey !== propKey) {
|
||||
e.preventDefault()
|
||||
@@ -84,21 +46,9 @@ function PropertyRow({ propKey, value, editingKey, displayMode, autoMode, vaultS
|
||||
}
|
||||
}
|
||||
|
||||
const handleContextMenu = useCallback((e: React.MouseEvent) => {
|
||||
if (!onPin && !onUnpin) return
|
||||
e.preventDefault()
|
||||
setCtxMenu({ x: e.clientX, y: e.clientY })
|
||||
}, [onPin, onUnpin])
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group/prop grid min-w-0 grid-cols-2 items-center gap-2 rounded px-1.5 outline-none transition-colors hover:bg-muted focus:bg-muted focus:ring-1 focus:ring-primary"
|
||||
style={isPinned ? { backgroundColor: 'color-mix(in srgb, var(--primary) 5%, transparent)' } : undefined}
|
||||
tabIndex={0} onKeyDown={handleKeyDown} onContextMenu={handleContextMenu}
|
||||
data-testid="editable-property"
|
||||
>
|
||||
<div className="group/prop grid min-w-0 grid-cols-2 items-center gap-2 rounded px-1.5 outline-none transition-colors hover:bg-muted focus:bg-muted focus:ring-1 focus:ring-primary" tabIndex={0} onKeyDown={handleKeyDown} data-testid="editable-property">
|
||||
<span className="font-mono-overline flex min-w-0 items-center gap-1 text-muted-foreground">
|
||||
{isPinned && <PushPin size={10} className="shrink-0 text-primary" style={{ opacity: 0.6 }} />}
|
||||
<span className="truncate">{propKey}</span>
|
||||
{onDelete && (
|
||||
<button className="border-none bg-transparent p-0 text-sm leading-none text-muted-foreground opacity-0 transition-all hover:text-destructive group-hover/prop:opacity-100" onClick={() => onDelete(propKey)} title="Delete property">×</button>
|
||||
@@ -108,13 +58,6 @@ function PropertyRow({ propKey, value, editingKey, displayMode, autoMode, vaultS
|
||||
<div className="min-w-0">
|
||||
<SmartPropertyValueCell propKey={propKey} value={value} displayMode={displayMode} isEditing={editingKey === propKey} vaultStatuses={vaultStatuses} vaultTags={vaultTags} onStartEdit={onStartEdit} onSave={onSave} onSaveList={onSaveList} onUpdate={onUpdate} />
|
||||
</div>
|
||||
{ctxMenu && (
|
||||
<PropertyPinMenu
|
||||
x={ctxMenu.x} y={ctxMenu.y} isPinned={isPinned}
|
||||
onPin={() => onPin?.(propKey)} onUnpin={() => onUnpin?.(propKey)}
|
||||
onClose={() => setCtxMenu(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -155,7 +98,6 @@ function NoteInfoSection({ entry, wordCount }: { entry: VaultEntry; wordCount: n
|
||||
export function DynamicPropertiesPanel({
|
||||
entry, content, frontmatter, entries,
|
||||
onUpdateProperty, onDeleteProperty, onAddProperty, onNavigate,
|
||||
isPinned, onPin, onUnpin,
|
||||
}: {
|
||||
entry: VaultEntry
|
||||
content: string | null
|
||||
@@ -165,9 +107,6 @@ export function DynamicPropertiesPanel({
|
||||
onDeleteProperty?: (key: string) => void
|
||||
onAddProperty?: (key: string, value: FrontmatterValue) => void
|
||||
onNavigate?: (target: string) => void
|
||||
isPinned?: (key: string) => boolean
|
||||
onPin?: (key: string) => void
|
||||
onUnpin?: (key: string) => void
|
||||
}) {
|
||||
const {
|
||||
editingKey, setEditingKey, showAddDialog, setShowAddDialog, displayOverrides,
|
||||
@@ -187,12 +126,10 @@ export function DynamicPropertiesPanel({
|
||||
editingKey={editingKey} displayMode={getEffectiveDisplayMode(key, value, displayOverrides)} autoMode={detectPropertyType(key, value)}
|
||||
vaultStatuses={vaultStatuses}
|
||||
vaultTags={vaultTagsByKey[key] ?? []}
|
||||
isPinned={isPinned?.(key) ?? false}
|
||||
onStartEdit={setEditingKey} onSave={handleSaveValue}
|
||||
onSaveList={handleSaveList} onUpdate={onUpdateProperty}
|
||||
onDelete={onDeleteProperty}
|
||||
onDisplayModeChange={handleDisplayModeChange}
|
||||
onPin={onPin} onUnpin={onUnpin}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -264,7 +264,6 @@ export const Editor = memo(function Editor(props: EditorProps) {
|
||||
isConflicted={isConflicted}
|
||||
onKeepMine={onKeepMine}
|
||||
onKeepTheirs={onKeepTheirs}
|
||||
onUpdateFrontmatter={onUpdateFrontmatter}
|
||||
/>
|
||||
}
|
||||
{(showAIChat || !inspectorCollapsed) && <ResizeHandle onResize={onInspectorResize} />}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type React from 'react'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import type { VaultEntry, NoteStatus } from '../types'
|
||||
import type { useCreateBlockNote } from '@blocknote/react'
|
||||
import type { FrontmatterValue } from './Inspector'
|
||||
import { DiffView } from './DiffView'
|
||||
import { BreadcrumbBar } from './BreadcrumbBar'
|
||||
import { TitleField } from './TitleField'
|
||||
@@ -14,8 +13,6 @@ import { RawEditorView } from './RawEditorView'
|
||||
import { countWords } from '../utils/wikilinks'
|
||||
import { SingleEditorView } from './SingleEditorView'
|
||||
import { isEmoji } from '../utils/emoji'
|
||||
import { parseFrontmatter } from '../utils/frontmatter'
|
||||
import { PinnedPropertiesBar } from './PinnedPropertiesBar'
|
||||
|
||||
interface Tab {
|
||||
entry: VaultEntry
|
||||
@@ -63,8 +60,6 @@ interface EditorContentProps {
|
||||
onKeepMine?: (path: string) => void
|
||||
/** Resolve conflict by keeping the remote version. */
|
||||
onKeepTheirs?: (path: string) => void
|
||||
/** Update a frontmatter property (for inline pinned-property editing). */
|
||||
onUpdateFrontmatter?: (path: string, key: string, value: FrontmatterValue) => Promise<void>
|
||||
}
|
||||
|
||||
function EditorLoadingSkeleton() {
|
||||
@@ -161,7 +156,6 @@ export function EditorContent({
|
||||
onDeleteNote, rawLatestContentRef, onTitleChange,
|
||||
onSetNoteIcon, onRemoveNoteIcon,
|
||||
isConflicted, onKeepMine, onKeepTheirs,
|
||||
onUpdateFrontmatter,
|
||||
...breadcrumbProps
|
||||
}: EditorContentProps) {
|
||||
// Look up trashed/archived from the latest vault entries, not the tab snapshot,
|
||||
@@ -181,12 +175,6 @@ export function EditorContent({
|
||||
if (activeTab) onRemoveNoteIcon?.(activeTab.entry.path)
|
||||
}, [activeTab, onRemoveNoteIcon])
|
||||
|
||||
const frontmatter = useMemo(
|
||||
() => parseFrontmatter(activeTab?.content ?? null),
|
||||
[activeTab?.content],
|
||||
)
|
||||
const currentEntry = freshEntry ?? activeTab?.entry ?? null
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col min-w-0 min-h-0">
|
||||
{activeTab && (
|
||||
@@ -227,15 +215,6 @@ export function EditorContent({
|
||||
editable={!isTrashed}
|
||||
onTitleChange={(newTitle) => onTitleChange?.(activeTab.entry.path, newTitle)}
|
||||
/>
|
||||
{currentEntry && (
|
||||
<PinnedPropertiesBar
|
||||
entry={currentEntry}
|
||||
entries={entries}
|
||||
frontmatter={frontmatter}
|
||||
onUpdateFrontmatter={onUpdateFrontmatter}
|
||||
onNavigate={onNavigateWikilink}
|
||||
/>
|
||||
)}
|
||||
<div className="title-section__separator" />
|
||||
</div>
|
||||
<SingleEditorView editor={editor} entries={entries} onNavigateWikilink={onNavigateWikilink} onChange={onEditorChange} vaultPath={vaultPath} editable={!isTrashed} />
|
||||
|
||||
@@ -8,7 +8,6 @@ import { DynamicPropertiesPanel } from './DynamicPropertiesPanel'
|
||||
import { DynamicRelationshipsPanel, BacklinksPanel, ReferencedByPanel, GitHistoryPanel, InstancesPanel } from './InspectorPanels'
|
||||
import { wikilinkTarget } from '../utils/wikilink'
|
||||
import type { ReferencedByItem, BacklinkItem } from './InspectorPanels'
|
||||
import { usePinnedProperties } from '../hooks/usePinnedProperties'
|
||||
|
||||
export type FrontmatterValue = string | number | boolean | string[] | null
|
||||
|
||||
@@ -139,11 +138,6 @@ export function Inspector({
|
||||
if (entry && onAddProperty) onAddProperty(entry.path, key, value)
|
||||
}, [entry, onAddProperty])
|
||||
|
||||
const { isPinned, pinProperty, unpinProperty } = usePinnedProperties({
|
||||
entry, entries, frontmatter,
|
||||
onUpdateTypeFrontmatter: onUpdateFrontmatter,
|
||||
})
|
||||
|
||||
return (
|
||||
<aside className={cn("flex flex-1 flex-col overflow-hidden border-l border-border bg-background text-foreground transition-[width] duration-200", collapsed && "!w-10 !min-w-10")}>
|
||||
<InspectorHeader collapsed={collapsed} onToggle={onToggle} />
|
||||
@@ -158,7 +152,6 @@ export function Inspector({
|
||||
onDeleteProperty={onDeleteProperty ? handleDeleteProperty : undefined}
|
||||
onAddProperty={onAddProperty ? handleAddProperty : undefined}
|
||||
onNavigate={onNavigate}
|
||||
isPinned={isPinned} onPin={pinProperty} onUnpin={unpinProperty}
|
||||
/>
|
||||
<DynamicRelationshipsPanel
|
||||
frontmatter={frontmatter} entries={entries} typeEntryMap={typeEntryMap} onNavigate={onNavigate}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMemo, type ComponentType, type SVGAttributes } from 'react'
|
||||
import type { VaultEntry, NoteStatus, PinnedPropertyConfig } from '../types'
|
||||
import type { VaultEntry, NoteStatus } from '../types'
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
Wrench, Flask, Target, ArrowsClockwise,
|
||||
@@ -9,7 +9,6 @@ import { getTypeColor, getTypeLightColor } from '../utils/typeColors'
|
||||
import { resolveIcon } from '../utils/iconRegistry'
|
||||
import { relativeDate, getDisplayDate } from '../utils/noteListHelpers'
|
||||
import { isEmoji } from '../utils/emoji'
|
||||
import { NoteListPinnedValues } from './NoteListPinnedValues'
|
||||
|
||||
const TYPE_ICON_MAP: Record<string, ComponentType<SVGAttributes<SVGSVGElement>>> = {
|
||||
Project: Wrench,
|
||||
@@ -28,15 +27,6 @@ export function getTypeIcon(isA: string | null, customIcon?: string | null): Com
|
||||
return (isA && TYPE_ICON_MAP[isA]) || FileText
|
||||
}
|
||||
|
||||
function defaultPinnedConfigs(entry: VaultEntry): PinnedPropertyConfig[] {
|
||||
if (!entry.isA) return []
|
||||
const pins: PinnedPropertyConfig[] = []
|
||||
if (entry.status != null) pins.push({ key: 'Status', icon: 'circle-dot' })
|
||||
if (entry.belongsTo.length > 0) pins.push({ key: 'Belongs to', icon: 'arrow-up-right' })
|
||||
if (entry.relatedTo.length > 0) pins.push({ key: 'Related to', icon: 'arrows-left-right' })
|
||||
return pins
|
||||
}
|
||||
|
||||
const THIRTY_DAYS_SECS = 86400 * 30
|
||||
|
||||
function TrashDateLine({ entry }: { entry: VaultEntry }) {
|
||||
@@ -101,37 +91,6 @@ function noteItemStyle(isSelected: boolean, isMultiSelected: boolean, typeColor:
|
||||
return base
|
||||
}
|
||||
|
||||
|
||||
function NoteItemTitle({ entry, noteStatus, isSelected }: { entry: VaultEntry; noteStatus: NoteStatus; isSelected: boolean }) {
|
||||
return (
|
||||
<div className="pr-5">
|
||||
<div className={cn("truncate text-[13px] text-foreground", isSelected ? "font-semibold" : "font-medium")}>
|
||||
{noteStatus !== 'clean' && <StatusDot noteStatus={noteStatus} />}
|
||||
{entry.icon && isEmoji(entry.icon) && <span className="mr-1">{entry.icon}</span>}
|
||||
{entry.title}
|
||||
<StateBadge archived={entry.archived} trashed={entry.trashed} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function NoteItemFooter({ entry, pinnedConfigs }: { entry: VaultEntry; pinnedConfigs: PinnedPropertyConfig[] }) {
|
||||
return (
|
||||
<>
|
||||
{entry.snippet && (
|
||||
<div className="mt-0.5 text-[12px] leading-[1.5] text-muted-foreground" data-testid="note-snippet" style={{ display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>
|
||||
{entry.snippet}
|
||||
</div>
|
||||
)}
|
||||
{pinnedConfigs.length > 0 && <NoteListPinnedValues entry={entry} pinnedConfigs={pinnedConfigs} />}
|
||||
{entry.trashed && entry.trashedAt
|
||||
? <TrashDateLine entry={entry} />
|
||||
: <div className="mt-0.5 text-[10px] text-muted-foreground">{relativeDate(getDisplayDate(entry))}</div>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function NoteItem({ entry, isSelected, isMultiSelected = false, isHighlighted = false, noteStatus = 'clean', typeEntryMap, onClickNote, onPrefetch }: {
|
||||
entry: VaultEntry
|
||||
isSelected: boolean
|
||||
@@ -146,10 +105,6 @@ export function NoteItem({ entry, isSelected, isMultiSelected = false, isHighlig
|
||||
const typeColor = getTypeColor(entry.isA ?? 'Note', te?.color)
|
||||
const typeLightColor = getTypeLightColor(entry.isA ?? 'Note', te?.color)
|
||||
const TypeIcon = useMemo(() => getTypeIcon(entry.isA, te?.icon), [entry.isA, te?.icon])
|
||||
const pinnedConfigs = useMemo((): PinnedPropertyConfig[] => {
|
||||
if (te?.pinnedProperties && te.pinnedProperties.length > 0) return te.pinnedProperties
|
||||
return defaultPinnedConfigs(entry)
|
||||
}, [te, entry])
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -167,8 +122,23 @@ export function NoteItem({ entry, isSelected, isMultiSelected = false, isHighlig
|
||||
>
|
||||
{/* eslint-disable-next-line react-hooks/static-components -- icon lookup from static map, no internal state */}
|
||||
<TypeIcon width={14} height={14} className="absolute right-3 top-2.5" style={{ color: typeColor }} data-testid="type-icon" />
|
||||
<NoteItemTitle entry={entry} noteStatus={noteStatus} isSelected={isSelected} />
|
||||
<NoteItemFooter entry={entry} pinnedConfigs={pinnedConfigs} />
|
||||
<div className="pr-5">
|
||||
<div className={cn("truncate text-[13px] text-foreground", isSelected ? "font-semibold" : "font-medium")}>
|
||||
{noteStatus !== 'clean' && <StatusDot noteStatus={noteStatus} />}
|
||||
{entry.icon && isEmoji(entry.icon) && <span className="mr-1">{entry.icon}</span>}
|
||||
{entry.title}
|
||||
<StateBadge archived={entry.archived} trashed={entry.trashed} />
|
||||
</div>
|
||||
</div>
|
||||
{entry.snippet && (
|
||||
<div className="mt-0.5 text-[12px] leading-[1.5] text-muted-foreground" data-testid="note-snippet" style={{ display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>
|
||||
{entry.snippet}
|
||||
</div>
|
||||
)}
|
||||
{entry.trashed && entry.trashedAt
|
||||
? <TrashDateLine entry={entry} />
|
||||
: <div className="mt-0.5 text-[10px] text-muted-foreground">{relativeDate(getDisplayDate(entry))}</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -264,11 +264,11 @@ describe('NoteList', () => {
|
||||
expect(titleTexts).toEqual(['Newest', 'Middle', 'Oldest'])
|
||||
})
|
||||
|
||||
it('shows pinned status values in note items', () => {
|
||||
it('does not render type badge or status on note items', () => {
|
||||
render(<NoteList {...defaultFilterProps} entries={mockEntries} selection={allSelection} selectedNote={null} onSelectNote={noopSelect} onReplaceActiveTab={noopReplace} onCreateNote={vi.fn()} />)
|
||||
// Pinned properties feature shows status values as chips in note items
|
||||
const activeChips = screen.queryAllByText('Active')
|
||||
expect(activeChips.length).toBeGreaterThanOrEqual(0) // status shown if entry has one
|
||||
// Type badges like "Project", "Note" etc. should not appear as separate badge elements
|
||||
// The word "Project" should only appear in the ALL CAPS pill "PROJECTS 1", not as a standalone badge
|
||||
expect(screen.queryByText('Active')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('header shows search and plus icons instead of count badge', () => {
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
import { memo } from 'react'
|
||||
import type { VaultEntry, PinnedPropertyConfig } from '../types'
|
||||
import { resolveIcon } from '../utils/iconRegistry'
|
||||
import { getStatusStyle } from '../utils/statusStyles'
|
||||
import { wikilinkDisplay } from '../utils/wikilink'
|
||||
import { resolvePinIcon } from '../hooks/usePinnedProperties'
|
||||
import type { FrontmatterValue } from './Inspector'
|
||||
|
||||
function resolveNoteValue(entry: VaultEntry, key: string): { value: FrontmatterValue; isRelationship: boolean } {
|
||||
const lowerKey = key.toLowerCase().replace(/\s+/g, '_')
|
||||
if (lowerKey === 'status') return { value: entry.status, isRelationship: false }
|
||||
const relValue = entry.relationships[key]
|
||||
if (relValue && relValue.length > 0) {
|
||||
return { value: relValue.length === 1 ? relValue[0] : relValue, isRelationship: true }
|
||||
}
|
||||
const propValue = entry.properties[key]
|
||||
if (propValue != null) return { value: propValue, isRelationship: false }
|
||||
return { value: null, isRelationship: false }
|
||||
}
|
||||
|
||||
function formatCompactValue(value: FrontmatterValue, isRelationship: boolean): string | null {
|
||||
if (value == null) return null
|
||||
if (typeof value === 'boolean') return value ? 'Yes' : 'No'
|
||||
if (typeof value === 'number') return String(value)
|
||||
if (typeof value === 'string') return isRelationship ? wikilinkDisplay(value) : value
|
||||
if (Array.isArray(value)) {
|
||||
const items = value.map((v) => (isRelationship ? wikilinkDisplay(String(v)) : String(v)))
|
||||
if (items.length <= 2) return items.join(', ')
|
||||
return `${items[0]}, +${items.length - 1}`
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function chipStyle(key: string, value: FrontmatterValue, isRelationship: boolean): { bg: string; color: string } {
|
||||
if (key.toLowerCase() === 'status' && typeof value === 'string') return getStatusStyle(value)
|
||||
if (isRelationship) return { bg: 'var(--accent-blue-light)', color: 'var(--accent-blue)' }
|
||||
return { bg: 'var(--muted)', color: 'var(--muted-foreground)' }
|
||||
}
|
||||
|
||||
function ChipIcon({ name }: { name: string }) {
|
||||
const Icon = resolveIcon(name)
|
||||
// eslint-disable-next-line react-hooks/static-components -- icon from static registry, no internal state
|
||||
return <Icon width={10} height={10} style={{ flexShrink: 0 }} />
|
||||
}
|
||||
|
||||
function NoteListChip({ config, entry }: { config: PinnedPropertyConfig; entry: VaultEntry }) {
|
||||
const { value, isRelationship } = resolveNoteValue(entry, config.key)
|
||||
const display = formatCompactValue(value, isRelationship)
|
||||
const iconName = resolvePinIcon(config.key, config.icon)
|
||||
const colors = chipStyle(config.key, value, isRelationship)
|
||||
|
||||
if (!display) return null
|
||||
|
||||
return (
|
||||
<span
|
||||
className="inline-flex items-center gap-0.5 truncate"
|
||||
style={{ fontSize: 11, fontWeight: 500, borderRadius: 4, padding: '2px 6px', backgroundColor: colors.bg, color: colors.color, maxWidth: 140 }}
|
||||
data-testid="pinned-chip"
|
||||
>
|
||||
<ChipIcon name={iconName} />
|
||||
<span className="truncate">{display}</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export const NoteListPinnedValues = memo(function NoteListPinnedValues({ entry, pinnedConfigs }: { entry: VaultEntry; pinnedConfigs: PinnedPropertyConfig[] }) {
|
||||
const configs = pinnedConfigs.filter((c) => c.key.toLowerCase() !== 'type')
|
||||
if (configs.length === 0) return null
|
||||
return (
|
||||
<div className="mt-1 flex flex-wrap items-center gap-1.5" style={{ maxHeight: 44, overflow: 'hidden' }} data-testid="pinned-values">
|
||||
{configs.map((cfg) => (
|
||||
<NoteListChip key={cfg.key} config={cfg} entry={entry} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
@@ -1,167 +0,0 @@
|
||||
import { memo, useRef, useState, useCallback, useEffect } from 'react'
|
||||
import { DndContext, closestCenter, PointerSensor, useSensor, useSensors, type DragEndEvent } from '@dnd-kit/core'
|
||||
import { SortableContext, useSortable, horizontalListSortingStrategy } from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import type { VaultEntry } from '../types'
|
||||
import type { FrontmatterValue } from './Inspector'
|
||||
import type { ParsedFrontmatter } from '../utils/frontmatter'
|
||||
import { usePinnedProperties, type ResolvedPinnedProperty } from '../hooks/usePinnedProperties'
|
||||
import { PinnedPropertyChip } from './PinnedPropertyChip'
|
||||
import { DotsThree } from '@phosphor-icons/react'
|
||||
|
||||
export const PinnedPropertiesBar = memo(function PinnedPropertiesBar({ entry, entries, frontmatter, onUpdateFrontmatter, onNavigate }: {
|
||||
entry: VaultEntry
|
||||
entries: VaultEntry[]
|
||||
frontmatter: ParsedFrontmatter
|
||||
onUpdateFrontmatter?: (path: string, key: string, value: FrontmatterValue) => Promise<void>
|
||||
onNavigate?: (target: string) => void
|
||||
}) {
|
||||
const { resolved, pinnedConfigs, reorderPins } = usePinnedProperties({ entry, entries, frontmatter, onUpdateTypeFrontmatter: onUpdateFrontmatter })
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
||||
)
|
||||
|
||||
const handleDragEnd = useCallback((event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
if (!over || active.id === over.id) return
|
||||
const oldIndex = pinnedConfigs.findIndex((c) => c.key === active.id)
|
||||
const newIndex = pinnedConfigs.findIndex((c) => c.key === over.id)
|
||||
if (oldIndex === -1 || newIndex === -1) return
|
||||
const reordered = [...pinnedConfigs]
|
||||
const [moved] = reordered.splice(oldIndex, 1)
|
||||
reordered.splice(newIndex, 0, moved)
|
||||
reorderPins(reordered)
|
||||
}, [pinnedConfigs, reorderPins])
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [visibleCount, setVisibleCount] = useState(resolved.length)
|
||||
const [showOverflow, setShowOverflow] = useState(false)
|
||||
|
||||
const measureOverflow = useCallback(() => {
|
||||
const el = containerRef.current
|
||||
if (!el) return
|
||||
const items = Array.from(el.querySelectorAll<HTMLElement>('[data-pinchip]'))
|
||||
if (items.length === 0) { setVisibleCount(0); return }
|
||||
const containerRight = el.getBoundingClientRect().right - 48
|
||||
let count = 0
|
||||
for (const child of items) {
|
||||
if (child.getBoundingClientRect().right <= containerRight) count++
|
||||
else break
|
||||
}
|
||||
setVisibleCount(Math.max(count, 1))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
// Defer measurement to avoid synchronous setState in effect
|
||||
requestAnimationFrame(measureOverflow)
|
||||
const observer = new ResizeObserver(measureOverflow)
|
||||
if (containerRef.current) observer.observe(containerRef.current)
|
||||
return () => observer.disconnect()
|
||||
}, [measureOverflow, resolved.length])
|
||||
|
||||
if (resolved.length === 0) return null
|
||||
|
||||
const handleSave = (key: string, value: string) => {
|
||||
onUpdateFrontmatter?.(entry.path, key, value)
|
||||
}
|
||||
|
||||
const hiddenCount = resolved.length - visibleCount
|
||||
|
||||
const sortableIds = resolved.map((p) => p.key)
|
||||
|
||||
return (
|
||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||
<SortableContext items={sortableIds} strategy={horizontalListSortingStrategy}>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="flex items-start overflow-hidden"
|
||||
style={{ gap: 16, padding: '8px 0', position: 'relative' }}
|
||||
data-testid="pinned-properties-bar"
|
||||
>
|
||||
{resolved.map((p, i) => (
|
||||
<SortableChip key={p.key} prop={p} visible={i < visibleCount} onSave={handleSave} onNavigate={onNavigate} />
|
||||
))}
|
||||
{hiddenCount > 0 && (
|
||||
<OverflowPopover
|
||||
count={hiddenCount}
|
||||
items={resolved.slice(visibleCount)}
|
||||
open={showOverflow}
|
||||
onToggle={() => setShowOverflow((v) => !v)}
|
||||
onSave={handleSave}
|
||||
onNavigate={onNavigate}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
)
|
||||
})
|
||||
|
||||
function SortableChip({ prop, visible, onSave, onNavigate }: {
|
||||
prop: ResolvedPinnedProperty; visible: boolean
|
||||
onSave: (key: string, value: string) => void; onNavigate?: (target: string) => void
|
||||
}) {
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: prop.key })
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
...(visible ? {} : { visibility: 'hidden' as const, position: 'absolute' as const }),
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={setNodeRef} data-pinchip style={style} {...attributes} {...listeners}>
|
||||
<PinnedPropertyChip
|
||||
propKey={prop.key} label={prop.label} value={prop.value} icon={prop.icon}
|
||||
isRelationship={prop.isRelationship}
|
||||
onSave={onSave} onNavigate={onNavigate}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function OverflowPopover({ count, items, open, onToggle, onSave, onNavigate }: {
|
||||
count: number
|
||||
items: ResolvedPinnedProperty[]
|
||||
open: boolean
|
||||
onToggle: () => void
|
||||
onSave: (key: string, value: string) => void
|
||||
onNavigate?: (target: string) => void
|
||||
}) {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) onToggle()
|
||||
}
|
||||
document.addEventListener('mousedown', handler)
|
||||
return () => document.removeEventListener('mousedown', handler)
|
||||
}, [open, onToggle])
|
||||
|
||||
return (
|
||||
<div ref={ref} className="relative shrink-0 self-end">
|
||||
<button
|
||||
className="flex items-center gap-1 rounded border-none bg-transparent cursor-pointer text-muted-foreground hover:text-foreground transition-colors"
|
||||
style={{ padding: '3px 6px', fontSize: 12 }}
|
||||
onClick={onToggle}
|
||||
title={`${count} more properties`}
|
||||
>
|
||||
<DotsThree weight="bold" width={16} height={16} />
|
||||
<span>+{count}</span>
|
||||
</button>
|
||||
{open && (
|
||||
<div className="absolute right-0 top-full z-50 mt-1 flex flex-wrap gap-4 rounded-lg border border-border bg-popover p-3 shadow-md" style={{ minWidth: 200, maxWidth: 400 }}>
|
||||
{items.map((p) => (
|
||||
<PinnedPropertyChip
|
||||
key={p.key} propKey={p.key} label={p.label} value={p.value} icon={p.icon}
|
||||
isRelationship={p.isRelationship}
|
||||
onSave={onSave} onNavigate={onNavigate}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
import { useState, useRef, useCallback, useEffect } from 'react'
|
||||
import type { FrontmatterValue } from './Inspector'
|
||||
import { resolveIcon } from '../utils/iconRegistry'
|
||||
import { getStatusStyle, DEFAULT_STATUS_STYLE } from '../utils/statusStyles'
|
||||
import { wikilinkDisplay } from '../utils/wikilink'
|
||||
|
||||
function formatChipDisplay(value: FrontmatterValue, isRelationship: boolean): string | null {
|
||||
if (value == null) return null
|
||||
if (typeof value === 'boolean') return value ? 'Yes' : 'No'
|
||||
if (typeof value === 'number') return String(value)
|
||||
if (typeof value === 'string') {
|
||||
if (isRelationship) return wikilinkDisplay(value)
|
||||
return value || null
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length === 0) return null
|
||||
const displays = value.map((v) => (typeof v === 'string' && isRelationship ? wikilinkDisplay(v) : String(v)))
|
||||
if (displays.length <= 2) return displays.join(', ')
|
||||
return `${displays[0]}, +${displays.length - 1}`
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
interface ChipStyle { bg: string; color: string }
|
||||
|
||||
function resolveChipStyle(value: FrontmatterValue, isRelationship: boolean): ChipStyle {
|
||||
if (!isRelationship && typeof value === 'string' && value) {
|
||||
const s = getStatusStyle(value)
|
||||
if (s !== DEFAULT_STATUS_STYLE) return { bg: s.bg, color: s.color }
|
||||
}
|
||||
if (isRelationship) return { bg: 'var(--accent-blue-light)', color: 'var(--accent-blue)' }
|
||||
return { bg: 'var(--muted)', color: 'var(--muted-foreground)' }
|
||||
}
|
||||
|
||||
function PropertyLabelIcon({ name }: { name: string }) {
|
||||
const IconCmp = resolveIcon(name)
|
||||
// eslint-disable-next-line react-hooks/static-components -- icon from static registry, no internal state
|
||||
return <IconCmp width={12} height={12} />
|
||||
}
|
||||
|
||||
function PropertyLabel({ icon, label }: { icon: string; label: string }) {
|
||||
return (
|
||||
<span className="flex items-center gap-1 text-muted-foreground" style={{ fontSize: 12 }}>
|
||||
<PropertyLabelIcon name={icon} />
|
||||
<span>{label}</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
/** Full chip for the editor pinned properties bar (icon + label above, value chip below). */
|
||||
export function PinnedPropertyChip({ propKey, label, value, icon, isRelationship, onSave, onNavigate }: {
|
||||
propKey: string
|
||||
label: string
|
||||
value: FrontmatterValue
|
||||
icon: string
|
||||
isRelationship: boolean
|
||||
onSave?: (key: string, value: string) => void
|
||||
onNavigate?: (target: string) => void
|
||||
}) {
|
||||
const [editing, setEditing] = useState(false)
|
||||
const [draft, setDraft] = useState('')
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const display = formatChipDisplay(value, isRelationship)
|
||||
const chipColors = resolveChipStyle(value, isRelationship)
|
||||
|
||||
const startEdit = useCallback(() => {
|
||||
if (isRelationship) return
|
||||
setDraft(display ?? '')
|
||||
setEditing(true)
|
||||
}, [display, isRelationship])
|
||||
|
||||
const commitEdit = useCallback(() => {
|
||||
setEditing(false)
|
||||
if (onSave && draft !== (display ?? '')) onSave(propKey, draft)
|
||||
}, [onSave, propKey, draft, display])
|
||||
|
||||
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') { e.preventDefault(); commitEdit() }
|
||||
if (e.key === 'Escape') { e.preventDefault(); setEditing(false) }
|
||||
}, [commitEdit])
|
||||
|
||||
useEffect(() => {
|
||||
if (editing) inputRef.current?.focus()
|
||||
}, [editing])
|
||||
|
||||
const handleChipClick = useCallback(() => {
|
||||
if (isRelationship && typeof value === 'string') {
|
||||
onNavigate?.(wikilinkDisplay(value))
|
||||
} else {
|
||||
startEdit()
|
||||
}
|
||||
}, [isRelationship, value, onNavigate, startEdit])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1 shrink-0" data-testid="pinned-property">
|
||||
<PropertyLabel icon={icon} label={label} />
|
||||
{editing ? (
|
||||
<input
|
||||
ref={inputRef}
|
||||
className="rounded border border-border bg-background px-2 text-foreground outline-none focus:ring-1 focus:ring-primary"
|
||||
style={{ fontSize: 12, fontWeight: 500, padding: '3px 8px', minWidth: 60, maxWidth: 160 }}
|
||||
value={draft}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
onBlur={commitEdit}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
className="inline-flex items-center gap-1 rounded border-none cursor-pointer transition-opacity hover:opacity-80"
|
||||
style={{ padding: '3px 8px', fontSize: 12, fontWeight: 500, backgroundColor: chipColors.bg, color: chipColors.color }}
|
||||
onClick={handleChipClick}
|
||||
title={typeof value === 'string' ? value : undefined}
|
||||
>
|
||||
{display ?? <span className="text-muted-foreground italic">—</span>}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { frontmatterToEntryPatch } from './frontmatterOps'
|
||||
|
||||
describe('frontmatterToEntryPatch — _pinned_properties', () => {
|
||||
it('patches pinnedProperties on update', () => {
|
||||
const result = frontmatterToEntryPatch('update', '_pinned_properties', ['Status:circle-dot', 'Date'])
|
||||
expect(result.patch.pinnedProperties).toEqual([
|
||||
{ key: 'Status', icon: 'circle-dot' },
|
||||
{ key: 'Date', icon: null },
|
||||
])
|
||||
expect(result.relationshipPatch).toBeNull()
|
||||
})
|
||||
|
||||
it('clears pinnedProperties on delete', () => {
|
||||
const result = frontmatterToEntryPatch('delete', '_pinned_properties')
|
||||
expect(result.patch.pinnedProperties).toEqual([])
|
||||
expect(result.relationshipPatch).toBeNull()
|
||||
})
|
||||
|
||||
it('ignores non-array _pinned_properties values', () => {
|
||||
const result = frontmatterToEntryPatch('update', '_pinned_properties', 'not-an-array')
|
||||
// Falls through to default handling (no patch for unknown keys)
|
||||
expect(result.patch.pinnedProperties).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -4,7 +4,6 @@ import type { VaultEntry } from '../types'
|
||||
import type { FrontmatterValue } from '../components/Inspector'
|
||||
import { updateMockFrontmatter, deleteMockFrontmatterProperty } from './mockFrontmatterHelpers'
|
||||
import { updateMockContent, trackMockChange } from '../mock-tauri'
|
||||
import { parsePinnedConfig } from './usePinnedProperties'
|
||||
|
||||
const ENTRY_DELETE_MAP: Record<string, Partial<VaultEntry>> = {
|
||||
type: { isA: null }, is_a: { isA: null }, status: { status: null }, color: { color: null },
|
||||
@@ -43,17 +42,9 @@ export function frontmatterToEntryPatch(
|
||||
): EntryPatchResult {
|
||||
const k = key.toLowerCase().replace(/\s+/g, '_')
|
||||
if (op === 'delete') {
|
||||
if (k === '_pinned_properties') return { patch: { pinnedProperties: [] }, relationshipPatch: null }
|
||||
const relPatch: RelationshipPatch = { [key]: null }
|
||||
return { patch: ENTRY_DELETE_MAP[k] ?? {}, relationshipPatch: relPatch }
|
||||
}
|
||||
|
||||
// Handle _pinned_properties for Type entries
|
||||
if (k === '_pinned_properties' && Array.isArray(value)) {
|
||||
const pinned = parsePinnedConfig(value.map(String))
|
||||
return { patch: { pinnedProperties: pinned }, relationshipPatch: null }
|
||||
}
|
||||
|
||||
const str = value != null ? String(value) : null
|
||||
const arr = Array.isArray(value) ? value.map(String) : []
|
||||
const updates: Record<string, Partial<VaultEntry>> = {
|
||||
|
||||
@@ -19,7 +19,7 @@ export function buildNewEntry({ path, slug, title, type, status }: NewEntryParam
|
||||
aliases: [], belongsTo: [], relatedTo: [],
|
||||
status, archived: false, trashed: false, trashedAt: null,
|
||||
modifiedAt: now, createdAt: now, fileSize: 0,
|
||||
snippet: '', wordCount: 0, relationships: {}, icon: null, color: null, order: null, outgoingLinks: [], sidebarLabel: null, template: null, sort: null, view: null, visible: null, properties: {}, pinnedProperties: [],
|
||||
snippet: '', wordCount: 0, relationships: {}, icon: null, color: null, order: null, outgoingLinks: [], sidebarLabel: null, template: null, sort: null, view: null, visible: null, properties: {},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import {
|
||||
resolvePinIcon,
|
||||
serialisePinnedConfig,
|
||||
parsePinnedConfig,
|
||||
} from './usePinnedProperties'
|
||||
import type { PinnedPropertyConfig } from '../types'
|
||||
|
||||
describe('resolvePinIcon', () => {
|
||||
it('returns explicit icon when provided', () => {
|
||||
expect(resolvePinIcon('status', 'star')).toBe('star')
|
||||
})
|
||||
|
||||
it('returns default icon for known keys', () => {
|
||||
expect(resolvePinIcon('status', null)).toBe('circle-dot')
|
||||
expect(resolvePinIcon('Status', null)).toBe('circle-dot')
|
||||
expect(resolvePinIcon('date', null)).toBe('calendar')
|
||||
expect(resolvePinIcon('Belongs to', null)).toBe('arrow-up-right')
|
||||
expect(resolvePinIcon('Related to', null)).toBe('arrows-left-right')
|
||||
expect(resolvePinIcon('Due date', null)).toBe('calendar')
|
||||
})
|
||||
|
||||
it('returns fallback icon for unknown keys', () => {
|
||||
expect(resolvePinIcon('Priority', null)).toBe('arrow-up-right')
|
||||
})
|
||||
})
|
||||
|
||||
describe('serialisePinnedConfig', () => {
|
||||
it('serialises configs with icons', () => {
|
||||
const configs: PinnedPropertyConfig[] = [
|
||||
{ key: 'Status', icon: 'circle-dot' },
|
||||
{ key: 'date', icon: 'calendar' },
|
||||
]
|
||||
expect(serialisePinnedConfig(configs)).toEqual(['Status:circle-dot', 'date:calendar'])
|
||||
})
|
||||
|
||||
it('serialises configs without icons', () => {
|
||||
const configs: PinnedPropertyConfig[] = [
|
||||
{ key: 'Status', icon: null },
|
||||
]
|
||||
expect(serialisePinnedConfig(configs)).toEqual(['Status'])
|
||||
})
|
||||
|
||||
it('handles empty array', () => {
|
||||
expect(serialisePinnedConfig([])).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('parsePinnedConfig', () => {
|
||||
it('parses key:icon format', () => {
|
||||
expect(parsePinnedConfig(['Status:circle-dot'])).toEqual([
|
||||
{ key: 'Status', icon: 'circle-dot' },
|
||||
])
|
||||
})
|
||||
|
||||
it('parses key-only format', () => {
|
||||
expect(parsePinnedConfig(['Status'])).toEqual([
|
||||
{ key: 'Status', icon: null },
|
||||
])
|
||||
})
|
||||
|
||||
it('handles mixed formats', () => {
|
||||
const result = parsePinnedConfig(['Status:circle-dot', 'Priority', 'date:calendar'])
|
||||
expect(result).toEqual([
|
||||
{ key: 'Status', icon: 'circle-dot' },
|
||||
{ key: 'Priority', icon: null },
|
||||
{ key: 'date', icon: 'calendar' },
|
||||
])
|
||||
})
|
||||
|
||||
it('round-trips with serialise', () => {
|
||||
const configs: PinnedPropertyConfig[] = [
|
||||
{ key: 'Status', icon: 'circle-dot' },
|
||||
{ key: 'Belongs to', icon: 'arrow-up-right' },
|
||||
]
|
||||
expect(parsePinnedConfig(serialisePinnedConfig(configs))).toEqual(configs)
|
||||
})
|
||||
})
|
||||
@@ -1,148 +0,0 @@
|
||||
import { useMemo, useCallback } from 'react'
|
||||
import type { VaultEntry, PinnedPropertyConfig } from '../types'
|
||||
import type { FrontmatterValue } from '../components/Inspector'
|
||||
import type { ParsedFrontmatter } from '../utils/frontmatter'
|
||||
|
||||
const DEFAULT_ICONS: Record<string, string> = {
|
||||
status: 'circle-dot',
|
||||
date: 'calendar',
|
||||
'belongs to': 'arrow-up-right',
|
||||
'related to': 'arrows-left-right',
|
||||
'due date': 'calendar',
|
||||
}
|
||||
|
||||
export function resolvePinIcon(key: string, explicit: string | null): string {
|
||||
if (explicit) return explicit
|
||||
return DEFAULT_ICONS[key.toLowerCase()] ?? 'arrow-up-right'
|
||||
}
|
||||
|
||||
export function serialisePinnedConfig(configs: PinnedPropertyConfig[]): string[] {
|
||||
return configs.map((c) => (c.icon ? `${c.key}:${c.icon}` : c.key))
|
||||
}
|
||||
|
||||
export function parsePinnedConfig(items: string[]): PinnedPropertyConfig[] {
|
||||
return items.map((s) => {
|
||||
const sep = s.indexOf(':')
|
||||
if (sep === -1) return { key: s.trim(), icon: null }
|
||||
return { key: s.slice(0, sep).trim(), icon: s.slice(sep + 1).trim() || null }
|
||||
})
|
||||
}
|
||||
|
||||
function computeDefaults(entries: VaultEntry[], typeName: string): PinnedPropertyConfig[] {
|
||||
const sample = entries.find((e) => e.isA === typeName)
|
||||
if (!sample) return []
|
||||
const pins: PinnedPropertyConfig[] = []
|
||||
if (sample.status != null) pins.push({ key: 'Status', icon: 'circle-dot' })
|
||||
if (sample.belongsTo.length > 0) pins.push({ key: 'Belongs to', icon: 'arrow-up-right' })
|
||||
if (sample.relatedTo.length > 0) pins.push({ key: 'Related to', icon: 'arrows-left-right' })
|
||||
return pins
|
||||
}
|
||||
|
||||
export interface ResolvedPinnedProperty {
|
||||
key: string
|
||||
icon: string
|
||||
label: string
|
||||
value: FrontmatterValue
|
||||
isRelationship: boolean
|
||||
}
|
||||
|
||||
function resolveValue(
|
||||
entry: VaultEntry,
|
||||
frontmatter: ParsedFrontmatter,
|
||||
key: string,
|
||||
): { value: FrontmatterValue; isRelationship: boolean } {
|
||||
const lowerKey = key.toLowerCase().replace(/\s+/g, '_')
|
||||
if (lowerKey === 'status') return { value: entry.status, isRelationship: false }
|
||||
const relValue = entry.relationships?.[key]
|
||||
if (relValue && relValue.length > 0) {
|
||||
return { value: relValue.length === 1 ? relValue[0] : relValue, isRelationship: true }
|
||||
}
|
||||
const propValue = entry.properties?.[key]
|
||||
if (propValue != null) return { value: propValue, isRelationship: false }
|
||||
const fmValue = frontmatter[key]
|
||||
if (fmValue != null) return { value: fmValue, isRelationship: false }
|
||||
return { value: null, isRelationship: false }
|
||||
}
|
||||
|
||||
function formatLabel(key: string): string {
|
||||
return key.replace(/_/g, ' ')
|
||||
}
|
||||
|
||||
/** Resolve a single pinned config to its display representation. */
|
||||
function resolveOne(
|
||||
entry: VaultEntry, frontmatter: ParsedFrontmatter, cfg: PinnedPropertyConfig,
|
||||
): ResolvedPinnedProperty {
|
||||
const { value, isRelationship } = resolveValue(entry, frontmatter, cfg.key)
|
||||
return { key: cfg.key, icon: resolvePinIcon(cfg.key, cfg.icon), label: formatLabel(cfg.key), value, isRelationship }
|
||||
}
|
||||
|
||||
/** Find the Type entry for a note's isA field. */
|
||||
function findTypeEntry(entries: VaultEntry[], isA: string | null): VaultEntry | null {
|
||||
if (!isA) return null
|
||||
return entries.find((e) => e.isA === 'Type' && e.title === isA) ?? null
|
||||
}
|
||||
|
||||
/** Determine which properties are pinned for a given type. */
|
||||
function resolvePinnedConfigs(
|
||||
isA: string | null, typeEntry: VaultEntry | null, entries: VaultEntry[],
|
||||
): PinnedPropertyConfig[] {
|
||||
if (!isA) return []
|
||||
if (typeEntry && typeEntry.pinnedProperties.length > 0) return typeEntry.pinnedProperties
|
||||
return computeDefaults(entries, isA)
|
||||
}
|
||||
|
||||
export interface UsePinnedPropertiesResult {
|
||||
pinnedConfigs: PinnedPropertyConfig[]
|
||||
resolved: ResolvedPinnedProperty[]
|
||||
pinProperty: (key: string, icon?: string) => void
|
||||
unpinProperty: (key: string) => void
|
||||
updatePinIcon: (key: string, icon: string) => void
|
||||
reorderPins: (configs: PinnedPropertyConfig[]) => void
|
||||
isPinned: (key: string) => boolean
|
||||
}
|
||||
|
||||
/** Hook to read and mutate pinned properties for a note based on its type. */
|
||||
export function usePinnedProperties({
|
||||
entry, entries, frontmatter, onUpdateTypeFrontmatter,
|
||||
}: {
|
||||
entry: VaultEntry | null
|
||||
entries: VaultEntry[]
|
||||
frontmatter: ParsedFrontmatter
|
||||
onUpdateTypeFrontmatter?: (typePath: string, key: string, value: FrontmatterValue) => Promise<void>
|
||||
}): UsePinnedPropertiesResult {
|
||||
const typeEntry = useMemo(() => findTypeEntry(entries, entry?.isA ?? null), [entry?.isA, entries])
|
||||
const pinnedConfigs = useMemo(
|
||||
() => resolvePinnedConfigs(entry?.isA ?? null, typeEntry, entries),
|
||||
[entry?.isA, typeEntry, entries],
|
||||
)
|
||||
const resolved = useMemo(
|
||||
() => (entry ? pinnedConfigs.map((cfg) => resolveOne(entry, frontmatter, cfg)) : []),
|
||||
[entry, pinnedConfigs, frontmatter],
|
||||
)
|
||||
const savePins = useCallback(
|
||||
(newConfigs: PinnedPropertyConfig[]) => {
|
||||
if (!typeEntry || !onUpdateTypeFrontmatter) return
|
||||
onUpdateTypeFrontmatter(typeEntry.path, '_pinned_properties', serialisePinnedConfig(newConfigs))
|
||||
},
|
||||
[typeEntry, onUpdateTypeFrontmatter],
|
||||
)
|
||||
const pinProperty = useCallback(
|
||||
(key: string, icon?: string) => {
|
||||
if (pinnedConfigs.some((c) => c.key === key)) return
|
||||
savePins([...pinnedConfigs, { key, icon: icon ?? null }])
|
||||
},
|
||||
[pinnedConfigs, savePins],
|
||||
)
|
||||
const unpinProperty = useCallback(
|
||||
(key: string) => savePins(pinnedConfigs.filter((c) => c.key !== key)),
|
||||
[pinnedConfigs, savePins],
|
||||
)
|
||||
const updatePinIcon = useCallback(
|
||||
(key: string, icon: string) => savePins(pinnedConfigs.map((c) => (c.key === key ? { ...c, icon } : c))),
|
||||
[pinnedConfigs, savePins],
|
||||
)
|
||||
const reorderPins = useCallback((configs: PinnedPropertyConfig[]) => savePins(configs), [savePins])
|
||||
const isPinned = useCallback((key: string) => pinnedConfigs.some((c) => c.key === key), [pinnedConfigs])
|
||||
|
||||
return { pinnedConfigs, resolved, pinProperty, unpinProperty, updatePinIcon, reorderPins, isPinned }
|
||||
}
|
||||
@@ -80,7 +80,6 @@ function collectAllVaultTags(entries: VaultEntry[] | undefined): Record<string,
|
||||
}
|
||||
|
||||
function isVisibleProperty([key, value]: [string, FrontmatterValue]): boolean {
|
||||
if (key.startsWith('_')) return false
|
||||
return !SKIP_KEYS.has(key.toLowerCase()) && !containsWikilinks(value)
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: ['q1-2026', 'software-development', 'matteo-cellini', 'maria-bianchi', 'marco-verdi'],
|
||||
properties: { Priority: 'High', 'Due date': '2026-06-15', Owner: 'Luca Rossi' },
|
||||
pinnedProperties: [],
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/grow-newsletter.md',
|
||||
@@ -73,7 +72,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: ['on-writing-well', 'engineering-leadership-101', 'ai-agents-primer', 'growth', 'writing'],
|
||||
properties: { Priority: 'High', Rating: 5, Cadence: 'Weekly', Owner: 'Luca Rossi' },
|
||||
pinnedProperties: [],
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/manage-sponsorships.md',
|
||||
@@ -103,7 +101,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: ['matteo-cellini'],
|
||||
properties: {},
|
||||
pinnedProperties: [],
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/write-weekly-essays.md',
|
||||
@@ -133,7 +130,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: ['grow-newsletter'],
|
||||
properties: { Owner: 'Luca Rossi', Cadence: 'Weekly' },
|
||||
pinnedProperties: [],
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/run-sponsorships.md',
|
||||
@@ -163,7 +159,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: ['manage-sponsorships'],
|
||||
properties: { Owner: 'Matteo Cellini', Cadence: 'Weekly' },
|
||||
pinnedProperties: [],
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/stock-screener.md',
|
||||
@@ -194,7 +189,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: ['trading', 'algorithmic-trading', 'ema200-backtest-results'],
|
||||
properties: { Priority: 'Low', 'Due date': '2026-03-01', Owner: 'Luca Rossi' },
|
||||
pinnedProperties: [],
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/facebook-ads-strategy.md',
|
||||
@@ -225,7 +219,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: ['26q1-laputa-app', 'growth', 'ads'],
|
||||
properties: { Priority: 'Medium', Rating: 4 },
|
||||
pinnedProperties: [],
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/budget-allocation.md',
|
||||
@@ -255,7 +248,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: ['26q1-laputa-app'],
|
||||
properties: {},
|
||||
pinnedProperties: [],
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/matteo-cellini.md',
|
||||
@@ -284,7 +276,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: [],
|
||||
properties: { Company: 'Acme Corp', Role: 'Engineering Lead' },
|
||||
pinnedProperties: [],
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/maria-bianchi.md',
|
||||
@@ -313,7 +304,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: [],
|
||||
properties: { Company: 'TechStart', Role: 'Product Manager' },
|
||||
pinnedProperties: [],
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/marco-verdi.md',
|
||||
@@ -342,7 +332,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: [],
|
||||
properties: {},
|
||||
pinnedProperties: [],
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/elena-russo.md',
|
||||
@@ -371,7 +360,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: [],
|
||||
properties: {},
|
||||
pinnedProperties: [],
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/2026-02-14-laputa-app-kickoff.md',
|
||||
@@ -401,7 +389,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: ['26q1-laputa-app', 'matteo-cellini'],
|
||||
properties: {},
|
||||
pinnedProperties: [],
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/software-development.md',
|
||||
@@ -431,7 +418,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: [],
|
||||
properties: {},
|
||||
pinnedProperties: [],
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/trading.md',
|
||||
@@ -461,7 +447,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: [],
|
||||
properties: {},
|
||||
pinnedProperties: [],
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/on-writing-well.md',
|
||||
@@ -491,7 +476,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: ['grow-newsletter'],
|
||||
properties: {},
|
||||
pinnedProperties: [],
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/engineering-leadership-101.md',
|
||||
@@ -522,7 +506,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: ['grow-newsletter', 'software-development'],
|
||||
properties: {},
|
||||
pinnedProperties: [],
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/ai-agents-primer.md',
|
||||
@@ -552,7 +535,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: ['grow-newsletter'],
|
||||
properties: {},
|
||||
pinnedProperties: [],
|
||||
},
|
||||
// --- Type documents ---
|
||||
{
|
||||
@@ -580,11 +562,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: [],
|
||||
properties: {},
|
||||
pinnedProperties: [
|
||||
{ key: 'Status', icon: 'circle-dot' },
|
||||
{ key: 'Belongs to', icon: 'arrow-up-right' },
|
||||
{ key: 'Due date', icon: 'calendar' },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/responsibility.md',
|
||||
@@ -611,7 +588,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: [],
|
||||
properties: {},
|
||||
pinnedProperties: [],
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/procedure.md',
|
||||
@@ -638,7 +614,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: [],
|
||||
properties: {},
|
||||
pinnedProperties: [],
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/experiment.md',
|
||||
@@ -665,7 +640,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: false,
|
||||
outgoingLinks: [],
|
||||
properties: {},
|
||||
pinnedProperties: [],
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/person.md',
|
||||
@@ -692,7 +666,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: [],
|
||||
properties: {},
|
||||
pinnedProperties: [],
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/event.md',
|
||||
@@ -719,7 +692,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: [],
|
||||
properties: {},
|
||||
pinnedProperties: [],
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/topic.md',
|
||||
@@ -746,7 +718,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: [],
|
||||
properties: {},
|
||||
pinnedProperties: [],
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/essay.md',
|
||||
@@ -773,7 +744,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: [],
|
||||
properties: {},
|
||||
pinnedProperties: [],
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/note.md',
|
||||
@@ -800,7 +770,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: [],
|
||||
properties: {},
|
||||
pinnedProperties: [],
|
||||
},
|
||||
// --- Custom type documents ---
|
||||
{
|
||||
@@ -828,7 +797,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: [],
|
||||
properties: {},
|
||||
pinnedProperties: [],
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/recipe.md',
|
||||
@@ -855,7 +823,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: [],
|
||||
properties: {},
|
||||
pinnedProperties: [],
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/book.md',
|
||||
@@ -882,7 +849,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: [],
|
||||
properties: {},
|
||||
pinnedProperties: [],
|
||||
},
|
||||
// --- Instances of custom types ---
|
||||
{
|
||||
@@ -912,7 +878,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: [],
|
||||
properties: {},
|
||||
pinnedProperties: [],
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/pasta-carbonara.md',
|
||||
@@ -941,7 +906,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: [],
|
||||
properties: { Difficulty: 'Easy', 'Prep time': '30 min', Servings: 4 },
|
||||
pinnedProperties: [],
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/designing-data-intensive-applications.md',
|
||||
@@ -970,7 +934,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: [],
|
||||
properties: { Author: 'Martin Kleppmann', Rating: 5, 'Year published': 2017 },
|
||||
pinnedProperties: [],
|
||||
},
|
||||
// --- Trashed entries ---
|
||||
{
|
||||
@@ -1001,7 +964,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: [],
|
||||
properties: {},
|
||||
pinnedProperties: [],
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/deprecated-api-notes.md',
|
||||
@@ -1030,7 +992,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: [],
|
||||
properties: {},
|
||||
pinnedProperties: [],
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/failed-seo-experiment.md',
|
||||
@@ -1060,7 +1021,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: [],
|
||||
properties: { Owner: 'Luca Rossi' },
|
||||
pinnedProperties: [],
|
||||
},
|
||||
// --- Archived entries ---
|
||||
{
|
||||
@@ -1082,7 +1042,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: [],
|
||||
properties: { Owner: 'Luca Rossi' },
|
||||
pinnedProperties: [],
|
||||
modifiedAt: now - 86400 * 120,
|
||||
createdAt: now - 86400 * 200,
|
||||
fileSize: 680,
|
||||
@@ -1112,7 +1071,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: [],
|
||||
properties: { Owner: 'Luca Rossi' },
|
||||
pinnedProperties: [],
|
||||
modifiedAt: now - 86400 * 90,
|
||||
createdAt: now - 86400 * 150,
|
||||
fileSize: 520,
|
||||
@@ -1149,7 +1107,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: [],
|
||||
properties: {},
|
||||
pinnedProperties: [],
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/refactoring-ideas.md',
|
||||
@@ -1176,7 +1133,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: [],
|
||||
properties: {},
|
||||
pinnedProperties: [],
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/refactoring-key-ideas.md',
|
||||
@@ -1203,7 +1159,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: [],
|
||||
properties: {},
|
||||
pinnedProperties: [],
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/refactoring-patterns.md',
|
||||
@@ -1230,7 +1185,6 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
outgoingLinks: [],
|
||||
properties: {},
|
||||
pinnedProperties: [],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1283,7 +1237,6 @@ function generateBulkEntries(count: number): VaultEntry[] {
|
||||
sidebarLabel: null,
|
||||
template: null, sort: null, view: null, visible: null,
|
||||
properties: {},
|
||||
pinnedProperties: [],
|
||||
})
|
||||
}
|
||||
return entries
|
||||
|
||||
@@ -137,7 +137,7 @@ const trimOrNull = (v: string | null | undefined): string | null => v?.trim() ||
|
||||
export const mockHandlers: Record<string, (args: any) => any> = {
|
||||
list_vault: () => MOCK_ENTRIES,
|
||||
reload_vault: () => MOCK_ENTRIES,
|
||||
reload_vault_entry: (args: { path: string }) => MOCK_ENTRIES.find(e => e.path === args.path) ?? { path: args.path, title: 'Unknown', filename: 'unknown.md', aliases: [], belongsTo: [], relatedTo: [], archived: false, trashed: false, snippet: '', wordCount: 0, fileSize: 0, relationships: {}, outgoingLinks: [], properties: {}, pinnedProperties: [] },
|
||||
reload_vault_entry: (args: { path: string }) => MOCK_ENTRIES.find(e => e.path === args.path) ?? { path: args.path, title: 'Unknown', filename: 'unknown.md', aliases: [], belongsTo: [], relatedTo: [], archived: false, trashed: false, snippet: '', wordCount: 0, fileSize: 0, relationships: {}, outgoingLinks: [], properties: {} },
|
||||
sync_note_title: () => false,
|
||||
get_note_content: (args: { path: string }) => MOCK_CONTENT[args.path] ?? '',
|
||||
get_all_content: () => MOCK_CONTENT,
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
/** A single pinned-property config item from a Type's `_pinned_properties` list. */
|
||||
export interface PinnedPropertyConfig {
|
||||
key: string
|
||||
icon: string | null
|
||||
}
|
||||
|
||||
export interface VaultEntry {
|
||||
path: string
|
||||
filename: string
|
||||
@@ -45,8 +39,6 @@ export interface VaultEntry {
|
||||
outgoingLinks: string[]
|
||||
/** Custom scalar frontmatter properties (non-relationship, non-structural). */
|
||||
properties: Record<string, string | number | boolean | null>
|
||||
/** Pinned properties config for Type entries. Parsed from `_pinned_properties` frontmatter. */
|
||||
pinnedProperties: PinnedPropertyConfig[]
|
||||
}
|
||||
|
||||
export type NoteStatus = 'new' | 'modified' | 'clean' | 'pendingSave' | 'unsaved'
|
||||
|
||||
Reference in New Issue
Block a user