From b11ad3e06957db6c3d97b7895f3cb04a06b28f31 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Thu, 16 Jul 2026 21:24:06 -0700 Subject: [PATCH] split add field input to separate component --- .../tag-editor/components/add-field-input.tsx | 109 ++++++++++++++++++ .../tag-editor/components/song-edit-modal.tsx | 98 ++-------------- 2 files changed, 118 insertions(+), 89 deletions(-) create mode 100644 src/renderer/features/tag-editor/components/add-field-input.tsx diff --git a/src/renderer/features/tag-editor/components/add-field-input.tsx b/src/renderer/features/tag-editor/components/add-field-input.tsx new file mode 100644 index 000000000..81b514a86 --- /dev/null +++ b/src/renderer/features/tag-editor/components/add-field-input.tsx @@ -0,0 +1,109 @@ +import { useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { KNOWN_TAG_MAP, resolveTagKey } from '../utils/known-tags'; + +import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; +import { Autocomplete } from '/@/shared/components/autocomplete/autocomplete'; +import { Group } from '/@/shared/components/group/group'; + +interface AddFieldInputProps { + availableFields: Array<{ label: string; value: string }>; + existingFieldKeys: string[]; + onAddField: (key: string) => void; +} + +export const AddFieldInput = ({ + availableFields, + existingFieldKeys, + onAddField, +}: AddFieldInputProps) => { + const { t } = useTranslation(); + const skipNextEnterRef = useRef(false); + const skipNextOnChangeResetRef = useRef(false); + const [input, setInput] = useState(''); + const [duplicateAttempted, setDuplicateAttempted] = useState(false); + + const trimmedInput = input.trim(); + const customKeyError = + trimmedInput && !KNOWN_TAG_MAP.has(trimmedInput) + ? trimmedInput.includes('=') + ? "Tag key cannot contain '='" + : // eslint-disable-next-line no-control-regex + /[^\x00-\x7F]/.test(trimmedInput) + ? 'Tag key must use ASCII characters only' + : null + : null; + + const resolvedInputKey = trimmedInput ? resolveTagKey(trimmedInput) : ''; + const duplicateError = + trimmedInput && !customKeyError && existingFieldKeys.includes(resolvedInputKey) + ? 'Field already exists' + : null; + const fieldError = customKeyError ?? (duplicateAttempted ? duplicateError : null); + + const addField = (key: string): boolean => { + const trimmed = key.trim(); + if (!trimmed) return false; + if ( + !KNOWN_TAG_MAP.has(trimmed) && + (trimmed.includes('=') || // eslint-disable-next-line no-control-regex + /[^\x00-\x7F]/.test(trimmed)) + ) + return false; + + const normalizedKey = resolveTagKey(trimmed); + if (existingFieldKeys.includes(normalizedKey)) { + setDuplicateAttempted(true); + return false; + } + + setDuplicateAttempted(false); + setInput(''); + onAddField(normalizedKey); + return true; + }; + + return ( + + { + setInput(value); + if (skipNextOnChangeResetRef.current) { + skipNextOnChangeResetRef.current = false; + } else { + setDuplicateAttempted(false); + } + }} + onKeyDown={(event) => { + if (event.key === 'Enter') { + if (skipNextEnterRef.current) { + skipNextEnterRef.current = false; + return; + } + addField(input); + } + }} + onOptionSubmit={(value) => { + skipNextEnterRef.current = true; + skipNextOnChangeResetRef.current = true; + const added = addField(value); + if (added) { + queueMicrotask(() => setInput('')); + } + }} + placeholder={t('page.itemDetail.addField', 'Add field…')} + value={input} + /> + addField(input)} + variant="filled" + /> + + ); +}; diff --git a/src/renderer/features/tag-editor/components/song-edit-modal.tsx b/src/renderer/features/tag-editor/components/song-edit-modal.tsx index 9ca239b51..62ef0254c 100644 --- a/src/renderer/features/tag-editor/components/song-edit-modal.tsx +++ b/src/renderer/features/tag-editor/components/song-edit-modal.tsx @@ -1,17 +1,14 @@ import { closeAllModals } from '@mantine/modals'; -import { useRef, useState } from 'react'; +import { useRef } from 'react'; import { useTranslation } from 'react-i18next'; -import { RiAddLine } from 'react-icons/ri'; import { useMetadataEditor } from '../hooks/use-metadata-editor'; -import { KNOWN_TAG_MAP, resolveTagKey } from '../utils/known-tags'; +import { AddFieldInput } from './add-field-input'; import { ArtworkPanel } from './artwork-panel'; import styles from './song-edit-modal.module.css'; import { TagEditorSettings } from './tag-editor-settings'; import { TagFieldRow } from './tag-field-row'; -import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; -import { Autocomplete } from '/@/shared/components/autocomplete/autocomplete'; import { Button } from '/@/shared/components/button/button'; import { Checkbox } from '/@/shared/components/checkbox/checkbox'; import { Group } from '/@/shared/components/group/group'; @@ -25,10 +22,6 @@ import { Song } from '/@/shared/types/domain-types'; export const SongEditModal = ({ songs }: { songs: Song[] }) => { const { t } = useTranslation(); const tableContainerRef = useRef(null); - const skipNextEnterRef = useRef(false); - const skipNextOnChangeResetRef = useRef(false); - const [addFieldInput, setAddFieldInput] = useState(''); - const [duplicateAttempted, setDuplicateAttempted] = useState(false); const editor = useMetadataEditor({ browser: window.api.browser, @@ -36,52 +29,15 @@ export const SongEditModal = ({ songs }: { songs: Song[] }) => { utils: window.api.utils, }); - const trimmedInput = addFieldInput.trim(); - const customKeyError = - trimmedInput && !KNOWN_TAG_MAP.has(trimmedInput) - ? trimmedInput.includes('=') - ? "Tag key cannot contain '='" - : // eslint-disable-next-line no-control-regex - /[^\x00-\x7F]/.test(trimmedInput) - ? 'Tag key must use ASCII characters only' - : null - : null; - - const resolvedInputKey = trimmedInput ? resolveTagKey(trimmedInput) : ''; - const duplicateError = - trimmedInput && - !customKeyError && - editor.sortedFieldEntries.some(([k]) => k === resolvedInputKey) - ? 'Field already exists' - : null; - const fieldError = customKeyError ?? (duplicateAttempted ? duplicateError : null); - - const handleAddField = (key: string): boolean => { - const trimmed = key.trim(); - if (!trimmed) return false; - if ( - !KNOWN_TAG_MAP.has(trimmed) && - (trimmed.includes('=') || // eslint-disable-next-line no-control-regex - /[^\x00-\x7F]/.test(trimmed)) - ) - return false; - const normalizedKey = resolveTagKey(trimmed); - if (editor.sortedFieldEntries.some(([k]) => k === normalizedKey)) { - setDuplicateAttempted(true); - return false; - } - setDuplicateAttempted(false); - editor.handleAddField(normalizedKey); - setAddFieldInput(''); - + const handleAddField = (key: string) => { + editor.handleAddField(key); requestAnimationFrame(() => { const row = tableContainerRef.current?.querySelector( - `[data-field-key="${normalizedKey}"]`, + `[data-field-key="${key}"]`, ); row?.scrollIntoView({ block: 'nearest' }); row?.querySelector('input, textarea')?.focus(); }); - return true; }; // While loading, shows a spinner @@ -125,46 +81,10 @@ export const SongEditModal = ({ songs }: { songs: Song[] }) => { {editor.readWarning} )} - { - setAddFieldInput(v); - if (skipNextOnChangeResetRef.current) { - skipNextOnChangeResetRef.current = false; - } else { - setDuplicateAttempted(false); - } - }} - onKeyDown={(e) => { - if (e.key === 'Enter') { - if (skipNextEnterRef.current) { - skipNextEnterRef.current = false; - return; - } - handleAddField(addFieldInput); - } - }} - onOptionSubmit={(value) => { - skipNextEnterRef.current = true; - skipNextOnChangeResetRef.current = true; - const added = handleAddField(value); - if (added) { - queueMicrotask(() => setAddFieldInput('')); - } - }} - placeholder={t('page.itemDetail.addField', 'Add field…')} - rightSection={ - handleAddField(addFieldInput)} - variant="filled" - > - - - } - rightSectionPointerEvents="all" - value={addFieldInput} + key)} + onAddField={handleAddField} />