diff --git a/packages/builder/src/builderStore/index.js b/packages/builder/src/builderStore/index.js index cf668670bb..a22678fa92 100644 --- a/packages/builder/src/builderStore/index.js +++ b/packages/builder/src/builderStore/index.js @@ -5,7 +5,7 @@ import { getThemeStore } from "./store/theme" import { derived, writable } from "svelte/store" import analytics from "analytics" import { FrontendTypes, LAYOUT_NAMES } from "../constants" -import { makePropsSafe } from "components/userInterface/assetParsing/createProps" +import { findComponent } from "./storeUtils" export const store = getFrontendStore() export const backendUiStore = getBackendUiStore() @@ -25,31 +25,10 @@ export const currentAsset = derived(store, $store => { export const selectedComponent = derived( [store, currentAsset], ([$store, $currentAsset]) => { - if (!$currentAsset || !$store.selectedComponentId) return null - - function traverse(node, callback) { - if (node._id === $store.selectedComponentId) return callback(node) - - if (node._children) { - node._children.forEach(child => traverse(child, callback)) - } - - if (node.props) { - traverse(node.props, callback) - } + if (!$currentAsset || !$store.selectedComponentId) { + return null } - - let component - traverse($currentAsset, found => { - const componentIdentifier = found._component ?? found.props._component - const componentDef = componentIdentifier.startsWith("##") - ? found - : $store.components[componentIdentifier] - - component = makePropsSafe(componentDef, found) - }) - - return component + return findComponent($currentAsset.props, $store.selectedComponentId) } ) diff --git a/packages/builder/src/builderStore/loadComponentLibraries.js b/packages/builder/src/builderStore/loadComponentLibraries.js index 9d534f86fe..e6516f7d79 100644 --- a/packages/builder/src/builderStore/loadComponentLibraries.js +++ b/packages/builder/src/builderStore/loadComponentLibraries.js @@ -21,12 +21,9 @@ export const fetchComponentLibDefinitions = async appId => { */ export const fetchComponentLibModules = async application => { const allLibraries = {} - for (let libraryName of application.componentLibraries) { const LIBRARY_URL = `/${application._id}/componentlibrary?library=${libraryName}` - const libraryModule = await import(LIBRARY_URL) - allLibraries[libraryName] = libraryModule + allLibraries[libraryName] = await import(LIBRARY_URL) } - return allLibraries } diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index acb5929b3c..6369e0e41a 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -1,9 +1,5 @@ import { get, writable } from "svelte/store" import { cloneDeep } from "lodash/fp" -import { - createProps, - getBuiltin, -} from "components/userInterface/assetParsing/createProps" import { allScreens, backendUiStore, @@ -15,14 +11,13 @@ import { import { fetchComponentLibDefinitions } from "../loadComponentLibraries" import api from "../api" import { FrontendTypes } from "../../constants" -import getNewComponentName from "../getNewComponentName" import analytics from "analytics" import { - findChildComponentType, - generateNewIdsForComponent, - getComponentDefinition, - findParent, + findComponentType, + findComponentParent, + findComponentPath, } from "../storeUtils" +import { uuid } from "../uuid" const INITIAL_FRONTEND_STATE = { apps: [], @@ -48,14 +43,7 @@ export const getFrontendStore = () => { store.actions = { initialise: async pkg => { const { layouts, screens, application } = pkg - - store.update(state => { - state.appId = application._id - return state - }) - const components = await fetchComponentLibDefinitions(pkg.application._id) - store.update(state => ({ ...state, libraries: pkg.application.componentLibraries, @@ -66,17 +54,14 @@ export const getFrontendStore = () => { layouts, screens, hasAppPackage: true, - builtins: [getBuiltin("##builtin/screenslot")], appInstance: pkg.application.instance, })) - await backendUiStore.actions.database.select(pkg.application.instance) }, routing: { fetch: async () => { const response = await api.get("/api/routing") const json = await response.json() - store.update(state => { state.routes = json.routes return state @@ -245,122 +230,194 @@ export const getFrontendStore = () => { return state }) }, - create: (componentToAdd, presetProps) => { - const selectedAsset = get(currentAsset) + getDefinition: componentName => { + if (!componentName) { + return null + } + const name = componentName.startsWith("@budibase") + ? componentName + : `@budibase/standard-components/${componentName}` + return get(store).components[name] + }, + createInstance: (componentName, presetProps) => { + const definition = store.actions.components.getDefinition(componentName) + if (!definition) { + return null + } - store.update(state => { - function findSlot(component_array) { - if (!component_array) { - return false + // Generate default props + let props = { ...presetProps } + if (definition.settings) { + definition.settings.forEach(setting => { + if (setting.defaultValue !== undefined) { + props[setting.key] = setting.defaultValue } - for (let component of component_array) { - if (component._component === "##builtin/screenslot") { - return true - } - - if (component._children) findSlot(component) - } - return false - } - - if ( - componentToAdd.startsWith("##") && - findSlot(selectedAsset?.props._children) - ) { - return state - } - - const component = getComponentDefinition(state, componentToAdd) - - const instanceId = get(backendUiStore).selectedDatabase._id - const instanceName = getNewComponentName(component, state) - - const newComponent = createProps(component, { - ...presetProps, - _instanceId: instanceId, - _instanceName: instanceName, }) + } - const selected = get(selectedComponent) + // Add any extra properties the component needs + let extras = {} + if (definition.hasChildren) { + extras._children = [] + } - const currentComponentDefinition = - state.components[selected._component] + return { + _id: uuid(), + _component: definition.component, + _styles: { normal: {}, hover: {}, active: {} }, + _instanceName: `New ${definition.component.split("/")[2]}`, + ...cloneDeep(props), + ...extras, + } + }, + create: (componentName, presetProps) => { + // Create new component + const componentInstance = store.actions.components.createInstance( + componentName, + presetProps + ) + if (!componentInstance) { + return + } - const allowsChildren = currentComponentDefinition.children - - // Determine where to put the new component. - let targetParent - if (allowsChildren) { - // Child of the selected component - targetParent = selected + // Find parent node to attach this component to + let parentComponent + const selected = get(selectedComponent) + const asset = get(currentAsset) + 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 { - // Sibling of selected component - targetParent = findParent(selectedAsset.props, selected) + // Otherwise we need to use the parent of this component + parentComponent = findComponentParent(asset.props, selected._id) } + } else { + // Use screen or layout if no component is selected + parentComponent = asset.props + } - // Don't continue if there's no parent - if (!targetParent) return state - - // Push the new component - targetParent._children.push(newComponent.props) - - store.actions.preview.saveSelected() + // Attach component + if (!parentComponent) { + return + } + if (!parentComponent._children) { + parentComponent._children = [] + } + parentComponent._children.push(componentInstance) + // Save components and update UI + store.actions.preview.saveSelected() + store.update(state => { state.currentView = "component" - state.selectedComponentId = newComponent.props._id - - analytics.captureEvent("Added Component", { - name: newComponent.props._component, - }) + state.selectedComponentId = componentInstance._id return state }) + + // Log event + analytics.captureEvent("Added Component", { + name: componentInstance._component, + }) + + return componentInstance + }, + delete: 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( + child => child._id !== component._id + ) + store.actions.components.select(parent) + } + store.actions.preview.saveSelected() }, copy: (component, cut = false) => { const selectedAsset = get(currentAsset) + if (!selectedAsset) { + return null + } + + // Update store with copied component store.update(state => { state.componentToPaste = cloneDeep(component) state.componentToPaste.isCut = cut - if (cut) { - const parent = findParent(selectedAsset.props, component._id) + return state + }) + + // Remove the component from its parent if we're cutting + if (cut) { + const parent = findComponentParent(selectedAsset.props, component._id) + if (parent) { parent._children = parent._children.filter( child => child._id !== component._id ) store.actions.components.select(parent) } - - return state - }) + } }, paste: async (targetComponent, mode) => { - const selectedAsset = get(currentAsset) let promises = [] 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 - - if (mode === "inside") { - targetComponent._children.push(componentToPaste) + // Stop if we have nothing to paste + if (!state.componentToPaste) { return state } - const parent = findParent(selectedAsset.props, targetComponent) + // Clone the component to paste + // Retain the same ID if cutting as things may be referencing this component + const cut = state.componentToPaste.isCut + delete state.componentToPaste.isCut + let componentToPaste = cloneDeep(state.componentToPaste) + if (cut) { + state.componentToPaste = null + } else { + componentToPaste._id = uuid() + } - const targetIndex = parent._children.indexOf(targetComponent) - const index = mode === "above" ? targetIndex : targetIndex + 1 - parent._children.splice(index, 0, cloneDeep(componentToPaste)) + if (mode === "inside") { + // Paste inside target component if chosen + if (!targetComponent._children) { + targetComponent._children = [] + } + 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()) store.actions.components.select(componentToPaste) - return state }) await Promise.all(promises) @@ -385,90 +442,68 @@ export const getFrontendStore = () => { await store.actions.preview.saveSelected() }, updateProp: (name, value) => { + let component = get(selectedComponent) + if (!name || !component) { + return + } + component[name] = value store.update(state => { - let current_component = get(selectedComponent) - current_component[name] = value - - state.selectedComponentId = current_component._id - store.actions.preview.saveSelected() + state.selectedComponentId = component._id return state }) + store.actions.preview.saveSelected() }, findRoute: component => { - // Gets all the components to needed to construct a path. const selectedAsset = get(currentAsset) - let pathComponents = [] - let parent = component - let root = false - while (!root) { - parent = findParent(selectedAsset.props, parent) - if (!parent) { - root = true - } else { - pathComponents.push(parent) - } + if (!component || !selectedAsset) { + return "/" } - // Remove root entry since it's the screen or layout. - // Reverse array since we need the correct order of the IDs - const reversedComponents = pathComponents.reverse().slice(1) + // Get the path to this component + const path = findComponentPath(selectedAsset.props, component._id) || [] - // Add component - const allComponents = [...reversedComponents, component] - - // Map IDs - const IdList = allComponents.map(c => c._id) - - // Construct ID Path: - return IdList.join("/") + // Remove root entry since it's the screen or layout + return path.slice(1).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 + if (!layout) { + return + } - // Clone an existing link if one exists - if (nav._children && nav._children.length) { - // Clone existing link style - newLink = cloneDeep(nav._children[0]) + // Find a nav bar in the main layout + const nav = findComponentType( + layout, + "@budibase/standard-components/navigation" + ) + if (!nav) { + return + } - // Manipulate IDs to ensure uniqueness - generateNewIdsForComponent(newLink, state, false) + 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._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 - nav._children = [...nav._children, newLink] - promises.push(store.actions.layouts.save(layout)) + // 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`, } - return state - }) - await Promise.all(promises) + } + + // Save layout + nav._children = [...nav._children, newLink] + await store.actions.layouts.save(layout) }, }, }, diff --git a/packages/builder/src/builderStore/store/screenTemplates/index.js b/packages/builder/src/builderStore/store/screenTemplates/index.js index ddf48cbe44..9258feb57e 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/index.js +++ b/packages/builder/src/builderStore/store/screenTemplates/index.js @@ -4,7 +4,6 @@ import rowListScreen from "./rowListScreen" import emptyNewRowScreen from "./emptyNewRowScreen" import createFromScratchScreen from "./createFromScratchScreen" import emptyRowDetailScreen from "./emptyRowDetailScreen" -import { generateNewIdsForComponent } from "../../storeUtils" import { uuid } from "builderStore/uuid" const allTemplates = tables => [ @@ -16,13 +15,21 @@ const allTemplates = tables => [ emptyRowDetailScreen, ] -// allows us to apply common behaviour to all create() functions +// Recurses through a component tree and generates new unique ID's +const makeUniqueIds = component => { + if (!component) { + return + } + component._id = uuid() + if (component._children) { + component._children.forEach(makeUniqueIds) + } +} + +// Allows us to apply common behaviour to all create() functions const createTemplateOverride = (frontendState, create) => () => { const screen = create() - for (let component of screen.props._children) { - generateNewIdsForComponent(component, frontendState, false) - } - screen.props._id = uuid() + makeUniqueIds(screen.props) screen.name = screen.props._id screen.routing.route = screen.routing.route.toLowerCase() return screen diff --git a/packages/builder/src/builderStore/storeUtils.js b/packages/builder/src/builderStore/storeUtils.js index 4ee2dd7ccc..0d13522711 100644 --- a/packages/builder/src/builderStore/storeUtils.js +++ b/packages/builder/src/builderStore/storeUtils.js @@ -1,80 +1,82 @@ -import { getBuiltin } from "components/userInterface/assetParsing/createProps" -import { uuid } from "./uuid" -import getNewComponentName from "./getNewComponentName" +/** + * Recursively searches for a specific component ID + */ +export const findComponent = (rootComponent, id) => { + return searchComponentTree(rootComponent, comp => comp._id === id) +} /** - * Find the parent component of the passed in child. - * @param {Object} rootProps - props to search for the parent in - * @param {String|Object} child - id of the child or the child itself to find the parent of + * Recursively searches for a specific component type */ -export const findParent = (rootProps, child) => { - let parent - walkProps(rootProps, (props, breakWalk) => { - if ( - props._children && - (props._children.includes(child) || - props._children.some(c => c._id === child)) - ) { - parent = props - breakWalk() - } - }) - return parent +export const findComponentType = (rootComponent, type) => { + return searchComponentTree(rootComponent, comp => comp._component === type) } -export const walkProps = (props, action, cancelToken = null) => { - cancelToken = cancelToken || { cancelled: false } - action(props, () => { - cancelToken.cancelled = true - }) - - if (props._children) { - for (let child of props._children) { - if (cancelToken.cancelled) return - walkProps(child, action, cancelToken) - } - } -} - -export const generateNewIdsForComponent = ( - component, - state, - changeName = true -) => - walkProps(component, prop => { - prop._id = uuid() - if (changeName) prop._instanceName = getNewComponentName(prop, state) - }) - -export const getComponentDefinition = (state, name) => - name.startsWith("##") ? getBuiltin(name) : state.components[name] - -export const findChildComponentType = (node, typeToFind) => { - // Stop recursion if invalid props - if (!node || !typeToFind) { +/** + * Recursively searches for the parent component of a specific component ID + */ +export const findComponentParent = (rootComponent, id, parentComponent) => { + if (!rootComponent || !id) { return null } - - // Stop recursion if this element matches - if (node._component === typeToFind) { - return node + if (rootComponent._id === id) { + return parentComponent } - - // Otherwise check if any children match - // Stop recursion if no valid children to process - const children = node._children || (node.props && node.props._children) - if (!children || !children.length) { + if (!rootComponent._children) { return null } - - // Recurse and check each child component - for (let child of children) { - const childResult = findChildComponentType(child, typeToFind) + for (const child of rootComponent._children) { + const childResult = findComponentParent(child, id, rootComponent) + if (childResult) { + return childResult + } + } + return null +} + +/** + * Recursively searches for a specific component ID and records the component + * path to this component + */ +export const findComponentPath = (rootComponent, id, path = []) => { + if (!rootComponent || !id) { + return null + } + if (rootComponent._id === id) { + return [...path, id] + } + if (!rootComponent._children) { + return null + } + for (const child of rootComponent._children) { + const newPath = [...path, rootComponent._id] + const childResult = findComponentPath(child, id, newPath) + if (childResult != null) { + return childResult + } + } + return null +} + +/** + * Recurses through a component tree evaluating a matching function against + * components until a match is found + */ +const searchComponentTree = (rootComponent, matchComponent) => { + if (!rootComponent || !matchComponent) { + return null + } + if (matchComponent(rootComponent)) { + return rootComponent + } + if (!rootComponent._children) { + return null + } + for (const child of rootComponent._children) { + const childResult = searchComponentTree(child, matchComponent) if (childResult) { return childResult } } - - // If we reach here then no children were valid return null } diff --git a/packages/builder/src/components/userInterface/AppPreview/CurrentItemPreview.svelte b/packages/builder/src/components/userInterface/AppPreview/CurrentItemPreview.svelte index c5bcb2951a..8e899c0bb1 100644 --- a/packages/builder/src/components/userInterface/AppPreview/CurrentItemPreview.svelte +++ b/packages/builder/src/components/userInterface/AppPreview/CurrentItemPreview.svelte @@ -14,7 +14,7 @@ const screenPlaceholder = new Screen() .name("Screen Placeholder") .route("*") - .component("@budibase/standard-components/screenslotplaceholder") + .component("@budibase/standard-components/screenslot") .instanceName("Content Placeholder") .json() diff --git a/packages/builder/src/components/userInterface/ComponentDropdownMenu.svelte b/packages/builder/src/components/userInterface/ComponentDropdownMenu.svelte index 251a60155f..0b97684d82 100644 --- a/packages/builder/src/components/userInterface/ComponentDropdownMenu.svelte +++ b/packages/builder/src/components/userInterface/ComponentDropdownMenu.svelte @@ -2,10 +2,9 @@ import { goto } from "@sveltech/routify" import { get } from "svelte/store" import { store, currentAsset } from "builderStore" - import { getComponentDefinition } from "builderStore/storeUtils" import ConfirmDialog from "components/common/ConfirmDialog.svelte" import { last } from "lodash/fp" - import { findParent } from "builderStore/storeUtils" + import { findComponentParent } from "builderStore/storeUtils" import { DropdownMenu } from "@budibase/bbui" import { DropdownContainer, DropdownItem } from "components/common/Dropdowns" @@ -17,7 +16,7 @@ $: noChildrenAllowed = !component || - !getComponentDefinition($store, component._component)?.children + !store.actions.components.getDefinition(component._component)?.hasChildren $: noPaste = !$store.componentToPaste const lastPartOfName = c => (c ? last(c._component.split("/")) : "") @@ -35,7 +34,7 @@ const moveUpComponent = () => { store.update(state => { const asset = get(currentAsset) - const parent = findParent(asset.props, component) + const parent = findComponentParent(asset.props, component) if (parent) { const currentIndex = parent._children.indexOf(component) @@ -55,7 +54,7 @@ const moveDownComponent = () => { store.update(state => { const asset = get(currentAsset) - const parent = findParent(asset.props, component) + const parent = findComponentParent(asset.props, component) if (parent) { const currentIndex = parent._children.indexOf(component) @@ -78,18 +77,7 @@ } const deleteComponent = () => { - store.update(state => { - const asset = get(currentAsset) - const parent = findParent(asset.props, component) - - if (parent) { - parent._children = parent._children.filter(child => child !== component) - selectComponent(parent) - } - - store.actions.preview.saveSelected() - return state - }) + store.actions.components.delete(component) } const storeComponentForCopy = (cut = false) => { diff --git a/packages/builder/src/components/userInterface/ComponentNavigationTree/ComponentTree.svelte b/packages/builder/src/components/userInterface/ComponentNavigationTree/ComponentTree.svelte index 320a81f00d..1fd3f423d3 100644 --- a/packages/builder/src/components/userInterface/ComponentNavigationTree/ComponentTree.svelte +++ b/packages/builder/src/components/userInterface/ComponentNavigationTree/ComponentTree.svelte @@ -1,7 +1,6 @@