Transparently hot reload app preview when inserting a custom component bundle and reload relevant components
This commit is contained in:
parent
4e916d2812
commit
3b3d48196e
|
@ -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")
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue