mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 17:45:53 +01:00
fix(ui): restore sticky plan confirmation CTAs
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
99bd10cb3c
commit
1aaa9eb0c2
1 file changed
+68
-9
@@ -1,10 +1,13 @@
|
||||
import { useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { Issue } from "@paperclipai/shared";
|
||||
import type { Issue, IssueThreadInteraction } from "@paperclipai/shared";
|
||||
import { issuesApi } from "@/api/issues";
|
||||
import { queryKeys } from "@/lib/queryKeys";
|
||||
import { IssuePlanDecompositionsSection } from "@/components/IssuePlanDecompositionsSection";
|
||||
import { MarkdownBody } from "@/components/MarkdownBody";
|
||||
import { DocumentAnnotationsCountChip, IssueDocumentAnnotations } from "@/components/IssueDocumentAnnotations";
|
||||
import { useIssuePlanDocument } from "@/hooks/useIssuePlanDocument";
|
||||
import { useLocation } from "@/lib/router";
|
||||
import { IssuePlanConfirmationActionBar } from "./IssuePlanConfirmationActionBar";
|
||||
|
||||
interface IssuePropertiesPlansTabProps {
|
||||
@@ -14,6 +17,16 @@ interface IssuePropertiesPlansTabProps {
|
||||
inline?: boolean;
|
||||
}
|
||||
|
||||
function hasPendingPlanConfirmation(interactions: IssueThreadInteraction[] | undefined): boolean {
|
||||
return (interactions ?? []).some(
|
||||
(interaction) =>
|
||||
interaction.kind === "request_confirmation"
|
||||
&& interaction.status === "pending"
|
||||
&& interaction.payload.target?.type === "issue_document"
|
||||
&& interaction.payload.target.key === "plan",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Plans tab of the redesigned properties pane (flag: enableTaskChatRedesign).
|
||||
*
|
||||
@@ -25,19 +38,42 @@ interface IssuePropertiesPlansTabProps {
|
||||
*/
|
||||
export function IssuePropertiesPlansTab({ issue, inline }: IssuePropertiesPlansTabProps) {
|
||||
const { data: planDocument, isLoading: planDocumentLoading } = useIssuePlanDocument(issue.id);
|
||||
const location = useLocation();
|
||||
const [annotationPanelOpen, setAnnotationPanelOpen] = useState(false);
|
||||
const { data } = useQuery({
|
||||
queryKey: queryKeys.issues.acceptedPlanDecompositions(issue.id),
|
||||
queryFn: () => issuesApi.listAcceptedPlanDecompositions(issue.id),
|
||||
});
|
||||
const { data: interactions } = useQuery({
|
||||
queryKey: queryKeys.issues.interactions(issue.id),
|
||||
queryFn: () => issuesApi.listInteractions(issue.id),
|
||||
});
|
||||
const hasPlans = (data?.length ?? 0) > 0;
|
||||
const pendingPlanConfirmation = hasPendingPlanConfirmation(interactions);
|
||||
|
||||
if (!planDocument && !hasPlans) {
|
||||
return (
|
||||
<div className="px-1 py-6 text-sm text-muted-foreground">
|
||||
{planDocumentLoading
|
||||
? "Loading plan…"
|
||||
: "No plan yet. The plan document, accepted plans, and their revisions will appear here."}
|
||||
</div>
|
||||
<>
|
||||
{/* This is deliberately outside the plan-document gate: an interaction
|
||||
can arrive before its plan document query resolves or persists. */}
|
||||
<IssuePlanConfirmationActionBar issue={issue} inline={inline} />
|
||||
<div className="px-1 py-6 text-sm text-muted-foreground">
|
||||
{planDocumentLoading ? (
|
||||
"Loading plan…"
|
||||
) : issue.workMode === "planning" ? (
|
||||
<div className="space-y-2">
|
||||
<p>This task is in plan mode but no plan document has been written yet.</p>
|
||||
{pendingPlanConfirmation ? (
|
||||
<p className="text-amber-foreground">
|
||||
A plan confirmation is pending, but the plan document it should confirm is missing.
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
"No plan yet. The plan document, accepted plans, and their revisions will appear here."
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -45,18 +81,41 @@ export function IssuePropertiesPlansTab({ issue, inline }: IssuePropertiesPlansT
|
||||
<div className="space-y-4 py-2">
|
||||
{/* Pending plan confirmation: its CTAs pin to the pane's footer slot so
|
||||
they stay visible while the plan scrolls. */}
|
||||
{planDocument ? <IssuePlanConfirmationActionBar issue={issue} inline={inline} /> : null}
|
||||
<IssuePlanConfirmationActionBar issue={issue} inline={inline} />
|
||||
{planDocument ? (
|
||||
<section data-testid="issue-plan-document" className="space-y-2">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
{`Revision ${planDocument.latestRevisionNumber ?? 1} · updated ${new Date(planDocument.updatedAt).toLocaleString([], {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
})}`}
|
||||
<DocumentAnnotationsCountChip
|
||||
issueId={issue.id}
|
||||
docKey="plan"
|
||||
panelOpen={annotationPanelOpen}
|
||||
onToggle={() => setAnnotationPanelOpen((open) => !open)}
|
||||
/>
|
||||
</div>
|
||||
<MarkdownBody>{planDocument.body}</MarkdownBody>
|
||||
<IssueDocumentAnnotations
|
||||
issueId={issue.id}
|
||||
doc={{
|
||||
key: "plan",
|
||||
latestRevisionId: planDocument.latestRevisionId,
|
||||
latestRevisionNumber: planDocument.latestRevisionNumber,
|
||||
}}
|
||||
bodyMarkdown={planDocument.body}
|
||||
draftDirty={false}
|
||||
draftConflicted={false}
|
||||
historicalPreview={false}
|
||||
locationHash={location.hash}
|
||||
panelOpen={annotationPanelOpen}
|
||||
onPanelOpenChange={setAnnotationPanelOpen}
|
||||
panelPlacement="inline"
|
||||
>
|
||||
<MarkdownBody>{planDocument.body}</MarkdownBody>
|
||||
</IssueDocumentAnnotations>
|
||||
</section>
|
||||
) : null}
|
||||
{hasPlans ? (
|
||||
|
||||
Reference in new issue
Block a user