diff --git a/sandpack-react/src/components/common/OpenInCodeSandboxButton/UnstyledOpenInCodeSandboxButton.tsx b/sandpack-react/src/components/common/OpenInCodeSandboxButton/UnstyledOpenInCodeSandboxButton.tsx index 62c2a50d0..2c25d037a 100644 --- a/sandpack-react/src/components/common/OpenInCodeSandboxButton/UnstyledOpenInCodeSandboxButton.tsx +++ b/sandpack-react/src/components/common/OpenInCodeSandboxButton/UnstyledOpenInCodeSandboxButton.tsx @@ -4,7 +4,7 @@ import LZString from "lz-string"; import * as React from "react"; import { useSandpack } from "../../../hooks/useSandpack"; -import type { SandboxEnvironment } from "../../../types"; +import type { SandboxEnvironment, SandpackState } from "../../../types"; // eslint-disable-next-line @typescript-eslint/no-explicit-any const getParameters = (parameters: Record): string => @@ -47,19 +47,70 @@ export const UnstyledOpenInCodeSandboxButton: React.FC< React.HtmlHTMLAttributes > = ({ children, ...props }) => { const { sandpack } = useSandpack(); - const formRef = React.useRef(null); + if (sandpack.exportOptions) { + return ; + } + + return ; +}; + +export const ExportToWorkspaceButton: React.FC< + React.HtmlHTMLAttributes & { state: SandpackState } +> = ({ children, state, ...props }) => { + const submit = async () => { + if (!state.exportOptions?.apiToken) { + throw new Error("Missing `apiToken` property"); + } + + const response = await fetch("https://api.codesandbox.io/sandbox", { + method: "POST", + body: JSON.stringify({ + files: state.files, + privacy: state.exportOptions.privacy === "public" ? 0 : 2, + }), + headers: { + Authorization: `Bearer ${state.exportOptions.apiToken}`, + "Content-Type": "application/json", + "X-CSB-API-Version": "2023-07-01", + }, + }); + + const data: { data: { alias: string } } = await response.json(); + + window.open( + `https://codesandbox.io/p/sandbox/${data.data.alias}?file=/src/App.js&utm-source=storybook-addon`, + "_blank" + ); + }; + + return ( + + ); +}; + +const RegularExportButton: React.FC< + React.HtmlHTMLAttributes & { state: SandpackState } +> = ({ children, state, ...props }) => { + const formRef = React.useRef(null); const [paramsValues, setParamsValues] = React.useState(); React.useEffect( function debounce() { const timer = setTimeout(() => { - const params = getFileParameters(sandpack.files, sandpack.environment); + const params = getFileParameters(state.files, state.environment); const searchParams = new URLSearchParams({ parameters: params, query: new URLSearchParams({ - file: sandpack.activeFile, + file: state.activeFile, utm_medium: "sandpack", }).toString(), }); @@ -71,7 +122,7 @@ export const UnstyledOpenInCodeSandboxButton: React.FC< clearTimeout(timer); }; }, - [sandpack.activeFile, sandpack.environment, sandpack.files] + [state.activeFile, state.environment, state.files] ); /** @@ -96,9 +147,7 @@ export const UnstyledOpenInCodeSandboxButton: React.FC< {Array.from( paramsValues as unknown as Array<[string, string]>, @@ -115,7 +164,7 @@ export const UnstyledOpenInCodeSandboxButton: React.FC< return ( = (props) => { ...clientOperations, autoReload: props.options?.autoReload ?? true, + teamId: props?.teamId, + exportOptions: props?.customSetup?.exportOptions, listen: addListener, dispatch: dispatchMessage, diff --git a/sandpack-react/src/contexts/utils/useAppState.ts b/sandpack-react/src/contexts/utils/useAppState.ts index 2f919a824..edba3dec8 100644 --- a/sandpack-react/src/contexts/utils/useAppState.ts +++ b/sandpack-react/src/contexts/utils/useAppState.ts @@ -7,7 +7,6 @@ import { getSandpackStateFromProps } from "../../utils/sandpackUtils"; interface SandpackAppState { editorState: "pristine" | "dirty"; - teamId?: string; } type UseAppState = ( @@ -18,7 +17,6 @@ type UseAppState = ( export const useAppState: UseAppState = (props, files) => { const [state, setState] = useState({ editorState: "pristine", - teamId: props.teamId, }); const originalStateFromProps = getSandpackStateFromProps(props); diff --git a/sandpack-react/src/types.ts b/sandpack-react/src/types.ts index f3448a7fd..c45a4b57a 100644 --- a/sandpack-react/src/types.ts +++ b/sandpack-react/src/types.ts @@ -241,6 +241,24 @@ export interface SandpackSetup { * ``` */ npmRegistries?: NpmRegistry[]; + + exportOptions?: SandpackExportOptions; +} + +interface SandpackExportOptions { + /** + * Workspace API key from codesandbox.io/t/permissions. + * When set, the sandbox will be create inside the given workspace id. + */ + apiToken: string; + + /** + * The default visibility of the new sandboxes inside the workspace. + * + * @note Use `private` if there is a private registry or private NPM + * configured in your workspace. + */ + privacy: "private" | "public"; } /** @@ -561,6 +579,7 @@ export interface SandpackState { */ editorState: EditorState; teamId?: string; + exportOptions?: SandpackExportOptions; error: SandpackError | null; files: SandpackBundlerFiles; environment?: SandboxEnvironment; diff --git a/website/docs/src/pages/getting-started/private-packages.mdx b/website/docs/src/pages/getting-started/private-packages.mdx index 2e3d95a62..72259cf8b 100644 --- a/website/docs/src/pages/getting-started/private-packages.mdx +++ b/website/docs/src/pages/getting-started/private-packages.mdx @@ -60,6 +60,29 @@ export default function App() { + +## Exporting sandboxes + +Once Sandpack is configured, you can use the "Open in CodeSandbox" button to export sandboxes to CodeSandbox effortlessly. However in order to seamless export the sandbox, you need to provide a CodeSandbox API key that will be used to create a sandbox inside your workspace. + +1. Generate a API token in your dashboard: [codesandbox.io/t/permissions](https://codesandbox.io/t/permissions). +2. Use the generate token to setup your Sandpack instance: + +```jsx +", + privacy: "private", // "public" | "private" + } + }} +/> +``` + + + This sandbox will be created inside the given workspace and can be shared with team members. + + ## Security It is important to us to ensure that the information and tokens of the npm registry are kept private. As such, we have added some extra measures to prevent any type of leakage.