fix: flat vault type resolution — remove type/ prefix from links and protected folders

- Type relationship links: [[type/essay]] → [[essay]] (matches flat vault structure)
- PROTECTED_FOLDERS / KEEP_FOLDERS: only attachments, _themes, assets
- TypeSelector navigation: type/slug → slug
- Updated all 14 affected test files
- All 612 Rust tests + 2151 frontend tests pass
This commit is contained in:
Test
2026-03-16 16:17:58 +01:00
parent 5a081b06d4
commit 08668854b9
53 changed files with 522 additions and 346 deletions

View File

@@ -306,14 +306,14 @@ describe('DynamicPropertiesPanel', () => {
/>
)
fireEvent.click(screen.getByText('Project'))
expect(onNavigate).toHaveBeenCalledWith('type/project')
expect(onNavigate).toHaveBeenCalledWith('project')
})
describe('TypeSelector', () => {
const typeEntries = [
makeEntry({ path: '/vault/type/project.md', title: 'Project', isA: 'Type' }),
makeEntry({ path: '/vault/type/person.md', title: 'Person', isA: 'Type' }),
makeEntry({ path: '/vault/type/topic.md', title: 'Topic', isA: 'Type' }),
makeEntry({ path: '/vault/project.md', title: 'Project', isA: 'Type' }),
makeEntry({ path: '/vault/person.md', title: 'Person', isA: 'Type' }),
makeEntry({ path: '/vault/topic.md', title: 'Topic', isA: 'Type' }),
]
it('renders as dropdown when editable', () => {

View File

@@ -372,7 +372,7 @@ This is a test note with some words to count.
fileSize: 500,
snippet: '',
wordCount: 0,
relationships: { 'Type': ['[[type/responsibility]]'] },
relationships: { 'Type': ['[[responsibility]]'] },
icon: null,
color: null,
order: null,
@@ -399,7 +399,7 @@ This is a test note with some words to count.
fileSize: 300,
snippet: '',
wordCount: 0,
relationships: { 'Belongs to': ['[[responsibility/grow-newsletter]]'], 'Type': ['[[type/essay]]'] },
relationships: { 'Belongs to': ['[[responsibility/grow-newsletter]]'], 'Type': ['[[essay]]'] },
icon: null,
color: null,
order: null,
@@ -426,7 +426,7 @@ This is a test note with some words to count.
fileSize: 400,
snippet: '',
wordCount: 0,
relationships: { 'Belongs to': ['[[responsibility/grow-newsletter]]'], 'Type': ['[[type/procedure]]'] },
relationships: { 'Belongs to': ['[[responsibility/grow-newsletter]]'], 'Type': ['[[procedure]]'] },
icon: null,
color: null,
order: null,
@@ -453,7 +453,7 @@ This is a test note with some words to count.
fileSize: 200,
snippet: '',
wordCount: 0,
relationships: { 'Related to': ['[[responsibility/grow-newsletter]]'], 'Type': ['[[type/experiment]]'] },
relationships: { 'Related to': ['[[responsibility/grow-newsletter]]'], 'Type': ['[[experiment]]'] },
icon: null,
color: null,
order: null,
@@ -532,13 +532,13 @@ Status: Active
it('skips Type relationships in referenced-by computation', () => {
const typeEntry: VaultEntry = {
...targetEntry,
path: '/Users/luca/Laputa/type/responsibility.md',
path: '/Users/luca/Laputa/responsibility.md',
filename: 'responsibility.md',
title: 'Responsibility',
isA: 'Type',
relationships: {},
}
// essayEntry has Type: [[type/responsibility]] — should NOT show as referenced-by
// essayEntry has Type: [[responsibility]] — should NOT show as referenced-by
render(
<Inspector
{...defaultProps}
@@ -549,7 +549,7 @@ Status: Active
/>
)
// On Writing Well references responsibility via "Belongs to" (path match), not via "Type"
// But the Type entry is at type/responsibility.md, so wikilinks to
// But the Type entry is at responsibility.md, so wikilinks to
// responsibility/grow-newsletter won't match. Section should be hidden
expect(screen.queryByText('Referenced by')).not.toBeInTheDocument()
})
@@ -561,7 +561,7 @@ Status: Active
}
const referrer: VaultEntry = {
...essayEntry,
relationships: { 'Topics': ['[[Newsletter]]'], 'Type': ['[[type/essay]]'] },
relationships: { 'Topics': ['[[Newsletter]]'], 'Type': ['[[essay]]'] },
}
render(
<Inspector
@@ -596,7 +596,7 @@ Status: Active
fileSize: 300,
snippet: '',
wordCount: 0,
relationships: { 'Belongs to': ['[[responsibility/grow-newsletter]]'], 'Type': ['[[type/essay]]'] },
relationships: { 'Belongs to': ['[[responsibility/grow-newsletter]]'], 'Type': ['[[essay]]'] },
icon: null,
color: null,
order: null,

View File

@@ -39,7 +39,7 @@ describe('DynamicRelationshipsPanel', () => {
const onNavigate = vi.fn()
const onAddProperty = vi.fn()
const personTypeEntry = makeEntry({
path: '/vault/type/person.md', filename: 'person.md', title: 'Person',
path: '/vault/person.md', filename: 'person.md', title: 'Person',
isA: 'Type', color: 'yellow', icon: 'user',
})
const entries = [
@@ -757,7 +757,7 @@ describe('GitHistoryPanel', () => {
describe('InstancesPanel', () => {
const onNavigate = vi.fn()
const quarterType = makeEntry({
path: '/vault/type/quarter.md', filename: 'quarter.md', title: 'Quarter',
path: '/vault/quarter.md', filename: 'quarter.md', title: 'Quarter',
isA: 'Type', color: 'blue', icon: 'calendar',
})
const typeEntryMap: Record<string, VaultEntry> = { Quarter: quarterType }

View File

@@ -38,7 +38,7 @@ const entries = [
]
const personTypeEntry = makeEntry({
path: '/vault/type/person.md', filename: 'person.md', title: 'Person',
path: '/vault/person.md', filename: 'person.md', title: 'Person',
isA: 'Type', color: 'yellow', icon: 'user',
})
const typeEntryMap: Record<string, VaultEntry> = { Person: personTypeEntry }

View File

@@ -422,7 +422,7 @@ describe('Sidebar', () => {
const entriesWithCustomTypes: VaultEntry[] = [
...mockEntries,
{
path: '/vault/type/recipe.md',
path: '/vault/recipe.md',
filename: 'recipe.md',
title: 'Recipe',
isA: 'Type',
@@ -450,7 +450,7 @@ describe('Sidebar', () => {
properties: {},
},
{
path: '/vault/type/book.md',
path: '/vault/book.md',
filename: 'book.md',
title: 'Book',
isA: 'Type',
@@ -592,7 +592,7 @@ describe('Sidebar', () => {
it('does not show built-in types as custom sections', () => {
const projectTypeEntry: VaultEntry = {
path: '/vault/type/project.md',
path: '/vault/project.md',
filename: 'project.md',
title: 'Project',
isA: 'Type',
@@ -629,7 +629,7 @@ describe('Sidebar', () => {
const entriesWithLabel: VaultEntry[] = [
...mockEntries,
{
path: '/vault/type/news.md', filename: 'news.md', title: 'News', isA: 'Type',
path: '/vault/news.md', filename: 'news.md', title: 'News', isA: 'Type',
aliases: [], belongsTo: [], relatedTo: [], status: null, owner: null, cadence: null,
archived: false, trashed: false, trashedAt: null, modifiedAt: 1700000000, createdAt: null,
fileSize: 200, snippet: '', wordCount: 0, relationships: {},
@@ -655,7 +655,7 @@ describe('Sidebar', () => {
const entriesWithBuiltInOverride: VaultEntry[] = [
...mockEntries,
{
path: '/vault/type/person.md', filename: 'person.md', title: 'Person', isA: 'Type',
path: '/vault/person.md', filename: 'person.md', title: 'Person', isA: 'Type',
aliases: [], belongsTo: [], relatedTo: [], status: null, owner: null, cadence: null,
archived: false, trashed: false, trashedAt: null, modifiedAt: 1700000000, createdAt: null,
fileSize: 200, snippet: '', wordCount: 0, relationships: {},
@@ -677,7 +677,7 @@ describe('Sidebar', () => {
describe('type visibility via visible property', () => {
const makeTypeEntry = (title: string, visible: boolean | null): VaultEntry => ({
path: `/vault/type/${title.toLowerCase()}.md`,
path: `/vault/${title.toLowerCase()}.md`,
filename: `${title.toLowerCase()}.md`,
title,
isA: 'Type',
@@ -809,7 +809,7 @@ describe('Sidebar', () => {
...mockEntries,
// Type entries with order values — reversed from default
{
path: '/vault/type/project.md', filename: 'project.md', title: 'Project', isA: 'Type',
path: '/vault/project.md', filename: 'project.md', title: 'Project', isA: 'Type',
aliases: [], belongsTo: [], relatedTo: [], status: null, owner: null, cadence: null,
archived: false, trashed: false, trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 200, snippet: '',
wordCount: 0,
@@ -817,7 +817,7 @@ describe('Sidebar', () => {
properties: {},
},
{
path: '/vault/type/topic.md', filename: 'topic.md', title: 'Topic', isA: 'Type',
path: '/vault/topic.md', filename: 'topic.md', title: 'Topic', isA: 'Type',
aliases: [], belongsTo: [], relatedTo: [], status: null, owner: null, cadence: null,
archived: false, trashed: false, trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 200, snippet: '',
wordCount: 0,
@@ -825,7 +825,7 @@ describe('Sidebar', () => {
properties: {},
},
{
path: '/vault/type/person.md', filename: 'person.md', title: 'Person', isA: 'Type',
path: '/vault/person.md', filename: 'person.md', title: 'Person', isA: 'Type',
aliases: [], belongsTo: [], relatedTo: [], status: null, owner: null, cadence: null,
archived: false, trashed: false, trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 200, snippet: '',
wordCount: 0,

View File

@@ -28,7 +28,7 @@ function ReadOnlyType({ isA, customColorKey, onNavigate }: { isA?: string | null
<button
className="min-w-0 truncate border-none text-right cursor-pointer hover:opacity-80"
style={{ background: getTypeLightColor(isA, customColorKey), color: getTypeColor(isA, customColorKey), borderRadius: 6, padding: '2px 8px', fontSize: 12, fontWeight: 500 }}
onClick={() => onNavigate(`type/${isA.toLowerCase()}`)} title={isA}
onClick={() => onNavigate(isA.toLowerCase())} title={isA}
>{isA}</button>
) : (
<span className="text-right text-[12px] text-secondary-foreground">{isA}</span>

View File

@@ -67,7 +67,7 @@ describe('useNoteListSort (via NoteList)', () => {
})
it('reads sort from type document for sectionGroup selection', () => {
const typeDoc = makeEntry({ path: '/type/note.md', title: 'Note', isA: 'Type', sort: 'title:asc' })
const typeDoc = makeEntry({ path: '/note.md', title: 'Note', isA: 'Type', sort: 'title:asc' })
const entries = [
typeDoc,
makeEntry({ path: '/c.md', title: 'Charlie', modifiedAt: 3000 }),
@@ -82,7 +82,7 @@ describe('useNoteListSort (via NoteList)', () => {
})
it('shows type title as header for sectionGroup selection', () => {
const typeDoc = makeEntry({ path: '/type/project.md', title: 'Project', isA: 'Type' })
const typeDoc = makeEntry({ path: '/project.md', title: 'Project', isA: 'Type' })
renderNoteList({ entries: [typeDoc], selection: { kind: 'sectionGroup', type: 'Project', label: 'Projects' } })
expect(screen.getByText('Project')).toBeInTheDocument()
})
@@ -91,7 +91,7 @@ describe('useNoteListSort (via NoteList)', () => {
localStorageMock.setItem('laputa-sort-preferences', JSON.stringify({ '__list__': { option: 'title', direction: 'asc' } }))
const onUpdateTypeSort = vi.fn()
const updateEntry = vi.fn()
const typeDoc = makeEntry({ path: '/type/project.md', title: 'Project', isA: 'Type', sort: null })
const typeDoc = makeEntry({ path: '/project.md', title: 'Project', isA: 'Type', sort: null })
const entries = [typeDoc, makeEntry()]
renderNoteList({
@@ -101,15 +101,15 @@ describe('useNoteListSort (via NoteList)', () => {
updateEntry,
})
expect(onUpdateTypeSort).toHaveBeenCalledWith('/type/project.md', 'sort', 'title:asc')
expect(updateEntry).toHaveBeenCalledWith('/type/project.md', { sort: 'title:asc' })
expect(onUpdateTypeSort).toHaveBeenCalledWith('/project.md', 'sort', 'title:asc')
expect(updateEntry).toHaveBeenCalledWith('/project.md', { sort: 'title:asc' })
})
it('does not migrate if type already has sort', () => {
localStorageMock.setItem('laputa-sort-preferences', JSON.stringify({ '__list__': { option: 'title', direction: 'asc' } }))
const onUpdateTypeSort = vi.fn()
const updateEntry = vi.fn()
const typeDoc = makeEntry({ path: '/type/project.md', title: 'Project', isA: 'Type', sort: 'modified:desc' })
const typeDoc = makeEntry({ path: '/project.md', title: 'Project', isA: 'Type', sort: 'modified:desc' })
const entries = [typeDoc, makeEntry()]
renderNoteList({