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

497 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/pagesParsing/createProps"
2020-11-24 19:11:34 +01:00
import { allScreens, backendUiStore, currentAsset } from "builderStore"
import { fetchComponentLibDefinitions } from "../loadComponentLibraries"
import api from "../api"
import { DEFAULT_PAGES_OBJECT } 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: "",
2020-11-24 19:11:34 +01:00
layouts: DEFAULT_PAGES_OBJECT,
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 => {
2020-11-24 19:11:34 +01:00
const layouts = pkg.layouts, screens = pkg.screens, application = pkg.application
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)
},
selectPageOrScreen: type => {
store.update(state => {
state.currentFrontEndType = type
2020-11-24 19:11:34 +01:00
const page = get(currentAsset)
const pageOrScreen = type === "page" ? page : page._screens[0]
state.currentComponentInfo = pageOrScreen ? pageOrScreen.props : null
state.currentPreviewItem = pageOrScreen
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: screenId => {
store.update(state => {
const screen = get(allScreens).find(screen => screen._id === screenId)
state.currentPreviewItem = screen
state.currentFrontEndType = "screen"
state.currentView = "detail"
store.actions.screens.regenerateCssForCurrentScreen()
const safeProps = makePropsSafe(
state.components[screen.props._component],
screen.props
)
screen.props = safeProps
state.currentComponentInfo = safeProps
return state
})
},
create: async screen => {
let savePromise
store.update(state => {
state.currentPreviewItem = screen
state.currentComponentInfo = screen.props
state.currentFrontEndType = "screen"
if (state.currentPreviewItem) {
store.actions.screens.regenerateCss(state.currentPreviewItem)
}
savePromise = store.actions.screens.save(screen)
return state
})
await savePromise
},
save: async screen => {
2020-11-24 19:11:34 +01:00
const page = get(currentAsset)
const currentPageScreens = page._screens
const creatingNewScreen = screen._id === undefined
let savePromise
const response = await api.post(`/api/screens/${page._id}`, screen)
const json = await response.json()
screen._rev = json.rev
screen._id = json.id
const foundScreen = page._screens.findIndex(el => el._id === screen._id)
if (foundScreen !== -1) {
page._screens.splice(foundScreen, 1)
}
page._screens.push(screen)
// TODO: should carry out all server updates to screen in a single call
store.update(state => {
page._screens = currentPageScreens
if (creatingNewScreen) {
state.currentPreviewItem = screen
const safeProps = makePropsSafe(
state.components[screen.props._component],
screen.props
)
state.currentComponentInfo = safeProps
screen.props = safeProps
}
2020-11-24 19:11:34 +01:00
savePromise = store.actions.layouts.save()
return state
})
if (savePromise) await savePromise
},
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()
},
regenerateCssForCurrentScreen: () => {
const { currentPreviewItem } = get(store)
if (currentPreviewItem) {
store.actions.screens.regenerateCss(currentPreviewItem)
}
},
delete: async screens => {
let deletePromise
const screensToDelete = Array.isArray(screens) ? screens : [screens]
store.update(state => {
2020-11-24 19:11:34 +01:00
const currentPage = get(currentAsset)
for (let screenToDelete of screensToDelete) {
// Remove screen from current page as well
// TODO: Should be done server side
currentPage._screens = currentPage._screens.filter(
scr => scr._id !== screenToDelete._id
)
2020-11-19 17:41:29 +01:00
deletePromise = api.delete(
`/api/screens/${screenToDelete._id}/${screenToDelete._rev}`
)
}
return state
})
await deletePromise
},
},
preview: {
2020-11-09 16:55:36 +01:00
saveSelected: async () => {
const state = get(store)
2020-11-09 16:55:36 +01:00
if (state.currentFrontEndType !== "page") {
await store.actions.screens.save(state.currentPreviewItem)
}
2020-11-24 19:11:34 +01:00
await store.actions.layouts.save()
},
2020-11-04 18:09:45 +01:00
},
2020-11-24 19:11:34 +01:00
layouts: {
2020-11-04 18:09:45 +01:00
select: pageName => {
store.update(state => {
2020-11-24 19:11:34 +01:00
const currentPage = state.layouts[pageName]
2020-11-04 18:09:45 +01:00
state.currentFrontEndType = "page"
state.currentView = "detail"
2020-11-24 19:11:34 +01:00
state.currentAssetId = pageName
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 currentPage sometimes has _props instead of props
// why
const safeProps = makePropsSafe(
state.components[currentPage.props._component],
currentPage.props
)
state.currentComponentInfo = safeProps
currentPage.props = safeProps
2020-11-24 19:11:34 +01:00
state.currentPreviewItem = state.layouts[pageName]
2020-11-04 18:09:45 +01:00
store.actions.screens.regenerateCssForCurrentScreen()
for (let screen of get(allScreens)) {
2020-11-24 19:11:34 +01:00
screen._css = store.actions.screens.regenerateCss(screen)
2020-11-04 18:09:45 +01:00
}
return state
})
},
2020-11-04 18:09:45 +01:00
save: async page => {
const storeContents = get(store)
2020-11-24 19:11:34 +01:00
const pageName = storeContents.currentAssetId || "main"
2020-11-04 18:09:45 +01:00
const pageToSave = page || storeContents.pages[pageName]
// TODO: revisit. This sends down a very weird payload
2020-11-13 11:29:54 +01:00
const response = await api.post(`/api/pages/${pageToSave._id}`, {
page: {
componentLibraries: storeContents.pages.componentLibraries,
...pageToSave,
},
screens: pageToSave._screens,
})
const json = await response.json()
if (!json.ok) throw new Error("Error updating page")
2020-11-04 18:09:45 +01:00
store.update(state => {
2020-11-24 19:11:34 +01:00
state.layouts[pageName]._rev = json.rev
2020-11-04 18:09:45 +01:00
return state
})
},
},
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) {
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("##") &&
2020-11-24 19:11:34 +01:00
findSlot(state.layouts[state.currentAssetId].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: (targetComponent, mode) => {
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))
2020-11-04 18:09:45 +01:00
store.actions.screens.regenerateCssForCurrentScreen()
store.actions.preview.saveSelected()
store.actions.components.select(componentToPaste)
2020-11-04 18:09:45 +01:00
return state
})
},
updateStyle: (type, name, value) => {
store.update(state => {
if (!state.currentComponentInfo._styles) {
state.currentComponentInfo._styles = {}
}
state.currentComponentInfo._styles[type][name] = value
store.actions.screens.regenerateCssForCurrentScreen()
// save without messing with the store
store.actions.preview.saveSelected()
return state
})
},
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
}
2020-11-04 18:09:45 +01:00
// Remove root entry since it's the screen or page layout.
// 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 savePromise
store.update(state => {
// Try to extract a nav component from the master screen
const nav = findChildComponentType(
2020-11-24 19:11:34 +01:00
state.layouts.main,
"@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 page and regenerate all CSS because otherwise weird things happen
nav._children = [...nav._children, newLink]
2020-11-24 19:11:34 +01:00
state.currentAssetId = "main"
store.actions.screens.regenerateCss(state.layouts.main)
for (let screen of state.layouts.main._screens) {
store.actions.screens.regenerateCss(screen)
}
2020-11-24 19:11:34 +01:00
savePromise = store.actions.layouts.save()
}
return state
})
await savePromise
},
},
},
}
2020-11-04 18:09:45 +01:00
return store
}