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
$: pluginLinks = generatePluginLinks($store.usedPlugins)
$: template = iframeTemplate
.replace(/\{\{ CLIENT_LIB_PATH }}/, $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("")
}
$: template = iframeTemplate.replace(
/\{\{ CLIENT_LIB_PATH }}/,
$store.clientLibPath
)
const placeholderScreen = new Screen()
.name("Screen Placeholder")

View File

@ -37,7 +37,6 @@ export default `
}
</style>
<script src='{{ CLIENT_LIB_PATH }}'></script>
{{ PLUGINS }}
<script>
function receiveMessage(event) {
if (!event.data) {
@ -67,7 +66,8 @@ export default `
customTheme,
previewDevice,
navigation,
hiddenComponentIds
hiddenComponentIds,
usedPlugins,
} = parsed
// 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_NAVIGATION##"] = navigation
window["##BUDIBASE_HIDDEN_COMPONENT_IDS##"] = hiddenComponentIds
window["##BUDIBASE_USED_PLUGINS##"] = usedPlugins
// Initialise app
try {

View File

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

View File

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

View File

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

View File

@ -19,7 +19,10 @@
label="Active screen"
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="Role" value={$authStore.roleId} />
</Layout>

View File

@ -27,6 +27,7 @@ const loadBudibase = () => {
previewDevice: window["##BUDIBASE_PREVIEW_DEVICE##"],
navigation: window["##BUDIBASE_PREVIEW_NAVIGATION##"],
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
@ -39,6 +40,11 @@ const loadBudibase = () => {
devToolsStore.actions.setEnabled(enableDevTools)
// Register any custom components
window.registerCustomComponent = plugin => {
componentStore.actions.registerCustomComponent(plugin)
console.log("registered!")
loadBudibase()
}
if (window["##BUDIBASE_CUSTOM_COMPONENTS##"]) {
window["##BUDIBASE_CUSTOM_COMPONENTS##"].forEach(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 createComponentStore = () => {
const store = writable({})
const store = writable({
customComponentManifest: {},
componentsAwaitingConstructors: {},
mountedComponents: {},
})
const derivedStore = derived(
[store, builderStore, devToolsStore, screenStore],
@ -29,9 +33,7 @@ const createComponentStore = () => {
asset = $screenState.activeScreen
}
const component = findComponentById(asset?.props, selectedComponentId)
const prefix = "@budibase/standard-components/"
const type = component?._component?.replace(prefix, "")
const definition = type ? Manifest[type] : null
const definition = getComponentDefinition(component?._component)
// Derive the selected component path
const path =
@ -39,32 +41,50 @@ const createComponentStore = () => {
return {
customComponentManifest: $store.customComponentManifest,
selectedComponentInstance: $store[selectedComponentId],
selectedComponentInstance:
$store.mountedComponents[selectedComponentId],
selectedComponent: component,
selectedComponentDefinition: definition,
selectedComponentPath: path?.map(component => component._id),
mountedComponents: Object.keys($store).length,
mountedComponentCount: Object.keys($store.mountedComponents).length,
currentAsset: asset,
}
}
)
const registerInstance = (id, instance) => {
store.update(state => ({
...state,
[id]: instance,
}))
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,
componentsAwaitingConstructors: cac,
mountedComponents: {
...state.mountedComponents,
[id]: instance,
},
}
})
}
const unregisterInstance = id => {
store.update(state => {
delete state[id]
delete state.mountedComponents[id]
return state
})
}
const isComponentRegistered = id => {
return get(store)[id] != null
return get(store).mountedComponents[id] != null
}
const getComponentById = id => {
@ -117,17 +137,32 @@ const createComponentStore = () => {
if (!Component || !schema?.schema?.name) {
return
}
const componentName = `plugin/${schema.schema.name}/1.0.0`
store.update(state => {
if (!state.customComponentManifest) {
state.customComponentManifest = {}
}
const componentName = `plugin/${schema.schema.name}/1.0.0`
state.customComponentManifest[componentName] = {
schema,
Component,
}
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 {