From c09b25d3c5dd272a55d90c6aa3dc31f59b32d7e8 Mon Sep 17 00:00:00 2001 From: lucaronin Date: Sun, 17 May 2026 16:34:09 +0200 Subject: [PATCH] fix: restore release build type safety --- src/components/note-list/NoteListHeader.tsx | 4 +- src/hooks/frontmatterOps.ts | 6 +-- src/hooks/rawEditorEntryState.ts | 10 ++--- src/hooks/usePropertyPanelState.ts | 11 +++-- src/utils/viewFilterArrayFields.ts | 45 ++++++++++++--------- src/utils/viewFilterArrayProperties.ts | 34 +++++++++++----- 6 files changed, 68 insertions(+), 42 deletions(-) diff --git a/src/components/note-list/NoteListHeader.tsx b/src/components/note-list/NoteListHeader.tsx index ecc1a17a..74c46c21 100644 --- a/src/components/note-list/NoteListHeader.tsx +++ b/src/components/note-list/NoteListHeader.tsx @@ -1,7 +1,7 @@ import { CircleNotch as Loader2, MagnifyingGlass, Plus, SidebarSimple, X } from '@phosphor-icons/react' import type { VaultEntry } from '../../types' import type { SortOption, SortDirection } from '../../utils/noteListHelpers' -import { translate, type AppLocale } from '../../lib/i18n' +import { translate, type AppLocale, type TranslationKey } from '../../lib/i18n' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { APP_COMMAND_EVENT_NAME, APP_COMMAND_IDS } from '../../hooks/appCommandDispatcher' @@ -14,7 +14,7 @@ import type { GitRepositoryOption } from '../../utils/gitRepositories' const NOTE_LIST_ACTION_BUTTON_CLASSNAME = '!h-auto !w-auto !min-w-0 !rounded-none !p-0 !text-muted-foreground hover:!bg-transparent hover:!text-foreground focus-visible:!bg-transparent data-[state=open]:!bg-transparent data-[state=open]:!text-foreground [&_svg]:!size-4' const NOTE_LIST_EXPAND_BUTTON_CLASSNAME = '!h-6 !w-6 !min-w-0 !rounded !p-0 !text-muted-foreground hover:!bg-accent hover:!text-foreground focus-visible:!bg-accent [&_svg]:!size-4' -const PROPERTY_TRIGGER_TITLE_KEYS: Record = { +const PROPERTY_TRIGGER_TITLE_KEYS: Record = { 'Customize columns': 'noteList.properties.customizeColumns', 'Customize All Notes columns': 'noteList.properties.customizeAllColumns', 'Customize Inbox columns': 'noteList.properties.customizeInboxColumns', diff --git a/src/hooks/frontmatterOps.ts b/src/hooks/frontmatterOps.ts index fd28514a..53dffaa0 100644 --- a/src/hooks/frontmatterOps.ts +++ b/src/hooks/frontmatterOps.ts @@ -1,6 +1,6 @@ import { invoke } from '@tauri-apps/api/core' import { isTauri, mockInvoke } from '../mock-tauri' -import type { VaultEntry } from '../types' +import type { VaultEntry, VaultPropertyValue } from '../types' import type { FrontmatterValue } from '../components/Inspector' import { updateMockFrontmatter, deleteMockFrontmatterProperty } from './mockFrontmatterHelpers' import { updateMockContent, trackMockChange } from '../mock-tauri' @@ -286,8 +286,8 @@ export interface FrontmatterRunRequest { /** Apply a properties patch by merging into the existing properties map. */ export function applyPropertiesPatch( - existing: Record, propPatch: PropertiesPatch, -): Record { + existing: Record, propPatch: PropertiesPatch, +): Record { return applyRecordPatch(existing, propPatch) } diff --git a/src/hooks/rawEditorEntryState.ts b/src/hooks/rawEditorEntryState.ts index aa3690db..8447580a 100644 --- a/src/hooks/rawEditorEntryState.ts +++ b/src/hooks/rawEditorEntryState.ts @@ -1,6 +1,6 @@ -import type { VaultEntry } from '../types' +import type { VaultEntry, VaultPropertyValue } from '../types' import { parseFrontmatter } from '../utils/frontmatter' -import { frontmatterToEntryPatch } from './frontmatterOps' +import { frontmatterToEntryPatch, type PropertiesPatch } from './frontmatterOps' function createRawEditorEntryState(): Partial { return { @@ -35,8 +35,8 @@ function mergeRelationships(target: Record, source: Record, - source: Record | null, + target: Record, + source: PropertiesPatch | null, ): void { if (!source) return for (const [key, value] of Object.entries(source)) { @@ -46,7 +46,7 @@ function mergeProperties( export function deriveRawEditorEntryState(content: string): Partial { const derived = createRawEditorEntryState() - const properties: Record = {} + const properties: Record = {} const relationships: Record = {} for (const [key, value] of Object.entries(parseFrontmatter(content))) { diff --git a/src/hooks/usePropertyPanelState.ts b/src/hooks/usePropertyPanelState.ts index c321572f..b5545439 100644 --- a/src/hooks/usePropertyPanelState.ts +++ b/src/hooks/usePropertyPanelState.ts @@ -1,5 +1,5 @@ import { useMemo, useState, useCallback } from 'react' -import type { VaultEntry } from '../types' +import type { VaultEntry, VaultPropertyValue } from '../types' import type { FrontmatterValue } from '../components/Inspector' import type { ParsedFrontmatter } from '../utils/frontmatter' import { @@ -91,6 +91,10 @@ function isVisibleProperty([key, value]: [string, FrontmatterValue]): boolean { return !isHiddenPropertyKey(key) && !containsWikilinks(value) } +function frontmatterValueFromVaultProperty(value: VaultPropertyValue): FrontmatterValue { + return Array.isArray(value) ? value.map(String) : value +} + function buildVisiblePropertyEntries(frontmatter: ParsedFrontmatter): PropertyEntry[] { const result: PropertyEntry[] = [] const seen = new Set() @@ -139,9 +143,10 @@ function buildTypeDerivedPropertyEntries({ for (const [key, value] of Object.entries(typeEntry.properties ?? {})) { const canonicalKey = canonicalFrontmatterKey(key) if (existingKeys.has(canonicalKey) || seen.has(canonicalKey) || isRelationshipSchemaKey(key)) continue - if (!isVisibleProperty([key, value])) continue + const propertyValue = frontmatterValueFromVaultProperty(value) + if (!isVisibleProperty([key, propertyValue])) continue seen.add(canonicalKey) - result.push([key, value]) + result.push([key, propertyValue]) } return result diff --git a/src/utils/viewFilterArrayFields.ts b/src/utils/viewFilterArrayFields.ts index b3c12d0d..e7541985 100644 --- a/src/utils/viewFilterArrayFields.ts +++ b/src/utils/viewFilterArrayFields.ts @@ -2,29 +2,32 @@ import type { FilterCondition } from '../types' import { evaluatePropertyArrayCondition } from './viewFilterArrayProperties' export type ViewFilterArrayKind = 'property' | 'relationship' +type ConditionText = string +type RelationshipValue = string +type RelationshipArrayOperator = (field: RelationshipArrayField, value: ConditionText, cond: FilterCondition) => boolean interface ArrayFieldCondition { cond: FilterCondition - values: string[] + values: RelationshipValue[] arrayKind: ViewFilterArrayKind - condVal: string + condVal: ConditionText regex: RegExp | null } -function toStringValue(value: unknown): string { +function toStringValue(value: unknown): ConditionText { if (value == null) return '' if (typeof value === 'string') return value return String(value) } -function conditionList(value: unknown): string[] | null { +function conditionList(value: unknown): ConditionText[] | null { return Array.isArray(value) ? value.map(toStringValue) : null } class WikilinkValue { - private readonly trimmed: string + private readonly trimmed: RelationshipValue - constructor(private readonly raw: string) { + constructor(raw: RelationshipValue) { this.trimmed = raw.trim() } @@ -32,11 +35,11 @@ class WikilinkValue { return this.trimmed.startsWith('[[') } - get normalizedStem(): string { + get normalizedStem(): RelationshipValue { return this.stem.toLowerCase() } - get candidates(): string[] { + get candidates(): ConditionText[] { const pipe = this.inner.indexOf('|') if (pipe >= 0) return [this.trimmed, this.inner.slice(0, pipe), this.inner.slice(pipe + 1)] return [this.trimmed, this.inner] @@ -51,17 +54,17 @@ class WikilinkValue { return this.parts.some((part) => targetParts.some((targetPart) => part === targetPart)) } - private get parts(): string[] { + private get parts(): ConditionText[] { const pipe = this.inner.indexOf('|') if (pipe >= 0) return [this.inner.substring(0, pipe).toLowerCase(), this.inner.substring(pipe + 1).toLowerCase()] return [this.inner.toLowerCase()] } - private get stem(): string { + private get stem(): RelationshipValue { return this.inner.split('|')[0] ?? this.inner } - private get inner(): string { + private get inner(): RelationshipValue { return this.trimmed.replace(/^\[\[/, '').replace(/\]\]$/, '') } } @@ -69,36 +72,42 @@ class WikilinkValue { class RelationshipArrayField { private readonly links: WikilinkValue[] - constructor(values: string[]) { + constructor(values: RelationshipValue[]) { this.links = values.map((value) => new WikilinkValue(value)) } - contains(targetValue: string): boolean { + contains(targetValue: ConditionText): boolean { const target = new WikilinkValue(targetValue) return this.links.some((link) => target.isBracketed ? link.equals(target) : link.includesStem(target)) } - equals(targetValue: string): boolean { + equals(targetValue: ConditionText): boolean { return this.links.length === 1 && this.links[0]?.equals(new WikilinkValue(targetValue)) === true } - matchesAny(targets: string[] | null): boolean { + matchesAny(targets: ConditionText[] | null): boolean { return targets?.some((target) => this.links.some((link) => link.equals(new WikilinkValue(target)))) ?? false } matchesRegex(regex: RegExp): boolean { return this.links.some((link) => link.candidates.some((candidate) => regex.test(candidate))) } + + isEmpty(): boolean { + return this.links.length === 0 + } } -const RELATIONSHIP_ARRAY_OPERATORS = { +const RELATIONSHIP_ARRAY_OPERATORS: Partial> = { contains: (field, value) => field.contains(value), not_contains: (field, value) => !field.contains(value), equals: (field, value) => field.equals(value), not_equals: (field, value) => !field.equals(value), any_of: (field, _value, cond) => field.matchesAny(conditionList(cond.value)), none_of: (field, _value, cond) => !field.matchesAny(conditionList(cond.value)), -} satisfies Partial boolean>> + is_empty: (field) => field.isEmpty(), + is_not_empty: (field) => !field.isEmpty(), +} function textMatchResult(op: FilterCondition['op'], matched: boolean): boolean { if (op === 'contains' || op === 'equals') return matched @@ -106,7 +115,7 @@ function textMatchResult(op: FilterCondition['op'], matched: boolean): boolean { return false } -function evaluateRelationshipArrayCondition(cond: FilterCondition, values: string[], condVal: string, regex: RegExp | null): boolean { +function evaluateRelationshipArrayCondition(cond: FilterCondition, values: RelationshipValue[], condVal: ConditionText, regex: RegExp | null): boolean { const { op } = cond const field = new RelationshipArrayField(values) if (regex) return textMatchResult(op, field.matchesRegex(regex)) diff --git a/src/utils/viewFilterArrayProperties.ts b/src/utils/viewFilterArrayProperties.ts index b118a461..0a6aefc1 100644 --- a/src/utils/viewFilterArrayProperties.ts +++ b/src/utils/viewFilterArrayProperties.ts @@ -1,12 +1,16 @@ import type { FilterCondition } from '../types' -function toStringValue(value: unknown): string { +type ConditionText = string +type PropertyValue = string +type PropertyArrayOperator = (field: PropertyArrayField, value: ConditionText, cond: FilterCondition) => boolean + +function toStringValue(value: unknown): ConditionText { if (value == null) return '' if (typeof value === 'string') return value return String(value) } -function conditionList(value: unknown): string[] | null { +function conditionList(value: unknown): ConditionText[] | null { return Array.isArray(value) ? value.map(toStringValue) : null } @@ -17,42 +21,50 @@ function textMatchResult(op: FilterCondition['op'], matched: boolean): boolean { } class PropertyArrayField { - private readonly normalizedValues: Set + private readonly values: PropertyValue[] + private readonly normalizedValues: Set - constructor(private readonly values: string[]) { + constructor(values: PropertyValue[]) { + this.values = values this.normalizedValues = new Set(values.map((value) => value.toLowerCase())) } - contains(target: string): boolean { + contains(target: ConditionText): boolean { return this.normalizedValues.has(target.toLowerCase()) } - equals(target: string): boolean { + equals(target: ConditionText): boolean { return this.values.length === 1 && this.contains(target) } - matchesAny(targets: string[] | null): boolean { + matchesAny(targets: ConditionText[] | null): boolean { return targets?.some((target) => this.contains(target)) ?? false } matchesRegex(regex: RegExp): boolean { return this.values.some((value) => regex.test(value)) } + + isEmpty(): boolean { + return this.values.length === 0 + } } -const PROPERTY_ARRAY_OPERATORS = { +const PROPERTY_ARRAY_OPERATORS: Partial> = { contains: (field, value) => field.contains(value), not_contains: (field, value) => !field.contains(value), equals: (field, value) => field.equals(value), not_equals: (field, value) => !field.equals(value), any_of: (field, _value, cond) => field.matchesAny(conditionList(cond.value)), none_of: (field, _value, cond) => !field.matchesAny(conditionList(cond.value)), -} satisfies Partial boolean>> + is_empty: (field) => field.isEmpty(), + is_not_empty: (field) => !field.isEmpty(), +} export function evaluatePropertyArrayCondition( cond: FilterCondition, - values: string[], - condVal: string, + values: PropertyValue[], + condVal: ConditionText, regex: RegExp | null, ): boolean { const field = new PropertyArrayField(values)