feat(transcription): decouple @mention display text from person search (#380) #629
@@ -7,6 +7,15 @@ import { m } from '$lib/paraglide/messages.js';
|
|||||||
|
|
||||||
type Person = components['schemas']['Person'];
|
type Person = components['schemas']['Person'];
|
||||||
|
|
||||||
|
// Layered defence cap on the @mention search query length (CWE-400
|
||||||
|
// amplification). The <input maxlength> attribute below caps direct
|
||||||
|
// user edits, but the editor-mirror path (Tiptap contenteditable -> mirror
|
||||||
|
// $effect -> searchQuery) is not covered by `maxlength` since the
|
||||||
|
// contenteditable has no such enforcement. Clipping at the mirror keeps
|
||||||
|
// the cap honest from both paths. Tracked server-side separately.
|
||||||
|
// Nora #1 on PR #629.
|
||||||
|
const MAX_QUERY_LENGTH = 100;
|
||||||
|
|
||||||
// The dropdown receives a single reactive state object. PersonMentionEditor
|
// The dropdown receives a single reactive state object. PersonMentionEditor
|
||||||
// mutates fields on this object (model.items = ..., etc.) and Svelte's $state
|
// mutates fields on this object (model.items = ..., etc.) and Svelte's $state
|
||||||
// proxy reactivity propagates the change here. This is the supported way to
|
// proxy reactivity propagates the change here. This is the supported way to
|
||||||
@@ -30,7 +39,7 @@ let {
|
|||||||
onSearch?: (query: string) => void;
|
onSearch?: (query: string) => void;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let searchQuery = $state(untrack(() => editorQuery));
|
let searchQuery = $state(untrack(() => editorQuery.slice(0, MAX_QUERY_LENGTH)));
|
||||||
let userHasEdited = $state(false);
|
let userHasEdited = $state(false);
|
||||||
|
|
||||||
// Mirror the editor's typed text until the user takes ownership.
|
// Mirror the editor's typed text until the user takes ownership.
|
||||||
@@ -42,7 +51,7 @@ let userHasEdited = $state(false);
|
|||||||
// user types into the input. Felix #1 on PR #629.
|
// user types into the input. Felix #1 on PR #629.
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!userHasEdited) {
|
if (!userHasEdited) {
|
||||||
searchQuery = editorQuery;
|
searchQuery = editorQuery.slice(0, MAX_QUERY_LENGTH);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -172,7 +181,7 @@ function selectItem(item: Person) {
|
|||||||
id="mention-search"
|
id="mention-search"
|
||||||
type="search"
|
type="search"
|
||||||
data-test-search-input
|
data-test-search-input
|
||||||
maxlength="100"
|
maxlength={MAX_QUERY_LENGTH}
|
||||||
class="min-h-[44px] w-full bg-transparent font-sans text-sm text-ink placeholder:text-ink-3 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-navy focus-visible:ring-inset"
|
class="min-h-[44px] w-full bg-transparent font-sans text-sm text-ink placeholder:text-ink-3 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-navy focus-visible:ring-inset"
|
||||||
placeholder={m.person_mention_search_prompt()}
|
placeholder={m.person_mention_search_prompt()}
|
||||||
bind:value={searchQuery}
|
bind:value={searchQuery}
|
||||||
|
|||||||
@@ -186,6 +186,16 @@ describe('MentionDropdown — search input', () => {
|
|||||||
expect(input.maxLength).toBe(100);
|
expect(input.maxLength).toBe(100);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('clips a long editorQuery mirror to 100 chars (CWE-400 layered — Nora #1 on PR #629)', async () => {
|
||||||
|
const longQuery = 'A'.repeat(200);
|
||||||
|
render(MentionDropdown, { props: { model: baseModel(), editorQuery: longQuery } });
|
||||||
|
|
||||||
|
const input = document.querySelector('[data-test-search-input]') as HTMLInputElement;
|
||||||
|
expect(input).not.toBeNull();
|
||||||
|
expect(input.value.length).toBe(100);
|
||||||
|
expect(input.value).toBe('A'.repeat(100));
|
||||||
|
});
|
||||||
|
|
||||||
it('invokes onSearch with the current value whenever the user types', async () => {
|
it('invokes onSearch with the current value whenever the user types', async () => {
|
||||||
const onSearch = vi.fn();
|
const onSearch = vi.fn();
|
||||||
render(MentionDropdown, { props: { model: baseModel(), onSearch } });
|
render(MentionDropdown, { props: { model: baseModel(), onSearch } });
|
||||||
|
|||||||
Reference in New Issue
Block a user