fix(issues): address document review comments
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -59,7 +59,7 @@ export interface IssueDocumentSummary {
|
|||||||
key: string;
|
key: string;
|
||||||
title: string | null;
|
title: string | null;
|
||||||
format: DocumentFormat;
|
format: DocumentFormat;
|
||||||
latestRevisionId: string;
|
latestRevisionId: string | null;
|
||||||
latestRevisionNumber: number;
|
latestRevisionNumber: number;
|
||||||
createdByAgentId: string | null;
|
createdByAgentId: string | null;
|
||||||
createdByUserId: string | null;
|
createdByUserId: string | null;
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ function mapIssueDocumentRow(
|
|||||||
title: row.title,
|
title: row.title,
|
||||||
format: row.format,
|
format: row.format,
|
||||||
...(includeBody ? { body: row.latestBody } : {}),
|
...(includeBody ? { body: row.latestBody } : {}),
|
||||||
latestRevisionId: row.latestRevisionId ?? "",
|
latestRevisionId: row.latestRevisionId ?? null,
|
||||||
latestRevisionNumber: row.latestRevisionNumber,
|
latestRevisionNumber: row.latestRevisionNumber,
|
||||||
createdByAgentId: row.createdByAgentId,
|
createdByAgentId: row.createdByAgentId,
|
||||||
createdByUserId: row.createdByUserId,
|
createdByUserId: row.createdByUserId,
|
||||||
@@ -419,7 +419,7 @@ export function documentService(db: Db) {
|
|||||||
return {
|
return {
|
||||||
...existing,
|
...existing,
|
||||||
body: existing.latestBody,
|
body: existing.latestBody,
|
||||||
latestRevisionId: existing.latestRevisionId ?? "",
|
latestRevisionId: existing.latestRevisionId ?? null,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ import {
|
|||||||
assets,
|
assets,
|
||||||
companies,
|
companies,
|
||||||
companyMemberships,
|
companyMemberships,
|
||||||
|
documents,
|
||||||
goals,
|
goals,
|
||||||
heartbeatRuns,
|
heartbeatRuns,
|
||||||
issueAttachments,
|
issueAttachments,
|
||||||
issueLabels,
|
issueLabels,
|
||||||
issueComments,
|
issueComments,
|
||||||
|
issueDocuments,
|
||||||
issueReadStates,
|
issueReadStates,
|
||||||
issues,
|
issues,
|
||||||
labels,
|
labels,
|
||||||
@@ -790,6 +792,10 @@ export function issueService(db: Db) {
|
|||||||
.select({ assetId: issueAttachments.assetId })
|
.select({ assetId: issueAttachments.assetId })
|
||||||
.from(issueAttachments)
|
.from(issueAttachments)
|
||||||
.where(eq(issueAttachments.issueId, id));
|
.where(eq(issueAttachments.issueId, id));
|
||||||
|
const issueDocumentIds = await tx
|
||||||
|
.select({ documentId: issueDocuments.documentId })
|
||||||
|
.from(issueDocuments)
|
||||||
|
.where(eq(issueDocuments.issueId, id));
|
||||||
|
|
||||||
const removedIssue = await tx
|
const removedIssue = await tx
|
||||||
.delete(issues)
|
.delete(issues)
|
||||||
@@ -803,6 +809,12 @@ export function issueService(db: Db) {
|
|||||||
.where(inArray(assets.id, attachmentAssetIds.map((row) => row.assetId)));
|
.where(inArray(assets.id, attachmentAssetIds.map((row) => row.assetId)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (removedIssue && issueDocumentIds.length > 0) {
|
||||||
|
await tx
|
||||||
|
.delete(documents)
|
||||||
|
.where(inArray(documents.id, issueDocumentIds.map((row) => row.documentId)));
|
||||||
|
}
|
||||||
|
|
||||||
if (!removedIssue) return null;
|
if (!removedIssue) return null;
|
||||||
const [enriched] = await withIssueLabels(tx, [removedIssue]);
|
const [enriched] = await withIssueLabels(tx, [removedIssue]);
|
||||||
return enriched;
|
return enriched;
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ type DraftState = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const DOCUMENT_AUTOSAVE_DEBOUNCE_MS = 900;
|
const DOCUMENT_AUTOSAVE_DEBOUNCE_MS = 900;
|
||||||
|
const DOCUMENT_KEY_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
|
||||||
|
|
||||||
function renderBody(body: string, className?: string) {
|
function renderBody(body: string, className?: string) {
|
||||||
return <MarkdownBody className={className}>{body}</MarkdownBody>;
|
return <MarkdownBody className={className}>{body}</MarkdownBody>;
|
||||||
@@ -51,6 +52,7 @@ export function IssueDocumentsSection({
|
|||||||
const [confirmDeleteKey, setConfirmDeleteKey] = useState<string | null>(null);
|
const [confirmDeleteKey, setConfirmDeleteKey] = useState<string | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [draft, setDraft] = useState<DraftState | null>(null);
|
const [draft, setDraft] = useState<DraftState | null>(null);
|
||||||
|
const [autosaveDocumentKey, setAutosaveDocumentKey] = useState<string | null>(null);
|
||||||
const autosaveDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const autosaveDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const {
|
const {
|
||||||
state: autosaveState,
|
state: autosaveState,
|
||||||
@@ -100,9 +102,23 @@ export function IssueDocumentsSection({
|
|||||||
}, [documents]);
|
}, [documents]);
|
||||||
|
|
||||||
const hasRealPlan = sortedDocuments.some((doc) => doc.key === "plan");
|
const hasRealPlan = sortedDocuments.some((doc) => doc.key === "plan");
|
||||||
|
const newDocumentKeyError =
|
||||||
|
draft?.isNew && draft.key.trim().length > 0 && !DOCUMENT_KEY_PATTERN.test(draft.key.trim())
|
||||||
|
? "Use lowercase letters, numbers, -, or _, and start with a letter or number."
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const resetAutosaveState = useCallback(() => {
|
||||||
|
setAutosaveDocumentKey(null);
|
||||||
|
reset();
|
||||||
|
}, [reset]);
|
||||||
|
|
||||||
|
const markDocumentDirty = useCallback((key: string) => {
|
||||||
|
setAutosaveDocumentKey(key);
|
||||||
|
markDirty();
|
||||||
|
}, [markDirty]);
|
||||||
|
|
||||||
const beginNewDocument = () => {
|
const beginNewDocument = () => {
|
||||||
reset();
|
resetAutosaveState();
|
||||||
setDraft({
|
setDraft({
|
||||||
key: "",
|
key: "",
|
||||||
title: "",
|
title: "",
|
||||||
@@ -116,7 +132,7 @@ export function IssueDocumentsSection({
|
|||||||
const beginEdit = (key: string) => {
|
const beginEdit = (key: string) => {
|
||||||
const doc = sortedDocuments.find((entry) => entry.key === key);
|
const doc = sortedDocuments.find((entry) => entry.key === key);
|
||||||
if (!doc) return;
|
if (!doc) return;
|
||||||
reset();
|
resetAutosaveState();
|
||||||
setDraft({
|
setDraft({
|
||||||
key: doc.key,
|
key: doc.key,
|
||||||
title: doc.title ?? "",
|
title: doc.title ?? "",
|
||||||
@@ -131,7 +147,7 @@ export function IssueDocumentsSection({
|
|||||||
if (autosaveDebounceRef.current) {
|
if (autosaveDebounceRef.current) {
|
||||||
clearTimeout(autosaveDebounceRef.current);
|
clearTimeout(autosaveDebounceRef.current);
|
||||||
}
|
}
|
||||||
reset();
|
resetAutosaveState();
|
||||||
setDraft(null);
|
setDraft(null);
|
||||||
setError(null);
|
setError(null);
|
||||||
};
|
};
|
||||||
@@ -152,7 +168,15 @@ export function IssueDocumentsSection({
|
|||||||
setError("Document body cannot be empty");
|
setError("Document body cannot be empty");
|
||||||
}
|
}
|
||||||
if (options?.trackAutosave) {
|
if (options?.trackAutosave) {
|
||||||
reset();
|
resetAutosaveState();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!DOCUMENT_KEY_PATTERN.test(normalizedKey)) {
|
||||||
|
setError("Document key must start with a letter or number and use only lowercase letters, numbers, -, or _.");
|
||||||
|
if (options?.trackAutosave) {
|
||||||
|
resetAutosaveState();
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -168,7 +192,7 @@ export function IssueDocumentsSection({
|
|||||||
setDraft((value) => (value?.key === normalizedKey ? null : value));
|
setDraft((value) => (value?.key === normalizedKey ? null : value));
|
||||||
}
|
}
|
||||||
if (options?.trackAutosave) {
|
if (options?.trackAutosave) {
|
||||||
reset();
|
resetAutosaveState();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -197,6 +221,7 @@ export function IssueDocumentsSection({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (options?.trackAutosave) {
|
if (options?.trackAutosave) {
|
||||||
|
setAutosaveDocumentKey(normalizedKey);
|
||||||
await runSave(save);
|
await runSave(save);
|
||||||
} else {
|
} else {
|
||||||
await save();
|
await save();
|
||||||
@@ -206,7 +231,7 @@ export function IssueDocumentsSection({
|
|||||||
setError(err instanceof Error ? err.message : "Failed to save document");
|
setError(err instanceof Error ? err.message : "Failed to save document");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}, [invalidateIssueDocuments, reset, runSave, sortedDocuments, upsertDocument]);
|
}, [invalidateIssueDocuments, resetAutosaveState, runSave, sortedDocuments, upsertDocument]);
|
||||||
|
|
||||||
const handleDraftBlur = async (event: React.FocusEvent<HTMLDivElement>) => {
|
const handleDraftBlur = async (event: React.FocusEvent<HTMLDivElement>) => {
|
||||||
if (event.currentTarget.contains(event.relatedTarget as Node | null)) return;
|
if (event.currentTarget.contains(event.relatedTarget as Node | null)) return;
|
||||||
@@ -248,11 +273,11 @@ export function IssueDocumentsSection({
|
|||||||
(existing.title ?? "") !== draft.title;
|
(existing.title ?? "") !== draft.title;
|
||||||
if (!hasChanges) {
|
if (!hasChanges) {
|
||||||
if (autosaveState !== "saved") {
|
if (autosaveState !== "saved") {
|
||||||
reset();
|
resetAutosaveState();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
markDirty();
|
markDocumentDirty(draft.key);
|
||||||
if (autosaveDebounceRef.current) {
|
if (autosaveDebounceRef.current) {
|
||||||
clearTimeout(autosaveDebounceRef.current);
|
clearTimeout(autosaveDebounceRef.current);
|
||||||
}
|
}
|
||||||
@@ -265,7 +290,7 @@ export function IssueDocumentsSection({
|
|||||||
clearTimeout(autosaveDebounceRef.current);
|
clearTimeout(autosaveDebounceRef.current);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [autosaveState, commitDraft, draft, markDirty, reset, sortedDocuments]);
|
}, [autosaveState, commitDraft, draft, markDocumentDirty, resetAutosaveState, sortedDocuments]);
|
||||||
|
|
||||||
const documentBodyShellClassName = "mt-3 overflow-hidden rounded-md border border-border bg-background";
|
const documentBodyShellClassName = "mt-3 overflow-hidden rounded-md border border-border bg-background";
|
||||||
const documentBodyPaddingClassName = "px-3 py-3";
|
const documentBodyPaddingClassName = "px-3 py-3";
|
||||||
@@ -297,6 +322,9 @@ export function IssueDocumentsSection({
|
|||||||
}
|
}
|
||||||
placeholder="Document key"
|
placeholder="Document key"
|
||||||
/>
|
/>
|
||||||
|
{newDocumentKeyError && (
|
||||||
|
<p className="text-xs text-destructive">{newDocumentKeyError}</p>
|
||||||
|
)}
|
||||||
{!isPlanKey(draft.key) && (
|
{!isPlanKey(draft.key) && (
|
||||||
<Input
|
<Input
|
||||||
value={draft.title}
|
value={draft.title}
|
||||||
@@ -420,7 +448,7 @@ export function IssueDocumentsSection({
|
|||||||
<Input
|
<Input
|
||||||
value={activeDraft.title}
|
value={activeDraft.title}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
markDirty();
|
markDocumentDirty(doc.key);
|
||||||
setDraft((current) => current ? { ...current, title: event.target.value } : current);
|
setDraft((current) => current ? { ...current, title: event.target.value } : current);
|
||||||
}}
|
}}
|
||||||
placeholder="Optional title"
|
placeholder="Optional title"
|
||||||
@@ -434,7 +462,7 @@ export function IssueDocumentsSection({
|
|||||||
<MarkdownEditor
|
<MarkdownEditor
|
||||||
value={activeDraft?.body ?? doc.body}
|
value={activeDraft?.body ?? doc.body}
|
||||||
onChange={(body) => {
|
onChange={(body) => {
|
||||||
markDirty();
|
markDocumentDirty(doc.key);
|
||||||
setDraft((current) => {
|
setDraft((current) => {
|
||||||
if (current && current.key === doc.key && !current.isNew) {
|
if (current && current.key === doc.key && !current.isNew) {
|
||||||
return { ...current, body };
|
return { ...current, body };
|
||||||
@@ -463,7 +491,7 @@ export function IssueDocumentsSection({
|
|||||||
autosaveState === "error" ? "text-destructive" : "text-muted-foreground"
|
autosaveState === "error" ? "text-destructive" : "text-muted-foreground"
|
||||||
} ${activeDraft ? "opacity-100" : "opacity-0"}`}
|
} ${activeDraft ? "opacity-100" : "opacity-0"}`}
|
||||||
>
|
>
|
||||||
{activeDraft
|
{activeDraft && autosaveDocumentKey === doc.key
|
||||||
? autosaveState === "saving"
|
? autosaveState === "saving"
|
||||||
? "Autosaving..."
|
? "Autosaving..."
|
||||||
: autosaveState === "saved"
|
: autosaveState === "saved"
|
||||||
|
|||||||
Reference in New Issue
Block a user