mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Route workflow editor panes
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
@@ -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) => {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<string, unknown>) => ({
|
||||
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 (
|
||||
<React.Suspense fallback={<ViewLoadingFallback kind="workflows" />}>
|
||||
<WorkflowsRouteScreen
|
||||
editor={{
|
||||
mode: view === "duplicate" ? "duplicate" : "edit",
|
||||
pane: parseWorkflowEditorPane(pane),
|
||||
workflowId,
|
||||
}}
|
||||
onEditorPaneChange={(nextPane) => {
|
||||
void navigate({
|
||||
resetScroll: false,
|
||||
search: {
|
||||
pane: serializeWorkflowEditorPane(nextPane),
|
||||
view,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</React.Suspense>
|
||||
);
|
||||
|
||||
@@ -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<string, unknown>) => ({
|
||||
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 (
|
||||
<React.Suspense fallback={<ViewLoadingFallback kind="workflows" />}>
|
||||
<WorkflowsRouteScreen
|
||||
editor={view === "create" ? { mode: "create" } : null}
|
||||
editor={
|
||||
view === "create"
|
||||
? { mode: "create", pane: parseWorkflowEditorPane(pane) }
|
||||
: null
|
||||
}
|
||||
onEditorPaneChange={(nextPane) => {
|
||||
void navigate({
|
||||
resetScroll: false,
|
||||
search: {
|
||||
pane: serializeWorkflowEditorPane(nextPane),
|
||||
view,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</React.Suspense>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<WorkflowDialog
|
||||
channels={channels}
|
||||
mode="create"
|
||||
onOpenChange={onOpenChange}
|
||||
open={open}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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<DialogMode, string> = {
|
||||
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 ? (
|
||||
<div className="space-y-1">
|
||||
<ChannelCombobox
|
||||
channels={channels}
|
||||
defaultOpen={mode === "create"}
|
||||
disabled={mutation.isPending}
|
||||
id="wf-channel-select"
|
||||
onChange={(value) => {
|
||||
mutation.reset();
|
||||
setSelectedChannelId(value);
|
||||
if (value) onEditorPaneChange({ type: "trigger" });
|
||||
}}
|
||||
required
|
||||
value={selectedChannelId}
|
||||
@@ -312,6 +327,7 @@ export function WorkflowDialog({
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
selectedNode={pane}
|
||||
workflowChannelId={selectedChannelId || null}
|
||||
yaml={yamlDefinition}
|
||||
/>
|
||||
|
||||
@@ -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<TriggerType, LucideIcon> = {
|
||||
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<SelectedNode, null>): number {
|
||||
function nodePosition(node: Exclude<WorkflowEditorPane, null>): 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<SelectedNode>({
|
||||
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<SelectedNode, null>) => {
|
||||
(nextNode: Exclude<WorkflowEditorPane, null>) => {
|
||||
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({
|
||||
<Button
|
||||
aria-label="Close inspector"
|
||||
className="h-8 w-8"
|
||||
onClick={() => setSelectedNode(null)}
|
||||
onClick={() => onSelectedNodeChange(null)}
|
||||
size="icon"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as React from "react";
|
||||
|
||||
import type { Channel } from "@/shared/api/types";
|
||||
import { ViewLoadingFallback } from "@/shared/ui/ViewLoadingFallback";
|
||||
import type { WorkflowEditorPane } from "./workflowEditorPane";
|
||||
|
||||
const WorkflowsView = React.lazy(async () => {
|
||||
const module = await import("@/features/workflows/ui/WorkflowsView");
|
||||
@@ -9,8 +10,12 @@ const WorkflowsView = React.lazy(async () => {
|
||||
});
|
||||
|
||||
export type WorkflowEditorRoute =
|
||||
| { mode: "create" }
|
||||
| { mode: "duplicate" | "edit"; workflowId: string };
|
||||
| { mode: "create"; pane: WorkflowEditorPane }
|
||||
| {
|
||||
mode: "duplicate" | "edit";
|
||||
pane: WorkflowEditorPane;
|
||||
workflowId: string;
|
||||
};
|
||||
|
||||
type WorkflowsScreenProps = {
|
||||
channels: Channel[];
|
||||
@@ -19,6 +24,7 @@ type WorkflowsScreenProps = {
|
||||
onCreateWorkflow: () => void;
|
||||
onDuplicateWorkflow: (workflowId: string) => void;
|
||||
onEditWorkflow: (workflowId: string) => void;
|
||||
onEditorPaneChange: (pane: WorkflowEditorPane) => void;
|
||||
};
|
||||
|
||||
export function WorkflowsScreen({
|
||||
@@ -28,6 +34,7 @@ export function WorkflowsScreen({
|
||||
onCreateWorkflow,
|
||||
onDuplicateWorkflow,
|
||||
onEditWorkflow,
|
||||
onEditorPaneChange,
|
||||
}: WorkflowsScreenProps) {
|
||||
return (
|
||||
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
|
||||
@@ -39,6 +46,7 @@ export function WorkflowsScreen({
|
||||
onCreateWorkflow={onCreateWorkflow}
|
||||
onDuplicateWorkflow={onDuplicateWorkflow}
|
||||
onEditWorkflow={onEditWorkflow}
|
||||
onEditorPaneChange={onEditorPaneChange}
|
||||
/>
|
||||
</React.Suspense>
|
||||
</div>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { WorkflowCard } from "@/features/workflows/ui/WorkflowCard";
|
||||
import { WorkflowDeleteDialog } from "@/features/workflows/ui/WorkflowDeleteDialog";
|
||||
import { WorkflowDialog } from "@/features/workflows/ui/WorkflowDialog";
|
||||
import type { WorkflowEditorRoute } from "@/features/workflows/ui/WorkflowsScreen";
|
||||
import type { WorkflowEditorPane } from "@/features/workflows/ui/workflowEditorPane";
|
||||
import {
|
||||
getWorkflowEnabled,
|
||||
withWorkflowEnabled,
|
||||
@@ -34,6 +35,7 @@ type WorkflowsViewProps = {
|
||||
onCreateWorkflow: () => void;
|
||||
onDuplicateWorkflow: (workflowId: string) => void;
|
||||
onEditWorkflow: (workflowId: string) => void;
|
||||
onEditorPaneChange: (pane: WorkflowEditorPane) => void;
|
||||
};
|
||||
|
||||
type WorkflowWithChannel = {
|
||||
@@ -88,6 +90,7 @@ export function WorkflowsView({
|
||||
onCreateWorkflow,
|
||||
onDuplicateWorkflow,
|
||||
onEditWorkflow,
|
||||
onEditorPaneChange,
|
||||
}: WorkflowsViewProps) {
|
||||
const [deleteTarget, setDeleteTarget] = React.useState<Workflow | null>(null);
|
||||
const queryClient = useQueryClient();
|
||||
@@ -282,10 +285,12 @@ export function WorkflowsView({
|
||||
: `${editor.mode}:${editor.workflowId}`
|
||||
}
|
||||
mode={editor.mode}
|
||||
onEditorPaneChange={onEditorPaneChange}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) onCloseEditor();
|
||||
}}
|
||||
open
|
||||
pane={editor.pane}
|
||||
workflow={editorWorkflow}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
parseWorkflowEditorPane,
|
||||
serializeWorkflowEditorPane,
|
||||
} from "./workflowEditorPane.ts";
|
||||
|
||||
test("parses trigger and step panes", () => {
|
||||
assert.deepEqual(parseWorkflowEditorPane("trigger"), { type: "trigger" });
|
||||
assert.deepEqual(parseWorkflowEditorPane("step-0"), {
|
||||
type: "step",
|
||||
index: 0,
|
||||
});
|
||||
assert.deepEqual(parseWorkflowEditorPane("step-12"), {
|
||||
type: "step",
|
||||
index: 12,
|
||||
});
|
||||
});
|
||||
|
||||
test("rejects malformed pane parameters", () => {
|
||||
for (const value of [undefined, null, "", "step", "step--1", "step-01"]) {
|
||||
assert.equal(parseWorkflowEditorPane(value), null);
|
||||
}
|
||||
});
|
||||
|
||||
test("serializes panes for route search", () => {
|
||||
assert.equal(serializeWorkflowEditorPane({ type: "trigger" }), "trigger");
|
||||
assert.equal(
|
||||
serializeWorkflowEditorPane({ type: "step", index: 3 }),
|
||||
"step-3",
|
||||
);
|
||||
assert.equal(serializeWorkflowEditorPane(null), undefined);
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
export type WorkflowEditorPane =
|
||||
| { type: "trigger" }
|
||||
| { type: "step"; index: number }
|
||||
| null;
|
||||
|
||||
const STEP_PANE_PATTERN = /^step-(0|[1-9]\d*)$/;
|
||||
|
||||
export function parseWorkflowEditorPane(value: unknown): WorkflowEditorPane {
|
||||
if (value === "trigger") return { type: "trigger" };
|
||||
if (typeof value !== "string") return null;
|
||||
|
||||
const match = STEP_PANE_PATTERN.exec(value);
|
||||
if (!match) return null;
|
||||
|
||||
const index = Number(match[1]);
|
||||
return Number.isSafeInteger(index) ? { type: "step", index } : null;
|
||||
}
|
||||
|
||||
export function serializeWorkflowEditorPane(
|
||||
pane: WorkflowEditorPane,
|
||||
): string | undefined {
|
||||
if (pane === null) return undefined;
|
||||
return pane.type === "trigger" ? "trigger" : `step-${pane.index}`;
|
||||
}
|
||||
@@ -26,6 +26,12 @@ async function createWorkflow(
|
||||
await page.getByRole("button", { name: "Create Workflow" }).click();
|
||||
const dialog = page.getByRole("dialog");
|
||||
await expect(dialog).toBeVisible();
|
||||
await dialog.getByRole("combobox", { name: "Channel" }).click();
|
||||
await dialog
|
||||
.getByTestId("channel-combobox-list")
|
||||
.getByRole("button")
|
||||
.first()
|
||||
.click();
|
||||
await dialog.getByLabel("Workflow name").fill(name);
|
||||
await dialog.getByRole("button", { name: "Add step" }).click();
|
||||
await page.getByRole("menuitem", { name: "Delay" }).click();
|
||||
@@ -106,7 +112,7 @@ test.fixme("direct forum thread links close back to the forum route", async ({
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("direct workflow detail links close back to workflows", async ({
|
||||
test("direct workflow editor links close back to workflows", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workflowName = `workflow_nav_${Date.now()}`;
|
||||
@@ -123,15 +129,45 @@ test("direct workflow detail links close back to workflows", async ({
|
||||
|
||||
expect(workflowId).toBeTruthy();
|
||||
|
||||
await page.goto(`/#/workflows/${workflowId}`);
|
||||
await page.goto(`/#/workflows/${workflowId}?pane=trigger`);
|
||||
|
||||
await expect(page.getByTestId("workflow-detail-panel")).toBeVisible();
|
||||
await page.getByRole("button", { name: "Close detail panel" }).click();
|
||||
const editor = page.getByRole("dialog", { name: "Edit workflow" });
|
||||
await expect(editor).toBeVisible();
|
||||
await editor.getByRole("button", { name: "Close", exact: true }).click();
|
||||
|
||||
await expect(page).toHaveURL(/#\/workflows$/);
|
||||
await expect(page.getByTestId("workflows-view")).toBeVisible();
|
||||
});
|
||||
|
||||
test("back and forward traverse workflow inspector panes", async ({ page }) => {
|
||||
await navigateToWorkflows(page);
|
||||
await page.getByRole("button", { name: "Create Workflow" }).click();
|
||||
|
||||
const dialog = page.getByRole("dialog", { name: "Create workflow" });
|
||||
const inspector = dialog.getByTestId("workflow-node-inspector");
|
||||
await expect(inspector).toContainText("Trigger");
|
||||
await expect.poll(() => new URL(page.url()).hash).toContain("pane=trigger");
|
||||
|
||||
await dialog.getByRole("button", { name: "Add step" }).click();
|
||||
await page.getByRole("menuitem", { name: "Delay" }).click();
|
||||
await expect(inspector).toContainText("Step 1");
|
||||
await expect.poll(() => new URL(page.url()).hash).toContain("pane=step-0");
|
||||
|
||||
await page.goBack();
|
||||
await expect(inspector).toContainText("Trigger");
|
||||
await expect.poll(() => new URL(page.url()).hash).toContain("pane=trigger");
|
||||
|
||||
await dialog.getByRole("button", { name: "Close inspector" }).click();
|
||||
await expect(inspector).not.toBeVisible();
|
||||
await expect.poll(() => new URL(page.url()).hash).not.toContain("pane=");
|
||||
|
||||
await page.goBack();
|
||||
await expect(inspector).toContainText("Trigger");
|
||||
|
||||
await page.goForward();
|
||||
await expect(inspector).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("forum reply deep links survive reload", async ({ page }) => {
|
||||
await page.goto(
|
||||
`/#/channels/${WATERCOLOR_CHANNEL_ID}/posts/${FORUM_POST_ID}?replyId=${FORUM_REPLY_ID}`,
|
||||
|
||||
@@ -1051,5 +1051,5 @@ test("opens a workflow in the edit modal from its card", async ({ page }) => {
|
||||
const dialog = page.getByRole("dialog", { name: "Edit workflow" });
|
||||
await expect(dialog).toBeVisible();
|
||||
await expect(dialog.getByLabel("Workflow name")).toHaveValue(workflowName);
|
||||
await expect(page.getByTestId("workflow-detail-panel")).not.toBeVisible();
|
||||
await expect(page).toHaveURL(/#\/workflows\/[^?]+\?pane=trigger/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user