Transparently hot reload app preview when inserting a custom component bundle and reload relevant components

This commit is contained in:
Andrew Kingston 2022-08-11 17:05:42 +01:00
parent 4e916d2812
commit 3b3d48196e
8 changed files with 87 additions and 38 deletions

View File

@ -41,23 +41,10 @@
} }
// Construct iframe template // Construct iframe template
$: pluginLinks = generatePluginLinks($store.usedPlugins) $: template = iframeTemplate.replace(
$: template = iframeTemplate /\{\{ CLIENT_LIB_PATH }}/,
.replace(/\{\{ CLIENT_LIB_PATH }}/, $store.clientLibPath) $store.clientLibPath
.replace(/\{\{ PLUGINS }}/, pluginLinks) )
const generatePluginLinks = plugins => {
if (!plugins?.length) {
return ""
}
return plugins
.map(plugin => {
// Split up like this as otherwise parsing fails because the script
// tags confuse svelte
return `<` + `script src="/plugins/${plugin.jsUrl}"></` + `script>`
})
.join("")
}
const placeholderScreen = new Screen() const placeholderScreen = new Screen()
.name("Screen Placeholder") .name("Screen Placeholder")

View File

@ -37,7 +37,6 @@ export default `
} }
</style> </style>
<script src='{{ CLIENT_LIB_PATH }}'></script> <script src='{{ CLIENT_LIB_PATH }}'></script>
{{ PLUGINS }}
<script> <script>
function receiveMessage(event) { function receiveMessage(event) {
if (!event.data) { if (!event.data) {
@ -67,7 +66,8 @@ export default `
customTheme, customTheme,
previewDevice, previewDevice,
navigation, navigation,
hiddenComponentIds hiddenComponentIds,
usedPlugins,
} = parsed } = parsed
// Set some flags so the app knows we're in the builder // Set some flags so the app knows we're in the builder
@ -82,6 +82,7 @@ export default `
window["##BUDIBASE_PREVIEW_DEVICE##"] = previewDevice window["##BUDIBASE_PREVIEW_DEVICE##"] = previewDevice
window["##BUDIBASE_PREVIEW_NAVIGATION##"] = navigation window["##BUDIBASE_PREVIEW_NAVIGATION##"] = navigation
window["##BUDIBASE_HIDDEN_COMPONENT_IDS##"] = hiddenComponentIds window["##BUDIBASE_HIDDEN_COMPONENT_IDS##"] = hiddenComponentIds
window["##BUDIBASE_USED_PLUGINS##"] = usedPlugins
// Initialise app // Initialise app
try { try {

View File

@ -15,6 +15,7 @@
import { API } from "api" import { API } from "api"
import { writable } from "svelte/store" import { writable } from "svelte/store"
import { redirect } from "@roxi/routify" import { redirect } from "@roxi/routify"
import { onMount } from "svelte"
// Only admins allowed here // Only admins allowed here
$: { $: {
@ -33,11 +34,11 @@
}) })
let loading = false let loading = false
async function uploadLogo(file) { async function uploadLogo() {
try { try {
let data = new FormData() let data = new FormData()
data.append("file", file) data.append("file", $values.logo)
await API.uploadLogo(data) await API.uploadPlugin(data)
} catch (error) { } catch (error) {
notifications.error("Error uploading logo") notifications.error("Error uploading logo")
} }
@ -71,6 +72,11 @@
} }
loading = false loading = false
} }
onMount(async () => {
const plugins = await API.getPlugins()
console.log(plugins)
})
</script> </script>
{#if $auth.isAdmin} {#if $auth.isAdmin}
@ -106,6 +112,7 @@
} }
}} }}
/> />
<button on:click={uploadLogo}>Upload</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -87,6 +87,14 @@
}) })
</script> </script>
<svelte:head>
{#if $builderStore.usedPlugins?.length}
{#each $builderStore.usedPlugins as plugin}
<script src={`/plugins/${plugin.jsUrl}`}></script>
{/each}
{/if}
</svelte:head>
{#if dataLoaded} {#if dataLoaded}
<div <div
id="spectrum-root" id="spectrum-root"

View File

@ -163,14 +163,14 @@
missingRequiredSettings, missingRequiredSettings,
}) })
const initialise = instance => { const initialise = (instance, force = false) => {
if (instance == null) { if (instance == null) {
return return
} }
// Ensure we're processing a new instance // Ensure we're processing a new instance
const instanceKey = Helpers.hashString(JSON.stringify(instance)) const instanceKey = Helpers.hashString(JSON.stringify(instance))
if (instanceKey === lastInstanceKey) { if (instanceKey === lastInstanceKey && !force) {
return return
} else { } else {
lastInstanceKey = instanceKey lastInstanceKey = instanceKey
@ -407,9 +407,11 @@
!componentStore.actions.isComponentRegistered(id) !componentStore.actions.isComponentRegistered(id)
) { ) {
componentStore.actions.registerInstance(id, { componentStore.actions.registerInstance(id, {
component: instance._component,
getSettings: () => cachedSettings, getSettings: () => cachedSettings,
getRawSettings: () => ({ ...staticSettings, ...dynamicSettings }), getRawSettings: () => ({ ...staticSettings, ...dynamicSettings }),
getDataContext: () => get(context), getDataContext: () => get(context),
reload: () => initialise(instance, true),
}) })
} }
}) })

View File

@ -19,7 +19,10 @@
label="Active screen" label="Active screen"
value={$screenStore.activeScreen?.routing.route} value={$screenStore.activeScreen?.routing.route}
/> />
<DevToolsStat label="Components" value={$componentStore.mountedComponents} /> <DevToolsStat
label="Components"
value={$componentStore.mountedComponentCount}
/>
<DevToolsStat label="User" value={$authStore.email} /> <DevToolsStat label="User" value={$authStore.email} />
<DevToolsStat label="Role" value={$authStore.roleId} /> <DevToolsStat label="Role" value={$authStore.roleId} />
</Layout> </Layout>

View File

@ -27,6 +27,7 @@ const loadBudibase = () => {
previewDevice: window["##BUDIBASE_PREVIEW_DEVICE##"], previewDevice: window["##BUDIBASE_PREVIEW_DEVICE##"],
navigation: window["##BUDIBASE_PREVIEW_NAVIGATION##"], navigation: window["##BUDIBASE_PREVIEW_NAVIGATION##"],
hiddenComponentIds: window["##BUDIBASE_HIDDEN_COMPONENT_IDS##"], hiddenComponentIds: window["##BUDIBASE_HIDDEN_COMPONENT_IDS##"],
usedPlugins: window["##BUDIBASE_USED_PLUGINS##"],
}) })
// Set app ID - this window flag is set by both the preview and the real // Set app ID - this window flag is set by both the preview and the real
@ -39,6 +40,11 @@ const loadBudibase = () => {
devToolsStore.actions.setEnabled(enableDevTools) devToolsStore.actions.setEnabled(enableDevTools)
// Register any custom components // Register any custom components
window.registerCustomComponent = plugin => {
componentStore.actions.registerCustomComponent(plugin)
console.log("registered!")
loadBudibase()
}
if (window["##BUDIBASE_CUSTOM_COMPONENTS##"]) { if (window["##BUDIBASE_CUSTOM_COMPONENTS##"]) {
window["##BUDIBASE_CUSTOM_COMPONENTS##"].forEach(component => { window["##BUDIBASE_CUSTOM_COMPONENTS##"].forEach(component => {
componentStore.actions.registerCustomComponent(component) componentStore.actions.registerCustomComponent(component)

View File

@ -10,7 +10,11 @@ import * as AppComponents from "../components/app/index.js"
const budibasePrefix = "@budibase/standard-components/" const budibasePrefix = "@budibase/standard-components/"
const createComponentStore = () => { const createComponentStore = () => {
const store = writable({}) const store = writable({
customComponentManifest: {},
componentsAwaitingConstructors: {},
mountedComponents: {},
})
const derivedStore = derived( const derivedStore = derived(
[store, builderStore, devToolsStore, screenStore], [store, builderStore, devToolsStore, screenStore],
@ -29,9 +33,7 @@ const createComponentStore = () => {
asset = $screenState.activeScreen asset = $screenState.activeScreen
} }
const component = findComponentById(asset?.props, selectedComponentId) const component = findComponentById(asset?.props, selectedComponentId)
const prefix = "@budibase/standard-components/" const definition = getComponentDefinition(component?._component)
const type = component?._component?.replace(prefix, "")
const definition = type ? Manifest[type] : null
// Derive the selected component path // Derive the selected component path
const path = const path =
@ -39,32 +41,50 @@ const createComponentStore = () => {
return { return {
customComponentManifest: $store.customComponentManifest, customComponentManifest: $store.customComponentManifest,
selectedComponentInstance: $store[selectedComponentId], selectedComponentInstance:
$store.mountedComponents[selectedComponentId],
selectedComponent: component, selectedComponent: component,
selectedComponentDefinition: definition, selectedComponentDefinition: definition,
selectedComponentPath: path?.map(component => component._id), selectedComponentPath: path?.map(component => component._id),
mountedComponents: Object.keys($store).length, mountedComponentCount: Object.keys($store.mountedComponents).length,
currentAsset: asset, currentAsset: asset,
} }
} }
) )
const registerInstance = (id, instance) => { const registerInstance = (id, instance) => {
store.update(state => ({ store.update(state => {
// If this is a custom component and does not have an implementation yet,
// store so we can reload this component later
const component = instance.component
let cac = state.componentsAwaitingConstructors
if (!getComponentConstructor(component)) {
if (!cac[component]) {
cac[component] = []
}
cac[component].push(id)
}
return {
...state, ...state,
componentsAwaitingConstructors: cac,
mountedComponents: {
...state.mountedComponents,
[id]: instance, [id]: instance,
})) },
}
})
} }
const unregisterInstance = id => { const unregisterInstance = id => {
store.update(state => { store.update(state => {
delete state[id] delete state.mountedComponents[id]
return state return state
}) })
} }
const isComponentRegistered = id => { const isComponentRegistered = id => {
return get(store)[id] != null return get(store).mountedComponents[id] != null
} }
const getComponentById = id => { const getComponentById = id => {
@ -117,17 +137,32 @@ const createComponentStore = () => {
if (!Component || !schema?.schema?.name) { if (!Component || !schema?.schema?.name) {
return return
} }
const componentName = `plugin/${schema.schema.name}/1.0.0`
store.update(state => { store.update(state => {
if (!state.customComponentManifest) { if (!state.customComponentManifest) {
state.customComponentManifest = {} state.customComponentManifest = {}
} }
const componentName = `plugin/${schema.schema.name}/1.0.0`
state.customComponentManifest[componentName] = { state.customComponentManifest[componentName] = {
schema, schema,
Component, Component,
} }
return state return state
}) })
// Reload any mounted components which depend on this definition
const state = get(store)
if (state.componentsAwaitingConstructors[componentName]?.length) {
state.componentsAwaitingConstructors[componentName].forEach(id => {
const instance = state.mountedComponents[id]
if (instance) {
instance.reload()
}
})
store.update(state => {
delete state.componentsAwaitingConstructors[componentName]
return state
})
}
} }
return { return {