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
$: store.actions.preview.registerEventHandler((name, payload) => {
$: sendPreviewEvent = (name, payload) => {
iframe?.contentWindow.postMessage(
JSON.stringify({
name,
@ -112,16 +112,38 @@
runtimeEvent: true,
})
)
})
}
$: store.actions.preview.registerEventHandler(sendPreviewEvent)
// Update the iframe with the builder info to render the correct preview
const refreshContent = message => {
iframe?.contentWindow.postMessage(message)
}
const receiveMessage = message => {
const handlers = {
[MessageTypes.READY]: () => {
const receiveMessage = async message => {
if (!message?.data?.type) {
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
if ($store.clientFeatures.messagePassing) {
if (!loading) return
@ -133,26 +155,11 @@
loading = false
}
refreshContent(json)
},
[MessageTypes.ERROR]: event => {
} else if (type === MessageTypes.ERROR) {
// Catch any app errors
loading = false
error = event.error || "An unknown error occurred"
},
}
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) {
} else if (type === "select-component" && data.id) {
$store.selectedComponentId = data.id
if (!$isActive("./components")) {
$goto("./components")
@ -225,9 +232,6 @@
} else {
console.warn(`Client sent unknown event type: ${type}`)
}
} catch (error) {
notifications.error(error || "Error handling event from app preview")
}
}
const confirmDeleteComponent = componentId => {
@ -259,42 +263,10 @@
onMount(() => {
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(() => {
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>

View File

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

View File

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

View File

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

View File

@ -11,9 +11,6 @@ const createDndStore = () => {
// Info about where the component would be dropped
drop: null,
// Whether the current drop has been completed successfully
dropped: false,
}
const store = writable(initialState)
@ -63,13 +60,6 @@ const createDndStore = () => {
store.set(initialState)
}
const markDropped = () => {
store.update(state => {
state.dropped = true
return state
})
}
return {
subscribe: store.subscribe,
actions: {
@ -78,7 +68,6 @@ const createDndStore = () => {
updateTarget,
updateDrop,
reset,
markDropped,
},
}
}
@ -91,7 +80,6 @@ export const dndStore = createDndStore()
// or components which depend on DND state unless values actually change.
export const dndParent = computed(dndStore, x => x.drop?.parent)
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 dndIsDragging = computed(dndStore, x => !!x.source)
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 { blockStore } from "./blocks.js"
export { environmentStore } from "./environment"
export { eventStore } from "./events.js"
export {
dndStore,
dndIndex,
@ -22,7 +23,6 @@ export {
dndBounds,
dndIsNewComponent,
dndIsDragging,
dndDropped,
} from "./dnd"
// 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 { builderStore } from "./builder"
import { appStore } from "./app"
import {
dndIndex,
dndParent,
dndIsNewComponent,
dndBounds,
dndDropped,
} from "./dnd.js"
import { dndIndex, dndParent, dndIsNewComponent, dndBounds } from "./dnd.js"
import { RoleUtils } from "@budibase/frontend-core"
import { findComponentById, findComponentParent } from "../utils/components.js"
import { Helpers } from "@budibase/bbui"
@ -24,7 +18,6 @@ const createScreenStore = () => {
dndIndex,
dndIsNewComponent,
dndBounds,
dndDropped,
],
([
$appStore,
@ -34,7 +27,6 @@ const createScreenStore = () => {
$dndIndex,
$dndIsNewComponent,
$dndBounds,
$dndDropped,
]) => {
let activeLayout, activeScreen
let screens
@ -80,9 +72,6 @@ const createScreenStore = () => {
activeScreen.props,
selectedComponentId
)
const selectedComponent = selectedParent?._children?.find(
x => x._id === selectedComponentId
)
// Remove selected component from tree if we are moving an existing
// component
@ -92,13 +81,8 @@ const createScreenStore = () => {
)
}
// Insert placeholder component if dragging, or artificially insert
// the dropped component in the new location if the drop completed
let componentToInsert
if ($dndDropped && !$dndIsNewComponent) {
componentToInsert = selectedComponent
} else {
componentToInsert = {
// Insert placeholder component
const componentToInsert = {
_component: "@budibase/standard-components/container",
_id: DNDPlaceholderID,
_styles: {
@ -110,8 +94,6 @@ const createScreenStore = () => {
},
static: true,
}
}
if (componentToInsert) {
let parent = findComponentById(activeScreen.props, $dndParent)
if (parent) {
if (!parent._children?.length) {
@ -121,7 +103,6 @@ const createScreenStore = () => {
}
}
}
}
// Assign ranks to screens, preferring higher roles and home screens
screens.forEach(screen => {