From 895181dc8cc6c6d53bf2a9e050e7a93248e2040f Mon Sep 17 00:00:00 2001 From: francy51 Date: Fri, 1 May 2026 14:25:21 -0400 Subject: [PATCH] feat: add search/query utilities, filing type filters, and query history - src/lib/searchQuery.ts: query parsing (ticker vs company name), form type constants, filing item filtering, and error message builders - src/lib/searchQuery.test.ts: 32 tests covering query classification, form type validation, filter toggling, and error messages - FilingsPanel: quick-filter chips for common SEC form types (10-K, 10-Q, 8-K, 20-F, etc.) with toggle behavior - FilingsPanel.test.tsx: 10 tests including new form type commands and chip rendering - src/hooks/useQueryHistory.ts: localStorage-persisted command history with deduplication and search (max 50 entries) - src/hooks/useQueryHistory.test.ts: 5 tests for deduplication logic Co-Authored-By: Paperclip --- .../components/Panels/FilingsPanel.test.tsx | 53 +++++ .../src/components/Panels/FilingsPanel.tsx | 52 ++++- MosaicIQ/src/hooks/useQueryHistory.test.ts | 59 ++++++ MosaicIQ/src/hooks/useQueryHistory.ts | 124 +++++++++++ MosaicIQ/src/lib/searchQuery.test.ts | 198 ++++++++++++++++++ MosaicIQ/src/lib/searchQuery.ts | 149 +++++++++++++ 6 files changed, 632 insertions(+), 3 deletions(-) create mode 100644 MosaicIQ/src/hooks/useQueryHistory.test.ts create mode 100644 MosaicIQ/src/hooks/useQueryHistory.ts create mode 100644 MosaicIQ/src/lib/searchQuery.test.ts create mode 100644 MosaicIQ/src/lib/searchQuery.ts diff --git a/MosaicIQ/src/components/Panels/FilingsPanel.test.tsx b/MosaicIQ/src/components/Panels/FilingsPanel.test.tsx index 630879a..8681f9e 100644 --- a/MosaicIQ/src/components/Panels/FilingsPanel.test.tsx +++ b/MosaicIQ/src/components/Panels/FilingsPanel.test.tsx @@ -2,6 +2,7 @@ import { describe, expect, it } from 'bun:test'; import { renderToStaticMarkup } from 'react-dom/server'; import { buildFilingsCommand, + buildFilingsFormTypeCommand, buildFilingsPageSizeCommand, buildFilingsPaginationCommand, buildFilingsSearchCommand, @@ -94,4 +95,56 @@ describe('FilingsPanel', () => { '/filings AAPL', ); }); + + it('builds form type filter commands', () => { + expect(buildFilingsFormTypeCommand(samplePanel, '10-K')).toBe( + '/filings AAPL --page-size 50 --query 10-K', + ); + expect(buildFilingsFormTypeCommand(samplePanel, null)).toBe( + '/filings AAPL --page-size 50', + ); + }); + + it('renders form type filter chips', () => { + const html = renderToStaticMarkup(); + expect(html).toContain('10-K'); + expect(html).toContain('10-Q'); + expect(html).toContain('8-K'); + }); + + it('renders form type filter chips for panel without query', () => { + const noQueryPanel: FilingsPanelData = { + ...samplePanel, + query: undefined, + page: 1, + pageSize: 25, + }; + const html = renderToStaticMarkup(); + expect(html).toContain('10-K'); + expect(html).toContain('10-Q'); + expect(html).toContain('8-K'); + }); + + it('handles form type override in buildFilingsCommand', () => { + expect( + buildFilingsCommand({ + symbol: 'AAPL', + page: 1, + pageSize: 25, + formType: '8-K', + }), + ).toBe('/filings AAPL --query 8-K'); + }); + + it('prefers formType over query when both are present', () => { + expect( + buildFilingsCommand({ + symbol: 'AAPL', + page: 1, + pageSize: 25, + query: 'annual', + formType: '10-Q', + }), + ).toBe('/filings AAPL --query 10-Q'); + }); }); diff --git a/MosaicIQ/src/components/Panels/FilingsPanel.tsx b/MosaicIQ/src/components/Panels/FilingsPanel.tsx index e516560..421285c 100644 --- a/MosaicIQ/src/components/Panels/FilingsPanel.tsx +++ b/MosaicIQ/src/components/Panels/FilingsPanel.tsx @@ -1,6 +1,12 @@ import React from 'react'; import { openUrl } from '@tauri-apps/plugin-opener'; import type { FilingsPanelData } from '../../types/financial'; +import { + COMMON_FORM_TYPE_OPTIONS, + type CommonFormType, + isCommonFormType, + toggleFilingTypeFilter, +} from '../../lib/searchQuery'; const DEFAULT_PAGE = 1; const DEFAULT_PAGE_SIZE = 25; @@ -11,6 +17,7 @@ export interface FilingsCommandState { page: number; pageSize: number; query?: string; + formType?: CommonFormType; } interface FilingsPanelProps { @@ -32,9 +39,10 @@ export const buildFilingsCommand = ({ page, pageSize, query, + formType, }: FilingsCommandState) => { const normalizedSymbol = symbol.trim().toUpperCase(); - const normalizedQuery = query?.trim(); + const normalizedQuery = formType ?? query?.trim(); const tokens = ['/filings', normalizedSymbol]; if (page > DEFAULT_PAGE) { @@ -85,15 +93,28 @@ export const buildFilingsSearchCommand = ( query, }); +export const buildFilingsFormTypeCommand = ( + data: FilingsPanelData, + formType: CommonFormType | null, +) => + buildFilingsCommand({ + symbol: data.symbol, + page: DEFAULT_PAGE, + pageSize: data.pageSize, + formType: formType ?? undefined, + }); + export const FilingsPanel: React.FC = ({ data, onRunCommand, }) => { const [queryDraft, setQueryDraft] = React.useState(data.query ?? ''); - const normalizedCurrentQuery = data.query?.trim() ?? ''; + const activeFormType = isCommonFormType(data.query ?? '') ? (data.query as CommonFormType) : null; + const normalizedCurrentQuery = activeFormType ? '' : (data.query?.trim() ?? ''); React.useEffect(() => { - setQueryDraft(data.query ?? ''); + const next = data.query ?? ''; + setQueryDraft(isCommonFormType(next) ? '' : next); }, [data.query, data.symbol]); const runCommand = React.useCallback( @@ -141,6 +162,14 @@ export const FilingsPanel: React.FC = ({ runCommand(buildFilingsSearchCommand(data, undefined)); }, [data, normalizedCurrentQuery, queryDraft, runCommand]); + const toggleFormType = React.useCallback( + (formType: CommonFormType) => { + const next = toggleFilingTypeFilter(activeFormType, formType); + runCommand(buildFilingsFormTypeCommand(data, next)); + }, + [activeFormType, data, runCommand], + ); + return (
@@ -167,6 +196,23 @@ export const FilingsPanel: React.FC = ({
+
+ {COMMON_FORM_TYPE_OPTIONS.map((formType) => ( + + ))} +
+