feat: Refactor tag and correspondent management to use canonical types and dedicated managers.
This commit is contained in:
@@ -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[] = [];
|
||||
|
||||
Reference in New Issue
Block a user