|
|
|
@ -1,6 +1,6 @@ |
|
|
|
import { get, writable } from "svelte/store" |
|
|
|
import { cloneDeep } from "lodash/fp" |
|
|
|
import { currentAsset, mainLayout, selectedComponent } from "builderStore" |
|
|
|
import { selectedScreen, selectedComponent } from "builderStore" |
|
|
|
import { |
|
|
|
datasources, |
|
|
|
integrations, |
|
|
|
@ -11,7 +11,6 @@ import { |
|
|
|
import { API } from "api" |
|
|
|
import analytics, { Events } from "analytics" |
|
|
|
import { |
|
|
|
findComponentType, |
|
|
|
findComponentParent, |
|
|
|
findClosestMatchingComponent, |
|
|
|
findAllMatchingComponents, |
|
|
|
@ -21,6 +20,7 @@ import { |
|
|
|
} from "../componentUtils" |
|
|
|
import { Helpers } from "@budibase/bbui" |
|
|
|
import { DefaultAppTheme, LAYOUT_NAMES } from "../../constants" |
|
|
|
import { Utils } from "@budibase/frontend-core" |
|
|
|
|
|
|
|
const INITIAL_FRONTEND_STATE = { |
|
|
|
apps: [], |
|
|
|
@ -61,6 +61,26 @@ const INITIAL_FRONTEND_STATE = { |
|
|
|
export const getFrontendStore = () => { |
|
|
|
const store = writable({ ...INITIAL_FRONTEND_STATE }) |
|
|
|
|
|
|
|
// This is a fake implementation of a "patch" API endpoint to try and prevent
|
|
|
|
// 409s. All screen doc mutations (aside from creation) use this function,
|
|
|
|
// which queues up invocations sequentially and ensures pending mutations are
|
|
|
|
// always applied to the most up-to-date doc revision.
|
|
|
|
// This is slightly better than just a traditional "patch" endpoint and this
|
|
|
|
// supports deeply mutating the current doc rather than just appending data.
|
|
|
|
const sequentialScreenPatch = Utils.sequential(async (patchFn, screenId) => { |
|
|
|
const state = get(store) |
|
|
|
const screen = state.screens.find(screen => screen._id === screenId) |
|
|
|
if (!screen) { |
|
|
|
return |
|
|
|
} |
|
|
|
let clone = cloneDeep(screen) |
|
|
|
const result = patchFn(clone) |
|
|
|
if (result === false) { |
|
|
|
return |
|
|
|
} |
|
|
|
return await store.actions.screens.save(clone) |
|
|
|
}) |
|
|
|
|
|
|
|
store.actions = { |
|
|
|
reset: () => { |
|
|
|
store.set({ ...INITIAL_FRONTEND_STATE }) |
|
|
|
@ -137,12 +157,12 @@ export const getFrontendStore = () => { |
|
|
|
theme: { |
|
|
|
save: async theme => { |
|
|
|
const appId = get(store).appId |
|
|
|
await API.saveAppMetadata({ |
|
|
|
const app = await API.saveAppMetadata({ |
|
|
|
appId, |
|
|
|
metadata: { theme }, |
|
|
|
}) |
|
|
|
store.update(state => { |
|
|
|
state.theme = theme |
|
|
|
state.theme = app.theme |
|
|
|
return state |
|
|
|
}) |
|
|
|
}, |
|
|
|
@ -150,12 +170,12 @@ export const getFrontendStore = () => { |
|
|
|
customTheme: { |
|
|
|
save: async customTheme => { |
|
|
|
const appId = get(store).appId |
|
|
|
await API.saveAppMetadata({ |
|
|
|
const app = await API.saveAppMetadata({ |
|
|
|
appId, |
|
|
|
metadata: { customTheme }, |
|
|
|
}) |
|
|
|
store.update(state => { |
|
|
|
state.customTheme = customTheme |
|
|
|
state.customTheme = app.customTheme |
|
|
|
return state |
|
|
|
}) |
|
|
|
}, |
|
|
|
@ -163,33 +183,35 @@ export const getFrontendStore = () => { |
|
|
|
navigation: { |
|
|
|
save: async navigation => { |
|
|
|
const appId = get(store).appId |
|
|
|
await API.saveAppMetadata({ |
|
|
|
const app = await API.saveAppMetadata({ |
|
|
|
appId, |
|
|
|
metadata: { navigation }, |
|
|
|
}) |
|
|
|
store.update(state => { |
|
|
|
state.navigation = navigation |
|
|
|
return state |
|
|
|
}) |
|
|
|
}, |
|
|
|
}, |
|
|
|
routing: { |
|
|
|
fetch: async () => { |
|
|
|
const response = await API.fetchAppRoutes() |
|
|
|
store.update(state => { |
|
|
|
state.routes = response.routes |
|
|
|
state.navigation = app.navigation |
|
|
|
return state |
|
|
|
}) |
|
|
|
}, |
|
|
|
}, |
|
|
|
screens: { |
|
|
|
select: screenId => { |
|
|
|
store.update(state => { |
|
|
|
let screens = state.screens |
|
|
|
let screen = |
|
|
|
screens.find(screen => screen._id === screenId) || screens[0] |
|
|
|
if (!screen) return state |
|
|
|
// Check this screen exists
|
|
|
|
const state = get(store) |
|
|
|
const screen = state.screens.find(screen => screen._id === screenId) |
|
|
|
if (!screen) { |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
// Check screen isn't already selected
|
|
|
|
if ( |
|
|
|
state.selectedScreenId === screen._id && |
|
|
|
state.selectedComponentId === screen.props?._id |
|
|
|
) { |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
// Select new screen
|
|
|
|
store.update(state => { |
|
|
|
state.selectedScreenId = screen._id |
|
|
|
state.selectedComponentId = screen.props?._id |
|
|
|
return state |
|
|
|
@ -198,25 +220,40 @@ export const getFrontendStore = () => { |
|
|
|
save: async screen => { |
|
|
|
const creatingNewScreen = screen._id === undefined |
|
|
|
const savedScreen = await API.saveScreen(screen) |
|
|
|
const routesResponse = await API.fetchAppRoutes() |
|
|
|
store.update(state => { |
|
|
|
// Update screen object
|
|
|
|
const idx = state.screens.findIndex(x => x._id === savedScreen._id) |
|
|
|
if (idx !== -1) { |
|
|
|
state.screens.splice(idx, 1, savedScreen) |
|
|
|
} else { |
|
|
|
state.screens.push(savedScreen) |
|
|
|
} |
|
|
|
return state |
|
|
|
}) |
|
|
|
|
|
|
|
// Refresh routes
|
|
|
|
await store.actions.routing.fetch() |
|
|
|
// Select the new screen if creating a new one
|
|
|
|
if (creatingNewScreen) { |
|
|
|
state.selectedScreenId = savedScreen._id |
|
|
|
state.selectedComponentId = savedScreen.props._id |
|
|
|
} |
|
|
|
|
|
|
|
// Update routes
|
|
|
|
state.routes = routesResponse.routes |
|
|
|
|
|
|
|
// Select the new screen if creating a new one
|
|
|
|
if (creatingNewScreen) { |
|
|
|
store.actions.screens.select(savedScreen._id) |
|
|
|
} |
|
|
|
return state |
|
|
|
}) |
|
|
|
return savedScreen |
|
|
|
}, |
|
|
|
patch: async (patchFn, screenId) => { |
|
|
|
// Default to the currently selected screen
|
|
|
|
if (!screenId) { |
|
|
|
const state = get(store) |
|
|
|
screenId = state.selectedScreenId |
|
|
|
} |
|
|
|
if (!screenId || !patchFn) { |
|
|
|
return |
|
|
|
} |
|
|
|
return await sequentialScreenPatch(patchFn, screenId) |
|
|
|
}, |
|
|
|
delete: async screens => { |
|
|
|
const screensToDelete = Array.isArray(screens) ? screens : [screens] |
|
|
|
|
|
|
|
@ -238,60 +275,78 @@ export const getFrontendStore = () => { |
|
|
|
promises.push(store.actions.links.delete(deleteUrls)) |
|
|
|
await Promise.all(promises) |
|
|
|
const deletedIds = screensToDelete.map(screen => screen._id) |
|
|
|
const routesResponse = await API.fetchAppRoutes() |
|
|
|
store.update(state => { |
|
|
|
// Remove deleted screens from state
|
|
|
|
state.screens = state.screens.filter(screen => { |
|
|
|
return !deletedIds.includes(screen._id) |
|
|
|
}) |
|
|
|
|
|
|
|
// Deselect the current screen if it was deleted
|
|
|
|
if (deletedIds.includes(state.selectedScreenId)) { |
|
|
|
state.selectedScreenId = null |
|
|
|
state.selectedComponentId = null |
|
|
|
} |
|
|
|
|
|
|
|
// Update routing
|
|
|
|
state.routes = routesResponse.routes |
|
|
|
|
|
|
|
return state |
|
|
|
}) |
|
|
|
|
|
|
|
// Refresh routes
|
|
|
|
await store.actions.routing.fetch() |
|
|
|
}, |
|
|
|
updateHomeScreen: async (screen, makeHomeScreen = true) => { |
|
|
|
let promises = [] |
|
|
|
|
|
|
|
// Find any existing home screen for this role so we can remove it,
|
|
|
|
// if we are setting this to be the new home screen
|
|
|
|
if (makeHomeScreen) { |
|
|
|
const roleId = screen.routing.roleId |
|
|
|
let existingHomeScreen = get(store).screens.find(s => { |
|
|
|
return ( |
|
|
|
s.routing.roleId === roleId && |
|
|
|
s.routing.homeScreen && |
|
|
|
s._id !== screen._id |
|
|
|
) |
|
|
|
}) |
|
|
|
if (existingHomeScreen) { |
|
|
|
existingHomeScreen.routing.homeScreen = false |
|
|
|
promises.push(store.actions.screens.save(existingHomeScreen)) |
|
|
|
updateSetting: async (screen, name, value) => { |
|
|
|
if (!screen || !name) { |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
// Apply setting update
|
|
|
|
const patch = screen => { |
|
|
|
if (!screen) { |
|
|
|
return false |
|
|
|
} |
|
|
|
// Skip update if the value is the same
|
|
|
|
if (Helpers.deepGet(screen, name) === value) { |
|
|
|
return false |
|
|
|
} |
|
|
|
Helpers.deepSet(screen, name, value) |
|
|
|
} |
|
|
|
await store.actions.screens.patch(patch, screen._id) |
|
|
|
|
|
|
|
// Update the passed in screen
|
|
|
|
screen.routing.homeScreen = makeHomeScreen |
|
|
|
promises.push(store.actions.screens.save(screen)) |
|
|
|
return await Promise.all(promises) |
|
|
|
// Ensure we don't have more than one home screen for this new role.
|
|
|
|
// This could happen after updating multiple different settings.
|
|
|
|
const state = get(store) |
|
|
|
const updatedScreen = state.screens.find(s => s._id === screen._id) |
|
|
|
if (!updatedScreen) { |
|
|
|
return |
|
|
|
} |
|
|
|
const otherHomeScreens = state.screens.filter(s => { |
|
|
|
return ( |
|
|
|
s.routing.roleId === updatedScreen.routing.roleId && |
|
|
|
s.routing.homeScreen && |
|
|
|
s._id !== screen._id |
|
|
|
) |
|
|
|
}) |
|
|
|
if (otherHomeScreens.length) { |
|
|
|
const patch = screen => { |
|
|
|
screen.routing.homeScreen = false |
|
|
|
} |
|
|
|
for (let otherHomeScreen of otherHomeScreens) { |
|
|
|
await store.actions.screens.patch(patch, otherHomeScreen._id) |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
removeCustomLayout: async screen => { |
|
|
|
// Pull relevant settings from old layout, if required
|
|
|
|
const layout = get(store).layouts.find(x => x._id === screen.layoutId) |
|
|
|
screen.layoutId = null |
|
|
|
screen.showNavigation = layout?.props.navigation !== "None" |
|
|
|
screen.width = layout?.props.width || "Large" |
|
|
|
await store.actions.screens.save(screen) |
|
|
|
const patch = screen => { |
|
|
|
screen.layoutId = null |
|
|
|
screen.showNavigation = layout?.props.navigation !== "None" |
|
|
|
screen.width = layout?.props.width || "Large" |
|
|
|
} |
|
|
|
await store.actions.screens.patch(patch, screen._id) |
|
|
|
}, |
|
|
|
}, |
|
|
|
preview: { |
|
|
|
saveSelected: async () => { |
|
|
|
const selectedAsset = get(currentAsset) |
|
|
|
return await store.actions.screens.save(selectedAsset) |
|
|
|
}, |
|
|
|
setDevice: device => { |
|
|
|
store.update(state => { |
|
|
|
state.previewDevice = device |
|
|
|
@ -301,41 +356,28 @@ export const getFrontendStore = () => { |
|
|
|
}, |
|
|
|
layouts: { |
|
|
|
select: layoutId => { |
|
|
|
// Check this layout exists
|
|
|
|
const state = get(store) |
|
|
|
const layout = state.layouts.find(layout => layout._id === layoutId) |
|
|
|
if (!layout) { |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
// Check layout isn't already selected
|
|
|
|
if ( |
|
|
|
state.selectedLayoutId === layout._id && |
|
|
|
state.selectedComponentId === layout.props?._id |
|
|
|
) { |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
// Select new layout
|
|
|
|
store.update(state => { |
|
|
|
const layout = |
|
|
|
store.actions.layouts.find(layoutId) || get(store).layouts[0] |
|
|
|
if (!layout) return |
|
|
|
state.selectedLayoutId = layout._id |
|
|
|
state.selectedComponentId = layout.props?._id |
|
|
|
return state |
|
|
|
}) |
|
|
|
}, |
|
|
|
save: async layout => { |
|
|
|
const creatingNewLayout = layout._id === undefined |
|
|
|
const savedLayout = await API.saveLayout(layout) |
|
|
|
store.update(state => { |
|
|
|
const idx = state.layouts.findIndex(x => x._id === savedLayout._id) |
|
|
|
if (idx !== -1) { |
|
|
|
state.layouts.splice(idx, 1, savedLayout) |
|
|
|
} else { |
|
|
|
state.layouts.push(savedLayout) |
|
|
|
} |
|
|
|
return state |
|
|
|
}) |
|
|
|
|
|
|
|
// Select layout if creating a new one
|
|
|
|
if (creatingNewLayout) { |
|
|
|
store.actions.layouts.select(savedLayout._id) |
|
|
|
} |
|
|
|
return savedLayout |
|
|
|
}, |
|
|
|
find: layoutId => { |
|
|
|
if (!layoutId) { |
|
|
|
return get(mainLayout) |
|
|
|
} |
|
|
|
const storeContents = get(store) |
|
|
|
return storeContents.layouts.find(layout => layout._id === layoutId) |
|
|
|
}, |
|
|
|
delete: async layout => { |
|
|
|
if (!layout?._id) { |
|
|
|
return |
|
|
|
@ -345,10 +387,6 @@ export const getFrontendStore = () => { |
|
|
|
layoutRev: layout._rev, |
|
|
|
}) |
|
|
|
store.update(state => { |
|
|
|
// Select main layout if we deleted the selected layout
|
|
|
|
if (layout._id === state.selectedLayoutId) { |
|
|
|
state.selectedLayoutId = get(mainLayout)._id |
|
|
|
} |
|
|
|
state.layouts = state.layouts.filter(x => x._id !== layout._id) |
|
|
|
return state |
|
|
|
}) |
|
|
|
@ -386,7 +424,7 @@ export const getFrontendStore = () => { |
|
|
|
} |
|
|
|
if (componentName.endsWith("/formstep")) { |
|
|
|
const parentForm = findClosestMatchingComponent( |
|
|
|
get(currentAsset).props, |
|
|
|
get(selectedScreen).props, |
|
|
|
get(selectedComponent)._id, |
|
|
|
component => component._component.endsWith("/form") |
|
|
|
) |
|
|
|
@ -407,48 +445,59 @@ export const getFrontendStore = () => { |
|
|
|
} |
|
|
|
}, |
|
|
|
create: async (componentName, presetProps) => { |
|
|
|
const selected = get(selectedComponent) |
|
|
|
const asset = get(currentAsset) |
|
|
|
|
|
|
|
// Create new component
|
|
|
|
const state = get(store) |
|
|
|
const componentInstance = store.actions.components.createInstance( |
|
|
|
componentName, |
|
|
|
presetProps |
|
|
|
) |
|
|
|
if (!componentInstance || !asset) { |
|
|
|
if (!componentInstance) { |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
// Find parent node to attach this component to
|
|
|
|
let parentComponent |
|
|
|
if (selected) { |
|
|
|
// Use current screen or layout as parent if no component is selected
|
|
|
|
const definition = store.actions.components.getDefinition( |
|
|
|
selected._component |
|
|
|
// Patch selected screen
|
|
|
|
await store.actions.screens.patch(screen => { |
|
|
|
// Find the selected component
|
|
|
|
const currentComponent = findComponent( |
|
|
|
screen.props, |
|
|
|
state.selectedComponentId |
|
|
|
) |
|
|
|
if (definition?.hasChildren) { |
|
|
|
// Use selected component if it allows children
|
|
|
|
parentComponent = selected |
|
|
|
if (!currentComponent) { |
|
|
|
return false |
|
|
|
} |
|
|
|
|
|
|
|
// Find parent node to attach this component to
|
|
|
|
let parentComponent |
|
|
|
if (currentComponent) { |
|
|
|
// Use selected component as parent if one is selected
|
|
|
|
const definition = store.actions.components.getDefinition( |
|
|
|
currentComponent._component |
|
|
|
) |
|
|
|
if (definition?.hasChildren) { |
|
|
|
// Use selected component if it allows children
|
|
|
|
parentComponent = currentComponent |
|
|
|
} else { |
|
|
|
// Otherwise we need to use the parent of this component
|
|
|
|
parentComponent = findComponentParent( |
|
|
|
screen.props, |
|
|
|
currentComponent._id |
|
|
|
) |
|
|
|
} |
|
|
|
} else { |
|
|
|
// Otherwise we need to use the parent of this component
|
|
|
|
parentComponent = findComponentParent(asset?.props, selected._id) |
|
|
|
// Use screen or layout if no component is selected
|
|
|
|
parentComponent = screen.props |
|
|
|
} |
|
|
|
} 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) |
|
|
|
// Attach new component
|
|
|
|
if (!parentComponent) { |
|
|
|
return false |
|
|
|
} |
|
|
|
if (!parentComponent._children) { |
|
|
|
parentComponent._children = [] |
|
|
|
} |
|
|
|
parentComponent._children.push(componentInstance) |
|
|
|
}) |
|
|
|
|
|
|
|
// Save components and update UI
|
|
|
|
await store.actions.preview.saveSelected() |
|
|
|
// Select new component
|
|
|
|
store.update(state => { |
|
|
|
state.selectedComponentId = componentInstance._id |
|
|
|
return state |
|
|
|
@ -461,50 +510,58 @@ export const getFrontendStore = () => { |
|
|
|
|
|
|
|
return componentInstance |
|
|
|
}, |
|
|
|
delete: async component => { |
|
|
|
if (!component) { |
|
|
|
return |
|
|
|
patch: async (patchFn, componentId, screenId) => { |
|
|
|
// Use selected component by default
|
|
|
|
if (!componentId && !screenId) { |
|
|
|
const state = get(store) |
|
|
|
componentId = state.selectedComponentId |
|
|
|
screenId = state.selectedScreenId |
|
|
|
} |
|
|
|
const asset = get(currentAsset) |
|
|
|
if (!asset) { |
|
|
|
// Invalid if only a screen or component ID provided
|
|
|
|
if (!componentId || !screenId || !patchFn) { |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
// Fetch full definition
|
|
|
|
component = findComponent(asset.props, component._id) |
|
|
|
|
|
|
|
// Ensure we aren't deleting the screen slot
|
|
|
|
if (component._component?.endsWith("/screenslot")) { |
|
|
|
throw "You can't delete the screen slot" |
|
|
|
const patchScreen = screen => { |
|
|
|
let component = findComponent(screen.props, componentId) |
|
|
|
if (!component) { |
|
|
|
return false |
|
|
|
} |
|
|
|
return patchFn(component, screen) |
|
|
|
} |
|
|
|
|
|
|
|
// Ensure we aren't deleting something that contains the screen slot
|
|
|
|
const screenslot = findComponentType( |
|
|
|
component, |
|
|
|
"@budibase/standard-components/screenslot" |
|
|
|
) |
|
|
|
if (screenslot != null) { |
|
|
|
throw "You can't delete a component that contains the screen slot" |
|
|
|
await store.actions.screens.patch(patchScreen, screenId) |
|
|
|
}, |
|
|
|
delete: async component => { |
|
|
|
if (!component) { |
|
|
|
return |
|
|
|
} |
|
|
|
let parentId |
|
|
|
|
|
|
|
// Patch screen
|
|
|
|
await store.actions.screens.patch(screen => { |
|
|
|
// Check component exists
|
|
|
|
component = findComponent(screen.props, component._id) |
|
|
|
if (!component) { |
|
|
|
return false |
|
|
|
} |
|
|
|
|
|
|
|
const parent = findComponentParent(asset.props, component._id) |
|
|
|
if (parent) { |
|
|
|
// Check component has a valid parent
|
|
|
|
const parent = findComponentParent(screen.props, component._id) |
|
|
|
if (!parent) { |
|
|
|
return false |
|
|
|
} |
|
|
|
parentId = parent._id |
|
|
|
parent._children = parent._children.filter( |
|
|
|
child => child._id !== component._id |
|
|
|
) |
|
|
|
store.update(state => { |
|
|
|
state.selectedComponentId = parent._id |
|
|
|
return state |
|
|
|
}) |
|
|
|
} |
|
|
|
await store.actions.preview.saveSelected() |
|
|
|
}) |
|
|
|
|
|
|
|
// Select the deleted component's parent
|
|
|
|
store.update(state => { |
|
|
|
state.selectedComponentId = parentId |
|
|
|
return state |
|
|
|
}) |
|
|
|
}, |
|
|
|
copy: (component, cut = false, selectParent = true) => { |
|
|
|
const selectedAsset = get(currentAsset) |
|
|
|
if (!selectedAsset) { |
|
|
|
return null |
|
|
|
} |
|
|
|
|
|
|
|
// Update store with copied component
|
|
|
|
store.update(state => { |
|
|
|
state.componentToPaste = cloneDeep(component) |
|
|
|
@ -512,13 +569,11 @@ export const getFrontendStore = () => { |
|
|
|
return state |
|
|
|
}) |
|
|
|
|
|
|
|
// Remove the component from its parent if we're cutting
|
|
|
|
// Select the parent if cutting
|
|
|
|
if (cut) { |
|
|
|
const parent = findComponentParent(selectedAsset.props, component._id) |
|
|
|
const screen = get(selectedScreen) |
|
|
|
const parent = findComponentParent(screen?.props, component._id) |
|
|
|
if (parent) { |
|
|
|
parent._children = parent._children.filter( |
|
|
|
child => child._id !== component._id |
|
|
|
) |
|
|
|
if (selectParent) { |
|
|
|
store.update(state => { |
|
|
|
state.selectedComponentId = parent._id |
|
|
|
@ -528,24 +583,42 @@ export const getFrontendStore = () => { |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
paste: async (targetComponent, mode) => { |
|
|
|
let promises = [] |
|
|
|
store.update(state => { |
|
|
|
// Stop if we have nothing to paste
|
|
|
|
if (!state.componentToPaste) { |
|
|
|
return state |
|
|
|
paste: async (targetComponent, mode, targetScreen) => { |
|
|
|
const state = get(store) |
|
|
|
if (!state.componentToPaste) { |
|
|
|
return |
|
|
|
} |
|
|
|
let newComponentId |
|
|
|
|
|
|
|
// Patch screen
|
|
|
|
const patch = screen => { |
|
|
|
// Get up to date ref to target
|
|
|
|
targetComponent = findComponent(screen.props, targetComponent._id) |
|
|
|
if (!targetComponent) { |
|
|
|
return |
|
|
|
} |
|
|
|
const cut = state.componentToPaste.isCut |
|
|
|
|
|
|
|
// Clone the component to paste and make unique if copying
|
|
|
|
delete state.componentToPaste.isCut |
|
|
|
const originalId = state.componentToPaste._id |
|
|
|
let componentToPaste = cloneDeep(state.componentToPaste) |
|
|
|
if (cut) { |
|
|
|
state.componentToPaste = null |
|
|
|
} else { |
|
|
|
delete componentToPaste.isCut |
|
|
|
|
|
|
|
// Make new component unique if copying
|
|
|
|
if (!cut) { |
|
|
|
makeComponentUnique(componentToPaste) |
|
|
|
} |
|
|
|
newComponentId = componentToPaste._id |
|
|
|
|
|
|
|
// Delete old component if cutting
|
|
|
|
if (cut) { |
|
|
|
const parent = findComponentParent(screen.props, originalId) |
|
|
|
if (parent?._children) { |
|
|
|
parent._children = parent._children.filter( |
|
|
|
component => component._id !== originalId |
|
|
|
) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Paste new component
|
|
|
|
if (mode === "inside") { |
|
|
|
// Paste inside target component if chosen
|
|
|
|
if (!targetComponent._children) { |
|
|
|
@ -553,66 +626,106 @@ export const getFrontendStore = () => { |
|
|
|
} |
|
|
|
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 |
|
|
|
} |
|
|
|
// Otherwise paste in the correct order in the parent's children
|
|
|
|
const parent = findComponentParent( |
|
|
|
selectedAsset.props, |
|
|
|
screen.props, |
|
|
|
targetComponent._id |
|
|
|
) |
|
|
|
if (!parent) { |
|
|
|
return state |
|
|
|
if (!parent?._children) { |
|
|
|
return false |
|
|
|
} |
|
|
|
|
|
|
|
// Insert the component in the correct position
|
|
|
|
const targetIndex = parent._children.indexOf(targetComponent) |
|
|
|
const targetIndex = parent._children.findIndex(component => { |
|
|
|
return component._id === targetComponent._id |
|
|
|
}) |
|
|
|
const index = mode === "above" ? targetIndex : targetIndex + 1 |
|
|
|
parent._children.splice(index, 0, cloneDeep(componentToPaste)) |
|
|
|
parent._children.splice(index, 0, componentToPaste) |
|
|
|
} |
|
|
|
} |
|
|
|
const targetScreenId = targetScreen?._id || state.selectedScreenId |
|
|
|
await store.actions.screens.patch(patch, targetScreenId) |
|
|
|
|
|
|
|
// Save and select the new component
|
|
|
|
promises.push(store.actions.preview.saveSelected()) |
|
|
|
state.selectedComponentId = componentToPaste._id |
|
|
|
store.update(state => { |
|
|
|
// Remove copied component if cutting
|
|
|
|
if (state.componentToPaste.isCut) { |
|
|
|
delete state.componentToPaste |
|
|
|
} |
|
|
|
state.selectedScreenId = targetScreenId |
|
|
|
state.selectedComponentId = newComponentId |
|
|
|
return state |
|
|
|
}) |
|
|
|
await Promise.all(promises) |
|
|
|
}, |
|
|
|
moveUp: async component => { |
|
|
|
await store.actions.screens.patch(screen => { |
|
|
|
const componentId = component?._id |
|
|
|
const parent = findComponentParent(screen.props, componentId) |
|
|
|
if (!parent?._children?.length) { |
|
|
|
return false |
|
|
|
} |
|
|
|
const currentIndex = parent._children.findIndex( |
|
|
|
child => child._id === componentId |
|
|
|
) |
|
|
|
if (currentIndex === 0) { |
|
|
|
return false |
|
|
|
} |
|
|
|
const originalComponent = cloneDeep(parent._children[currentIndex]) |
|
|
|
const newChildren = parent._children.filter( |
|
|
|
component => component._id !== componentId |
|
|
|
) |
|
|
|
newChildren.splice(currentIndex - 1, 0, originalComponent) |
|
|
|
parent._children = newChildren |
|
|
|
}) |
|
|
|
}, |
|
|
|
moveDown: async component => { |
|
|
|
await store.actions.screens.patch(screen => { |
|
|
|
const componentId = component?._id |
|
|
|
const parent = findComponentParent(screen.props, componentId) |
|
|
|
if (!parent?._children?.length) { |
|
|
|
return false |
|
|
|
} |
|
|
|
const currentIndex = parent._children.findIndex( |
|
|
|
child => child._id === componentId |
|
|
|
) |
|
|
|
if (currentIndex === parent._children.length - 1) { |
|
|
|
return false |
|
|
|
} |
|
|
|
const originalComponent = cloneDeep(parent._children[currentIndex]) |
|
|
|
const newChildren = parent._children.filter( |
|
|
|
component => component._id !== componentId |
|
|
|
) |
|
|
|
newChildren.splice(currentIndex + 1, 0, originalComponent) |
|
|
|
parent._children = newChildren |
|
|
|
}) |
|
|
|
}, |
|
|
|
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() |
|
|
|
await store.actions.components.patch(component => { |
|
|
|
if (value == null || value === "") { |
|
|
|
delete component._styles.normal[name] |
|
|
|
} else { |
|
|
|
component._styles.normal[name] = value |
|
|
|
} |
|
|
|
}) |
|
|
|
}, |
|
|
|
updateCustomStyle: async style => { |
|
|
|
const selected = get(selectedComponent) |
|
|
|
selected._styles.custom = style |
|
|
|
await store.actions.preview.saveSelected() |
|
|
|
await store.actions.components.patch(component => { |
|
|
|
component._styles.custom = style |
|
|
|
}) |
|
|
|
}, |
|
|
|
updateConditions: async conditions => { |
|
|
|
const selected = get(selectedComponent) |
|
|
|
selected._conditions = conditions |
|
|
|
await store.actions.preview.saveSelected() |
|
|
|
await store.actions.components.patch(component => { |
|
|
|
component._conditions = conditions |
|
|
|
}) |
|
|
|
}, |
|
|
|
updateProp: async (name, value) => { |
|
|
|
let component = get(selectedComponent) |
|
|
|
if (!name || !component) { |
|
|
|
return |
|
|
|
} |
|
|
|
if (component[name] === value) { |
|
|
|
return |
|
|
|
} |
|
|
|
component[name] = value |
|
|
|
store.update(state => { |
|
|
|
state.selectedComponentId = component._id |
|
|
|
return state |
|
|
|
updateSetting: async (name, value) => { |
|
|
|
await store.actions.components.patch(component => { |
|
|
|
if (!name || !component) { |
|
|
|
return false |
|
|
|
} |
|
|
|
// Skip update if the value is the same
|
|
|
|
if (component[name] === value) { |
|
|
|
return false |
|
|
|
} |
|
|
|
component[name] = value |
|
|
|
}) |
|
|
|
await store.actions.preview.saveSelected() |
|
|
|
}, |
|
|
|
}, |
|
|
|
links: { |
|
|
|
|