Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(sandbox): allow the sandbox to load plugins when running locally #367

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
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
8 changes: 4 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ This repository is a monorepo that includes multiple packages. We use Yarn Works

- Library (`packages/field-plugin`): This is the core library. It exports the `createFieldPlugin()` function, which sends and receives events between Storyblok's Visual Editor and your Field Plugin.
- Demo (`packages/demo`): This is a demo Field Plugin that covers the basic functionalities of the Field Plugin SDK.
- Container (`packages/container`): In production, a Field Plugin is hosted within Storyblok's Visual Editor, and they exchange events. Similarly, our demo Field Plugin needs an environment like the Visual Editor. That's what Container is - a simulated environment to test your Field Plugin.
- Sandbox (`packages/sandbox`): In production, a Field Plugin is hosted within Storyblok's Visual Editor, and they exchange events. Similarly, our demo Field Plugin needs an environment like the Visual Editor. That's what the Sandbox is - a simulated container environment to test your Field Plugin.
- CLI (`packages/cli`): This is a CLI package that helps you create and deploy Field Plugins. It can be accessed with `npx @storyblok/field-plugin@beta`.
- Templates (`packages/cli/templates/*`): We maintain templates to create Field Plugin in React, Vue 3, Vue 2, and plain JavaScript.
- Helpers (`packages/helper-*`): While `createFieldPlugin` from `@storyblok/field-plugin` is framework-agnostic, we provide framework-specific helpers such as the `useFieldPlugin` hook. These helpers are not released independently to NPM, but are included within the library and accessible as a submodule, for example, `import { useFieldPlugin } from '@storyblok/field-plugin/react'`.
Expand Down Expand Up @@ -75,15 +75,15 @@ At the root of this repository, run the following command to run unit tests:
yarn test:lib
```

#### Test with demo and Container
#### Test with demo and Sandbox

To test the library with a demo, you need to run three commands in parallel:

- `yarn dev:lib`: Watches file changes in the library and updates the bundle output.
- `yarn dev:demo`: Runs the demo Field Plugin located at `packages/demo`. Update it to test changes to the library.
- `yarn dev:container`: Runs Container locally.
- `yarn dev:sandbox`: Runs the Sandbox locally.

Run all the commands in three separate terminals, then open the Container at `http://localhost:7070/`. This Container hosts the demo Field Plugin. Whenever you change a file in the library, the bundle output updates automatically and the demo app does Hot-Module Replacement (HMR). You can then seamlessly test it in the Container.
Run all the commands in three separate terminals, then open the Sandbox at `http://localhost:7070/`. This Container hosts the demo Field Plugin. Whenever you change a file in the library, the bundle output updates automatically and the demo app does Hot-Module Replacement (HMR). You can then seamlessly test it in the running Sandbox application.

### CLI

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"build": "yarn build:lib && yarn workspaces foreach --exclude @storyblok/field-plugin run build",
"build:lib": "yarn workspace @storyblok/field-plugin build",
"build:cli": "yarn workspace @storyblok/field-plugin-cli build",
"build:container": "yarn build:lib && yarn workspace container build",
"build:sandbox": "yarn build:lib && yarn workspace sandbox build",
"prepare-dev-configs": "./scripts/prepare-dev-vite-configs.mjs",
"test": "yarn test:lib",
"test:lib": "yarn workspace @storyblok/field-plugin test",
Expand All @@ -48,7 +48,7 @@
"lint": "eslint .",
"dev:demo": "yarn workspace demo dev",
"dev:lib": "yarn workspace @storyblok/field-plugin dev",
"dev:container": "yarn workspace container dev",
"dev:sandbox": "yarn workspace sandbox dev",
"dev:template": "yarn workspace field-plugin-${0}-template dev --config node_modules/.${0}-vite.config.ts",
"dev:react": "yarn dev:template react",
"dev:js": "yarn dev:template js",
Expand Down
1 change: 1 addition & 0 deletions packages/demo/src/components/FieldPluginDemo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export type PluginComponent = FunctionComponent<{
export const FieldPluginDemo: FunctionComponent = () => {
const { type, data, actions } = useFieldPlugin({
validateContent,
allowAllOrigins: true,
})

if (type === 'loading') {
Expand Down
2 changes: 2 additions & 0 deletions packages/field-plugin/helpers/react/src/useFieldPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useEffect, useState } from 'react'

export const useFieldPlugin = <Content>({
validateContent,
allowAllOrigins,
}: Omit<
CreateFieldPluginOptions<Content>,
'onUpdateState'
Expand All @@ -32,6 +33,7 @@ export const useFieldPlugin = <Content>({
}
},
validateContent,
allowAllOrigins,
})
}, [])

Expand Down
2 changes: 2 additions & 0 deletions packages/field-plugin/helpers/vue3/src/useFieldPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const updateObjectWithoutChangingReference = (

export const useFieldPlugin = <Content>({
validateContent,
allowAllOrigins,
}: Omit<CreateFieldPluginOptions<Content>, 'onUpdateState'> = {}): UnwrapRef<
FieldPluginResponse<Content>
> => {
Expand Down Expand Up @@ -89,6 +90,7 @@ export const useFieldPlugin = <Content>({
}
},
validateContent,
allowAllOrigins,
})
})

Expand Down
11 changes: 8 additions & 3 deletions packages/field-plugin/src/createFieldPlugin/createFieldPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { isCloneable } from '../utils/isCloneable'
export type CreateFieldPluginOptions<Content> = {
onUpdateState: (state: FieldPluginResponse<Content>) => void
validateContent?: ValidateContent<Content>
allowAllOrigins?: boolean
}

export type CreateFieldPlugin = <Content = unknown>(
Expand All @@ -22,6 +23,7 @@ export type CreateFieldPlugin = <Content = unknown>(
export const createFieldPlugin: CreateFieldPlugin = ({
onUpdateState,
validateContent,
allowAllOrigins = false,
}) => {
const isEmbedded = window.parent !== window

Expand Down Expand Up @@ -49,9 +51,11 @@ export const createFieldPlugin: CreateFieldPlugin = ({

const { uid, host } = params
const origin =
host === 'plugin-sandbox.storyblok.com'
? 'https://plugin-sandbox.storyblok.com'
: 'https://app.storyblok.com'
allowAllOrigins === true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I would extract this for better code readability.

? '*'
: host === 'plugin-sandbox.storyblok.com'
? 'https://plugin-sandbox.storyblok.com'
: 'https://app.storyblok.com'

const postToContainer = (message: unknown) => {
try {
Expand Down Expand Up @@ -104,6 +108,7 @@ export const createFieldPlugin: CreateFieldPlugin = ({
const cleanupMessageListenerSideEffects = createPluginMessageListener(
params.uid,
origin,
allowAllOrigins,
messageCallbacks,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type PluginMessageCallbacks = {
export type CreatePluginMessageListener = (
uid: string,
origin: string,
allowAllOrigins: boolean,
callbacks: PluginMessageCallbacks,
) => () => void

Expand All @@ -28,10 +29,11 @@ export type CreatePluginMessageListener = (
export const createPluginMessageListener: CreatePluginMessageListener = (
uid,
origin,
allowAllOrigins,
callbacks,
) => {
const handleEvent = (event: MessageEvent<unknown>) => {
if (event.origin === origin) {
if (allowAllOrigins || event.origin === origin) {
handlePluginMessage(event.data, uid, callbacks)
}
}
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "container",
"name": "sandbox",
"private": true,
"version": "0.0.0",
"type": "module",
Expand Down
File renamed without changes
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
NotificationProvider,
} from '@storyblok/mui'
import { CssBaseline, ThemeProvider } from '@mui/material'
import { FieldPluginContainer } from './FieldPluginContainer'
import { FieldPluginSandbox } from './FieldPluginSandbox'
import { SandboxAppHeader } from './SandboxAppHeader'
import { BrowserRouter, Route, Routes, Navigate } from 'react-router-dom'
import { ReactRouter6Adapter } from 'use-query-params/adapters/react-router-6'
Expand All @@ -29,7 +29,7 @@ export const App: FunctionComponent = () => (
<AppContainer>
<SandboxAppHeader />
<AppContent>
<FieldPluginContainer />
<FieldPluginSandbox />
</AppContent>
</AppContainer>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import {
import { CenteredContent, useNotifications } from '@storyblok/mui'
import { SchemaEditor } from './SchemaEditor'
import { FieldTypePreview } from './FieldTypePreview'
import { createContainerMessageListener } from '../dom/createContainerMessageListener'
import { createSandboxMessageListener } from '../dom/createSandboxMessageListener'
import { useDebounce } from 'use-debounce'
import { ContentView } from './ContentView'
import {
Expand All @@ -52,6 +52,7 @@ import { ObjectView } from './ObjectView'
import { UrlView } from './UrlView'
import { usePluginParams } from './usePluginParams'
import { LanguageView } from './LanguageView'
import { TranslatableCheckbox } from './TranslatableCheckbox'

const defaultUrl = 'http://localhost:8080'
const initialStory: StoryData = {
Expand Down Expand Up @@ -111,15 +112,17 @@ const useSandbox = (
const [schema, setSchema] = useState<FieldPluginSchema>({
field_type: 'preview',
options: manifest.options,
translatable: false,
})
const [content, setContent] = useState<unknown>(initialContent)
const [language, setLanguage] = useState<string>('')
const [language, setLanguage] = useState<string>('default')
const [stateChangedCallbackId, setStateChangedCallbackId] = useState<string>()

const stateChangedData = useMemo<StateChangedMessage>(
() => ({
model: content,
schema: schema,
interfaceLanguage: 'en',
action: 'state-changed',
uid,
blockId: undefined,
Expand All @@ -130,6 +133,8 @@ const useSandbox = (
token: null,
isModalOpen,
callbackId: stateChangedCallbackId,
releases: [],
releaseId: undefined,
}),
[
uid,
Expand Down Expand Up @@ -254,7 +259,7 @@ const useSandbox = (

useEffect(
() =>
createContainerMessageListener(
createSandboxMessageListener(
{
setContent: onUpdate,
setPluginReady: onLoaded,
Expand Down Expand Up @@ -306,7 +311,7 @@ const useSandbox = (
] as const
}

export const FieldPluginContainer: FunctionComponent = () => {
export const FieldPluginSandbox: FunctionComponent = () => {
const { error } = useNotifications()
const [
{
Expand Down Expand Up @@ -371,13 +376,29 @@ export const FieldPluginContainer: FunctionComponent = () => {
</Accordion>
<Accordion defaultExpanded>
<AccordionSummary>
<Typography variant="h3">Options</Typography>
<Typography variant="h3">Settings</Typography>
</AccordionSummary>
<AccordionDetails>
<SchemaEditor
schema={schema}
setSchema={setSchema}
/>
<Stack
width="xs"
gap={5}
>
<SchemaEditor
schema={schema}
setSchema={setSchema}
/>
<TranslatableCheckbox
isTranslatable={schema.translatable}
setTranslatable={(e) => setSchema({ ...schema, translatable: e })}
/>
<LanguageView
sx={{
alignSelf: 'flex-start',
}}
language={language}
setLanguage={setLanguage}
/>
</Stack>
</AccordionDetails>
</Accordion>
<Accordion defaultExpanded>
Expand All @@ -393,13 +414,6 @@ export const FieldPluginContainer: FunctionComponent = () => {
content={content}
setContent={setContent}
/>
<LanguageView
sx={{
alignSelf: 'flex-start',
}}
language={language}
setLanguage={setLanguage}
/>
</Stack>
</AccordionDetails>
</Accordion>
Expand All @@ -418,6 +432,8 @@ export const FieldPluginContainer: FunctionComponent = () => {
{
content,
isModalOpen,
translatable: schema.translatable,
storyLang: language,
options: recordFromFieldPluginOptions(schema.options),
} satisfies Partial<FieldPluginData<unknown>>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const FieldTypeModal: FunctionComponent<
</Box>
)

const FieldTypeContainer: FunctionComponent<
const FieldTypeSandbox: FunctionComponent<
PropsWithChildren<{
isModal: boolean
}>
Expand Down Expand Up @@ -89,7 +89,7 @@ export const FieldTypePreview = forwardRef<
sx={{ zIndex: ({ zIndex }) => zIndex.drawer }}
/>
<FieldTypeModal isModal={props.isModal}>
<FieldTypeContainer isModal={props.isModal}>
<FieldTypeSandbox isModal={props.isModal}>
{typeof props.src !== 'undefined' ? (
<Box
ref={ref}
Expand All @@ -115,7 +115,7 @@ export const FieldTypePreview = forwardRef<
<Typography>Please enter a valid URL.</Typography>
</Alert>
)}
</FieldTypeContainer>
</FieldTypeSandbox>
</FieldTypeModal>
</Box>
)
Expand Down
21 changes: 21 additions & 0 deletions packages/sandbox/src/components/TranslatableCheckbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Checkbox, FormControl, FormLabel } from '@mui/material'
import { FunctionComponent } from 'react'

export const TranslatableCheckbox: FunctionComponent<{
isTranslatable: boolean
setTranslatable: (value: boolean) => void
}> = (props) => {
return (
<FormControl>
<FormLabel htmlFor="translatable-checkbox">Translatable</FormLabel>
<Checkbox
sx={{
alignSelf: 'flex-start',
}}
aria-describedby="translatable-checkbox"
value={props.isTranslatable}
onChange={(e) => props.setTranslatable(e.target.checked)}
/>
</FormControl>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
ValueChangeMessage,
} from '@storyblok/field-plugin'

type ContainerActions = {
type SandboxActions = {
setHeight: (message: HeightChangeMessage) => void
setContent: (message: ValueChangeMessage) => void
setModalOpen: (message: ModalChangeMessage) => void
Expand All @@ -23,16 +23,16 @@ type ContainerActions = {
selectAsset: (message: AssetModalChangeMessage) => void
}

export type CreateContainerListener = (
eventHandlers: ContainerActions,
export type CreateSandboxListener = (
eventHandlers: SandboxActions,
options: {
window: Window
iframeOrigin: string | undefined
uid: string
},
) => () => void

export const createContainerMessageListener: CreateContainerListener = (
export const createSandboxMessageListener: CreateSandboxListener = (
eventHandlers,
options,
) => {
Expand Down Expand Up @@ -62,7 +62,7 @@ export const createContainerMessageListener: CreateContainerListener = (
eventHandlers.requestContext(message)
} else {
console.warn(
`Container received unknown message from plugin: ${JSON.stringify(
`The Sandbox received unknown message from plugin: ${JSON.stringify(
message,
)}`,
)
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading
Loading