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

526 lines
16 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 {
createProps,
getBuiltin,
makePropsSafe,
} from "components/userInterface/assetParsing/createProps"
import {
allScreens,
backendUiStore,
currentAsset,
mainLayout,
} from "builderStore"
import { fetchComponentLibDefinitions } from "../loadComponentLibraries"
import api from "../api"
import { FrontendTypes } from "../../constants"
import getNewComponentName from "../getNewComponentName"
import analytics from "analytics"
2020-11-06 13:31:47 +01:00
import {
findChildComponentType,
generateNewIdsForComponent,
getComponentDefinition,
getParent,
} from "../storeUtils"
const INITIAL_FRONTEND_STATE = {
apps: [],
name: "",
description: "",
layouts: [],
2020-11-24 19:11:34 +01:00
screens: [],
mainUi: {},
unauthenticatedUi: {},
components: [],
currentPreviewItem: null,
currentComponentInfo: null,
currentFrontEndType: "none",
2020-11-24 19:11:34 +01:00
currentAssetId: "",
currentComponentProps: null,
errors: [],
hasAppPackage: false,
libraries: null,
appId: "",
2020-11-19 22:07:25 +01:00
routes: {},
}
export const getFrontendStore = () => {
const store = writable({ ...INITIAL_FRONTEND_STATE })
store.actions = {
initialise: async pkg => {
const { layouts, screens, application } = pkg
2020-11-05 15:38:44 +01:00
store.update(state => {
2020-11-24 19:11:34 +01:00
state.appId = application._id
2020-11-05 15:38:44 +01:00
return state
})
2020-11-04 18:09:45 +01:00
const components = await fetchComponentLibDefinitions(pkg.application._id)
store.update(state => ({
...state,
libraries: pkg.application.componentLibraries,
components,
name: pkg.application.name,
description: pkg.application.description,
appId: pkg.application._id,
2020-11-24 19:11:34 +01:00
layouts,
screens,
2020-11-04 18:09:45 +01:00
hasAppPackage: true,
builtins: [getBuiltin("##builtin/screenslot")],
appInstance: pkg.application.instance,
}))
await backendUiStore.actions.database.select(pkg.application.instance)
},
selectAssetType: type => {
store.update(state => {
state.currentFrontEndType = type
const asset = get(currentAsset)
state.currentComponentInfo = asset && asset.props ? asset.props : null
state.currentPreviewItem = asset
state.currentView = "detail"
return state
})
},
2020-11-19 22:07:25 +01:00
routing: {
fetch: async () => {
const response = await api.get("/api/routing")
const json = await response.json()
store.update(state => {
state.routes = json.routes
return state
})
},
},
screens: {
select: async screenId => {
let promise
store.update(state => {
const screen = get(allScreens).find(screen => screen._id === screenId)
state.currentPreviewItem = screen
state.currentFrontEndType = FrontendTypes.SCREEN
state.currentAssetId = screenId
state.currentView = "detail"
promise = store.actions.screens.regenerateCss(screen)
const safeProps = makePropsSafe(
state.components[screen.props._component],
screen.props
)
screen.props = safeProps
state.currentComponentInfo = safeProps
return state
})
await promise
},
create: async screen => {
screen = await store.actions.screens.save(screen)
store.update(state => {
state.currentPreviewItem = screen
state.currentComponentInfo = screen.props
state.currentFrontEndType = FrontendTypes.SCREEN
return state
})
return screen
},
save: async screen => {
const creatingNewScreen = screen._id === undefined
const response = await api.post(`/api/screens`, screen)
screen = await response.json()
store.update(state => {
const foundScreen = state.screens.findIndex(
el => el._id === screen._id
)
if (foundScreen !== -1) {
state.screens.splice(foundScreen, 1)
}
state.screens.push(screen)
if (creatingNewScreen) {
state.currentPreviewItem = screen
const safeProps = makePropsSafe(
state.components[screen.props._component],
screen.props
)
state.currentComponentInfo = safeProps
screen.props = safeProps
}
return state
})
return screen
},
2020-11-24 19:11:34 +01:00
regenerateCss: async asset => {
const response = await api.post("/api/css/generate", asset)
asset._css = (await response.json())?.css
},
regenerateCssForCurrentScreen: async () => {
const { currentPreviewItem } = get(store)
if (currentPreviewItem) {
await store.actions.screens.regenerateCss(currentPreviewItem)
}
},
delete: async screens => {
const screensToDelete = Array.isArray(screens) ? screens : [screens]
const screenDeletePromises = []
store.update(state => {
for (let screenToDelete of screensToDelete) {
state.screens = state.screens.filter(
screen => screen._id !== screenToDelete._id
)
screenDeletePromises.push(
api.delete(
`/api/screens/${screenToDelete._id}/${screenToDelete._rev}`
)
)
}
return state
})
await Promise.all(screenDeletePromises)
},
},
preview: {
2020-11-09 16:55:36 +01:00
saveSelected: async () => {
const state = get(store)
2020-12-02 14:41:00 +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: {
2020-12-02 15:15:07 +01:00
select: async layoutId => {
2020-11-04 18:09:45 +01:00
store.update(state => {
2020-12-02 15:15:07 +01:00
const layout = store.actions.layouts.find(layoutId)
state.currentFrontEndType = FrontendTypes.LAYOUT
2020-11-04 18:09:45 +01:00
state.currentView = "detail"
state.currentAssetId = layout._id
2020-11-04 18:09:45 +01:00
// This is the root of many problems.
// Uncaught (in promise) TypeError: Cannot read property '_component' of undefined
// it appears that the currentLayout sometimes has _props instead of props
2020-11-04 18:09:45 +01:00
// why
const safeProps = makePropsSafe(
state.components[layout.props._component],
layout.props
2020-11-04 18:09:45 +01:00
)
state.currentComponentInfo = safeProps
layout.props = safeProps
state.currentPreviewItem = layout
2020-11-04 18:09:45 +01:00
return state
})
let cssPromises = []
cssPromises.push(store.actions.screens.regenerateCssForCurrentScreen())
for (let screen of get(allScreens)) {
cssPromises.push(store.actions.screens.regenerateCss(screen))
}
await Promise.all(cssPromises)
},
save: async layout => {
2020-12-02 14:41:00 +01:00
const layoutToSave = cloneDeep(layout)
delete layoutToSave._css
const response = await api.post(`/api/layouts`, layoutToSave)
2020-11-13 11:29:54 +01:00
const json = await response.json()
if (!json.ok) throw new Error("Error updating layout")
2020-11-04 18:09:45 +01:00
store.update(state => {
2020-12-05 10:43:00 +01:00
layoutToSave._rev = json.rev
layoutToSave._id = json.id
const layoutIdx = state.layouts.findIndex(
stateLayout => stateLayout._id === layoutToSave._id
)
2020-12-05 10:43:00 +01:00
if (layoutIdx >= 0) {
// update existing layout
state.layouts.splice(layoutIdx, 1, layoutToSave)
} else {
2020-12-05 10:43:00 +01:00
// save new layout
state.layouts.push(layoutToSave)
}
2020-12-05 10:43:00 +01:00
2020-11-04 18:09:45 +01:00
return state
})
},
2020-12-02 15:15:07 +01:00
find: layoutId => {
if (!layoutId) {
return get(mainLayout)
}
const storeContents = get(store)
2020-12-02 15:15:07 +01:00
return storeContents.layouts.find(layout => layout._id === layoutId)
},
2020-12-05 00:42:22 +01:00
delete: async layoutToDelete => {
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)
}
store.update(state => {
state.layouts = state.layouts.filter(
layout => layout._id !== layoutToDelete._id
)
return state
})
},
2020-11-04 18:09:45 +01:00
},
components: {
select: component => {
store.update(state => {
const componentDef = component._component.startsWith("##")
? component
: state.components[component._component]
state.currentComponentInfo = makePropsSafe(componentDef, component)
state.currentView = "component"
return state
})
},
create: (componentToAdd, presetProps) => {
store.update(state => {
function findSlot(component_array) {
if (!component_array) {
return false
}
for (let component of component_array) {
if (component._component === "##builtin/screenslot") {
2020-11-04 18:09:45 +01:00
return true
}
if (component._children) findSlot(component)
}
2020-11-04 18:09:45 +01:00
return false
}
2020-11-04 18:09:45 +01:00
if (
componentToAdd.startsWith("##") &&
findSlot(get(currentAsset)?.props._children)
2020-11-04 18:09:45 +01:00
) {
return state
}
2020-11-04 18:09:45 +01:00
const component = getComponentDefinition(state, componentToAdd)
2020-11-04 18:09:45 +01:00
const instanceId = get(backendUiStore).selectedDatabase._id
const instanceName = getNewComponentName(component, state)
2020-11-06 13:31:47 +01:00
const newComponent = createProps(component, {
...presetProps,
_instanceId: instanceId,
_instanceName: instanceName,
})
2020-11-04 18:09:45 +01:00
const currentComponent =
state.components[state.currentComponentInfo._component]
2020-11-04 18:09:45 +01:00
const targetParent = currentComponent.children
? state.currentComponentInfo
: getParent(
state.currentPreviewItem.props,
2020-11-04 18:09:45 +01:00
state.currentComponentInfo
)
2020-11-04 18:09:45 +01:00
// Don't continue if there's no parent
if (!targetParent) {
return state
2020-11-04 18:09:45 +01:00
}
2020-11-04 18:09:45 +01:00
targetParent._children = targetParent._children.concat(
newComponent.props
)
2020-11-04 18:09:45 +01:00
store.actions.preview.saveSelected()
state.currentView = "component"
state.currentComponentInfo = newComponent.props
analytics.captureEvent("Added Component", {
name: newComponent.props._component,
})
return state
})
},
copy: (component, cut = false) => {
store.update(state => {
state.componentToPaste = cloneDeep(component)
2020-11-04 18:09:45 +01:00
state.componentToPaste.isCut = cut
if (cut) {
const parent = getParent(
state.currentPreviewItem.props,
2020-11-04 18:09:45 +01:00
component._id
)
2020-11-04 18:09:45 +01:00
parent._children = parent._children.filter(
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
return state
})
},
paste: async (targetComponent, mode) => {
let promises = []
2020-11-04 18:09:45 +01:00
store.update(state => {
if (!state.componentToPaste) return state
const componentToPaste = cloneDeep(state.componentToPaste)
// retain the same ids as things may be referencing this component
if (componentToPaste.isCut) {
// in case we paste a second time
state.componentToPaste.isCut = false
} else {
generateNewIdsForComponent(componentToPaste, state)
}
delete componentToPaste.isCut
2020-11-04 18:09:45 +01:00
if (mode === "inside") {
targetComponent._children.push(componentToPaste)
return state
2020-11-04 18:09:45 +01:00
}
const parent = getParent(
state.currentPreviewItem.props,
targetComponent
)
2020-11-04 18:09:45 +01:00
const targetIndex = parent._children.indexOf(targetComponent)
const index = mode === "above" ? targetIndex : targetIndex + 1
parent._children.splice(index, 0, cloneDeep(componentToPaste))
promises.push(store.actions.screens.regenerateCssForCurrentScreen())
promises.push(store.actions.preview.saveSelected())
2020-11-04 18:09:45 +01:00
store.actions.components.select(componentToPaste)
2020-11-04 18:09:45 +01:00
return state
})
await Promise.all(promises)
2020-11-04 18:09:45 +01:00
},
updateStyle: async (type, name, value) => {
let promises = []
2020-11-04 18:09:45 +01:00
store.update(state => {
if (!state.currentComponentInfo._styles) {
state.currentComponentInfo._styles = {}
}
state.currentComponentInfo._styles[type][name] = value
promises.push(store.actions.screens.regenerateCssForCurrentScreen())
2020-11-04 18:09:45 +01:00
// save without messing with the store
promises.push(store.actions.preview.saveSelected())
2020-11-04 18:09:45 +01:00
return state
})
await Promise.all(promises)
2020-11-04 18:09:45 +01:00
},
updateProp: (name, value) => {
store.update(state => {
let current_component = state.currentComponentInfo
current_component[name] = value
state.currentComponentInfo = current_component
store.actions.preview.saveSelected()
return state
})
},
findRoute: component => {
// Gets all the components to needed to construct a path.
const tempStore = get(store)
let pathComponents = []
let parent = component
let root = false
while (!root) {
parent = getParent(tempStore.currentPreviewItem.props, parent)
if (!parent) {
root = true
} else {
pathComponents.push(parent)
}
2020-11-04 18:09:45 +01:00
}
// Remove root entry since it's the screen or layout.
2020-11-04 18:09:45 +01:00
// Reverse array since we need the correct order of the IDs
const reversedComponents = pathComponents.reverse().slice(1)
2020-11-04 18:09:45 +01:00
// Add component
const allComponents = [...reversedComponents, component]
2020-11-04 18:09:45 +01:00
// Map IDs
const IdList = allComponents.map(c => c._id)
2020-11-04 18:09:45 +01:00
// Construct ID Path:
return IdList.join("/")
},
links: {
save: async (url, title) => {
let promises = []
const layout = get(mainLayout)
store.update(state => {
// Try to extract a nav component from the master layout
const nav = findChildComponentType(
layout,
"@budibase/standard-components/navigation"
)
if (nav) {
let newLink
// Clone an existing link if one exists
if (nav._children && nav._children.length) {
// Clone existing link style
newLink = cloneDeep(nav._children[0])
// Manipulate IDs to ensure uniqueness
generateNewIdsForComponent(newLink, state, false)
// Set our new props
newLink._instanceName = `${title} Link`
newLink.url = url
newLink.text = title
} else {
// Otherwise create vanilla new link
const component = getComponentDefinition(
state,
"@budibase/standard-components/link"
)
const instanceId = get(backendUiStore).selectedDatabase._id
newLink = createProps(component, {
url,
text: title,
_instanceName: `${title} Link`,
_instanceId: instanceId,
}).props
}
// Save layout and regenerate all CSS because otherwise weird things happen
nav._children = [...nav._children, newLink]
state.currentAssetId = layout._id
promises.push(store.actions.screens.regenerateCss(layout))
for (let screen of get(allScreens)) {
promises.push(store.actions.screens.regenerateCss(screen))
}
promises.push(store.actions.layouts.save(layout))
}
return state
})
await Promise.all(promises)
},
},
},
}
2020-11-04 18:09:45 +01:00
return store
}