Refactor app preview event sending to support async callbacks in client library

This commit is contained in:
Andrew Kingston 2022-10-24 09:02:50 +01:00
parent 986141b8b1
commit 8d644c5a10
8 changed files with 199 additions and 216 deletions

View File

@ -103,7 +103,7 @@
) )
// Register handler to send custom to the preview // Register handler to send custom to the preview
$: store.actions.preview.registerEventHandler((name, payload) => { $: sendPreviewEvent = (name, payload) => {
iframe?.contentWindow.postMessage( iframe?.contentWindow.postMessage(
JSON.stringify({ JSON.stringify({
name, name,
@ -112,16 +112,38 @@
runtimeEvent: true, runtimeEvent: true,
}) })
) )
}) }
$: store.actions.preview.registerEventHandler(sendPreviewEvent)
// Update the iframe with the builder info to render the correct preview // Update the iframe with the builder info to render the correct preview
const refreshContent = message => { const refreshContent = message => {
iframe?.contentWindow.postMessage(message) iframe?.contentWindow.postMessage(message)
} }
const receiveMessage = message => { const receiveMessage = async message => {
const handlers = { if (!message?.data?.type) {
[MessageTypes.READY]: () => { return
}
// Await the event handler
try {
await handleBudibaseEvent(message)
} catch (error) {
notifications.error(error || "Error handling event from app preview")
}
// Reply that the event has been completed
if (message.data?.id) {
sendPreviewEvent("event-completed", message.data?.id)
}
}
const handleBudibaseEvent = async event => {
const { type, data } = event.data || event.detail
if (!type) {
return
}
if (type === MessageTypes.READY) {
// Initialise the app when mounted // Initialise the app when mounted
if ($store.clientFeatures.messagePassing) { if ($store.clientFeatures.messagePassing) {
if (!loading) return if (!loading) return
@ -133,26 +155,11 @@
loading = false loading = false
} }
refreshContent(json) refreshContent(json)
}, } else if (type === MessageTypes.ERROR) {
[MessageTypes.ERROR]: event => {
// Catch any app errors // Catch any app errors
loading = false loading = false
error = event.error || "An unknown error occurred" error = event.error || "An unknown error occurred"
}, } else if (type === "select-component" && data.id) {
}
const messageHandler = handlers[message.data.type] || handleBudibaseEvent
messageHandler(message)
}
const handleBudibaseEvent = async event => {
const { type, data } = event.data || event.detail
if (!type) {
return
}
try {
if (type === "select-component" && data.id) {
$store.selectedComponentId = data.id $store.selectedComponentId = data.id
if (!$isActive("./components")) { if (!$isActive("./components")) {
$goto("./components") $goto("./components")
@ -225,9 +232,6 @@
} else { } else {
console.warn(`Client sent unknown event type: ${type}`) console.warn(`Client sent unknown event type: ${type}`)
} }
} catch (error) {
notifications.error(error || "Error handling event from app preview")
}
} }
const confirmDeleteComponent = componentId => { const confirmDeleteComponent = componentId => {
@ -259,42 +263,10 @@
onMount(() => { onMount(() => {
window.addEventListener("message", receiveMessage) window.addEventListener("message", receiveMessage)
if (!$store.clientFeatures.messagePassing) {
// Legacy - remove in later versions of BB
iframe.contentWindow.addEventListener(
"ready",
() => {
receiveMessage({ data: { type: MessageTypes.READY } })
},
{ once: true }
)
iframe.contentWindow.addEventListener(
"error",
event => {
receiveMessage({
data: { type: MessageTypes.ERROR, error: event.detail },
})
},
{ once: true }
)
// Add listener for events sent by client library in preview
iframe.contentWindow.addEventListener("bb-event", handleBudibaseEvent)
}
}) })
// Remove all iframe event listeners on component destroy
onDestroy(() => { onDestroy(() => {
window.removeEventListener("message", receiveMessage) window.removeEventListener("message", receiveMessage)
if (iframe.contentWindow) {
if (!$store.clientFeatures.messagePassing) {
// Legacy - remove in later versions of BB
iframe.contentWindow.removeEventListener(
"bb-event",
handleBudibaseEvent
)
}
}
}) })
</script> </script>

View File

@ -22,6 +22,9 @@
$: target = $dndStore.target $: target = $dndStore.target
$: drop = $dndStore.drop $: drop = $dndStore.drop
// Local flag for whether we are awaiting an async drop event
let dropping = false
const insideGrid = e => { const insideGrid = e => {
return e.target return e.target
?.closest?.(".component") ?.closest?.(".component")
@ -48,6 +51,10 @@
// Callback when drag stops (whether dropped or not) // Callback when drag stops (whether dropped or not)
const stopDragging = () => { const stopDragging = () => {
if (dropping) {
return
}
// Reset listener // Reset listener
if (source?.id) { if (source?.id) {
const component = document.getElementsByClassName(source?.id)[0] const component = document.getElementsByClassName(source?.id)[0]
@ -57,10 +64,8 @@
} }
// Reset state // Reset state
if (!$dndStore.dropped) {
dndStore.actions.reset() dndStore.actions.reset()
} }
}
// Callback when initially starting a drag on a draggable component // Callback when initially starting a drag on a draggable component
const onDragStart = e => { const onDragStart = e => {
@ -253,18 +258,21 @@
} }
// Callback when dropping a drag on top of some component // Callback when dropping a drag on top of some component
const onDrop = () => { const onDrop = async () => {
if (!source || !drop?.parent || drop?.index == null) { if (!source || !drop?.parent || drop?.index == null) {
return return
} }
// Check if we're adding a new component rather than moving one // Check if we're adding a new component rather than moving one
if (source.newComponentType) { if (source.newComponentType) {
builderStore.actions.dropNewComponent( dropping = true
await builderStore.actions.dropNewComponent(
source.newComponentType, source.newComponentType,
drop.parent, drop.parent,
drop.index drop.index
) )
dropping = false
stopDragging()
return return
} }
@ -301,12 +309,14 @@
} }
if (legacyDropTarget && legacyDropMode) { if (legacyDropTarget && legacyDropMode) {
dndStore.actions.markDropped() dropping = true
builderStore.actions.moveComponent( await builderStore.actions.moveComponent(
source.id, source.id,
legacyDropTarget, legacyDropTarget,
legacyDropMode legacyDropMode
) )
dropping = false
stopDragging()
} }
} }

View File

@ -7,6 +7,7 @@ import {
componentStore, componentStore,
environmentStore, environmentStore,
dndStore, dndStore,
eventStore,
} from "./stores" } from "./stores"
import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-rollup.js" import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-rollup.js"
import { get } from "svelte/store" import { get } from "svelte/store"
@ -42,9 +43,9 @@ const loadBudibase = async () => {
}) })
// Reset DND state if we completed a successful drop // Reset DND state if we completed a successful drop
if (get(dndStore).dropped) { // if (get(dndStore).dropped) {
dndStore.actions.reset() // dndStore.actions.reset()
} // }
// 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
// server rendered app HTML // server rendered app HTML
@ -59,15 +60,17 @@ const loadBudibase = async () => {
devToolsStore.actions.setEnabled(enableDevTools) devToolsStore.actions.setEnabled(enableDevTools)
// Register handler for runtime events from the builder // Register handler for runtime events from the builder
window.handleBuilderRuntimeEvent = (name, payload) => { window.handleBuilderRuntimeEvent = (type, data) => {
if (!window["##BUDIBASE_IN_BUILDER##"]) { if (!window["##BUDIBASE_IN_BUILDER##"]) {
return return
} }
if (name === "eject-block") { if (type === "event-completed") {
const block = blockStore.actions.getBlock(payload) eventStore.actions.resolveEvent(data)
} else if (type === "eject-block") {
const block = blockStore.actions.getBlock(data)
block?.eject() block?.eject()
} else if (name === "dragging-new-component") { } else if (type === "dragging-new-component") {
const { dragging, component } = payload const { dragging, component } = data
if (dragging) { if (dragging) {
const definition = const definition =
componentStore.actions.getComponentDefinition(component) componentStore.actions.getComponentDefinition(component)

View File

@ -1,10 +1,7 @@
import { writable, get } from "svelte/store" import { writable, get } from "svelte/store"
import { API } from "api" import { API } from "api"
import { devToolsStore } from "./devTools.js" import { devToolsStore } from "./devTools.js"
import { eventStore } from "./events.js"
const dispatchEvent = (type, data = {}) => {
window.parent.postMessage({ type, data })
}
const createBuilderStore = () => { const createBuilderStore = () => {
const initialState = { const initialState = {
@ -19,6 +16,7 @@ const createBuilderStore = () => {
navigation: null, navigation: null,
hiddenComponentIds: [], hiddenComponentIds: [],
usedPlugins: null, usedPlugins: null,
eventResolvers: {},
// Legacy - allow the builder to specify a layout // Legacy - allow the builder to specify a layout
layout: null, layout: null,
@ -35,25 +33,25 @@ const createBuilderStore = () => {
selectedComponentId: id, selectedComponentId: id,
})) }))
devToolsStore.actions.setAllowSelection(false) devToolsStore.actions.setAllowSelection(false)
dispatchEvent("select-component", { id }) eventStore.actions.dispatchEvent("select-component", { id })
}, },
updateProp: (prop, value) => { updateProp: (prop, value) => {
dispatchEvent("update-prop", { prop, value }) eventStore.actions.dispatchEvent("update-prop", { prop, value })
}, },
updateStyles: (styles, id) => { updateStyles: (styles, id) => {
dispatchEvent("update-styles", { styles, id }) eventStore.actions.dispatchEvent("update-styles", { styles, id })
}, },
keyDown: (key, ctrlKey) => { keyDown: (key, ctrlKey) => {
dispatchEvent("key-down", { key, ctrlKey }) eventStore.actions.dispatchEvent("key-down", { key, ctrlKey })
}, },
duplicateComponent: id => { duplicateComponent: id => {
dispatchEvent("duplicate-component", { id }) eventStore.actions.dispatchEvent("duplicate-component", { id })
}, },
deleteComponent: id => { deleteComponent: id => {
dispatchEvent("delete-component", { id }) eventStore.actions.dispatchEvent("delete-component", { id })
}, },
notifyLoaded: () => { notifyLoaded: () => {
dispatchEvent("preview-loaded") eventStore.actions.dispatchEvent("preview-loaded")
}, },
analyticsPing: async () => { analyticsPing: async () => {
try { try {
@ -62,15 +60,15 @@ const createBuilderStore = () => {
// Do nothing // Do nothing
} }
}, },
moveComponent: (componentId, destinationComponentId, mode) => { moveComponent: async (componentId, destinationComponentId, mode) => {
dispatchEvent("move-component", { await eventStore.actions.dispatchEvent("move-component", {
componentId, componentId,
destinationComponentId, destinationComponentId,
mode, mode,
}) })
}, },
dropNewComponent: (component, parent, index) => { dropNewComponent: (component, parent, index) => {
dispatchEvent("drop-new-component", { eventStore.actions.dispatchEvent("drop-new-component", {
component, component,
parent, parent,
index, index,
@ -83,16 +81,16 @@ const createBuilderStore = () => {
store.update(state => ({ ...state, editMode: enabled })) store.update(state => ({ ...state, editMode: enabled }))
}, },
clickNav: () => { clickNav: () => {
dispatchEvent("click-nav") eventStore.actions.dispatchEvent("click-nav")
}, },
requestAddComponent: () => { requestAddComponent: () => {
dispatchEvent("request-add-component") eventStore.actions.dispatchEvent("request-add-component")
}, },
highlightSetting: setting => { highlightSetting: setting => {
dispatchEvent("highlight-setting", { setting }) eventStore.actions.dispatchEvent("highlight-setting", { setting })
}, },
ejectBlock: (id, definition) => { ejectBlock: (id, definition) => {
dispatchEvent("eject-block", { id, definition }) eventStore.actions.dispatchEvent("eject-block", { id, definition })
}, },
updateUsedPlugin: (name, hash) => { updateUsedPlugin: (name, hash) => {
// Check if we used this plugin // Check if we used this plugin
@ -109,7 +107,7 @@ const createBuilderStore = () => {
} }
// Notify the builder so we can reload component definitions // Notify the builder so we can reload component definitions
dispatchEvent("reload-plugin") eventStore.actions.dispatchEvent("reload-plugin")
}, },
} }
return { return {

View File

@ -11,9 +11,6 @@ const createDndStore = () => {
// Info about where the component would be dropped // Info about where the component would be dropped
drop: null, drop: null,
// Whether the current drop has been completed successfully
dropped: false,
} }
const store = writable(initialState) const store = writable(initialState)
@ -63,13 +60,6 @@ const createDndStore = () => {
store.set(initialState) store.set(initialState)
} }
const markDropped = () => {
store.update(state => {
state.dropped = true
return state
})
}
return { return {
subscribe: store.subscribe, subscribe: store.subscribe,
actions: { actions: {
@ -78,7 +68,6 @@ const createDndStore = () => {
updateTarget, updateTarget,
updateDrop, updateDrop,
reset, reset,
markDropped,
}, },
} }
} }
@ -91,7 +80,6 @@ export const dndStore = createDndStore()
// or components which depend on DND state unless values actually change. // or components which depend on DND state unless values actually change.
export const dndParent = computed(dndStore, x => x.drop?.parent) export const dndParent = computed(dndStore, x => x.drop?.parent)
export const dndIndex = computed(dndStore, x => x.drop?.index) export const dndIndex = computed(dndStore, x => x.drop?.index)
export const dndDropped = computed(dndStore, x => x.dropped)
export const dndBounds = computed(dndStore, x => x.source?.bounds) export const dndBounds = computed(dndStore, x => x.source?.bounds)
export const dndIsDragging = computed(dndStore, x => !!x.source) export const dndIsDragging = computed(dndStore, x => !!x.source)
export const dndIsNewComponent = computed( export const dndIsNewComponent = computed(

View File

@ -0,0 +1,31 @@
import { writable, get } from "svelte/store"
const createEventStore = () => {
const initialState = {
eventResolvers: {},
}
const store = writable(initialState)
const actions = {
dispatchEvent: (type, data) => {
const id = Math.random()
return new Promise(resolve => {
window.parent.postMessage({ type, data, id })
store.update(state => {
state.eventResolvers[id] = resolve
return state
})
})
},
resolveEvent: data => {
get(store).eventResolvers[data]?.()
},
}
return {
subscribe: store.subscribe,
actions,
}
}
export const eventStore = createEventStore()

View File

@ -15,6 +15,7 @@ export { uploadStore } from "./uploads.js"
export { rowSelectionStore } from "./rowSelection.js" export { rowSelectionStore } from "./rowSelection.js"
export { blockStore } from "./blocks.js" export { blockStore } from "./blocks.js"
export { environmentStore } from "./environment" export { environmentStore } from "./environment"
export { eventStore } from "./events.js"
export { export {
dndStore, dndStore,
dndIndex, dndIndex,
@ -22,7 +23,6 @@ export {
dndBounds, dndBounds,
dndIsNewComponent, dndIsNewComponent,
dndIsDragging, dndIsDragging,
dndDropped,
} from "./dnd" } from "./dnd"
// Context stores are layered and duplicated, so it is not a singleton // Context stores are layered and duplicated, so it is not a singleton

View File

@ -2,13 +2,7 @@ import { derived } from "svelte/store"
import { routeStore } from "./routes" import { routeStore } from "./routes"
import { builderStore } from "./builder" import { builderStore } from "./builder"
import { appStore } from "./app" import { appStore } from "./app"
import { import { dndIndex, dndParent, dndIsNewComponent, dndBounds } from "./dnd.js"
dndIndex,
dndParent,
dndIsNewComponent,
dndBounds,
dndDropped,
} from "./dnd.js"
import { RoleUtils } from "@budibase/frontend-core" import { RoleUtils } from "@budibase/frontend-core"
import { findComponentById, findComponentParent } from "../utils/components.js" import { findComponentById, findComponentParent } from "../utils/components.js"
import { Helpers } from "@budibase/bbui" import { Helpers } from "@budibase/bbui"
@ -24,7 +18,6 @@ const createScreenStore = () => {
dndIndex, dndIndex,
dndIsNewComponent, dndIsNewComponent,
dndBounds, dndBounds,
dndDropped,
], ],
([ ([
$appStore, $appStore,
@ -34,7 +27,6 @@ const createScreenStore = () => {
$dndIndex, $dndIndex,
$dndIsNewComponent, $dndIsNewComponent,
$dndBounds, $dndBounds,
$dndDropped,
]) => { ]) => {
let activeLayout, activeScreen let activeLayout, activeScreen
let screens let screens
@ -80,9 +72,6 @@ const createScreenStore = () => {
activeScreen.props, activeScreen.props,
selectedComponentId selectedComponentId
) )
const selectedComponent = selectedParent?._children?.find(
x => x._id === selectedComponentId
)
// Remove selected component from tree if we are moving an existing // Remove selected component from tree if we are moving an existing
// component // component
@ -92,13 +81,8 @@ const createScreenStore = () => {
) )
} }
// Insert placeholder component if dragging, or artificially insert // Insert placeholder component
// the dropped component in the new location if the drop completed const componentToInsert = {
let componentToInsert
if ($dndDropped && !$dndIsNewComponent) {
componentToInsert = selectedComponent
} else {
componentToInsert = {
_component: "@budibase/standard-components/container", _component: "@budibase/standard-components/container",
_id: DNDPlaceholderID, _id: DNDPlaceholderID,
_styles: { _styles: {
@ -110,8 +94,6 @@ const createScreenStore = () => {
}, },
static: true, static: true,
} }
}
if (componentToInsert) {
let parent = findComponentById(activeScreen.props, $dndParent) let parent = findComponentById(activeScreen.props, $dndParent)
if (parent) { if (parent) {
if (!parent._children?.length) { if (!parent._children?.length) {
@ -121,7 +103,6 @@ const createScreenStore = () => {
} }
} }
} }
}
// Assign ranks to screens, preferring higher roles and home screens // Assign ranks to screens, preferring higher roles and home screens
screens.forEach(screen => { screens.forEach(screen => {