Fix routine modal scrolling
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
9684e7bf30
commit
db4e146551
1 changed files with 186 additions and 181 deletions
|
|
@ -251,8 +251,11 @@ export function Routines() {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogContent showCloseButton={false} className="max-w-3xl gap-0 overflow-hidden p-0">
|
<DialogContent
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-border/60 px-5 py-3">
|
showCloseButton={false}
|
||||||
|
className="flex max-h-[calc(100dvh-2rem)] max-w-3xl flex-col gap-0 overflow-hidden p-0"
|
||||||
|
>
|
||||||
|
<div className="shrink-0 flex flex-wrap items-center justify-between gap-3 border-b border-border/60 px-5 py-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-medium uppercase tracking-[0.2em] text-muted-foreground">New routine</p>
|
<p className="text-xs font-medium uppercase tracking-[0.2em] text-muted-foreground">New routine</p>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
|
|
@ -272,197 +275,199 @@ export function Routines() {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="px-5 pt-5 pb-3">
|
<div className="min-h-0 flex-1 overflow-y-auto">
|
||||||
<textarea
|
<div className="px-5 pt-5 pb-3">
|
||||||
ref={titleInputRef}
|
<textarea
|
||||||
className="w-full resize-none overflow-hidden bg-transparent text-xl font-semibold outline-none placeholder:text-muted-foreground/50"
|
ref={titleInputRef}
|
||||||
placeholder="Routine title"
|
className="w-full resize-none overflow-hidden bg-transparent text-xl font-semibold outline-none placeholder:text-muted-foreground/50"
|
||||||
rows={1}
|
placeholder="Routine title"
|
||||||
value={draft.title}
|
rows={1}
|
||||||
onChange={(event) => {
|
value={draft.title}
|
||||||
setDraft((current) => ({ ...current, title: event.target.value }));
|
onChange={(event) => {
|
||||||
autoResizeTextarea(event.target);
|
setDraft((current) => ({ ...current, title: event.target.value }));
|
||||||
}}
|
autoResizeTextarea(event.target);
|
||||||
onKeyDown={(event) => {
|
}}
|
||||||
if (event.key === "Enter" && !event.metaKey && !event.ctrlKey && !event.nativeEvent.isComposing) {
|
onKeyDown={(event) => {
|
||||||
event.preventDefault();
|
if (event.key === "Enter" && !event.metaKey && !event.ctrlKey && !event.nativeEvent.isComposing) {
|
||||||
descriptionEditorRef.current?.focus();
|
event.preventDefault();
|
||||||
return;
|
descriptionEditorRef.current?.focus();
|
||||||
}
|
return;
|
||||||
if (event.key === "Tab" && !event.shiftKey) {
|
|
||||||
event.preventDefault();
|
|
||||||
if (draft.assigneeAgentId) {
|
|
||||||
if (draft.projectId) {
|
|
||||||
descriptionEditorRef.current?.focus();
|
|
||||||
} else {
|
|
||||||
projectSelectorRef.current?.focus();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
assigneeSelectorRef.current?.focus();
|
|
||||||
}
|
}
|
||||||
}
|
if (event.key === "Tab" && !event.shiftKey) {
|
||||||
}}
|
event.preventDefault();
|
||||||
autoFocus
|
if (draft.assigneeAgentId) {
|
||||||
/>
|
if (draft.projectId) {
|
||||||
</div>
|
descriptionEditorRef.current?.focus();
|
||||||
|
} else {
|
||||||
<div className="px-5 pb-3">
|
projectSelectorRef.current?.focus();
|
||||||
<div className="overflow-x-auto overscroll-x-contain">
|
}
|
||||||
<div className="inline-flex min-w-full flex-wrap items-center gap-2 text-sm text-muted-foreground sm:min-w-max sm:flex-nowrap">
|
|
||||||
<span>For</span>
|
|
||||||
<InlineEntitySelector
|
|
||||||
ref={assigneeSelectorRef}
|
|
||||||
value={draft.assigneeAgentId}
|
|
||||||
options={assigneeOptions}
|
|
||||||
placeholder="Assignee"
|
|
||||||
noneLabel="No assignee"
|
|
||||||
searchPlaceholder="Search assignees..."
|
|
||||||
emptyMessage="No assignees found."
|
|
||||||
onChange={(assigneeAgentId) => {
|
|
||||||
if (assigneeAgentId) trackRecentAssignee(assigneeAgentId);
|
|
||||||
setDraft((current) => ({ ...current, assigneeAgentId }));
|
|
||||||
}}
|
|
||||||
onConfirm={() => {
|
|
||||||
if (draft.projectId) {
|
|
||||||
descriptionEditorRef.current?.focus();
|
|
||||||
} else {
|
} else {
|
||||||
projectSelectorRef.current?.focus();
|
assigneeSelectorRef.current?.focus();
|
||||||
}
|
}
|
||||||
}}
|
}
|
||||||
renderTriggerValue={(option) =>
|
}}
|
||||||
option ? (
|
autoFocus
|
||||||
currentAssignee ? (
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-5 pb-3">
|
||||||
|
<div className="overflow-x-auto overscroll-x-contain">
|
||||||
|
<div className="inline-flex min-w-full flex-wrap items-center gap-2 text-sm text-muted-foreground sm:min-w-max sm:flex-nowrap">
|
||||||
|
<span>For</span>
|
||||||
|
<InlineEntitySelector
|
||||||
|
ref={assigneeSelectorRef}
|
||||||
|
value={draft.assigneeAgentId}
|
||||||
|
options={assigneeOptions}
|
||||||
|
placeholder="Assignee"
|
||||||
|
noneLabel="No assignee"
|
||||||
|
searchPlaceholder="Search assignees..."
|
||||||
|
emptyMessage="No assignees found."
|
||||||
|
onChange={(assigneeAgentId) => {
|
||||||
|
if (assigneeAgentId) trackRecentAssignee(assigneeAgentId);
|
||||||
|
setDraft((current) => ({ ...current, assigneeAgentId }));
|
||||||
|
}}
|
||||||
|
onConfirm={() => {
|
||||||
|
if (draft.projectId) {
|
||||||
|
descriptionEditorRef.current?.focus();
|
||||||
|
} else {
|
||||||
|
projectSelectorRef.current?.focus();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
renderTriggerValue={(option) =>
|
||||||
|
option ? (
|
||||||
|
currentAssignee ? (
|
||||||
|
<>
|
||||||
|
<AgentIcon icon={currentAssignee.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||||
|
<span className="truncate">{option.label}</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span className="truncate">{option.label}</span>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground">Assignee</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
renderOption={(option) => {
|
||||||
|
if (!option.id) return <span className="truncate">{option.label}</span>;
|
||||||
|
const assignee = agentById.get(option.id);
|
||||||
|
return (
|
||||||
<>
|
<>
|
||||||
<AgentIcon icon={currentAssignee.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
{assignee ? <AgentIcon icon={assignee.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" /> : null}
|
||||||
|
<span className="truncate">{option.label}</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span>in</span>
|
||||||
|
<InlineEntitySelector
|
||||||
|
ref={projectSelectorRef}
|
||||||
|
value={draft.projectId}
|
||||||
|
options={projectOptions}
|
||||||
|
placeholder="Project"
|
||||||
|
noneLabel="No project"
|
||||||
|
searchPlaceholder="Search projects..."
|
||||||
|
emptyMessage="No projects found."
|
||||||
|
onChange={(projectId) => setDraft((current) => ({ ...current, projectId }))}
|
||||||
|
onConfirm={() => descriptionEditorRef.current?.focus()}
|
||||||
|
renderTriggerValue={(option) =>
|
||||||
|
option && currentProject ? (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className="h-3.5 w-3.5 shrink-0 rounded-sm"
|
||||||
|
style={{ backgroundColor: currentProject.color ?? "#64748b" }}
|
||||||
|
/>
|
||||||
<span className="truncate">{option.label}</span>
|
<span className="truncate">{option.label}</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<span className="truncate">{option.label}</span>
|
<span className="text-muted-foreground">Project</span>
|
||||||
)
|
)
|
||||||
) : (
|
}
|
||||||
<span className="text-muted-foreground">Assignee</span>
|
renderOption={(option) => {
|
||||||
)
|
if (!option.id) return <span className="truncate">{option.label}</span>;
|
||||||
}
|
const project = projectById.get(option.id);
|
||||||
renderOption={(option) => {
|
return (
|
||||||
if (!option.id) return <span className="truncate">{option.label}</span>;
|
<>
|
||||||
const assignee = agentById.get(option.id);
|
<span
|
||||||
return (
|
className="h-3.5 w-3.5 shrink-0 rounded-sm"
|
||||||
<>
|
style={{ backgroundColor: project?.color ?? "#64748b" }}
|
||||||
{assignee ? <AgentIcon icon={assignee.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" /> : null}
|
/>
|
||||||
<span className="truncate">{option.label}</span>
|
<span className="truncate">{option.label}</span>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span>in</span>
|
</div>
|
||||||
<InlineEntitySelector
|
|
||||||
ref={projectSelectorRef}
|
|
||||||
value={draft.projectId}
|
|
||||||
options={projectOptions}
|
|
||||||
placeholder="Project"
|
|
||||||
noneLabel="No project"
|
|
||||||
searchPlaceholder="Search projects..."
|
|
||||||
emptyMessage="No projects found."
|
|
||||||
onChange={(projectId) => setDraft((current) => ({ ...current, projectId }))}
|
|
||||||
onConfirm={() => descriptionEditorRef.current?.focus()}
|
|
||||||
renderTriggerValue={(option) =>
|
|
||||||
option && currentProject ? (
|
|
||||||
<>
|
|
||||||
<span
|
|
||||||
className="h-3.5 w-3.5 shrink-0 rounded-sm"
|
|
||||||
style={{ backgroundColor: currentProject.color ?? "#64748b" }}
|
|
||||||
/>
|
|
||||||
<span className="truncate">{option.label}</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<span className="text-muted-foreground">Project</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
renderOption={(option) => {
|
|
||||||
if (!option.id) return <span className="truncate">{option.label}</span>;
|
|
||||||
const project = projectById.get(option.id);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<span
|
|
||||||
className="h-3.5 w-3.5 shrink-0 rounded-sm"
|
|
||||||
style={{ backgroundColor: project?.color ?? "#64748b" }}
|
|
||||||
/>
|
|
||||||
<span className="truncate">{option.label}</span>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-border/60 px-5 py-4">
|
||||||
|
<MarkdownEditor
|
||||||
|
ref={descriptionEditorRef}
|
||||||
|
value={draft.description}
|
||||||
|
onChange={(description) => setDraft((current) => ({ ...current, description }))}
|
||||||
|
placeholder="Add instructions..."
|
||||||
|
bordered={false}
|
||||||
|
contentClassName="min-h-[160px] text-sm text-muted-foreground"
|
||||||
|
onSubmit={() => {
|
||||||
|
if (!createRoutine.isPending && draft.title.trim() && draft.projectId && draft.assigneeAgentId) {
|
||||||
|
createRoutine.mutate();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-border/60 px-5 py-3">
|
||||||
|
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
|
||||||
|
<CollapsibleTrigger className="flex w-full items-center justify-between text-left">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium">Advanced delivery settings</p>
|
||||||
|
<p className="text-sm text-muted-foreground">Keep policy controls secondary to the work definition.</p>
|
||||||
|
</div>
|
||||||
|
{advancedOpen ? <ChevronDown className="h-4 w-4 text-muted-foreground" /> : <ChevronRight className="h-4 w-4 text-muted-foreground" />}
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent className="pt-3">
|
||||||
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-xs font-medium uppercase tracking-[0.18em] text-muted-foreground">Concurrency</p>
|
||||||
|
<Select
|
||||||
|
value={draft.concurrencyPolicy}
|
||||||
|
onValueChange={(concurrencyPolicy) => setDraft((current) => ({ ...current, concurrencyPolicy }))}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{concurrencyPolicies.map((value) => (
|
||||||
|
<SelectItem key={value} value={value}>{value.replaceAll("_", " ")}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-xs text-muted-foreground">{concurrencyPolicyDescriptions[draft.concurrencyPolicy]}</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-xs font-medium uppercase tracking-[0.18em] text-muted-foreground">Catch-up</p>
|
||||||
|
<Select
|
||||||
|
value={draft.catchUpPolicy}
|
||||||
|
onValueChange={(catchUpPolicy) => setDraft((current) => ({ ...current, catchUpPolicy }))}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{catchUpPolicies.map((value) => (
|
||||||
|
<SelectItem key={value} value={value}>{value.replaceAll("_", " ")}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-xs text-muted-foreground">{catchUpPolicyDescriptions[draft.catchUpPolicy]}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border-t border-border/60 px-5 py-4">
|
<div className="shrink-0 flex flex-col gap-3 border-t border-border/60 px-5 py-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<MarkdownEditor
|
|
||||||
ref={descriptionEditorRef}
|
|
||||||
value={draft.description}
|
|
||||||
onChange={(description) => setDraft((current) => ({ ...current, description }))}
|
|
||||||
placeholder="Add instructions..."
|
|
||||||
bordered={false}
|
|
||||||
contentClassName="min-h-[160px] text-sm text-muted-foreground"
|
|
||||||
onSubmit={() => {
|
|
||||||
if (!createRoutine.isPending && draft.title.trim() && draft.projectId && draft.assigneeAgentId) {
|
|
||||||
createRoutine.mutate();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border-t border-border/60 px-5 py-3">
|
|
||||||
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
|
|
||||||
<CollapsibleTrigger className="flex w-full items-center justify-between text-left">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium">Advanced delivery settings</p>
|
|
||||||
<p className="text-sm text-muted-foreground">Keep policy controls secondary to the work definition.</p>
|
|
||||||
</div>
|
|
||||||
{advancedOpen ? <ChevronDown className="h-4 w-4 text-muted-foreground" /> : <ChevronRight className="h-4 w-4 text-muted-foreground" />}
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
<CollapsibleContent className="pt-3">
|
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-xs font-medium uppercase tracking-[0.18em] text-muted-foreground">Concurrency</p>
|
|
||||||
<Select
|
|
||||||
value={draft.concurrencyPolicy}
|
|
||||||
onValueChange={(concurrencyPolicy) => setDraft((current) => ({ ...current, concurrencyPolicy }))}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{concurrencyPolicies.map((value) => (
|
|
||||||
<SelectItem key={value} value={value}>{value.replaceAll("_", " ")}</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<p className="text-xs text-muted-foreground">{concurrencyPolicyDescriptions[draft.concurrencyPolicy]}</p>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-xs font-medium uppercase tracking-[0.18em] text-muted-foreground">Catch-up</p>
|
|
||||||
<Select
|
|
||||||
value={draft.catchUpPolicy}
|
|
||||||
onValueChange={(catchUpPolicy) => setDraft((current) => ({ ...current, catchUpPolicy }))}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{catchUpPolicies.map((value) => (
|
|
||||||
<SelectItem key={value} value={value}>{value.replaceAll("_", " ")}</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<p className="text-xs text-muted-foreground">{catchUpPolicyDescriptions[draft.catchUpPolicy]}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CollapsibleContent>
|
|
||||||
</Collapsible>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-3 border-t border-border/60 px-5 py-4 sm:flex-row sm:items-center sm:justify-between">
|
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
After creation, Paperclip takes you straight to trigger setup for schedules, webhooks, or internal runs.
|
After creation, Paperclip takes you straight to trigger setup for schedules, webhooks, or internal runs.
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue