diff --git a/desktop/src/app/navigation/useAppNavigation.ts b/desktop/src/app/navigation/useAppNavigation.ts index ec5e9dca1..32adf7c16 100644 --- a/desktop/src/app/navigation/useAppNavigation.ts +++ b/desktop/src/app/navigation/useAppNavigation.ts @@ -155,6 +155,7 @@ export function useAppNavigation() { params: { workflowId, }, + search: { pane: "trigger" }, }, behavior, ), @@ -181,7 +182,7 @@ export function useAppNavigation() { params: { workflowId, }, - search: { view: "duplicate" }, + search: { pane: "trigger", view: "duplicate" }, }, behavior, ), @@ -294,13 +295,8 @@ export function useAppNavigation() { }, [canGoBack, goHome, router.history]); const closeWorkflowEditor = React.useCallback(() => { - if (canGoBack) { - router.history.back(); - return; - } - - void goWorkflows({ replace: true }); - }, [canGoBack, goWorkflows, router.history]); + void goWorkflows(); + }, [goWorkflows]); const closeForumPost = React.useCallback( (channelId: string) => { diff --git a/desktop/src/app/routes/WorkflowsRouteScreen.tsx b/desktop/src/app/routes/WorkflowsRouteScreen.tsx index 9c984f291..879ca957b 100644 --- a/desktop/src/app/routes/WorkflowsRouteScreen.tsx +++ b/desktop/src/app/routes/WorkflowsRouteScreen.tsx @@ -4,13 +4,16 @@ import { type WorkflowEditorRoute, WorkflowsScreen, } from "@/features/workflows/ui/WorkflowsScreen"; +import type { WorkflowEditorPane } from "@/features/workflows/ui/workflowEditorPane"; type WorkflowsRouteScreenProps = { editor?: WorkflowEditorRoute | null; + onEditorPaneChange: (pane: WorkflowEditorPane) => void; }; export function WorkflowsRouteScreen({ editor = null, + onEditorPaneChange, }: WorkflowsRouteScreenProps) { const { closeWorkflowEditor, @@ -36,6 +39,7 @@ export function WorkflowsRouteScreen({ onEditWorkflow={(workflowId) => { void goWorkflow(workflowId); }} + onEditorPaneChange={onEditorPaneChange} /> ); } diff --git a/desktop/src/app/routes/workflows.$workflowId.tsx b/desktop/src/app/routes/workflows.$workflowId.tsx index d773e8959..393cffcbb 100644 --- a/desktop/src/app/routes/workflows.$workflowId.tsx +++ b/desktop/src/app/routes/workflows.$workflowId.tsx @@ -1,12 +1,17 @@ import * as React from "react"; import { createFileRoute } from "@tanstack/react-router"; +import { + parseWorkflowEditorPane, + serializeWorkflowEditorPane, +} from "@/features/workflows/ui/workflowEditorPane"; import { usePreviewFeatureWarning } from "@/shared/features"; import { ViewLoadingFallback } from "@/shared/ui/ViewLoadingFallback"; export const Route = createFileRoute("/workflows/$workflowId")({ component: WorkflowEditorRouteComponent, validateSearch: (search: Record) => ({ + pane: serializeWorkflowEditorPane(parseWorkflowEditorPane(search.pane)), view: search.view === "duplicate" ? search.view : undefined, }), }); @@ -18,16 +23,27 @@ const WorkflowsRouteScreen = React.lazy(async () => { function WorkflowEditorRouteComponent() { usePreviewFeatureWarning("workflows"); + const navigate = Route.useNavigate(); const { workflowId } = Route.useParams(); - const { view } = Route.useSearch(); + const { pane, view } = Route.useSearch(); return ( }> { + void navigate({ + resetScroll: false, + search: { + pane: serializeWorkflowEditorPane(nextPane), + view, + }, + }); + }} /> ); diff --git a/desktop/src/app/routes/workflows.tsx b/desktop/src/app/routes/workflows.tsx index 7358f4e29..95a95bb87 100644 --- a/desktop/src/app/routes/workflows.tsx +++ b/desktop/src/app/routes/workflows.tsx @@ -1,12 +1,17 @@ import * as React from "react"; import { createFileRoute } from "@tanstack/react-router"; +import { + parseWorkflowEditorPane, + serializeWorkflowEditorPane, +} from "@/features/workflows/ui/workflowEditorPane"; import { usePreviewFeatureWarning } from "@/shared/features"; import { ViewLoadingFallback } from "@/shared/ui/ViewLoadingFallback"; export const Route = createFileRoute("/workflows")({ component: WorkflowsRouteComponent, validateSearch: (search: Record) => ({ + pane: serializeWorkflowEditorPane(parseWorkflowEditorPane(search.pane)), view: search.view === "create" ? search.view : undefined, }), }); @@ -18,12 +23,26 @@ const WorkflowsRouteScreen = React.lazy(async () => { function WorkflowsRouteComponent() { usePreviewFeatureWarning("workflows"); - const { view } = Route.useSearch(); + const navigate = Route.useNavigate(); + const { pane, view } = Route.useSearch(); return ( }> { + void navigate({ + resetScroll: false, + search: { + pane: serializeWorkflowEditorPane(nextPane), + view, + }, + }); + }} /> ); diff --git a/desktop/src/features/workflows/ui/ChannelCombobox.tsx b/desktop/src/features/workflows/ui/ChannelCombobox.tsx index dbdaf37ce..fac1cfa7d 100644 --- a/desktop/src/features/workflows/ui/ChannelCombobox.tsx +++ b/desktop/src/features/workflows/ui/ChannelCombobox.tsx @@ -34,6 +34,7 @@ type ChannelComboboxProps = { allowEmpty?: boolean; ariaLabel?: string; channels: Channel[]; + defaultOpen?: boolean; disabled?: boolean; emptyLabel?: string; id?: string; @@ -48,6 +49,7 @@ export function ChannelCombobox({ allowEmpty = false, ariaLabel = "Channel", channels, + defaultOpen = false, disabled, emptyLabel = "Choose a channel", id, @@ -57,7 +59,7 @@ export function ChannelCombobox({ variant = "header", value, }: ChannelComboboxProps) { - const [open, setOpen] = React.useState(false); + const [open, setOpen] = React.useState(defaultOpen); const [query, setQuery] = React.useState(""); const [highlightedIndex, setHighlightedIndex] = React.useState(0); diff --git a/desktop/src/features/workflows/ui/CreateWorkflowDialog.tsx b/desktop/src/features/workflows/ui/CreateWorkflowDialog.tsx deleted file mode 100644 index 6e381db66..000000000 --- a/desktop/src/features/workflows/ui/CreateWorkflowDialog.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import type { Channel } from "@/shared/api/types"; -import { WorkflowDialog } from "./WorkflowDialog"; - -type CreateWorkflowDialogProps = { - channels: Channel[]; - onOpenChange: (open: boolean) => void; - open: boolean; -}; - -export function CreateWorkflowDialog({ - channels, - onOpenChange, - open, -}: CreateWorkflowDialogProps) { - return ( - - ); -} diff --git a/desktop/src/features/workflows/ui/WorkflowDialog.tsx b/desktop/src/features/workflows/ui/WorkflowDialog.tsx index b599a909a..63bd428e2 100644 --- a/desktop/src/features/workflows/ui/WorkflowDialog.tsx +++ b/desktop/src/features/workflows/ui/WorkflowDialog.tsx @@ -34,6 +34,7 @@ import { type WorkflowEditorMode, } from "./WorkflowFormBuilder"; import { WorkflowWebhookSecretDialog } from "./WorkflowWebhookSecretDialog"; +import type { WorkflowEditorPane } from "./workflowEditorPane"; import { yamlToFormState } from "./workflowFormTypes"; type DialogMode = "create" | "edit" | "duplicate"; @@ -41,8 +42,10 @@ type DialogMode = "create" | "edit" | "duplicate"; type WorkflowDialogProps = { channels: Channel[]; mode: DialogMode; + onEditorPaneChange: (pane: WorkflowEditorPane) => void; onOpenChange: (open: boolean) => void; open: boolean; + pane: WorkflowEditorPane; workflow?: Workflow | null; }; @@ -84,8 +87,10 @@ const PENDING_LABELS: Record = { export function WorkflowDialog({ channels, mode, + onEditorPaneChange, onOpenChange, open, + pane, workflow, }: WorkflowDialogProps) { const channelId = @@ -161,7 +166,14 @@ export function WorkflowDialog({ selectedChannelId !== initialValuesRef.current.channelId; const navigationBlocker = useBlocker({ enableBeforeUnload: isDirty, - shouldBlockFn: () => isDirty && !allowNavigationRef.current, + shouldBlockFn: ({ current, next }) => { + const currentSearch = current.search as { view?: unknown }; + const nextSearch = next.search as { view?: unknown }; + const staysInEditor = + current.pathname === next.pathname && + currentSearch.view === nextSearch.view; + return isDirty && !allowNavigationRef.current && !staysInEditor; + }, withResolver: true, }); @@ -283,17 +295,20 @@ export function WorkflowDialog({ mutation.reset(); setYamlDefinition(yaml); }} + onSelectedNodeChange={onEditorPaneChange} parseError={editorParseError} scopeField={ showChannelSelector ? (
{ mutation.reset(); setSelectedChannelId(value); + if (value) onEditorPaneChange({ type: "trigger" }); }} required value={selectedChannelId} @@ -312,6 +327,7 @@ export function WorkflowDialog({
) : null } + selectedNode={pane} workflowChannelId={selectedChannelId || null} yaml={yamlDefinition} /> diff --git a/desktop/src/features/workflows/ui/WorkflowFormBuilder.tsx b/desktop/src/features/workflows/ui/WorkflowFormBuilder.tsx index ac39bb2ee..848de3e11 100644 --- a/desktop/src/features/workflows/ui/WorkflowFormBuilder.tsx +++ b/desktop/src/features/workflows/ui/WorkflowFormBuilder.tsx @@ -60,6 +60,7 @@ import type { import { defaultScheduleTrigger } from "./workflowSchedule"; import { workflowStepDescription } from "./workflowStepDescription"; import { useWorkflowTriggerPresentation } from "./useWorkflowTriggerPresentation"; +import type { WorkflowEditorPane } from "./workflowEditorPane"; const TRIGGER_ICONS: Record = { diff_posted: GitPullRequest, @@ -149,20 +150,17 @@ type WorkflowFormBuilderProps = { footerLeadingContainer?: HTMLElement | null; mode: WorkflowEditorMode; onChange: (yaml: string) => void; + onSelectedNodeChange: (node: WorkflowEditorPane) => void; parseError: string | null; scopeField?: React.ReactNode; + selectedNode: WorkflowEditorPane; yaml: string; workflowChannelId?: string | null; }; export type WorkflowEditorMode = "form" | "yaml"; -type SelectedNode = - | { type: "trigger" } - | { type: "step"; index: number } - | null; - -function nodePosition(node: Exclude): number { +function nodePosition(node: Exclude): number { return node.type === "trigger" ? 0 : node.index + 1; } @@ -370,8 +368,10 @@ export function WorkflowFormBuilder({ footerLeadingContainer, mode, onChange, + onSelectedNodeChange, parseError, scopeField, + selectedNode: selectedRouteNode, yaml, workflowChannelId, }: WorkflowFormBuilderProps) { @@ -382,9 +382,13 @@ export function WorkflowFormBuilder({ ? initialParseRef.current.state : DEFAULT_FORM_STATE, ); - const [selectedNode, setSelectedNode] = React.useState({ - type: "trigger", - }); + const selectedNode = + mode === "form" && + (selectedRouteNode?.type === "trigger" || + (selectedRouteNode?.type === "step" && + selectedRouteNode.index < formState.steps.length)) + ? selectedRouteNode + : null; const [selectionDirection, setSelectionDirection] = React.useState<1 | -1>(1); const shouldReduceMotion = useReducedMotion(); const previousModeRef = React.useRef(mode); @@ -412,18 +416,14 @@ export function WorkflowFormBuilder({ if (previousModeRef.current === mode) return; previousModeRef.current = mode; - if (mode === "yaml") { - setSelectedNode(null); - return; - } + if (mode === "yaml") return; const result = yamlToFormState(yaml); if (result.ok) setFormState(result.state); - setSelectedNode({ type: "trigger" }); }, [mode, yaml]); const selectNode = React.useCallback( - (nextNode: Exclude) => { + (nextNode: Exclude) => { if (selectedNode) { const currentPosition = nodePosition(selectedNode); const nextPosition = nodePosition(nextNode); @@ -431,9 +431,9 @@ export function WorkflowFormBuilder({ setSelectionDirection(nextPosition < currentPosition ? -1 : 1); } } - setSelectedNode(nextNode); + onSelectedNodeChange(nextNode); }, - [selectedNode], + [onSelectedNodeChange, selectedNode], ); const insertStep = React.useCallback( @@ -470,17 +470,20 @@ export function WorkflowFormBuilder({ if (selectedNode.index === index) { setSelectionDirection(-1); - setSelectedNode( + onSelectedNodeChange( index === 0 ? { type: "trigger" } : { type: "step", index: index - 1 }, ); } else if (selectedNode.index > index) { setSelectionDirection(-1); - setSelectedNode({ type: "step", index: selectedNode.index - 1 }); + onSelectedNodeChange({ + type: "step", + index: selectedNode.index - 1, + }); } }, - [formState, selectedNode, updateFormState], + [formState, onSelectedNodeChange, selectedNode, updateFormState], ); const updateStep = React.useCallback( @@ -744,7 +747,7 @@ export function WorkflowFormBuilder({