From f7e90da7ae99dc7451ecf8e4ec3c99ecd5b4b5a3 Mon Sep 17 00:00:00 2001 From: Andrew Jiang Date: Tue, 10 Sep 2024 16:03:37 -0400 Subject: [PATCH] feat: share current playground state --- .../react-commons/src/useCopyToClipboard.ts | 17 ++++-- .../src/playground/PlaygroundEndpoint.scss | 2 +- .../src/playground/PlaygroundEndpointPath.tsx | 5 ++ .../src/playground/PlaygroundShareButton.tsx | 53 +++++++++++++++++++ 4 files changed, 72 insertions(+), 5 deletions(-) create mode 100644 packages/ui/app/src/playground/PlaygroundShareButton.tsx diff --git a/packages/commons/react/react-commons/src/useCopyToClipboard.ts b/packages/commons/react/react-commons/src/useCopyToClipboard.ts index 48b93e4c22..9d3bae8e2c 100644 --- a/packages/commons/react/react-commons/src/useCopyToClipboard.ts +++ b/packages/commons/react/react-commons/src/useCopyToClipboard.ts @@ -1,4 +1,4 @@ -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useTimeout } from "./useTimeout"; export declare namespace useCopyToClipboard { @@ -11,17 +11,26 @@ export declare namespace useCopyToClipboard { export function useCopyToClipboard( content: string | (() => string | Promise) | undefined, ): useCopyToClipboard.Return { + const contentRef = useRef(content); + + useEffect(() => { + contentRef.current = content; + }, [content]); + const [wasJustCopied, setWasJustCopied] = useState(false); const copyToClipboard = useMemo(() => { - if (content == null) { + const currentContent = contentRef.current; + if (currentContent == null) { return undefined; } return async () => { setWasJustCopied(true); - await navigator.clipboard.writeText(typeof content === "function" ? await content() : content); + await navigator.clipboard.writeText( + typeof currentContent === "function" ? await currentContent() : currentContent, + ); }; - }, [content]); + }, []); useTimeout( () => { diff --git a/packages/ui/app/src/playground/PlaygroundEndpoint.scss b/packages/ui/app/src/playground/PlaygroundEndpoint.scss index 956ed6daf5..f92e1688c0 100644 --- a/packages/ui/app/src/playground/PlaygroundEndpoint.scss +++ b/packages/ui/app/src/playground/PlaygroundEndpoint.scss @@ -18,7 +18,7 @@ } */ .playground-endpoint-copy-button { - @apply -mt-1 ml-auto -mr-3; + @apply ml-auto -mr-3; } .playground-endpoint-baseurl { diff --git a/packages/ui/app/src/playground/PlaygroundEndpointPath.tsx b/packages/ui/app/src/playground/PlaygroundEndpointPath.tsx index d21d2410b1..d06c756321 100644 --- a/packages/ui/app/src/playground/PlaygroundEndpointPath.tsx +++ b/packages/ui/app/src/playground/PlaygroundEndpointPath.tsx @@ -11,6 +11,7 @@ import { HttpMethodTag } from "../components/HttpMethodTag"; import { MaybeEnvironmentDropdown } from "../components/MaybeEnvironmentDropdown"; import { ResolvedEndpointPathParts, ResolvedObjectProperty } from "../resolver/types"; import { PlaygroundSendRequestButton } from "./PlaygroundSendRequestButton"; +import { PlaygroundShareButton } from "./PlaygroundShareButton"; import { PlaygroundRequestFormState } from "./types"; import { buildRequestUrl, unknownToString } from "./utils"; @@ -121,6 +122,10 @@ export const PlaygroundEndpointPath: FC = ({ /> +
+ +
+ } size="large" rounded variant="outlined" /> diff --git a/packages/ui/app/src/playground/PlaygroundShareButton.tsx b/packages/ui/app/src/playground/PlaygroundShareButton.tsx new file mode 100644 index 0000000000..08085b12ae --- /dev/null +++ b/packages/ui/app/src/playground/PlaygroundShareButton.tsx @@ -0,0 +1,53 @@ +import type { FernNavigation } from "@fern-api/fdr-sdk"; +import { FernButton } from "@fern-ui/components"; +import { useCopyToClipboard } from "@fern-ui/react-commons"; +import { ShareIos } from "iconoir-react"; +import { useAtom } from "jotai"; +import { useSearchParams } from "next/navigation"; +import { useEffect, type ReactElement } from "react"; +import { usePlaygroundFormStateAtom, usePlaygroundNode } from "../atoms"; +import { useToHref } from "../hooks/useHref"; + +interface PlaygroundShareButtonProps { + node: FernNavigation.NavigationNodeApiLeaf; +} + +export const PlaygroundShareButtonInternal = ({ node }: PlaygroundShareButtonProps): ReactElement => { + const searchParams = useSearchParams(); + const initialState = searchParams.get("initialState"); + const initialPlaygroundHref = searchParams.get("playground"); + const toHref = useToHref(); + + const [formState, setFormState] = useAtom(usePlaygroundFormStateAtom(node.id)); + const { copyToClipboard } = useCopyToClipboard(() => { + const url = new URL(window.location.href); + + url.searchParams.set("playground", toHref(node.slug)); + url.searchParams.set("initialState", btoa(JSON.stringify(formState))); + + return url.toString(); + }); + + useEffect(() => { + if (formState == null && initialState != null && initialPlaygroundHref === toHref(node.slug)) { + try { + setFormState(JSON.parse(atob(initialState))); + } catch (e) { + // eslint-disable-next-line no-console + console.error("Failed to parse initial state", e); + } + } + }, [formState, initialPlaygroundHref, initialState, node.slug, setFormState, toHref]); + + return } onClick={copyToClipboard} rounded variant="outlined" size="large" />; +}; + +export const PlaygroundShareButton = (): ReactElement | null => { + const node = usePlaygroundNode(); + + if (node == null) { + return null; + } + + return ; +};