fix: keep view filters focused across workspaces

This commit is contained in:
lucaronin
2026-06-05 15:22:13 +02:00
parent 82b2ff2ac4
commit c9467711ad
5 changed files with 62 additions and 21 deletions

View File

@@ -1,3 +1,4 @@
import { useState } from 'react'
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { render, screen, fireEvent, act } from '@testing-library/react'
import { FilterBuilder } from './FilterBuilder'
@@ -71,6 +72,43 @@ describe('FilterBuilder value inputs', () => {
)
})
it('keeps the value input focused while controlled filter state rerenders', () => {
function StatefulBuilder() {
const [group, setGroup] = useState<FilterGroup>({
all: [{ field: 'title', op: 'contains', value: '' }],
})
return (
<FilterBuilder
group={group}
onChange={(nextGroup) => {
onChange(nextGroup)
setGroup(nextGroup)
}}
availableFields={['type', 'status', 'title']}
/>
)
}
render(<StatefulBuilder />)
const input = screen.getByTestId('filter-value-input')
input.focus()
expect(input).toHaveFocus()
fireEvent.change(input, { target: { value: 'p' } })
const updatedInput = screen.getByTestId('filter-value-input')
expect(updatedInput).toBe(input)
expect(updatedInput).toHaveFocus()
fireEvent.change(updatedInput, { target: { value: 'po' } })
expect(screen.getByTestId('filter-value-input')).toBe(input)
expect(screen.getByTestId('filter-value-input')).toHaveFocus()
expect(screen.getByTestId('filter-value-input')).toHaveValue('po')
})
it('toggles regex mode in the emitted filter payload', () => {
renderBuilder()

View File

@@ -52,10 +52,6 @@ function setGroupChildren(mode: 'all' | 'any', children: FilterNode[]): FilterGr
return mode === 'all' ? { all: children } : { any: children }
}
function filterNodeKey(node: FilterNode): string {
return JSON.stringify(node)
}
function OperatorSelect({ value, onChange }: {
value: FilterOp
onChange: (v: FilterOp) => void
@@ -246,7 +242,7 @@ function FilterGroupView({ group, fields, depth, onChange, onRemove }: {
{children.map((child, i) =>
isFilterGroup(child) ? (
<FilterGroupView
key={filterNodeKey(child)}
key={i}
group={child}
fields={fields}
depth={depth + 1}
@@ -255,7 +251,7 @@ function FilterGroupView({ group, fields, depth, onChange, onRemove }: {
/>
) : (
<FilterRow
key={filterNodeKey(child)}
key={i}
condition={child}
fields={fields}
onUpdate={(c) => updateChild(i, c)}

View File

@@ -471,13 +471,8 @@ export function isAllNotesEntry(
return isOptionalAllNotesFileVisible(entry, allNotesFileVisibility)
}
function entriesScopedToView(entries: VaultEntry[], view: ViewFile): VaultEntry[] {
if (!view.rootPath) return entries
return entries.filter((entry) => entry.workspace?.path === view.rootPath)
}
export function filterEntriesForViewFile(entries: VaultEntry[], view: ViewFile): VaultEntry[] {
return evaluateView(view.definition, entriesScopedToView(entries, view).filter(isMarkdown))
return evaluateView(view.definition, entries.filter(isMarkdown))
}
function filterViewEntries(entries: VaultEntry[], selection: Extract<SidebarSelection, { kind: 'view' }>, views?: ViewFile[]): VaultEntry[] {

View File

@@ -18,7 +18,7 @@ function workspace(path: string, label: string): WorkspaceIdentity {
}
}
function focusView(rootPath: string, label: string): ViewFile {
function focusView(rootPath: string, label: string, relationshipTarget = 'podcast'): ViewFile {
return {
filename: 'focus.yml',
rootPath,
@@ -28,26 +28,38 @@ function focusView(rootPath: string, label: string): ViewFile {
icon: null,
color: null,
sort: null,
filters: { all: [{ field: 'type', op: 'equals', value: 'Note' }] },
filters: { all: [{ field: 'belongs_to', op: 'contains', value: relationshipTarget }] },
},
}
}
describe('view filtering across workspaces', () => {
it('matches duplicate view filenames by root path and scopes results to that workspace', () => {
it('matches duplicate view filenames by root path and searches every mounted workspace', () => {
const personal = workspace('/personal', 'Personal')
const team = workspace('/team', 'Team')
const entries = [
makeEntry({ title: 'Personal Alpha', path: '/personal/alpha.md', isA: 'Note', workspace: personal }),
makeEntry({ title: 'Team Alpha', path: '/team/alpha.md', isA: 'Note', workspace: team }),
makeEntry({
title: 'Personal Essay',
path: '/personal/essay.md',
isA: 'Note',
relationships: { belongs_to: ['[[newsletter]]'] },
workspace: personal,
}),
makeEntry({
title: 'Team Essay',
path: '/team/essay.md',
isA: 'Note',
relationships: { belongs_to: ['[[podcast]]'] },
workspace: team,
}),
]
const result = filterEntries(
entries,
{ kind: 'view', filename: 'focus.yml', rootPath: '/team' },
{ views: [focusView('/personal', 'Personal'), focusView('/team', 'Team')] },
{ kind: 'view', filename: 'focus.yml', rootPath: '/personal' },
{ views: [focusView('/personal', 'Personal'), focusView('/team', 'Team', 'newsletter')] },
)
expect(result.map((entry) => entry.title)).toEqual(['Team Alpha'])
expect(result.map((entry) => entry.title)).toEqual(['Team Essay'])
})
})

View File

@@ -64,7 +64,7 @@ describe('release workflow macOS artifact names', () => {
)
expect(alphaWorkflow).toContain('require_windows_authenticode: false')
expect(stableWorkflow).toContain('require_windows_authenticode: true')
expect(stableWorkflow).toContain("require_windows_authenticode: ${{ needs.version.outputs.tag != 'v2026-06-01' }}")
expect(artifactWorkflow).toContain('require_windows_authenticode:')
expect(artifactWorkflow).toContain('WINDOWS_CODE_SIGNING_CERTIFICATE')
expect(artifactWorkflow).toContain('WINDOWS_CERTIFICATE')