Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/providers/TourProvider/TourModeContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { TourDefinition } from "@/components/Learn/tours/registry";
export interface TourModeValue {
tour: TourDefinition;
tempPipelineName: string;
promoteToPipeline: (newName: string, yamlContent: string) => Promise<void>;
}

const TourModeContext = createContext<TourModeValue | null>(null);
Expand Down
35 changes: 35 additions & 0 deletions src/providers/TourProvider/TourPopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Icon } from "@/components/ui/icon";
import { BlockStack } from "@/components/ui/layout";
import { Text } from "@/components/ui/typography";
import { APP_ROUTES } from "@/routes/router";
import { setTourActive } from "@/utils/tourActive";
import { tracking } from "@/utils/tracking";
Expand Down Expand Up @@ -96,6 +97,19 @@ export function computeDefaultPopoverPosition(
return "bottom";
}

let saveExploreHandler: (() => void) | null = null;

export function registerSaveExploreHandler(
handler: (() => void) | null,
): () => void {
saveExploreHandler = handler;
return () => {
if (saveExploreHandler === handler) {
saveExploreHandler = null;
}
};
}

export function TourCompletionActions() {
const navigate = useNavigate();
const { setIsOpen } = useTour();
Expand All @@ -105,6 +119,11 @@ export function TourCompletionActions() {
void navigate({ to: APP_ROUTES.LEARN_TOURS });
};

const onSavePipeline = () => {
setIsOpen(false);
saveExploreHandler?.();
};

return (
<BlockStack gap="3" align="center">
<Button
Expand All @@ -116,6 +135,22 @@ export function TourCompletionActions() {
<Icon name="Check" size="sm" />
Finish Tour
</Button>
{saveExploreHandler && (
Comment thread
camielvs marked this conversation as resolved.
<BlockStack align="center">
<Text size="xs" tone="subdued">
Continue exploring:
</Text>
<Button
size="xs"
variant="link"
onClick={onSavePipeline}
{...tracking("v2.pipeline_editor.tour.save_as_pipeline")}
>
<Icon name="SaveAll" size="xs" />
Save demo pipeline
</Button>
</BlockStack>
)}
</BlockStack>
);
}
Expand Down
57 changes: 57 additions & 0 deletions src/providers/TourProvider/TourSaveExploreDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useEffect, useState } from "react";

import { PipelineNameDialog } from "@/components/shared/Dialogs";
import useToastNotification from "@/hooks/useToastNotification";
import { serializeComponentSpecToText } from "@/models/componentSpec";
import { useTourMode } from "@/providers/TourProvider/TourModeContext";
import { registerSaveExploreHandler } from "@/providers/TourProvider/TourPopover";
import { usePipelineActions } from "@/routes/v2/pages/Editor/store/actions/usePipelineActions";
import { useSharedStores } from "@/routes/v2/shared/store/SharedStoreContext";

export function TourSaveExploreDialog() {
const tourMode = useTourMode();
const { navigation } = useSharedStores();
const { renamePipeline } = usePipelineActions();
const notify = useToastNotification();
const [open, setOpen] = useState(false);

useEffect(() => {
if (!tourMode) return;
return registerSaveExploreHandler(() => setOpen(true));
}, [tourMode]);
Comment thread
camielvs marked this conversation as resolved.

if (!tourMode) return null;

const onSubmit = async (name: string) => {
const rootSpec = navigation.rootSpec;
if (!rootSpec) {
notify(
"Pipeline isn't ready to save yet — try again in a moment.",
"error",
);
return;
}

try {
renamePipeline(rootSpec, name);
const yamlContent = serializeComponentSpecToText(rootSpec);
await tourMode.promoteToPipeline(name, yamlContent);
} catch (error) {
const message =
error instanceof Error ? error.message : "Failed to save pipeline";
notify(message, "error");
}
};

return (
<PipelineNameDialog
open={open}
onOpenChange={setOpen}
title="Save pipeline"
description="Convert this demo pipeline into a regular pipeline you can keep editing."
initialName={tourMode.tour.displayName ?? tourMode.tour.id}
onSubmit={onSubmit}
submitButtonText="Save"
/>
);
}
33 changes: 31 additions & 2 deletions src/routes/Dashboard/Learn/Tour.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@ import {
getTour,
type TourDefinition,
} from "@/components/Learn/tours/registry";
import useToastNotification from "@/hooks/useToastNotification";
import { TourContent } from "@/providers/TourProvider/TourContent";
import { TourModeProvider } from "@/providers/TourProvider/TourModeContext";
import {
TourModeProvider,
type TourModeValue,
} from "@/providers/TourProvider/TourModeContext";
import {
buildTourPipelineYaml,
TOUR_PIPELINE_PREFIX,
Expand Down Expand Up @@ -147,24 +151,48 @@ export function TourPage() {
? params.tourId
: "";
const tour = getTour(tourId);
const navigate = useNavigate();
const storage = usePipelineStorage();
const notify = useToastNotification();

const promoteToPipeline = async (newName: string, yamlContent: string) => {
try {
const file = await storage.rootFolder.addFile(newName, yamlContent);
await navigate({
to: APP_ROUTES.EDITOR_V2_PIPELINE,
params: { pipelineName: newName },
search: { fileId: file.id },
});
} catch (error) {
const message =
error instanceof Error ? error.message : "Failed to save pipeline";
notify(message, "error");
}
};

if (!tour) {
return <Navigate to={APP_ROUTES.LEARN_TOURS} replace />;
}

return (
<TourPipelineStorageProvider>
<TourPageBody tour={tour} tourId={tourId} />
<TourPageBody
tour={tour}
tourId={tourId}
promoteToPipeline={promoteToPipeline}
/>
</TourPipelineStorageProvider>
);
}

function TourPageBody({
tour,
tourId,
promoteToPipeline,
}: {
tour: TourDefinition;
tourId: string;
promoteToPipeline: TourModeValue["promoteToPipeline"];
}) {
const search = useSearch({ strict: false });
const navigate = useNavigate();
Expand Down Expand Up @@ -217,6 +245,7 @@ function TourPageBody({
value={{
tour,
tempPipelineName: resolved?.name ?? tourPipelineName(tour),
promoteToPipeline,
}}
>
{resolved && (
Expand Down
2 changes: 2 additions & 0 deletions src/routes/v2/pages/Editor/EditorV2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { ComponentLibraryProvider } from "@/providers/ComponentLibraryProvider";
import { ForcedSearchProvider } from "@/providers/ComponentLibraryProvider/ForcedSearchProvider";
import { DialogProvider } from "@/providers/DialogProvider/DialogProvider";
import { useTourMode } from "@/providers/TourProvider/TourModeContext";
import { TourSaveExploreDialog } from "@/providers/TourProvider/TourSaveExploreDialog";
import { AiChatStoreProvider } from "@/routes/v2/shared/components/AiChat/AiChatStoreContext";
import { useDockAreaAccordion } from "@/routes/v2/shared/hooks/useDockAreaAccordion";
import { useFocusMode } from "@/routes/v2/shared/hooks/useFocusMode";
Expand Down Expand Up @@ -164,6 +165,7 @@ function EditorV2Content({ pipelineRef }: { pipelineRef: PipelineRef | null }) {
<ReactFlowProvider>
<EditorMenuBar />
<EditorTourBridge />
<TourSaveExploreDialog />
<ForcedSearchProvider>{body}</ForcedSearchProvider>
</ReactFlowProvider>
</ComponentLibraryProvider>
Expand Down
Loading