budibase/packages/builder/src/builderStore/store/frontend.js

609 lines
18 KiB
JavaScript
Raw Normal View History

import { get, writable } from "svelte/store"
2020-11-06 13:31:47 +01:00
import { cloneDeep } from "lodash/fp"
import {
allScreens,
hostingStore,
currentAsset,
mainLayout,
selectedComponent,
selectedAccessRole,
} from "builderStore"
2021-03-23 13:31:18 +01:00
import {
datasources,
integrations,
queries,
database,
tables,
2021-04-01 11:29:47 +02:00
} from "stores/backend"
import { fetchComponentLibDefinitions } from "../loadComponentLibraries"
import api from "../api"
import { FrontendTypes } from "constants"
import analytics from "analytics"
import { findComponentType, findComponentParent } from "../storeUtils"
import { uuid } from "../uuid"
import { removeBindings } from "../dataBinding"
const INITIAL_FRONTEND_STATE = {
apps: [],
name: "",
url: "",
description: "",
layouts: [],
2020-11-24 19:11:34 +01:00
screens: [],
components: [],
clientFeatures: {
spectrumThemes: false,
intelligentLoading: false,
},
currentFrontEndType: "none",
selectedScreenId: "",
selectedLayoutId: "",
selectedComponentId: "",
errors: [],
hasAppPackage: false,
libraries: null,
appId: "",
2020-11-19 22:07:25 +01:00
routes: {},
clientLibPath: "",
theme: "",
}
export const getFrontendStore = () => {
const store = writable({ ...INITIAL_FRONTEND_STATE })
store.actions = {
2021-05-04 12:32:22 +02:00
initialise: async pkg => {
const { layouts, screens, application, clientLibPath } = pkg
2021-05-16 22:25:37 +02:00
const components = await fetchComponentLibDefinitions(application.appId)
2021-05-04 12:32:22 +02:00
store.update(state => ({
2020-11-04 18:09:45 +01:00
...state,
libraries: application.componentLibraries,
2020-11-04 18:09:45 +01:00
components,
clientFeatures: {
...state.clientFeatures,
...components.features,
},
name: application.name,
description: application.description,
2021-05-16 22:25:37 +02:00
appId: application.appId,
url: application.url,
2020-11-24 19:11:34 +01:00
layouts,
screens,
theme: application.theme || "spectrum--light",
2020-11-04 18:09:45 +01:00
hasAppPackage: true,
appInstance: application.instance,
clientLibPath,
previousTopNavPath: {},
version: application.version,
revertableVersion: application.revertableVersion,
2020-11-04 18:09:45 +01:00
}))
await hostingStore.actions.fetch()
// Initialise backend stores
2021-04-01 08:40:30 +02:00
const [_integrations] = await Promise.all([
2021-05-04 12:32:22 +02:00
api.get("/api/integrations").then(r => r.json()),
])
2021-03-29 14:57:01 +02:00
datasources.init()
integrations.set(_integrations)
2021-03-29 17:45:18 +02:00
queries.init()
database.set(application.instance)
tables.init()
},
theme: {
save: async theme => {
const appId = get(store).appId
const response = await api.put(`/api/applications/${appId}`, { theme })
if (response.status === 200) {
store.update(state => {
state.theme = theme
return state
})
} else {
throw new Error("Error updating theme")
}
},
},
2020-11-19 22:07:25 +01:00
routing: {
fetch: async () => {
const response = await api.get("/api/routing")
const json = await response.json()
2021-05-04 12:32:22 +02:00
store.update(state => {
2020-11-19 22:07:25 +01:00
state.routes = json.routes
return state
})
},
},
screens: {
2021-05-04 12:32:22 +02:00
select: screenId => {
store.update(state => {
let screens = get(allScreens)
let screen =
2021-05-04 12:32:22 +02:00
screens.find(screen => screen._id === screenId) || screens[0]
2020-12-08 17:55:43 +01:00
if (!screen) return state
// Update role to the screen's role setting so that it will always
// be visible
selectedAccessRole.set(screen.routing.roleId)
state.currentFrontEndType = FrontendTypes.SCREEN
state.selectedScreenId = screen._id
state.currentView = "detail"
2020-12-08 17:55:43 +01:00
state.selectedComponentId = screen.props?._id
return state
})
},
2021-05-04 12:32:22 +02:00
create: async screen => {
screen = await store.actions.screens.save(screen)
2021-05-04 12:32:22 +02:00
store.update(state => {
state.selectedScreenId = screen._id
2020-12-07 21:29:41 +01:00
state.selectedComponentId = screen.props._id
state.currentFrontEndType = FrontendTypes.SCREEN
selectedAccessRole.set(screen.routing.roleId)
return state
})
return screen
},
2021-05-04 12:32:22 +02:00
save: async screen => {
const creatingNewScreen = screen._id === undefined
const response = await api.post(`/api/screens`, screen)
if (response.status !== 200) {
return
}
screen = await response.json()
await store.actions.routing.fetch()
2021-05-04 12:32:22 +02:00
store.update(state => {
const foundScreen = state.screens.findIndex(
2021-05-04 12:32:22 +02:00
el => el._id === screen._id
)
if (foundScreen !== -1) {
state.screens.splice(foundScreen, 1)
}
state.screens.push(screen)
return state
})
if (creatingNewScreen) {
store.actions.screens.select(screen._id)
}
return screen
},
2021-05-04 12:32:22 +02:00
delete: async screens => {
const screensToDelete = Array.isArray(screens) ? screens : [screens]
const screenDeletePromises = []
2021-05-04 12:32:22 +02:00
store.update(state => {
for (let screenToDelete of screensToDelete) {
state.screens = state.screens.filter(
2021-05-04 12:32:22 +02:00
screen => screen._id !== screenToDelete._id
)
screenDeletePromises.push(
api.delete(
`/api/screens/${screenToDelete._id}/${screenToDelete._rev}`
)
)
if (screenToDelete._id === state.selectedScreenId) {
state.selectedScreenId = null
2020-12-08 17:55:43 +01:00
}
}
return state
})
await Promise.all(screenDeletePromises)
},
},
preview: {
2020-12-07 21:29:41 +01:00
saveSelected: async () => {
const state = get(store)
2020-12-07 21:29:41 +01:00
const selectedAsset = get(currentAsset)
if (state.currentFrontEndType !== FrontendTypes.LAYOUT) {
2020-12-02 14:41:00 +01:00
await store.actions.screens.save(selectedAsset)
} else {
await store.actions.layouts.save(selectedAsset)
2020-11-09 16:55:36 +01:00
}
},
2020-11-04 18:09:45 +01:00
},
2020-11-24 19:11:34 +01:00
layouts: {
2021-05-04 12:32:22 +02:00
select: layoutId => {
store.update(state => {
const layout =
store.actions.layouts.find(layoutId) || get(store).layouts[0]
if (!layout) return
state.currentFrontEndType = FrontendTypes.LAYOUT
2020-11-04 18:09:45 +01:00
state.currentView = "detail"
state.selectedLayoutId = layout._id
2020-12-08 17:55:43 +01:00
state.selectedComponentId = layout.props?._id
2020-11-04 18:09:45 +01:00
return state
})
},
2021-05-04 12:32:22 +02:00
save: async layout => {
2020-12-02 14:41:00 +01:00
const layoutToSave = cloneDeep(layout)
const creatingNewLayout = layoutToSave._id === undefined
2020-12-02 14:41:00 +01:00
const response = await api.post(`/api/layouts`, layoutToSave)
const savedLayout = await response.json()
2020-11-13 11:29:54 +01:00
// Abort if saving failed
if (response.status !== 200) {
return
}
2021-05-04 12:32:22 +02:00
store.update(state => {
2020-12-05 10:43:00 +01:00
const layoutIdx = state.layouts.findIndex(
2021-05-04 12:32:22 +02:00
stateLayout => stateLayout._id === savedLayout._id
)
2020-12-05 10:43:00 +01:00
if (layoutIdx >= 0) {
// update existing layout
state.layouts.splice(layoutIdx, 1, savedLayout)
} else {
2020-12-05 10:43:00 +01:00
// save new layout
state.layouts.push(savedLayout)
}
2020-11-04 18:09:45 +01:00
return state
})
// Select layout if creating a new one
if (creatingNewLayout) {
store.actions.layouts.select(savedLayout._id)
}
return savedLayout
2020-11-04 18:09:45 +01:00
},
2021-05-04 12:32:22 +02:00
find: layoutId => {
2020-12-02 15:15:07 +01:00
if (!layoutId) {
return get(mainLayout)
}
const storeContents = get(store)
2021-05-04 12:32:22 +02:00
return storeContents.layouts.find(layout => layout._id === layoutId)
},
2021-05-04 12:32:22 +02:00
delete: async layoutToDelete => {
2020-12-05 00:42:22 +01:00
const response = await api.delete(
`/api/layouts/${layoutToDelete._id}/${layoutToDelete._rev}`
)
if (response.status !== 200) {
const json = await response.json()
throw new Error(json.message)
}
2021-05-04 12:32:22 +02:00
store.update(state => {
2020-12-05 00:42:22 +01:00
state.layouts = state.layouts.filter(
2021-05-04 12:32:22 +02:00
layout => layout._id !== layoutToDelete._id
2020-12-05 00:42:22 +01:00
)
if (layoutToDelete._id === state.selectedLayoutId) {
state.selectedLayoutId = get(mainLayout)._id
}
2020-12-05 00:42:22 +01:00
return state
})
},
2020-11-04 18:09:45 +01:00
},
components: {
2021-05-04 12:32:22 +02:00
select: component => {
if (!component) {
return
}
// If this is the root component, select the asset instead
const asset = get(currentAsset)
const parent = findComponentParent(asset.props, component._id)
if (parent == null) {
const state = get(store)
const isLayout = state.currentFrontEndType === FrontendTypes.LAYOUT
if (isLayout) {
store.actions.layouts.select(asset._id)
} else {
store.actions.screens.select(asset._id)
}
return
}
// Otherwise select the component
2021-05-04 12:32:22 +02:00
store.update(state => {
state.selectedComponentId = component._id
2020-11-04 18:09:45 +01:00
state.currentView = "component"
return state
})
},
2021-05-04 12:32:22 +02:00
getDefinition: componentName => {
if (!componentName) {
return null
}
if (!componentName.startsWith("@budibase")) {
componentName = `@budibase/standard-components/${componentName}`
}
return get(store).components[componentName]
},
createInstance: (componentName, presetProps) => {
const definition = store.actions.components.getDefinition(componentName)
if (!definition) {
return null
}
// Generate default props
let props = { ...presetProps }
if (definition.settings) {
2021-05-04 12:32:22 +02:00
definition.settings.forEach(setting => {
if (setting.defaultValue !== undefined) {
props[setting.key] = setting.defaultValue
}
2020-11-06 13:31:47 +01:00
})
}
// Add any extra properties the component needs
let extras = {}
if (definition.hasChildren) {
extras._children = []
}
return {
_id: uuid(),
_component: definition.component,
_styles: { normal: {}, hover: {}, active: {} },
_instanceName: `New ${definition.name}`,
...cloneDeep(props),
...extras,
}
},
create: async (componentName, presetProps) => {
const selected = get(selectedComponent)
const asset = get(currentAsset)
// Create new component
const componentInstance = store.actions.components.createInstance(
componentName,
presetProps
)
if (!componentInstance) {
return
}
// Find parent node to attach this component to
let parentComponent
if (!asset) {
return
}
if (selected) {
// Use current screen or layout as parent if no component is selected
const definition = store.actions.components.getDefinition(
selected._component
)
if (definition?.hasChildren) {
// Use selected component if it allows children
parentComponent = selected
} else {
// Otherwise we need to use the parent of this component
parentComponent = findComponentParent(asset.props, selected._id)
2020-11-04 18:09:45 +01:00
}
} else {
// Use screen or layout if no component is selected
parentComponent = asset.props
}
// Attach component
if (!parentComponent) {
return
}
if (!parentComponent._children) {
parentComponent._children = []
}
parentComponent._children.push(componentInstance)
2020-11-04 18:09:45 +01:00
// Save components and update UI
await store.actions.preview.saveSelected()
2021-05-04 12:32:22 +02:00
store.update(state => {
2020-11-04 18:09:45 +01:00
state.currentView = "component"
state.selectedComponentId = componentInstance._id
2020-11-04 18:09:45 +01:00
return state
})
// Log event
analytics.captureEvent("Added Component", {
name: componentInstance._component,
})
return componentInstance
},
2021-05-04 12:32:22 +02:00
delete: async component => {
if (!component) {
return
}
const asset = get(currentAsset)
if (!asset) {
return
}
const parent = findComponentParent(asset.props, component._id)
if (parent) {
parent._children = parent._children.filter(
2021-05-04 12:32:22 +02:00
child => child._id !== component._id
)
store.actions.components.select(parent)
}
await store.actions.preview.saveSelected()
2020-11-04 18:09:45 +01:00
},
copy: (component, cut = false) => {
const selectedAsset = get(currentAsset)
if (!selectedAsset) {
return null
}
// Update store with copied component
2021-05-04 12:32:22 +02:00
store.update(state => {
state.componentToPaste = cloneDeep(component)
2020-11-04 18:09:45 +01:00
state.componentToPaste.isCut = cut
return state
})
// Remove the component from its parent if we're cutting
if (cut) {
const parent = findComponentParent(selectedAsset.props, component._id)
if (parent) {
2020-11-04 18:09:45 +01:00
parent._children = parent._children.filter(
2021-05-04 12:32:22 +02:00
child => child._id !== component._id
2020-11-04 18:09:45 +01:00
)
store.actions.components.select(parent)
}
}
2020-11-04 18:09:45 +01:00
},
paste: async (targetComponent, mode) => {
let promises = []
2021-05-04 12:32:22 +02:00
store.update(state => {
// Stop if we have nothing to paste
if (!state.componentToPaste) {
return state
}
2020-11-04 18:09:45 +01:00
// defines if this is a copy or a cut
const cut = state.componentToPaste.isCut
// immediately need to remove bindings, currently these aren't valid when pasted
if (!cut) {
state.componentToPaste = removeBindings(state.componentToPaste)
}
// Clone the component to paste
// Retain the same ID if cutting as things may be referencing this component
delete state.componentToPaste.isCut
let componentToPaste = cloneDeep(state.componentToPaste)
if (cut) {
state.componentToPaste = null
2020-11-04 18:09:45 +01:00
} else {
2021-05-04 12:32:22 +02:00
const randomizeIds = component => {
if (!component) {
return
}
component._id = uuid()
2021-02-04 19:05:05 +01:00
component._children?.forEach(randomizeIds)
}
randomizeIds(componentToPaste)
2020-11-04 18:09:45 +01:00
}
2020-11-04 18:09:45 +01:00
if (mode === "inside") {
// Paste inside target component if chosen
if (!targetComponent._children) {
targetComponent._children = []
}
2020-11-04 18:09:45 +01:00
targetComponent._children.push(componentToPaste)
} else {
// Otherwise find the parent so we can paste in the correct order
// in the parents child components
const selectedAsset = get(currentAsset)
if (!selectedAsset) {
return state
}
const parent = findComponentParent(
selectedAsset.props,
targetComponent._id
)
if (!parent) {
return state
}
// Insert the component in the correct position
const targetIndex = parent._children.indexOf(targetComponent)
const index = mode === "above" ? targetIndex : targetIndex + 1
parent._children.splice(index, 0, cloneDeep(componentToPaste))
}
// Save and select the new component
promises.push(store.actions.preview.saveSelected())
2020-11-04 18:09:45 +01:00
store.actions.components.select(componentToPaste)
return state
})
await Promise.all(promises)
2020-11-04 18:09:45 +01:00
},
updateStyle: async (name, value) => {
const selected = get(selectedComponent)
if (value == null || value === "") {
delete selected._styles.normal[name]
} else {
selected._styles.normal[name] = value
}
await store.actions.preview.saveSelected()
},
2021-05-04 12:32:22 +02:00
updateCustomStyle: async style => {
const selected = get(selectedComponent)
selected._styles.custom = style
await store.actions.preview.saveSelected()
2020-11-04 18:09:45 +01:00
},
resetStyles: async () => {
const selected = get(selectedComponent)
selected._styles = { normal: {}, hover: {}, active: {} }
await store.actions.preview.saveSelected()
},
updateConditions: async conditions => {
const selected = get(selectedComponent)
selected._conditions = conditions
await store.actions.preview.saveSelected()
},
updateProp: async (name, value) => {
let component = get(selectedComponent)
if (!name || !component) {
return
}
component[name] = value
2021-05-04 12:32:22 +02:00
store.update(state => {
state.selectedComponentId = component._id
2020-11-04 18:09:45 +01:00
return state
})
await store.actions.preview.saveSelected()
2020-11-04 18:09:45 +01:00
},
links: {
save: async (url, title) => {
const layout = get(mainLayout)
if (!layout) {
return
}
// Add link setting to main layout
if (layout.props._component.endsWith("layout")) {
// If using a new SDK, add to the layout component settings
if (!layout.props.links) {
layout.props.links = []
}
layout.props.links.push({
text: title,
url,
})
} else {
// If using an old SDK, add to the navigation component
// TODO: remove this when we can assume everyone has updated
const nav = findComponentType(
layout.props,
"@budibase/standard-components/navigation"
)
if (!nav) {
return
}
let newLink
if (nav._children && nav._children.length) {
// Clone an existing link if one exists
newLink = cloneDeep(nav._children[0])
// Set our new props
newLink._id = uuid()
newLink._instanceName = `${title} Link`
newLink.url = url
newLink.text = title
} else {
// Otherwise create vanilla new link
newLink = {
...store.actions.components.createInstance("link"),
url,
text: title,
_instanceName: `${title} Link`,
}
nav._children = [...nav._children, newLink]
}
}
// Save layout
await store.actions.layouts.save(layout)
},
},
},
}
2020-11-04 18:09:45 +01:00
return store
}