feat: Refactor tag and correspondent management to use canonical types and dedicated managers.

This commit is contained in:
2025-12-09 18:56:11 +01:00
parent ce821c1817
commit 3ccc1f6698
34 changed files with 790 additions and 427 deletions
@@ -15,25 +15,12 @@ import {
import { describeDocumentSummary, type DocumentSummaryRow } from '../logic/documentSummary';
import { useFolderManager } from '../../folders/FolderManagerContext';
import type { Document, Tag, Correspondent } from '../../types/documents';
import type { FolderId, Identifier, TagId } from '../../types/identifiers';
interface TagEntry {
id?: TagId;
label?: string;
color?: string | null;
}
interface CorrespondentEntry {
id?: Identifier;
name?: string;
count?: number;
}
import type { Document } from '../../types/documents';
interface TagSectionProps {
tags?: TagEntry[];
onRemove?: (tag: TagEntry) => void;
tags?: Tag[];
onRemove?: (tag: Tag) => void;
onAdd?: (payload: { value: string; option?: unknown; input?: unknown }) => void;
emptyMessage?: string;
addPlaceholder?: string;
@@ -43,8 +30,8 @@ interface TagSectionProps {
}
interface CorrespondentSectionProps {
entries?: CorrespondentEntry[];
onRemove?: (entry: CorrespondentEntry) => void;
entries?: Correspondent[];
onRemove?: (entry: Correspondent) => void;
onAdd?: (payload: { name: string; option?: unknown; input?: unknown }) => void;
showCount?: boolean;
addPlaceholder?: string;
@@ -55,11 +42,12 @@ interface CorrespondentSectionProps {
export interface DocumentSummarySectionProps {
document?: Document | null;
tagLookupById?: Map<TagId, TagEntry>;
tagLookupById?: Map<TagId, Tag>;
tagOptions?: SelectionAssignmentMenuItem[];
onTagAdd?: (doc: Document, value: string, context?: { option?: unknown }) => void;
onTagRemove?: (docId: Identifier | undefined, tagId: TagId | undefined) => void;
correspondents?: CorrespondentEntry[];
correspondents?: Correspondent[];
correspondentLookupById?: Map<Identifier, Correspondent>;
correspondentOptions?: SelectionAssignmentMenuItem[];
onCorrespondentAdd?: (payload: { document: Document; name: string; option?: unknown }) => void;
onCorrespondentRemove?: (payload: { documentId: Identifier | undefined; correspondentId: Identifier | undefined }) => void;
@@ -77,10 +65,9 @@ interface MetaItem {
error?: string | null;
}
export const sortCorrespondents = (entries = []) =>
export const sortCorrespondents = (entries: Correspondent[] = []) =>
entries
.filter((entry) => entry && entry.name)
.map(({ id, name, count }) => ({ id, name, count }))
.sort((a, b) => a.name.localeCompare(b.name));
export const buildCorrespondentOptions = (entries = []) => {
@@ -237,8 +224,8 @@ const TagSection: React.FC<TagSectionProps> = ({
}
if (item.state === 'all' && onRemove) {
const payload = item.payload && typeof item.payload === 'object' && 'id' in item.payload
? (item.payload as TagEntry)
: tags.find((tag) => (tag.id ?? tag.label) === item.id) ?? { id: item.id, label: item.label };
? (item.payload as Tag)
: tags.find((tag) => (tag.id ?? tag.label) === item.id) ?? { id: item.id, label: item.label } as unknown as Tag;
onRemove(payload);
return;
}
@@ -344,7 +331,7 @@ const CorrespondentSection: React.FC<CorrespondentSectionProps> = ({
return;
}
const key = label.toLowerCase();
const payload = { id: entry.id, name: label };
const payload = entry;
if (map.has(key)) {
const item = map.get(key);
if (item) {
@@ -370,9 +357,7 @@ const CorrespondentSection: React.FC<CorrespondentSectionProps> = ({
return;
}
if (item.state === 'all' && onRemove) {
const payload = item.payload && typeof item.payload === 'object' && 'id' in item.payload
? (item.payload as CorrespondentEntry)
: entries.find((entry) => (entry.id ?? entry.name) === item.id) ?? { id: item.id, name: item.label };
const payload = (item.payload || { id: item.id, name: item.label }) as Correspondent;
onRemove(payload);
return;
}
@@ -389,7 +374,7 @@ const CorrespondentSection: React.FC<CorrespondentSectionProps> = ({
: { id: null, name: resolvedName };
onAdd({ name: resolvedName, option: payload, input: null });
},
[entries, onAdd, onRemove],
[onAdd, onRemove],
);
return (
@@ -401,7 +386,7 @@ const CorrespondentSection: React.FC<CorrespondentSectionProps> = ({
<span key={key} className="correspondent-pill">
<span className="correspondent-pill__label">
{entry.name}
{showCount && entry.count ? ` (${entry.count})` : ''}
{showCount && entry.usage_count ? ` (${entry.usage_count})` : ''}
</span>
{onRemove ? (
<button
@@ -446,6 +431,7 @@ const DocumentSummarySection: React.FC<DocumentSummarySectionProps> = ({
onTagAdd,
onTagRemove,
correspondents,
correspondentLookupById,
correspondentOptions = [],
onCorrespondentAdd,
onCorrespondentRemove,
@@ -456,7 +442,7 @@ const DocumentSummarySection: React.FC<DocumentSummarySectionProps> = ({
}) => {
const folderManager = useFolderManager();
const isCompactLayout = layout === 'compact';
const summaryRows = useMemo(() => describeDocumentSummary(document), [document]);
const summaryRows = useMemo(() => describeDocumentSummary(document, { tagLookupById }), [document, tagLookupById]);
const issuedDateLabel = useMemo(
() => formatDate(document?.issued_at, { fallback: null }),
[document?.issued_at],
@@ -469,23 +455,27 @@ const DocumentSummarySection: React.FC<DocumentSummarySectionProps> = ({
if (!Array.isArray(document?.tags)) {
return [];
}
return document.tags.map((tag) => ({
id: tag.id,
label: tag.label,
color: tag.color ?? tagLookupById.get(tag.id)?.color ?? null,
})).sort((a, b) => {
const labelA = (a.label || '').toLowerCase();
const labelB = (b.label || '').toLowerCase();
return labelA.localeCompare(labelB);
});
return document.tags.map((tagId) => tagLookupById.get(tagId))
.filter((t): t is Tag => Boolean(t))
.sort((a, b) => {
const labelA = (a.label || '').toLowerCase();
const labelB = (b.label || '').toLowerCase();
return labelA.localeCompare(labelB);
});
}, [document?.tags, tagLookupById]);
const resolvedCorrespondents = useMemo(() => {
if (Array.isArray(correspondents) && correspondents.length) {
return correspondents;
}
return sortCorrespondents(document?.correspondents || []);
}, [correspondents, document?.correspondents]);
if (!document?.correspondents) return [];
return document.correspondents
.map((id) => correspondentLookupById?.get(id))
.sort((a, b) => a.name.localeCompare(b.name));
}, [correspondents, document?.correspondents, correspondentLookupById]);
const extraSummaryRows = useMemo(() => {
const rows: DocumentSummaryRow[] = [];