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,37 +112,30 @@
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]: () => {
// Initialise the app when mounted
if ($store.clientFeatures.messagePassing) {
if (!loading) return
}
// Display preview immediately if the intelligent loading feature
// is not supported
if (!$store.clientFeatures.intelligentLoading) {
loading = false
}
refreshContent(json)
},
[MessageTypes.ERROR]: event => {
// Catch any app errors
loading = false
error = event.error || "An unknown error occurred"
},
const receiveMessage = async message => {
if (!message?.data?.type) {
return
}
const messageHandler = handlers[message.data.type] || handleBudibaseEvent
messageHandler(message)
// 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 => {
@ -150,83 +143,94 @@
if (!type) {
return
}
try {
if (type === "select-component" && data.id) {
$store.selectedComponentId = data.id
if (!$isActive("./components")) {
$goto("./components")
}
} else if (type === "update-prop") {
await store.actions.components.updateSetting(data.prop, data.value)
} else if (type === "update-styles") {
await store.actions.components.updateStyles(data.styles, data.id)
} else if (type === "delete-component" && data.id) {
// Legacy type, can be deleted in future
confirmDeleteComponent(data.id)
} else if (type === "key-down") {
const { key, ctrlKey } = data
document.dispatchEvent(new KeyboardEvent("keydown", { key, ctrlKey }))
} else if (type === "duplicate-component" && data.id) {
const rootComponent = get(currentAsset).props
const component = findComponent(rootComponent, data.id)
store.actions.components.copy(component)
await store.actions.components.paste(component)
} else if (type === "preview-loaded") {
// Wait for this event to show the client library if intelligent
// loading is supported
loading = false
} else if (type === "move-component") {
const { componentId, destinationComponentId } = data
const rootComponent = get(currentAsset).props
// Get source and destination components
const source = findComponent(rootComponent, componentId)
const destination = findComponent(rootComponent, destinationComponentId)
// Stop if the target is a child of source
const path = findComponentPath(source, destinationComponentId)
const ids = path.map(component => component._id)
if (ids.includes(data.destinationComponentId)) {
return
}
// Cut and paste the component to the new destination
if (source && destination) {
store.actions.components.copy(source, true, false)
await store.actions.components.paste(destination, data.mode)
}
} else if (type === "click-nav") {
if (!$isActive("./navigation")) {
$goto("./navigation")
}
} else if (type === "request-add-component") {
toggleAddComponent()
} else if (type === "highlight-setting") {
store.actions.settings.highlight(data.setting)
// Also scroll setting into view
const selector = `[data-cy="${data.setting}-prop-control"`
const element = document.querySelector(selector)?.parentElement
if (element) {
element.scrollIntoView({
behavior: "smooth",
block: "center",
})
}
} else if (type === "eject-block") {
const { id, definition } = data
await store.actions.components.handleEjectBlock(id, definition)
} else if (type === "reload-plugin") {
await store.actions.components.refreshDefinitions()
} else if (type === "drop-new-component") {
const { component, parent, index } = data
await store.actions.components.create(component, null, parent, index)
} else {
console.warn(`Client sent unknown event type: ${type}`)
if (type === MessageTypes.READY) {
// Initialise the app when mounted
if ($store.clientFeatures.messagePassing) {
if (!loading) return
}
} catch (error) {
notifications.error(error || "Error handling event from app preview")
// Display preview immediately if the intelligent loading feature
// is not supported
if (!$store.clientFeatures.intelligentLoading) {
loading = false
}
refreshContent(json)
} else if (type === MessageTypes.ERROR) {
// Catch any app errors
loading = false
error = event.error || "An unknown error occurred"
} else if (type === "select-component" && data.id) {
$store.selectedComponentId = data.id
if (!$isActive("./components")) {
$goto("./components")
}
} else if (type === "update-prop") {
await store.actions.components.updateSetting(data.prop, data.value)
} else if (type === "update-styles") {
await store.actions.components.updateStyles(data.styles, data.id)
} else if (type === "delete-component" && data.id) {
// Legacy type, can be deleted in future
confirmDeleteComponent(data.id)
} else if (type === "key-down") {
const { key, ctrlKey } = data
document.dispatchEvent(new KeyboardEvent("keydown", { key, ctrlKey }))
} else if (type === "duplicate-component" && data.id) {
const rootComponent = get(currentAsset).props
const component = findComponent(rootComponent, data.id)
store.actions.components.copy(component)
await store.actions.components.paste(component)
} else if (type === "preview-loaded") {
// Wait for this event to show the client library if intelligent
// loading is supported
loading = false
} else if (type === "move-component") {
const { componentId, destinationComponentId } = data
const rootComponent = get(currentAsset).props
// Get source and destination components
const source = findComponent(rootComponent, componentId)
const destination = findComponent(rootComponent, destinationComponentId)
// Stop if the target is a child of source
const path = findComponentPath(source, destinationComponentId)
const ids = path.map(component => component._id)
if (ids.includes(data.destinationComponentId)) {
return
}
// Cut and paste the component to the new destination
if (source && destination) {
store.actions.components.copy(source, true, false)
await store.actions.components.paste(destination, data.mode)
}
} else if (type === "click-nav") {
if (!$isActive("./navigation")) {
$goto("./navigation")
}
} else if (type === "request-add-component") {
toggleAddComponent()
} else if (type === "highlight-setting") {
store.actions.settings.highlight(data.setting)
// Also scroll setting into view
const selector = `[data-cy="${data.setting}-prop-control"`
const element = document.querySelector(selector)?.parentElement
if (element) {
element.scrollIntoView({
behavior: "smooth",
block: "center",
})
}
} else if (type === "eject-block") {
const { id, definition } = data
await store.actions.components.handleEjectBlock(id, definition)
} else if (type === "reload-plugin") {
await store.actions.components.refreshDefinitions()
} else if (type === "drop-new-component") {
const { component, parent, index } = data
await store.actions.components.create(component, null, parent, index)
} else {
console.warn(`Client sent unknown event type: ${type}`)
}
}
@ -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,9 +64,7 @@
}
// Reset state
if (!$dndStore.dropped) {
dndStore.actions.reset()
}
dndStore.actions.reset()
}
// Callback when initially starting a drag on a draggable component
@ -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,33 +81,25 @@ 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 = {
_component: "@budibase/standard-components/container",
_id: DNDPlaceholderID,
_styles: {
normal: {
width: `${$dndBounds?.width || 400}px`,
height: `${$dndBounds?.height || 200}px`,
opacity: 0,
},
// Insert placeholder component
const componentToInsert = {
_component: "@budibase/standard-components/container",
_id: DNDPlaceholderID,
_styles: {
normal: {
width: `${$dndBounds?.width || 400}px`,
height: `${$dndBounds?.height || 200}px`,
opacity: 0,
},
static: true,
}
},
static: true,
}
if (componentToInsert) {
let parent = findComponentById(activeScreen.props, $dndParent)
if (parent) {
if (!parent._children?.length) {
parent._children = [componentToInsert]
} else {
parent._children.splice($dndIndex, 0, componentToInsert)
}
let parent = findComponentById(activeScreen.props, $dndParent)
if (parent) {
if (!parent._children?.length) {
parent._children = [componentToInsert]
} else {
parent._children.splice($dndIndex, 0, componentToInsert)
}
}
}