mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-19 04:00:38 +09:00
feat(routines): add workspace-aware routine runs
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
36376968af
commit
909e8cd4c8
38 changed files with 15468 additions and 250 deletions
|
|
@ -3,6 +3,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|||
import { useNavigate } from "@/lib/router";
|
||||
import { ChevronDown, ChevronRight, MoreHorizontal, Play, Plus, Repeat } from "lucide-react";
|
||||
import { routinesApi } from "../api/routines";
|
||||
import { instanceSettingsApi } from "../api/instanceSettings";
|
||||
import { agentsApi } from "../api/agents";
|
||||
import { projectsApi } from "../api/projects";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
|
|
@ -15,6 +16,12 @@ import { PageSkeleton } from "../components/PageSkeleton";
|
|||
import { AgentIcon } from "../components/AgentIconPicker";
|
||||
import { InlineEntitySelector, type InlineEntityOption } from "../components/InlineEntitySelector";
|
||||
import { MarkdownEditor, type MarkdownEditorRef } from "../components/MarkdownEditor";
|
||||
import {
|
||||
RoutineRunVariablesDialog,
|
||||
routineRunNeedsConfiguration,
|
||||
type RoutineRunDialogSubmitData,
|
||||
} from "../components/RoutineRunVariablesDialog";
|
||||
import { RoutineVariablesEditor, RoutineVariablesHint } from "../components/RoutineVariablesEditor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
|
|
@ -33,6 +40,7 @@ import {
|
|||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import type { RoutineListItem, RoutineVariable } from "@paperclipai/shared";
|
||||
|
||||
const concurrencyPolicies = ["coalesce_if_active", "always_enqueue", "skip_if_active"];
|
||||
const catchUpPolicies = ["skip_missed", "enqueue_missed_with_cap"];
|
||||
|
|
@ -74,9 +82,19 @@ export function Routines() {
|
|||
const projectSelectorRef = useRef<HTMLButtonElement | null>(null);
|
||||
const [runningRoutineId, setRunningRoutineId] = useState<string | null>(null);
|
||||
const [statusMutationRoutineId, setStatusMutationRoutineId] = useState<string | null>(null);
|
||||
const [runDialogRoutine, setRunDialogRoutine] = useState<RoutineListItem | null>(null);
|
||||
const [composerOpen, setComposerOpen] = useState(false);
|
||||
const [advancedOpen, setAdvancedOpen] = useState(false);
|
||||
const [draft, setDraft] = useState({
|
||||
const [draft, setDraft] = useState<{
|
||||
title: string;
|
||||
description: string;
|
||||
projectId: string;
|
||||
assigneeAgentId: string;
|
||||
priority: string;
|
||||
concurrencyPolicy: string;
|
||||
catchUpPolicy: string;
|
||||
variables: RoutineVariable[];
|
||||
}>({
|
||||
title: "",
|
||||
description: "",
|
||||
projectId: "",
|
||||
|
|
@ -84,6 +102,7 @@ export function Routines() {
|
|||
priority: "medium",
|
||||
concurrencyPolicy: "coalesce_if_active",
|
||||
catchUpPolicy: "skip_missed",
|
||||
variables: [],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -105,6 +124,11 @@ export function Routines() {
|
|||
queryFn: () => projectsApi.list(selectedCompanyId!),
|
||||
enabled: !!selectedCompanyId,
|
||||
});
|
||||
const { data: experimentalSettings } = useQuery({
|
||||
queryKey: queryKeys.instance.experimentalSettings,
|
||||
queryFn: () => instanceSettingsApi.getExperimental(),
|
||||
retry: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
autoResizeTextarea(titleInputRef.current);
|
||||
|
|
@ -125,6 +149,7 @@ export function Routines() {
|
|||
priority: "medium",
|
||||
concurrencyPolicy: "coalesce_if_active",
|
||||
catchUpPolicy: "skip_missed",
|
||||
variables: [],
|
||||
});
|
||||
setComposerOpen(false);
|
||||
setAdvancedOpen(false);
|
||||
|
|
@ -162,11 +187,21 @@ export function Routines() {
|
|||
});
|
||||
|
||||
const runRoutine = useMutation({
|
||||
mutationFn: (id: string) => routinesApi.run(id),
|
||||
onMutate: (id) => {
|
||||
mutationFn: ({ id, data }: { id: string; data?: RoutineRunDialogSubmitData }) => routinesApi.run(id, {
|
||||
...(data?.variables && Object.keys(data.variables).length > 0 ? { variables: data.variables } : {}),
|
||||
...(data?.executionWorkspaceId !== undefined ? { executionWorkspaceId: data.executionWorkspaceId } : {}),
|
||||
...(data?.executionWorkspacePreference !== undefined
|
||||
? { executionWorkspacePreference: data.executionWorkspacePreference }
|
||||
: {}),
|
||||
...(data?.executionWorkspaceSettings !== undefined
|
||||
? { executionWorkspaceSettings: data.executionWorkspaceSettings }
|
||||
: {}),
|
||||
}),
|
||||
onMutate: ({ id }) => {
|
||||
setRunningRoutineId(id);
|
||||
},
|
||||
onSuccess: async (_, id) => {
|
||||
onSuccess: async (_, { id }) => {
|
||||
setRunDialogRoutine(null);
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.routines.list(selectedCompanyId!) }),
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.routines.detail(id) }),
|
||||
|
|
@ -214,9 +249,24 @@ export function Routines() {
|
|||
() => new Map((projects ?? []).map((project) => [project.id, project])),
|
||||
[projects],
|
||||
);
|
||||
const runDialogProject = runDialogRoutine?.projectId ? projectById.get(runDialogRoutine.projectId) ?? null : null;
|
||||
const currentAssignee = draft.assigneeAgentId ? agentById.get(draft.assigneeAgentId) ?? null : null;
|
||||
const currentProject = draft.projectId ? projectById.get(draft.projectId) ?? null : null;
|
||||
|
||||
function handleRunNow(routine: RoutineListItem) {
|
||||
const project = routine.projectId ? projectById.get(routine.projectId) ?? null : null;
|
||||
const needsConfiguration = routineRunNeedsConfiguration({
|
||||
variables: routine.variables ?? [],
|
||||
project,
|
||||
isolatedWorkspacesEnabled: experimentalSettings?.enableIsolatedWorkspaces === true,
|
||||
});
|
||||
if (needsConfiguration) {
|
||||
setRunDialogRoutine(routine);
|
||||
return;
|
||||
}
|
||||
runRoutine.mutate({ id: routine.id, data: {} });
|
||||
}
|
||||
|
||||
if (!selectedCompanyId) {
|
||||
return <EmptyState icon={Repeat} message="Select a company to view routines." />;
|
||||
}
|
||||
|
|
@ -414,6 +464,14 @@ export function Routines() {
|
|||
}
|
||||
}}
|
||||
/>
|
||||
<div className="mt-3 space-y-3">
|
||||
<RoutineVariablesHint />
|
||||
<RoutineVariablesEditor
|
||||
description={draft.description}
|
||||
value={draft.variables}
|
||||
onChange={(variables) => setDraft((current) => ({ ...current, variables }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border/60 px-5 py-3">
|
||||
|
|
@ -623,7 +681,7 @@ export function Routines() {
|
|||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
disabled={runningRoutineId === routine.id || isArchived}
|
||||
onClick={() => runRoutine.mutate(routine.id)}
|
||||
onClick={() => handleRunNow(routine)}
|
||||
>
|
||||
{runningRoutineId === routine.id ? "Running..." : "Run now"}
|
||||
</DropdownMenuItem>
|
||||
|
|
@ -661,6 +719,21 @@ export function Routines() {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<RoutineRunVariablesDialog
|
||||
open={runDialogRoutine !== null}
|
||||
onOpenChange={(next) => {
|
||||
if (!next) setRunDialogRoutine(null);
|
||||
}}
|
||||
companyId={selectedCompanyId}
|
||||
project={runDialogProject}
|
||||
variables={runDialogRoutine?.variables ?? []}
|
||||
isPending={runRoutine.isPending}
|
||||
onSubmit={(data) => {
|
||||
if (!runDialogRoutine) return;
|
||||
runRoutine.mutate({ id: runDialogRoutine.id, data });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue