ui: smooth new issue submit state
This commit is contained in:
@@ -34,6 +34,7 @@ import {
|
|||||||
Tag,
|
Tag,
|
||||||
Calendar,
|
Calendar,
|
||||||
Paperclip,
|
Paperclip,
|
||||||
|
Loader2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { cn } from "../lib/utils";
|
import { cn } from "../lib/utils";
|
||||||
import { extractProviderIdWithFallback } from "../lib/model-utils";
|
import { extractProviderIdWithFallback } from "../lib/model-utils";
|
||||||
@@ -420,7 +421,7 @@ export function NewIssueDialog() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
if (!effectiveCompanyId || !title.trim()) return;
|
if (!effectiveCompanyId || !title.trim() || createIssue.isPending) return;
|
||||||
const assigneeAdapterOverrides = buildAssigneeAdapterOverrides({
|
const assigneeAdapterOverrides = buildAssigneeAdapterOverrides({
|
||||||
adapterType: assigneeAdapterType,
|
adapterType: assigneeAdapterType,
|
||||||
modelOverride: assigneeModelOverride,
|
modelOverride: assigneeModelOverride,
|
||||||
@@ -516,6 +517,11 @@ export function NewIssueDialog() {
|
|||||||
})),
|
})),
|
||||||
[orderedProjects],
|
[orderedProjects],
|
||||||
);
|
);
|
||||||
|
const savedDraft = loadDraft();
|
||||||
|
const hasSavedDraft = Boolean(savedDraft?.title.trim() || savedDraft?.description.trim());
|
||||||
|
const canDiscardDraft = hasDraft || hasSavedDraft;
|
||||||
|
const createIssueErrorMessage =
|
||||||
|
createIssue.error instanceof Error ? createIssue.error.message : "Failed to create issue. Try again.";
|
||||||
|
|
||||||
const handleProjectChange = useCallback((nextProjectId: string) => {
|
const handleProjectChange = useCallback((nextProjectId: string) => {
|
||||||
setProjectId(nextProjectId);
|
setProjectId(nextProjectId);
|
||||||
@@ -563,7 +569,7 @@ export function NewIssueDialog() {
|
|||||||
<Dialog
|
<Dialog
|
||||||
open={newIssueOpen}
|
open={newIssueOpen}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (!open) closeNewIssue();
|
if (!open && !createIssue.isPending) closeNewIssue();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
@@ -576,7 +582,16 @@ export function NewIssueDialog() {
|
|||||||
: "sm:max-w-lg"
|
: "sm:max-w-lg"
|
||||||
)}
|
)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
|
onEscapeKeyDown={(event) => {
|
||||||
|
if (createIssue.isPending) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}}
|
||||||
onPointerDownOutside={(event) => {
|
onPointerDownOutside={(event) => {
|
||||||
|
if (createIssue.isPending) {
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Radix Dialog's modal DismissableLayer calls preventDefault() on
|
// Radix Dialog's modal DismissableLayer calls preventDefault() on
|
||||||
// pointerdown events that originate outside the Dialog DOM tree.
|
// pointerdown events that originate outside the Dialog DOM tree.
|
||||||
// Popover portals render at the body level (outside the Dialog), so
|
// Popover portals render at the body level (outside the Dialog), so
|
||||||
@@ -654,6 +669,7 @@ export function NewIssueDialog() {
|
|||||||
size="icon-xs"
|
size="icon-xs"
|
||||||
className="text-muted-foreground"
|
className="text-muted-foreground"
|
||||||
onClick={() => setExpanded(!expanded)}
|
onClick={() => setExpanded(!expanded)}
|
||||||
|
disabled={createIssue.isPending}
|
||||||
>
|
>
|
||||||
{expanded ? <Minimize2 className="h-3.5 w-3.5" /> : <Maximize2 className="h-3.5 w-3.5" />}
|
{expanded ? <Minimize2 className="h-3.5 w-3.5" /> : <Maximize2 className="h-3.5 w-3.5" />}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -662,6 +678,7 @@ export function NewIssueDialog() {
|
|||||||
size="icon-xs"
|
size="icon-xs"
|
||||||
className="text-muted-foreground"
|
className="text-muted-foreground"
|
||||||
onClick={() => closeNewIssue()}
|
onClick={() => closeNewIssue()}
|
||||||
|
disabled={createIssue.isPending}
|
||||||
>
|
>
|
||||||
<span className="text-lg leading-none">×</span>
|
<span className="text-lg leading-none">×</span>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -680,6 +697,7 @@ export function NewIssueDialog() {
|
|||||||
e.target.style.height = "auto";
|
e.target.style.height = "auto";
|
||||||
e.target.style.height = `${e.target.scrollHeight}px`;
|
e.target.style.height = `${e.target.scrollHeight}px`;
|
||||||
}}
|
}}
|
||||||
|
readOnly={createIssue.isPending}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter" && !e.metaKey && !e.ctrlKey) {
|
if (e.key === "Enter" && !e.metaKey && !e.ctrlKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -998,18 +1016,37 @@ export function NewIssueDialog() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
className="text-muted-foreground"
|
className="text-muted-foreground"
|
||||||
onClick={discardDraft}
|
onClick={discardDraft}
|
||||||
disabled={!hasDraft && !loadDraft()}
|
disabled={createIssue.isPending || !canDiscardDraft}
|
||||||
>
|
>
|
||||||
Discard Draft
|
Discard Draft
|
||||||
</Button>
|
</Button>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="min-h-5 text-right">
|
||||||
|
{createIssue.isPending ? (
|
||||||
|
<span className="inline-flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
|
||||||
|
<Loader2 className="h-3 w-3 animate-spin" />
|
||||||
|
Creating issue...
|
||||||
|
</span>
|
||||||
|
) : createIssue.isError ? (
|
||||||
|
<span className="text-xs text-destructive">{createIssueErrorMessage}</span>
|
||||||
|
) : canDiscardDraft ? (
|
||||||
|
<span className="text-xs text-muted-foreground">Draft autosaves locally</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
|
className="min-w-[8.5rem] disabled:opacity-100"
|
||||||
disabled={!title.trim() || createIssue.isPending}
|
disabled={!title.trim() || createIssue.isPending}
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
|
aria-busy={createIssue.isPending}
|
||||||
>
|
>
|
||||||
{createIssue.isPending ? "Creating..." : "Create Issue"}
|
<span className="inline-flex items-center justify-center gap-1.5">
|
||||||
|
{createIssue.isPending ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : null}
|
||||||
|
<span>{createIssue.isPending ? "Creating..." : "Create Issue"}</span>
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user