Implement hot-reloading extension toolbars
This commit is contained in:
parent
cd6f44d9dd
commit
4f00443ee1
8 changed files with 213 additions and 6 deletions
|
@ -32,5 +32,31 @@ export function useExtensionButton(
|
||||||
}, [id])
|
}, [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
|
// FontAwesome FaStepForward
|
||||||
const stepForward = `<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 448 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M384 44v424c0 6.6-5.4 12-12 12h-48c-6.6 0-12-5.4-12-12V291.6l-195.5 181C95.9 489.7 64 475.4 64 448V64c0-27.4 31.9-41.7 52.5-24.6L312 219.3V44c0-6.6 5.4-12 12-12h48c6.6 0 12 5.4 12 12z"></path></svg>`
|
const stepForward = `<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 448 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M384 44v424c0 6.6-5.4 12-12 12h-48c-6.6 0-12-5.4-12-12V291.6l-195.5 181C95.9 489.7 64 475.4 64 448V64c0-27.4 31.9-41.7 52.5-24.6L312 219.3V44c0-6.6 5.4-12 12-12h48c6.6 0 12 5.4 12 12z"></path></svg>`
|
||||||
|
|
37
packages/playground/src/tests/reconfigure-extension/App.tsx
Normal file
37
packages/playground/src/tests/reconfigure-extension/App.tsx
Normal file
|
@ -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 (
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
// return setBgIndex((bgIndex) => (bgIndex + 1) % bgs.length)
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
height: '100vh',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Canvas
|
||||||
|
dpr={[1.5, 2]}
|
||||||
|
linear
|
||||||
|
gl={{preserveDrawingBuffer: true}}
|
||||||
|
frameloop="demand"
|
||||||
|
>
|
||||||
|
<SheetProvider sheet={getProject('Space').sheet('Scene')}>
|
||||||
|
<ambientLight intensity={0.75} />
|
||||||
|
<e.group theatreKey="trefoil">
|
||||||
|
<TorusKnot scale={[1, 1, 1]} args={[1, 0.3, 128, 64]}>
|
||||||
|
<meshNormalMaterial />
|
||||||
|
</TorusKnot>
|
||||||
|
</e.group>
|
||||||
|
<Stars radius={500} depth={50} count={1000} factor={10} />
|
||||||
|
</SheetProvider>
|
||||||
|
</Canvas>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
|
@ -0,0 +1,11 @@
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Theatre.js Playground</title>
|
||||||
|
<script src="./index.tsx" type="module"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -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},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
]
|
|
@ -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()
|
||||||
|
})
|
||||||
|
})
|
|
@ -374,6 +374,21 @@ export interface IStudio {
|
||||||
* The extension's definition
|
* The extension's definition
|
||||||
*/
|
*/
|
||||||
extension: IExtension,
|
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
|
): void
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -504,8 +519,11 @@ export default class TheatreStudio implements IStudio {
|
||||||
return studio.initialize(opts)
|
return studio.initialize(opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
extend(extension: IExtension): void {
|
extend(
|
||||||
getStudio().extend(extension)
|
extension: IExtension,
|
||||||
|
opts?: {__experimental_reconfigure?: boolean},
|
||||||
|
): void {
|
||||||
|
getStudio().extend(extension, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction(fn: (api: ITransactionAPI) => void): void {
|
transaction(fn: (api: ITransactionAPI) => void): void {
|
||||||
|
|
|
@ -31,14 +31,16 @@ const ExtensionToolsetRender: React.FC<{
|
||||||
}> = ({extension, toolbarId}) => {
|
}> = ({extension, toolbarId}) => {
|
||||||
const toolsetConfigBox = useMemo(() => new Atom<ToolsetConfig>([]), [])
|
const toolsetConfigBox = useMemo(() => new Atom<ToolsetConfig>([]), [])
|
||||||
|
|
||||||
|
const attachFn = extension.toolbars?.[toolbarId]
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const detach = extension.toolbars?.[toolbarId]?.(
|
const detach = attachFn?.(
|
||||||
toolsetConfigBox.set.bind(toolsetConfigBox),
|
toolsetConfigBox.set.bind(toolsetConfigBox),
|
||||||
getStudio()!.publicApi,
|
getStudio()!.publicApi,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (typeof detach === 'function') return detach
|
if (typeof detach === 'function') return detach
|
||||||
}, [extension, toolbarId])
|
}, [extension, toolbarId, attachFn])
|
||||||
|
|
||||||
const config = useVal(toolsetConfigBox.prism)
|
const config = useVal(toolsetConfigBox.prism)
|
||||||
|
|
||||||
|
@ -69,7 +71,7 @@ export const ExtensionToolbar: React.FC<{
|
||||||
if (groups.length === 0) return null
|
if (groups.length === 0) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container data-test-id={`theatre-extensionToolbar-${toolbarId}`}>
|
||||||
{showLeftDivider ? <GroupDivider></GroupDivider> : undefined}
|
{showLeftDivider ? <GroupDivider></GroupDivider> : undefined}
|
||||||
{groups}
|
{groups}
|
||||||
</Container>
|
</Container>
|
||||||
|
|
|
@ -15,10 +15,12 @@ const Container = styled(ToolbarIconButton)`
|
||||||
|
|
||||||
const IconButton: React.FC<{
|
const IconButton: React.FC<{
|
||||||
config: ToolConfigIcon
|
config: ToolConfigIcon
|
||||||
}> = ({config}) => {
|
testId?: string
|
||||||
|
}> = ({config, testId}) => {
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
onClick={config.onClick}
|
onClick={config.onClick}
|
||||||
|
data-testid={testId}
|
||||||
title={config.title}
|
title={config.title}
|
||||||
dangerouslySetInnerHTML={{__html: config['svgSource'] ?? ''}}
|
dangerouslySetInnerHTML={{__html: config['svgSource'] ?? ''}}
|
||||||
/>
|
/>
|
||||||
|
|
Loading…
Reference in a new issue