feat(ui): deep link issue documents

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta
2026-03-14 06:48:43 -05:00
parent 07d13e1738
commit 1e3a485408
2 changed files with 54 additions and 5 deletions

View File

@@ -154,6 +154,7 @@ When posting issue comments, use concise markdown with:
- Issues: `/<prefix>/issues/<issue-identifier>` (e.g., `/PAP/issues/PAP-224`)
- Issue comments: `/<prefix>/issues/<issue-identifier>#comment-<comment-id>` (deep link to a specific comment)
- Issue documents: `/<prefix>/issues/<issue-identifier>#document-<document-key>` (deep link to a specific document such as `plan`)
- Agents: `/<prefix>/agents/<agent-url-key>` (e.g., `/PAP/agents/claudecoder`)
- Projects: `/<prefix>/projects/<project-url-key>` (id fallback allowed)
- Approvals: `/<prefix>/approvals/<approval-id>`
@@ -177,6 +178,13 @@ Submitted CTO hire request and linked it for board review.
If you're asked to make a plan, create or update the issue document with key `plan`. Do not append plans into the issue description anymore. If you're asked for plan revisions, update that same `plan` document. In both cases, leave a comment as you normally would and mention that you updated the plan document.
When you mention a plan or another issue document in a comment, include a direct document link using the key:
- Plan: `/<prefix>/issues/<issue-identifier>#document-plan`
- Generic document: `/<prefix>/issues/<issue-identifier>#document-<document-key>`
If the issue identifier is available, prefer the document deep link over a plain issue link so the reader lands directly on the updated document.
If you're asked to make a plan, _do not mark the issue as done_. Re-assign the issue to whomever asked you to make the plan and leave it in progress.
Recommended API flow:

View File

@@ -1,10 +1,11 @@
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { Issue } from "@paperclipai/shared";
import { useLocation } from "@/lib/router";
import { issuesApi } from "../api/issues";
import { useAutosaveIndicator } from "../hooks/useAutosaveIndicator";
import { queryKeys } from "../lib/queryKeys";
import { relativeTime } from "../lib/utils";
import { cn, relativeTime } from "../lib/utils";
import { MarkdownBody } from "./MarkdownBody";
import { MarkdownEditor, type MentionOption } from "./MarkdownEditor";
import { Button } from "@/components/ui/button";
@@ -73,12 +74,15 @@ export function IssueDocumentsSection({
extraActions?: ReactNode;
}) {
const queryClient = useQueryClient();
const location = useLocation();
const [confirmDeleteKey, setConfirmDeleteKey] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [draft, setDraft] = useState<DraftState | null>(null);
const [foldedDocumentKeys, setFoldedDocumentKeys] = useState<string[]>(() => loadFoldedDocumentKeys(issue.id));
const [autosaveDocumentKey, setAutosaveDocumentKey] = useState<string | null>(null);
const [highlightDocumentKey, setHighlightDocumentKey] = useState<string | null>(null);
const autosaveDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const hasScrolledToHashRef = useRef(false);
const {
state: autosaveState,
markDirty,
@@ -287,6 +291,10 @@ export function IssueDocumentsSection({
setFoldedDocumentKeys(loadFoldedDocumentKeys(issue.id));
}, [issue.id]);
useEffect(() => {
hasScrolledToHashRef.current = false;
}, [issue.id, location.hash]);
useEffect(() => {
const validKeys = new Set(sortedDocuments.map((doc) => doc.key));
setFoldedDocumentKeys((current) => {
@@ -302,6 +310,23 @@ export function IssueDocumentsSection({
saveFoldedDocumentKeys(issue.id, foldedDocumentKeys);
}, [foldedDocumentKeys, issue.id]);
useEffect(() => {
const hash = location.hash;
if (!hash.startsWith("#document-")) return;
const documentKey = decodeURIComponent(hash.slice("#document-".length));
const targetExists = sortedDocuments.some((doc) => doc.key === documentKey)
|| (documentKey === "plan" && Boolean(issue.legacyPlanDocument));
if (!targetExists || hasScrolledToHashRef.current) return;
setFoldedDocumentKeys((current) => current.filter((key) => key !== documentKey));
const element = document.getElementById(`document-${documentKey}`);
if (!element) return;
hasScrolledToHashRef.current = true;
setHighlightDocumentKey(documentKey);
element.scrollIntoView({ behavior: "smooth", block: "center" });
const timer = setTimeout(() => setHighlightDocumentKey((current) => current === documentKey ? null : current), 3000);
return () => clearTimeout(timer);
}, [issue.legacyPlanDocument, location.hash, sortedDocuments]);
useEffect(() => {
return () => {
if (autosaveDebounceRef.current) {
@@ -430,7 +455,13 @@ export function IssueDocumentsSection({
)}
{!hasRealPlan && issue.legacyPlanDocument ? (
<div className="rounded-lg border border-amber-500/30 bg-amber-500/5 p-3">
<div
id="document-plan"
className={cn(
"rounded-lg border border-amber-500/30 bg-amber-500/5 p-3 transition-colors duration-1000",
highlightDocumentKey === "plan" && "border-primary/50 bg-primary/5",
)}
>
<div className="mb-2 flex items-center gap-2">
<FileText className="h-4 w-4 text-amber-600" />
<span className="rounded-full border border-amber-500/30 px-2 py-0.5 font-mono text-[10px] uppercase tracking-[0.16em] text-amber-700 dark:text-amber-300">
@@ -450,7 +481,14 @@ export function IssueDocumentsSection({
const showTitle = !isPlanKey(doc.key) && !!doc.title?.trim() && !titlesMatchKey(doc.title, doc.key);
return (
<div key={doc.id} className="rounded-lg border border-border p-3">
<div
key={doc.id}
id={`document-${doc.key}`}
className={cn(
"rounded-lg border border-border p-3 transition-colors duration-1000",
highlightDocumentKey === doc.key && "border-primary/50 bg-primary/5",
)}
>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="flex items-center gap-2">
@@ -466,9 +504,12 @@ export function IssueDocumentsSection({
<span className="rounded-full border border-border px-2 py-0.5 font-mono text-[10px] uppercase tracking-[0.16em] text-muted-foreground">
{doc.key}
</span>
<span className="text-[11px] text-muted-foreground">
<a
href={`#document-${encodeURIComponent(doc.key)}`}
className="text-[11px] text-muted-foreground transition-colors hover:text-foreground hover:underline"
>
rev {doc.latestRevisionNumber} updated {relativeTime(doc.updatedAt)}
</span>
</a>
</div>
{showTitle && <p className="mt-2 text-sm font-medium">{doc.title}</p>}
</div>