From b123a056aba49c9169d96b223b65e46c251d1dc3 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 11 Aug 2022 17:04:47 +0100 Subject: [PATCH 1/3] Notify client when plugins are added when saving screens --- packages/server/src/api/controllers/screen.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/src/api/controllers/screen.js b/packages/server/src/api/controllers/screen.js index 52a22f1073..1d5d876dc4 100644 --- a/packages/server/src/api/controllers/screen.js +++ b/packages/server/src/api/controllers/screen.js @@ -41,6 +41,7 @@ exports.save = async ctx => { // Find any custom components being used let pluginNames = [] + let pluginAdded = false findPlugins(screen.props, pluginNames) if (pluginNames.length) { const globalDB = getGlobalDB() @@ -62,7 +63,6 @@ exports.save = async ctx => { const application = await db.get(DocumentTypes.APP_METADATA) let usedPlugins = application.usedPlugins || [] - let pluginAdded = false requiredPlugins.forEach(plugin => { if (!usedPlugins.find(x => x._id === plugin._id)) { pluginAdded = true @@ -76,7 +76,6 @@ exports.save = async ctx => { }) if (pluginAdded) { - console.log("plugin added! new plugins", usedPlugins) await updateAppPackage({ usedPlugins }, ctx.appId) } } @@ -89,6 +88,7 @@ exports.save = async ctx => { ...screen, _id: response.id, _rev: response.rev, + pluginAdded, } } From 5d554df8538d708abae4ed8f305ee28700214f07 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 11 Aug 2022 17:05:04 +0100 Subject: [PATCH 2/3] Reload app metadata when adding custom components --- packages/builder/src/builderStore/store/frontend.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index 16d16de8a2..359f287abe 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -227,9 +227,18 @@ export const getFrontendStore = () => { }) }, save: async screen => { + const state = get(store) const creatingNewScreen = screen._id === undefined const savedScreen = await API.saveScreen(screen) const routesResponse = await API.fetchAppRoutes() + let usedPlugins = state.usedPlugins + + // If plugins changed we need to fetch the latest app metadata + if (savedScreen.pluginAdded) { + const { application } = await API.fetchAppPackage(state.appId) + usedPlugins = application.usedPlugins || [] + } + store.update(state => { // Update screen object const idx = state.screens.findIndex(x => x._id === savedScreen._id) @@ -248,6 +257,9 @@ export const getFrontendStore = () => { // Update routes state.routes = routesResponse.routes + // Update used plugins + state.usedPlugins = usedPlugins + return state }) return savedScreen From 3a07002310fad102205e2a153672ebbad734ed64 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 11 Aug 2022 17:05:42 +0100 Subject: [PATCH 3/3] Transparently hot reload app preview when inserting a custom component bundle and reload relevant components --- .../[screenId]/_components/AppPreview.svelte | 21 ++----- .../[screenId]/_components/iframeTemplate.js | 5 +- .../portal/settings/organisation.svelte | 13 +++- .../client/src/components/ClientApp.svelte | 8 +++ .../client/src/components/Component.svelte | 6 +- .../devtools/DevToolsStatsTab.svelte | 5 +- packages/client/src/index.js | 6 ++ packages/client/src/stores/components.js | 61 +++++++++++++++---- 8 files changed, 87 insertions(+), 38 deletions(-) diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte index cd86d2e87c..ff880cd5e8 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte @@ -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}">` - }) - .join("") - } + $: template = iframeTemplate.replace( + /\{\{ CLIENT_LIB_PATH }}/, + $store.clientLibPath + ) const placeholderScreen = new Screen() .name("Screen Placeholder") diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/iframeTemplate.js b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/iframeTemplate.js index 2b24ee4fa0..8711f5d670 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/iframeTemplate.js +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/iframeTemplate.js @@ -37,7 +37,6 @@ export default ` } - {{ PLUGINS }} {#if $auth.isAdmin} @@ -106,6 +112,7 @@ } }} /> + diff --git a/packages/client/src/components/ClientApp.svelte b/packages/client/src/components/ClientApp.svelte index 64b1712b89..86fd56feb1 100644 --- a/packages/client/src/components/ClientApp.svelte +++ b/packages/client/src/components/ClientApp.svelte @@ -87,6 +87,14 @@ }) + + {#if $builderStore.usedPlugins?.length} + {#each $builderStore.usedPlugins as plugin} + + {/each} + {/if} + + {#if dataLoaded}
{ + 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), }) } }) diff --git a/packages/client/src/components/devtools/DevToolsStatsTab.svelte b/packages/client/src/components/devtools/DevToolsStatsTab.svelte index b20b9fafa0..24f587332c 100644 --- a/packages/client/src/components/devtools/DevToolsStatsTab.svelte +++ b/packages/client/src/components/devtools/DevToolsStatsTab.svelte @@ -19,7 +19,10 @@ label="Active screen" value={$screenStore.activeScreen?.routing.route} /> - + diff --git a/packages/client/src/index.js b/packages/client/src/index.js index 32b242bc69..bef3ab9c12 100644 --- a/packages/client/src/index.js +++ b/packages/client/src/index.js @@ -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) diff --git a/packages/client/src/stores/components.js b/packages/client/src/stores/components.js index a07c9da996..94dd16a957 100644 --- a/packages/client/src/stores/components.js +++ b/packages/client/src/stores/components.js @@ -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 {