From 4f00443ee181423bc864ea59ab4de54dd22c2979 Mon Sep 17 00:00:00 2001 From: Aria Minaei Date: Thu, 3 Aug 2023 10:54:54 +0200 Subject: [PATCH] Implement hot-reloading extension toolbars --- .../src/shared/utils/useExtensionButton.ts | 26 ++++++ .../src/tests/reconfigure-extension/App.tsx | 37 ++++++++ .../tests/reconfigure-extension/index.html | 11 +++ .../src/tests/reconfigure-extension/index.tsx | 86 +++++++++++++++++++ .../tests/reconfigure-extension/test.e2e.ts | 25 ++++++ theatre/studio/src/TheatreStudio.ts | 22 ++++- .../ExtensionToolbar/ExtensionToolbar.tsx | 8 +- .../ExtensionToolbar/tools/IconButton.tsx | 4 +- 8 files changed, 213 insertions(+), 6 deletions(-) create mode 100644 packages/playground/src/tests/reconfigure-extension/App.tsx create mode 100644 packages/playground/src/tests/reconfigure-extension/index.html create mode 100644 packages/playground/src/tests/reconfigure-extension/index.tsx create mode 100644 packages/playground/src/tests/reconfigure-extension/test.e2e.ts diff --git a/packages/playground/src/shared/utils/useExtensionButton.ts b/packages/playground/src/shared/utils/useExtensionButton.ts index dbc03fd..985550e 100644 --- a/packages/playground/src/shared/utils/useExtensionButton.ts +++ b/packages/playground/src/shared/utils/useExtensionButton.ts @@ -32,5 +32,31 @@ export function useExtensionButton( }, [id]) } +export function extensionButton( + title: string, + callback: () => void, + svgSource?: string, +) { + const id = 'useExtensionButton#' + idCounter++ + studio.extend({ + id: id, + toolbars: { + global(set) { + set([ + { + type: 'Icon', + title, + onClick() { + callback() + }, + svgSource: svgSource ?? stepForward, + }, + ]) + return () => {} + }, + }, + }) +} + // FontAwesome FaStepForward const stepForward = `` diff --git a/packages/playground/src/tests/reconfigure-extension/App.tsx b/packages/playground/src/tests/reconfigure-extension/App.tsx new file mode 100644 index 0000000..daa8cdf --- /dev/null +++ b/packages/playground/src/tests/reconfigure-extension/App.tsx @@ -0,0 +1,37 @@ +import {editable as e, SheetProvider} from '@theatre/r3f' +import {Stars, TorusKnot} from '@react-three/drei' +import {getProject} from '@theatre/core' +import React from 'react' +import {Canvas} from '@react-three/fiber' + +function App() { + return ( +
{ + // return setBgIndex((bgIndex) => (bgIndex + 1) % bgs.length) + }} + style={{ + height: '100vh', + }} + > + + + + + + + + + + + +
+ ) +} + +export default App diff --git a/packages/playground/src/tests/reconfigure-extension/index.html b/packages/playground/src/tests/reconfigure-extension/index.html new file mode 100644 index 0000000..b3a037a --- /dev/null +++ b/packages/playground/src/tests/reconfigure-extension/index.html @@ -0,0 +1,11 @@ + + + + + Theatre.js Playground + + + +
+ + diff --git a/packages/playground/src/tests/reconfigure-extension/index.tsx b/packages/playground/src/tests/reconfigure-extension/index.tsx new file mode 100644 index 0000000..36f5730 --- /dev/null +++ b/packages/playground/src/tests/reconfigure-extension/index.tsx @@ -0,0 +1,86 @@ +import type {IExtension} from '@theatre/studio' +import studio from '@theatre/studio' +import '@theatre/core' +import {extensionButton} from '../../shared/utils/useExtensionButton' + +const ext1: IExtension = { + id: '@theatre/hello-world-extension', + toolbars: { + global(set, studio) { + console.log('mount 1') + + set([ + { + type: 'Icon', + title: 'Icon 1', + svgSource: '1', + onClick: () => { + console.log('Icon 1') + }, + }, + ]) + + return () => { + console.log('unmount 1') + } + }, + }, + panes: [], +} + +studio.initialize() + +let currentStep = -1 + +extensionButton( + 'Forward', + () => { + if (currentStep < steps.length - 1) { + currentStep++ + steps[currentStep]() + } + }, + '>', +) + +const steps = [ + function step1() { + studio.extend(ext1) + }, + function step2() { + studio.extend( + { + ...ext1, + toolbars: { + global(set, studio) { + console.log('mount 2') + + set([ + { + type: 'Icon', + title: 'Icon 2', + svgSource: '2', + onClick: () => { + console.log('Icon 2') + }, + }, + ]) + return () => { + console.log('unmount 2') + } + }, + }, + }, + {__experimental_reconfigure: true}, + ) + }, + function step3() { + studio.extend( + { + ...ext1, + toolbars: {}, + }, + {__experimental_reconfigure: true}, + ) + }, +] diff --git a/packages/playground/src/tests/reconfigure-extension/test.e2e.ts b/packages/playground/src/tests/reconfigure-extension/test.e2e.ts new file mode 100644 index 0000000..fdd181c --- /dev/null +++ b/packages/playground/src/tests/reconfigure-extension/test.e2e.ts @@ -0,0 +1,25 @@ +import {test, expect} from '@playwright/test' + +test.describe('reconfigure-extension', () => { + test('works', async ({page}) => { + await page.goto('./tests/reconfigure-extension/') + + const toolbar = page.locator( + '[data-test-id="theatre-extensionToolbar-global"]', + ) + + const forwardButton = toolbar.getByRole('button', {name: '>'}) + await forwardButton.click() + + const otherButton = toolbar.getByRole('button').nth(1) + + expect(await otherButton.textContent()).toEqual('1') + + await forwardButton.click() + expect(await otherButton.textContent()).toEqual('2') + await forwardButton.click() + + // expect otherButton not to exist + await expect(otherButton).not.toBeAttached() + }) +}) diff --git a/theatre/studio/src/TheatreStudio.ts b/theatre/studio/src/TheatreStudio.ts index 2801951..f769279 100644 --- a/theatre/studio/src/TheatreStudio.ts +++ b/theatre/studio/src/TheatreStudio.ts @@ -374,6 +374,21 @@ export interface IStudio { * The extension's definition */ extension: IExtension, + opts?: { + /** + * Whether to reconfigure the extension. This is useful if you're + * hot-reloading the extension. + * + * Mind you, that if the old version of the extension defines a pane, + * and the new version doesn't, all instances of that pane will disappear, as expected. + * _However_, if you again reconfigure the extension with the old version, the instances + * of the pane that pane will re-appear. + * + * We're not sure about whether this behavior makes sense or not. If not, let us know + * in the discord server or open an issue on github. + */ + __experimental_reconfigure?: boolean + }, ): void /** @@ -504,8 +519,11 @@ export default class TheatreStudio implements IStudio { return studio.initialize(opts) } - extend(extension: IExtension): void { - getStudio().extend(extension) + extend( + extension: IExtension, + opts?: {__experimental_reconfigure?: boolean}, + ): void { + getStudio().extend(extension, opts) } transaction(fn: (api: ITransactionAPI) => void): void { diff --git a/theatre/studio/src/toolbars/ExtensionToolbar/ExtensionToolbar.tsx b/theatre/studio/src/toolbars/ExtensionToolbar/ExtensionToolbar.tsx index b4754f9..d3f0d48 100644 --- a/theatre/studio/src/toolbars/ExtensionToolbar/ExtensionToolbar.tsx +++ b/theatre/studio/src/toolbars/ExtensionToolbar/ExtensionToolbar.tsx @@ -31,14 +31,16 @@ const ExtensionToolsetRender: React.FC<{ }> = ({extension, toolbarId}) => { const toolsetConfigBox = useMemo(() => new Atom([]), []) + const attachFn = extension.toolbars?.[toolbarId] + useLayoutEffect(() => { - const detach = extension.toolbars?.[toolbarId]?.( + const detach = attachFn?.( toolsetConfigBox.set.bind(toolsetConfigBox), getStudio()!.publicApi, ) if (typeof detach === 'function') return detach - }, [extension, toolbarId]) + }, [extension, toolbarId, attachFn]) const config = useVal(toolsetConfigBox.prism) @@ -69,7 +71,7 @@ export const ExtensionToolbar: React.FC<{ if (groups.length === 0) return null return ( - + {showLeftDivider ? : undefined} {groups} diff --git a/theatre/studio/src/toolbars/ExtensionToolbar/tools/IconButton.tsx b/theatre/studio/src/toolbars/ExtensionToolbar/tools/IconButton.tsx index 1a53bcf..41f5868 100644 --- a/theatre/studio/src/toolbars/ExtensionToolbar/tools/IconButton.tsx +++ b/theatre/studio/src/toolbars/ExtensionToolbar/tools/IconButton.tsx @@ -15,10 +15,12 @@ const Container = styled(ToolbarIconButton)` const IconButton: React.FC<{ config: ToolConfigIcon -}> = ({config}) => { + testId?: string +}> = ({config, testId}) => { return (