2020-11-06 13:30:30 +01:00
|
|
|
import { get, writable } from "svelte/store"
|
2020-11-06 13:31:47 +01:00
|
|
|
import { cloneDeep } from "lodash/fp"
|
2020-12-01 17:22:06 +01:00
|
|
|
import {
|
|
|
|
allScreens,
|
2020-12-15 18:27:45 +01:00
|
|
|
hostingStore,
|
2020-12-01 17:22:06 +01:00
|
|
|
currentAsset,
|
|
|
|
mainLayout,
|
2020-12-07 16:27:46 +01:00
|
|
|
selectedComponent,
|
2020-12-09 20:04:46 +01:00
|
|
|
selectedAccessRole,
|
2020-12-01 17:22:06 +01:00
|
|
|
} from "builderStore"
|
2021-10-01 16:01:16 +02:00
|
|
|
import { notifications } from "@budibase/bbui"
|
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"
|
2021-03-22 12:02:55 +01:00
|
|
|
|
2020-11-04 17:13:50 +01:00
|
|
|
import { fetchComponentLibDefinitions } from "../loadComponentLibraries"
|
|
|
|
import api from "../api"
|
2021-01-14 10:09:23 +01:00
|
|
|
import { FrontendTypes } from "constants"
|
2021-09-21 21:21:15 +02:00
|
|
|
import analytics, { Events } from "analytics"
|
2021-08-20 15:12:52 +02:00
|
|
|
import {
|
|
|
|
findComponentType,
|
|
|
|
findComponentParent,
|
|
|
|
findClosestMatchingComponent,
|
|
|
|
findAllMatchingComponents,
|
2021-10-01 16:01:16 +02:00
|
|
|
findComponent,
|
2021-08-20 15:12:52 +02:00
|
|
|
} from "../storeUtils"
|
2021-01-12 21:00:35 +01:00
|
|
|
import { uuid } from "../uuid"
|
2021-02-22 16:49:57 +01:00
|
|
|
import { removeBindings } from "../dataBinding"
|
2020-11-04 17:13:50 +01:00
|
|
|
|
|
|
|
const INITIAL_FRONTEND_STATE = {
|
|
|
|
apps: [],
|
|
|
|
name: "",
|
2021-01-14 18:01:31 +01:00
|
|
|
url: "",
|
2020-11-04 17:13:50 +01:00
|
|
|
description: "",
|
2020-12-02 17:15:14 +01:00
|
|
|
layouts: [],
|
2020-11-24 19:11:34 +01:00
|
|
|
screens: [],
|
2020-11-04 17:13:50 +01:00
|
|
|
components: [],
|
2021-07-07 14:54:21 +02:00
|
|
|
clientFeatures: {
|
|
|
|
spectrumThemes: false,
|
|
|
|
intelligentLoading: false,
|
2021-08-13 12:24:47 +02:00
|
|
|
deviceAwareness: false,
|
2021-09-02 12:38:41 +02:00
|
|
|
state: false,
|
|
|
|
customThemes: false,
|
2021-09-08 10:46:20 +02:00
|
|
|
devicePreview: false,
|
2021-07-07 14:54:21 +02:00
|
|
|
},
|
2020-11-04 17:13:50 +01:00
|
|
|
currentFrontEndType: "none",
|
2020-12-14 12:14:16 +01:00
|
|
|
selectedScreenId: "",
|
|
|
|
selectedLayoutId: "",
|
2020-12-07 16:27:46 +01:00
|
|
|
selectedComponentId: "",
|
2020-11-04 17:13:50 +01:00
|
|
|
errors: [],
|
|
|
|
hasAppPackage: false,
|
|
|
|
libraries: null,
|
|
|
|
appId: "",
|
2020-11-19 22:07:25 +01:00
|
|
|
routes: {},
|
2021-04-01 15:10:49 +02:00
|
|
|
clientLibPath: "",
|
2021-06-28 13:55:11 +02:00
|
|
|
theme: "",
|
2021-09-02 12:38:41 +02:00
|
|
|
customTheme: {},
|
2021-09-07 17:02:11 +02:00
|
|
|
previewDevice: "desktop",
|
2020-11-04 17:13:50 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
export const getFrontendStore = () => {
|
|
|
|
const store = writable({ ...INITIAL_FRONTEND_STATE })
|
|
|
|
|
|
|
|
store.actions = {
|
2021-05-04 12:32:22 +02:00
|
|
|
initialise: async pkg => {
|
2021-04-01 15:10:49 +02:00
|
|
|
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,
|
2021-01-14 10:09:23 +01:00
|
|
|
libraries: application.componentLibraries,
|
2020-11-04 18:09:45 +01:00
|
|
|
components,
|
2021-07-07 14:54:21 +02:00
|
|
|
clientFeatures: {
|
|
|
|
...state.clientFeatures,
|
|
|
|
...components.features,
|
|
|
|
},
|
2021-01-14 10:09:23 +01:00
|
|
|
name: application.name,
|
|
|
|
description: application.description,
|
2021-05-16 22:25:37 +02:00
|
|
|
appId: application.appId,
|
2021-01-20 12:14:36 +01:00
|
|
|
url: application.url,
|
2020-11-24 19:11:34 +01:00
|
|
|
layouts,
|
|
|
|
screens,
|
2021-08-02 16:28:40 +02:00
|
|
|
theme: application.theme || "spectrum--light",
|
2021-09-02 12:38:41 +02:00
|
|
|
customTheme: application.customTheme,
|
2020-11-04 18:09:45 +01:00
|
|
|
hasAppPackage: true,
|
2021-01-14 10:09:23 +01:00
|
|
|
appInstance: application.instance,
|
2021-04-01 15:10:49 +02:00
|
|
|
clientLibPath,
|
2021-05-25 13:42:55 +02:00
|
|
|
previousTopNavPath: {},
|
2021-07-07 18:07:42 +02:00
|
|
|
version: application.version,
|
2021-07-07 18:35:28 +02:00
|
|
|
revertableVersion: application.revertableVersion,
|
2020-11-04 18:09:45 +01:00
|
|
|
}))
|
2020-12-15 18:27:45 +01:00
|
|
|
await hostingStore.actions.fetch()
|
2021-03-22 12:02:55 +01:00
|
|
|
|
|
|
|
// 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-22 12:02:55 +01:00
|
|
|
])
|
2021-03-29 14:57:01 +02:00
|
|
|
datasources.init()
|
2021-03-22 13:23:36 +01:00
|
|
|
integrations.set(_integrations)
|
2021-03-29 17:45:18 +02:00
|
|
|
queries.init()
|
2021-03-22 16:33:08 +01:00
|
|
|
database.set(application.instance)
|
2021-03-30 11:32:01 +02:00
|
|
|
tables.init()
|
2020-11-04 17:13:50 +01:00
|
|
|
},
|
2021-06-28 13:55:11 +02:00
|
|
|
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")
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
2021-09-02 12:38:41 +02:00
|
|
|
customTheme: {
|
|
|
|
save: async customTheme => {
|
|
|
|
const appId = get(store).appId
|
|
|
|
const response = await api.put(`/api/applications/${appId}`, {
|
|
|
|
customTheme,
|
|
|
|
})
|
|
|
|
if (response.status === 200) {
|
|
|
|
store.update(state => {
|
|
|
|
state.customTheme = customTheme
|
|
|
|
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
|
|
|
|
})
|
|
|
|
},
|
|
|
|
},
|
2020-11-04 17:13:50 +01:00
|
|
|
screens: {
|
2021-05-04 12:32:22 +02:00
|
|
|
select: screenId => {
|
|
|
|
store.update(state => {
|
2020-12-09 17:01:16 +01:00
|
|
|
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
|
2020-12-14 12:14:16 +01:00
|
|
|
|
|
|
|
// Update role to the screen's role setting so that it will always
|
|
|
|
// be visible
|
|
|
|
selectedAccessRole.set(screen.routing.roleId)
|
|
|
|
|
2020-12-01 17:22:06 +01:00
|
|
|
state.currentFrontEndType = FrontendTypes.SCREEN
|
2020-12-14 12:14:16 +01:00
|
|
|
state.selectedScreenId = screen._id
|
2020-11-04 17:13:50 +01:00
|
|
|
state.currentView = "detail"
|
2020-12-08 17:55:43 +01:00
|
|
|
state.selectedComponentId = screen.props?._id
|
2020-11-04 17:13:50 +01:00
|
|
|
return state
|
|
|
|
})
|
|
|
|
},
|
2021-05-04 12:32:22 +02:00
|
|
|
create: async screen => {
|
2020-12-03 13:08:33 +01:00
|
|
|
screen = await store.actions.screens.save(screen)
|
2021-05-04 12:32:22 +02:00
|
|
|
store.update(state => {
|
2020-12-14 12:14:16 +01:00
|
|
|
state.selectedScreenId = screen._id
|
2020-12-07 21:29:41 +01:00
|
|
|
state.selectedComponentId = screen.props._id
|
2020-12-01 17:22:06 +01:00
|
|
|
state.currentFrontEndType = FrontendTypes.SCREEN
|
2020-12-11 16:29:25 +01:00
|
|
|
selectedAccessRole.set(screen.routing.roleId)
|
2020-11-04 17:13:50 +01:00
|
|
|
return state
|
|
|
|
})
|
2020-12-03 13:08:33 +01:00
|
|
|
return screen
|
2020-11-04 17:13:50 +01:00
|
|
|
},
|
2021-05-04 12:32:22 +02:00
|
|
|
save: async screen => {
|
2020-11-09 11:28:49 +01:00
|
|
|
const creatingNewScreen = screen._id === undefined
|
2020-11-25 18:56:09 +01:00
|
|
|
const response = await api.post(`/api/screens`, screen)
|
2021-07-06 16:22:01 +02:00
|
|
|
if (response.status !== 200) {
|
|
|
|
return
|
|
|
|
}
|
2020-12-03 13:08:33 +01:00
|
|
|
screen = await response.json()
|
2020-12-09 19:18:47 +01:00
|
|
|
await store.actions.routing.fetch()
|
2020-11-04 17:13:50 +01:00
|
|
|
|
2021-05-04 12:32:22 +02:00
|
|
|
store.update(state => {
|
2020-12-01 17:22:06 +01:00
|
|
|
const foundScreen = state.screens.findIndex(
|
2021-05-04 12:32:22 +02:00
|
|
|
el => el._id === screen._id
|
2020-12-01 17:22:06 +01:00
|
|
|
)
|
2020-11-25 18:56:09 +01:00
|
|
|
if (foundScreen !== -1) {
|
|
|
|
state.screens.splice(foundScreen, 1)
|
|
|
|
}
|
|
|
|
state.screens.push(screen)
|
2020-11-04 17:13:50 +01:00
|
|
|
return state
|
|
|
|
})
|
2020-12-09 15:51:42 +01:00
|
|
|
|
|
|
|
if (creatingNewScreen) {
|
|
|
|
store.actions.screens.select(screen._id)
|
|
|
|
}
|
|
|
|
|
2020-12-03 13:08:33 +01:00
|
|
|
return screen
|
2020-11-04 17:13:50 +01:00
|
|
|
},
|
2021-05-04 12:32:22 +02:00
|
|
|
delete: async screens => {
|
2020-11-19 12:15:29 +01:00
|
|
|
const screensToDelete = Array.isArray(screens) ? screens : [screens]
|
|
|
|
|
2020-11-25 18:56:09 +01:00
|
|
|
const screenDeletePromises = []
|
2021-05-04 12:32:22 +02:00
|
|
|
store.update(state => {
|
2020-11-19 12:15:29 +01:00
|
|
|
for (let screenToDelete of screensToDelete) {
|
2020-12-01 17:22:06 +01:00
|
|
|
state.screens = state.screens.filter(
|
2021-05-04 12:32:22 +02:00
|
|
|
screen => screen._id !== screenToDelete._id
|
2020-11-19 12:15:29 +01:00
|
|
|
)
|
2020-12-01 17:22:06 +01:00
|
|
|
screenDeletePromises.push(
|
|
|
|
api.delete(
|
|
|
|
`/api/screens/${screenToDelete._id}/${screenToDelete._rev}`
|
|
|
|
)
|
2020-11-04 17:13:50 +01:00
|
|
|
)
|
2020-12-14 12:14:16 +01:00
|
|
|
if (screenToDelete._id === state.selectedScreenId) {
|
|
|
|
state.selectedScreenId = null
|
2020-12-08 17:55:43 +01:00
|
|
|
}
|
2021-09-21 15:16:10 +02:00
|
|
|
//remove the link for this screen
|
|
|
|
screenDeletePromises.push(
|
|
|
|
store.actions.components.links.delete(
|
|
|
|
screenToDelete.routing.route,
|
|
|
|
screenToDelete.props._instanceName
|
|
|
|
)
|
|
|
|
)
|
2020-11-04 17:13:50 +01:00
|
|
|
}
|
|
|
|
return state
|
|
|
|
})
|
2020-11-25 18:56:09 +01:00
|
|
|
await Promise.all(screenDeletePromises)
|
2020-11-04 17:13:50 +01:00
|
|
|
},
|
|
|
|
},
|
|
|
|
preview: {
|
2020-12-07 21:29:41 +01:00
|
|
|
saveSelected: async () => {
|
2020-11-04 17:13:50 +01:00
|
|
|
const state = get(store)
|
2020-12-07 21:29:41 +01:00
|
|
|
const selectedAsset = get(currentAsset)
|
|
|
|
|
2020-12-01 17:22:06 +01:00
|
|
|
if (state.currentFrontEndType !== FrontendTypes.LAYOUT) {
|
2020-12-02 14:41:00 +01:00
|
|
|
await store.actions.screens.save(selectedAsset)
|
2020-12-03 16:15:14 +01:00
|
|
|
} else {
|
|
|
|
await store.actions.layouts.save(selectedAsset)
|
2020-11-09 16:55:36 +01:00
|
|
|
}
|
2020-11-04 17:13:50 +01:00
|
|
|
},
|
2021-09-07 17:02:11 +02:00
|
|
|
setDevice: device => {
|
|
|
|
store.update(state => {
|
|
|
|
state.previewDevice = device
|
|
|
|
return state
|
|
|
|
})
|
|
|
|
},
|
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 => {
|
2020-12-14 12:14:16 +01:00
|
|
|
const layout =
|
|
|
|
store.actions.layouts.find(layoutId) || get(store).layouts[0]
|
2020-12-09 16:57:32 +01:00
|
|
|
if (!layout) return
|
2020-12-01 17:22:06 +01:00
|
|
|
state.currentFrontEndType = FrontendTypes.LAYOUT
|
2020-11-04 18:09:45 +01:00
|
|
|
state.currentView = "detail"
|
2020-12-14 12:14:16 +01:00
|
|
|
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
|
|
|
|
})
|
2020-11-04 17:13:50 +01:00
|
|
|
},
|
2021-05-04 12:32:22 +02:00
|
|
|
save: async layout => {
|
2020-12-02 14:41:00 +01:00
|
|
|
const layoutToSave = cloneDeep(layout)
|
2020-12-09 15:51:42 +01:00
|
|
|
const creatingNewLayout = layoutToSave._id === undefined
|
2020-12-02 14:41:00 +01:00
|
|
|
const response = await api.post(`/api/layouts`, layoutToSave)
|
2020-12-09 15:51:42 +01:00
|
|
|
const savedLayout = await response.json()
|
2020-11-13 11:29:54 +01:00
|
|
|
|
2021-06-30 10:35:31 +02: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-01 17:22:06 +01:00
|
|
|
)
|
2020-12-05 10:43:00 +01:00
|
|
|
if (layoutIdx >= 0) {
|
|
|
|
// update existing layout
|
2020-12-09 15:51:42 +01:00
|
|
|
state.layouts.splice(layoutIdx, 1, savedLayout)
|
2020-12-05 00:16:07 +01:00
|
|
|
} else {
|
2020-12-05 10:43:00 +01:00
|
|
|
// save new layout
|
2020-12-09 15:51:42 +01:00
|
|
|
state.layouts.push(savedLayout)
|
2020-11-25 18:56:09 +01:00
|
|
|
}
|
2020-11-04 18:09:45 +01:00
|
|
|
return state
|
|
|
|
})
|
2020-12-09 15:51:42 +01:00
|
|
|
|
|
|
|
// 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) {
|
2020-11-25 18:56:09 +01:00
|
|
|
return get(mainLayout)
|
|
|
|
}
|
|
|
|
const storeContents = get(store)
|
2021-05-04 12:32:22 +02:00
|
|
|
return storeContents.layouts.find(layout => layout._id === layoutId)
|
2020-11-25 18:56:09 +01:00
|
|
|
},
|
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
|
|
|
)
|
2020-12-14 12:14:16 +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 => {
|
2021-01-14 10:09:23 +01:00
|
|
|
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 => {
|
2020-12-07 16:27:46 +01:00
|
|
|
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 => {
|
2021-01-12 21:00:35 +01:00
|
|
|
if (!componentName) {
|
|
|
|
return null
|
|
|
|
}
|
2021-01-14 10:09:23 +01:00
|
|
|
if (!componentName.startsWith("@budibase")) {
|
|
|
|
componentName = `@budibase/standard-components/${componentName}`
|
|
|
|
}
|
|
|
|
return get(store).components[componentName]
|
2021-01-12 21:00:35 +01:00
|
|
|
},
|
|
|
|
createInstance: (componentName, presetProps) => {
|
|
|
|
const definition = store.actions.components.getDefinition(componentName)
|
|
|
|
if (!definition) {
|
|
|
|
return null
|
|
|
|
}
|
2020-11-04 17:13:50 +01:00
|
|
|
|
2021-01-12 21:00:35 +01:00
|
|
|
// Generate default props
|
|
|
|
let props = { ...presetProps }
|
|
|
|
if (definition.settings) {
|
2021-05-04 12:32:22 +02:00
|
|
|
definition.settings.forEach(setting => {
|
2021-01-12 21:00:35 +01:00
|
|
|
if (setting.defaultValue !== undefined) {
|
|
|
|
props[setting.key] = setting.defaultValue
|
2020-11-04 17:13:50 +01:00
|
|
|
}
|
2020-11-06 13:31:47 +01:00
|
|
|
})
|
2021-01-12 21:00:35 +01:00
|
|
|
}
|
2020-11-04 17:13:50 +01:00
|
|
|
|
2021-01-12 21:00:35 +01:00
|
|
|
// Add any extra properties the component needs
|
|
|
|
let extras = {}
|
|
|
|
if (definition.hasChildren) {
|
|
|
|
extras._children = []
|
|
|
|
}
|
2021-08-20 15:12:52 +02:00
|
|
|
if (componentName.endsWith("/formstep")) {
|
|
|
|
const parentForm = findClosestMatchingComponent(
|
|
|
|
get(currentAsset).props,
|
|
|
|
get(selectedComponent)._id,
|
|
|
|
component => component._component.endsWith("/form")
|
|
|
|
)
|
|
|
|
const formSteps = findAllMatchingComponents(parentForm, component =>
|
|
|
|
component._component.endsWith("/formstep")
|
|
|
|
)
|
|
|
|
extras.step = formSteps.length + 1
|
|
|
|
extras._instanceName = `Step ${formSteps.length + 1}`
|
|
|
|
}
|
2020-11-04 17:13:50 +01:00
|
|
|
|
2021-01-12 21:00:35 +01:00
|
|
|
return {
|
|
|
|
_id: uuid(),
|
|
|
|
_component: definition.component,
|
|
|
|
_styles: { normal: {}, hover: {}, active: {} },
|
2021-01-19 12:52:33 +01:00
|
|
|
_instanceName: `New ${definition.name}`,
|
2021-01-12 21:00:35 +01:00
|
|
|
...cloneDeep(props),
|
|
|
|
...extras,
|
|
|
|
}
|
|
|
|
},
|
2021-02-23 17:29:50 +01:00
|
|
|
create: async (componentName, presetProps) => {
|
2021-01-14 10:09:23 +01:00
|
|
|
const selected = get(selectedComponent)
|
|
|
|
const asset = get(currentAsset)
|
|
|
|
|
2021-01-12 21:00:35 +01:00
|
|
|
// Create new component
|
|
|
|
const componentInstance = store.actions.components.createInstance(
|
|
|
|
componentName,
|
|
|
|
presetProps
|
|
|
|
)
|
|
|
|
if (!componentInstance) {
|
|
|
|
return
|
|
|
|
}
|
2020-12-07 16:27:46 +01:00
|
|
|
|
2021-01-12 21:00:35 +01:00
|
|
|
// Find parent node to attach this component to
|
|
|
|
let parentComponent
|
2021-01-14 10:09:23 +01:00
|
|
|
|
2021-01-12 21:00:35 +01:00
|
|
|
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
|
2020-12-07 16:27:46 +01:00
|
|
|
} else {
|
2021-01-12 21:00:35 +01:00
|
|
|
// Otherwise we need to use the parent of this component
|
|
|
|
parentComponent = findComponentParent(asset.props, selected._id)
|
2020-11-04 18:09:45 +01:00
|
|
|
}
|
2021-01-12 21:00:35 +01:00
|
|
|
} else {
|
|
|
|
// Use screen or layout if no component is selected
|
|
|
|
parentComponent = asset.props
|
|
|
|
}
|
2020-11-04 17:13:50 +01:00
|
|
|
|
2021-01-12 21:00:35 +01:00
|
|
|
// Attach component
|
|
|
|
if (!parentComponent) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if (!parentComponent._children) {
|
|
|
|
parentComponent._children = []
|
|
|
|
}
|
|
|
|
parentComponent._children.push(componentInstance)
|
2020-11-04 18:09:45 +01:00
|
|
|
|
2021-01-12 21:00:35 +01:00
|
|
|
// Save components and update UI
|
2021-02-23 17:29:50 +01:00
|
|
|
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"
|
2021-01-12 21:00:35 +01:00
|
|
|
state.selectedComponentId = componentInstance._id
|
2020-11-04 18:09:45 +01:00
|
|
|
return state
|
|
|
|
})
|
2021-01-12 21:00:35 +01:00
|
|
|
|
|
|
|
// Log event
|
2021-09-21 21:21:15 +02:00
|
|
|
analytics.captureEvent(Events.COMPONENT.CREATED, {
|
2021-01-12 21:00:35 +01:00
|
|
|
name: componentInstance._component,
|
|
|
|
})
|
|
|
|
|
|
|
|
return componentInstance
|
|
|
|
},
|
2021-05-04 12:32:22 +02:00
|
|
|
delete: async component => {
|
2021-01-12 21:00:35 +01:00
|
|
|
if (!component) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
const asset = get(currentAsset)
|
|
|
|
if (!asset) {
|
|
|
|
return
|
|
|
|
}
|
2021-10-01 16:01:16 +02:00
|
|
|
|
|
|
|
// Fetch full definition
|
|
|
|
component = findComponent(asset.props, component._id)
|
|
|
|
|
|
|
|
// Ensure we aren't deleting the screen slot
|
|
|
|
if (component._component?.endsWith("/screenslot")) {
|
|
|
|
notifications.error("You can't delete the screen slot")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure we aren't deleting something that contains the screen slot
|
|
|
|
const screenslot = findComponentType(
|
|
|
|
component,
|
|
|
|
"@budibase/standard-components/screenslot"
|
|
|
|
)
|
|
|
|
if (screenslot != null) {
|
|
|
|
notifications.error(
|
|
|
|
"You can't delete a component that contains the screen slot"
|
|
|
|
)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-01-12 21:00:35 +01:00
|
|
|
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
|
2021-01-12 21:00:35 +01:00
|
|
|
)
|
|
|
|
store.actions.components.select(parent)
|
|
|
|
}
|
2021-02-23 17:29:50 +01:00
|
|
|
await store.actions.preview.saveSelected()
|
2020-11-04 18:09:45 +01:00
|
|
|
},
|
|
|
|
copy: (component, cut = false) => {
|
2020-12-07 16:27:46 +01:00
|
|
|
const selectedAsset = get(currentAsset)
|
2021-01-12 21:00:35 +01:00
|
|
|
if (!selectedAsset) {
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update store with copied component
|
2021-05-04 12:32:22 +02:00
|
|
|
store.update(state => {
|
2020-11-06 13:30:30 +01:00
|
|
|
state.componentToPaste = cloneDeep(component)
|
2020-11-04 18:09:45 +01:00
|
|
|
state.componentToPaste.isCut = cut
|
2021-01-12 21:00:35 +01:00
|
|
|
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)
|
|
|
|
}
|
2021-01-12 21:00:35 +01:00
|
|
|
}
|
2020-11-04 18:09:45 +01:00
|
|
|
},
|
2020-11-25 18:56:09 +01:00
|
|
|
paste: async (targetComponent, mode) => {
|
|
|
|
let promises = []
|
2021-05-04 12:32:22 +02:00
|
|
|
store.update(state => {
|
2021-01-12 21:00:35 +01:00
|
|
|
// Stop if we have nothing to paste
|
|
|
|
if (!state.componentToPaste) {
|
|
|
|
return state
|
|
|
|
}
|
2020-11-04 18:09:45 +01:00
|
|
|
|
2021-02-23 11:26:37 +01:00
|
|
|
// defines if this is a copy or a cut
|
|
|
|
const cut = state.componentToPaste.isCut
|
|
|
|
|
2021-02-22 16:49:57 +01:00
|
|
|
// immediately need to remove bindings, currently these aren't valid when pasted
|
2021-02-23 11:26:37 +01:00
|
|
|
if (!cut) {
|
|
|
|
state.componentToPaste = removeBindings(state.componentToPaste)
|
|
|
|
}
|
2021-02-22 16:49:57 +01:00
|
|
|
|
2021-01-12 21:00:35 +01:00
|
|
|
// 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 => {
|
2021-02-04 19:00:16 +01:00
|
|
|
if (!component) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
component._id = uuid()
|
2021-02-04 19:05:05 +01:00
|
|
|
component._children?.forEach(randomizeIds)
|
2021-02-04 19:00:16 +01:00
|
|
|
}
|
|
|
|
randomizeIds(componentToPaste)
|
2020-11-04 18:09:45 +01:00
|
|
|
}
|
2020-11-04 17:13:50 +01:00
|
|
|
|
2020-11-04 18:09:45 +01:00
|
|
|
if (mode === "inside") {
|
2021-01-12 21:00:35 +01:00
|
|
|
// Paste inside target component if chosen
|
|
|
|
if (!targetComponent._children) {
|
|
|
|
targetComponent._children = []
|
|
|
|
}
|
2020-11-04 18:09:45 +01:00
|
|
|
targetComponent._children.push(componentToPaste)
|
2021-01-12 21:00:35 +01:00
|
|
|
} 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
|
|
|
|
}
|
2020-11-04 17:13:50 +01:00
|
|
|
|
2021-01-12 21:00:35 +01:00
|
|
|
// 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))
|
|
|
|
}
|
2020-11-04 17:13:50 +01:00
|
|
|
|
2021-01-12 21:00:35 +01:00
|
|
|
// Save and select the new component
|
2020-11-25 18:56:09 +01:00
|
|
|
promises.push(store.actions.preview.saveSelected())
|
2020-11-04 18:09:45 +01:00
|
|
|
store.actions.components.select(componentToPaste)
|
|
|
|
return state
|
|
|
|
})
|
2020-11-25 18:56:09 +01:00
|
|
|
await Promise.all(promises)
|
2020-11-04 18:09:45 +01:00
|
|
|
},
|
2021-06-22 10:14:17 +02:00
|
|
|
updateStyle: async (name, value) => {
|
2020-12-07 16:27:46 +01:00
|
|
|
const selected = get(selectedComponent)
|
2021-01-05 12:44:58 +01:00
|
|
|
if (value == null || value === "") {
|
2021-06-22 10:14:17 +02:00
|
|
|
delete selected._styles.normal[name]
|
2021-01-05 12:44:58 +01:00
|
|
|
} else {
|
2021-06-22 10:14:17 +02:00
|
|
|
selected._styles.normal[name] = value
|
2021-01-04 19:39:17 +01:00
|
|
|
}
|
|
|
|
await store.actions.preview.saveSelected()
|
|
|
|
},
|
2021-05-04 12:32:22 +02:00
|
|
|
updateCustomStyle: async style => {
|
2021-01-04 19:39:17 +01:00
|
|
|
const selected = get(selectedComponent)
|
|
|
|
selected._styles.custom = style
|
|
|
|
await store.actions.preview.saveSelected()
|
2020-11-04 18:09:45 +01:00
|
|
|
},
|
2021-01-05 12:44:58 +01:00
|
|
|
resetStyles: async () => {
|
|
|
|
const selected = get(selectedComponent)
|
2021-01-06 11:17:51 +01:00
|
|
|
selected._styles = { normal: {}, hover: {}, active: {} }
|
2021-01-05 12:44:58 +01:00
|
|
|
await store.actions.preview.saveSelected()
|
|
|
|
},
|
2021-07-21 15:03:49 +02:00
|
|
|
updateConditions: async conditions => {
|
|
|
|
const selected = get(selectedComponent)
|
|
|
|
selected._conditions = conditions
|
|
|
|
await store.actions.preview.saveSelected()
|
|
|
|
},
|
2021-02-23 17:29:50 +01:00
|
|
|
updateProp: async (name, value) => {
|
2021-01-12 21:00:35 +01:00
|
|
|
let component = get(selectedComponent)
|
|
|
|
if (!name || !component) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
component[name] = value
|
2021-05-04 12:32:22 +02:00
|
|
|
store.update(state => {
|
2021-01-12 21:00:35 +01:00
|
|
|
state.selectedComponentId = component._id
|
2020-11-04 18:09:45 +01:00
|
|
|
return state
|
|
|
|
})
|
2021-02-23 17:29:50 +01:00
|
|
|
await store.actions.preview.saveSelected()
|
2020-11-04 18:09:45 +01:00
|
|
|
},
|
2020-11-05 12:44:18 +01:00
|
|
|
links: {
|
|
|
|
save: async (url, title) => {
|
2020-11-25 18:56:09 +01:00
|
|
|
const layout = get(mainLayout)
|
2021-01-12 21:00:35 +01:00
|
|
|
if (!layout) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-06-18 13:23:33 +02:00
|
|
|
// 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({
|
2021-01-12 21:00:35 +01:00
|
|
|
text: title,
|
2021-06-18 13:23:33 +02:00
|
|
|
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]
|
2020-11-05 12:44:18 +01:00
|
|
|
}
|
2021-01-12 21:00:35 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Save layout
|
|
|
|
await store.actions.layouts.save(layout)
|
2020-11-05 12:44:18 +01:00
|
|
|
},
|
2021-09-21 15:16:10 +02:00
|
|
|
delete: 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
|
|
|
|
layout.props.links = layout.props.links.filter(
|
|
|
|
link => !(link.text === title && link.url === 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
|
|
|
|
}
|
|
|
|
|
|
|
|
nav._children = nav._children.filter(
|
|
|
|
child => !(child.url === url && child.text === title)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
// Save layout
|
|
|
|
await store.actions.layouts.save(layout)
|
|
|
|
},
|
2020-11-05 12:44:18 +01:00
|
|
|
},
|
2020-11-04 17:13:50 +01:00
|
|
|
},
|
|
|
|
}
|
2020-11-04 18:09:45 +01:00
|
|
|
|
|
|
|
return store
|
2020-11-04 17:13:50 +01:00
|
|
|
}
|