Add tag editing and PDF preview to documents feature
Each document's tags are now editable inline: click Edit to enter a tag editor (Enter/comma to add, × to remove, Save to persist). The View button opens the PDF in a new browser tab via blob URL. Both features work through the existing proxy — no proxy changes needed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,11 +6,13 @@ import {
|
||||
uploadDocument,
|
||||
deleteDocument,
|
||||
downloadDocument,
|
||||
viewDocument,
|
||||
getDocumentStatus,
|
||||
listCategories,
|
||||
createCategory,
|
||||
assignCategory,
|
||||
removeCategory,
|
||||
updateDocumentTags,
|
||||
type DocumentOut,
|
||||
type CategoryOut,
|
||||
} from "../api/client";
|
||||
@@ -87,6 +89,110 @@ function SuggestionChip({ name, existing, onAccept, onDismiss }: SuggestionChipP
|
||||
|
||||
// ── Document row ────────────────────────────────────────────────────────────
|
||||
|
||||
// ── Tag editor ──────────────────────────────────────────────────────────────
|
||||
|
||||
function TagEditor({
|
||||
docId,
|
||||
initialTags,
|
||||
onSaved,
|
||||
}: {
|
||||
docId: string;
|
||||
initialTags: string[];
|
||||
onSaved: () => void;
|
||||
}) {
|
||||
const [tags, setTags] = useState<string[]>(initialTags);
|
||||
const [input, setInput] = useState("");
|
||||
const [editing, setEditing] = useState(false);
|
||||
|
||||
const saveMut = useMutation({
|
||||
mutationFn: (t: string[]) => updateDocumentTags(docId, t),
|
||||
onSuccess: () => { onSaved(); setEditing(false); },
|
||||
});
|
||||
|
||||
const addTag = () => {
|
||||
const val = input.trim();
|
||||
if (!val || tags.map((t) => t.toLowerCase()).includes(val.toLowerCase())) {
|
||||
setInput("");
|
||||
return;
|
||||
}
|
||||
const next = [...tags, val];
|
||||
setTags(next);
|
||||
setInput("");
|
||||
};
|
||||
|
||||
const removeTag = (idx: number) => {
|
||||
setTags((prev) => prev.filter((_, i) => i !== idx));
|
||||
};
|
||||
|
||||
if (!editing) {
|
||||
return (
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<strong>Tags:</strong>{" "}
|
||||
{tags.length === 0 && <span style={{ color: "#aaa", fontSize: 13 }}>none</span>}
|
||||
{tags.map((t) => (
|
||||
<span key={t} style={{ fontSize: 12, background: "#eee", borderRadius: 3, padding: "2px 6px", marginRight: 4 }}>
|
||||
{t}
|
||||
</span>
|
||||
))}
|
||||
<button
|
||||
onClick={() => setEditing(true)}
|
||||
style={{ fontSize: 11, marginLeft: 6, cursor: "pointer", color: "#555", background: "none", border: "1px solid #ccc", borderRadius: 3, padding: "1px 6px" }}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<strong>Tags:</strong>
|
||||
<div style={{ marginTop: 6, display: "flex", flexWrap: "wrap", gap: 6, alignItems: "center" }}>
|
||||
{tags.map((t, i) => (
|
||||
<span key={t} style={{ fontSize: 12, background: "#eee", borderRadius: 3, padding: "2px 6px", display: "inline-flex", alignItems: "center", gap: 4 }}>
|
||||
{t}
|
||||
<button
|
||||
onClick={() => removeTag(i)}
|
||||
style={{ fontSize: 10, cursor: "pointer", background: "none", border: "none", color: "#888", lineHeight: 1 }}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
<input
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === ",") { e.preventDefault(); addTag(); }
|
||||
if (e.key === "Escape") { setEditing(false); setTags(initialTags); setInput(""); }
|
||||
}}
|
||||
placeholder="Add tag…"
|
||||
style={{ fontSize: 12, padding: "2px 6px", border: "1px solid #ccc", borderRadius: 3, width: 100 }}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginTop: 6, display: "flex", gap: 6 }}>
|
||||
<button
|
||||
onClick={() => saveMut.mutate(tags)}
|
||||
disabled={saveMut.isPending}
|
||||
style={{ fontSize: 12, cursor: "pointer", padding: "3px 10px", background: "#222", color: "#fff", border: "none", borderRadius: 3 }}
|
||||
>
|
||||
{saveMut.isPending ? "Saving…" : "Save"}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setEditing(false); setTags(initialTags); setInput(""); }}
|
||||
style={{ fontSize: 12, cursor: "pointer", padding: "3px 10px", border: "1px solid #ccc", borderRadius: 3, background: "none" }}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
<p style={{ fontSize: 11, color: "#999", margin: "4px 0 0" }}>Enter or comma to add · Esc to cancel</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Document row ────────────────────────────────────────────────────────────
|
||||
|
||||
function DocumentRow({
|
||||
doc,
|
||||
categories,
|
||||
@@ -192,6 +298,12 @@ function DocumentRow({
|
||||
<span style={{ fontSize: 12, color: "#999" }}>
|
||||
{(doc.file_size / 1024).toFixed(0)} KB
|
||||
</span>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); viewDocument(doc.id); }}
|
||||
style={{ fontSize: 12, cursor: "pointer" }}
|
||||
>
|
||||
View
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); downloadDocument(doc.id, doc.filename); }}
|
||||
style={{ fontSize: 12, cursor: "pointer" }}
|
||||
@@ -213,17 +325,12 @@ function DocumentRow({
|
||||
{expanded && (
|
||||
<div style={{ padding: "0 16px 16px", borderTop: "1px solid #eee" }}>
|
||||
|
||||
{/* Tags */}
|
||||
{tags.length > 0 && (
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<strong>Tags:</strong>{" "}
|
||||
{tags.map((t) => (
|
||||
<span key={t} style={{ fontSize: 12, background: "#eee", borderRadius: 3, padding: "2px 6px", marginRight: 4 }}>
|
||||
{t}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{/* Tags — always shown, editable */}
|
||||
<TagEditor
|
||||
docId={doc.id}
|
||||
initialTags={tags}
|
||||
onSaved={() => qc.invalidateQueries({ queryKey: ["documents"] })}
|
||||
/>
|
||||
|
||||
{/* Extracted fields (excluding internal-only keys) */}
|
||||
{extractedData && (
|
||||
|
||||
Reference in New Issue
Block a user