Merge branch 'master' into convert-client-builder-store

This commit is contained in:
Adria Navarro 2025-02-13 11:31:35 +01:00
commit b6f5497bc7
94 changed files with 1042 additions and 837 deletions

View File

@ -13,6 +13,11 @@
import { fly } from "svelte/transition" import { fly } from "svelte/transition"
import { findComponentPath } from "@/helpers/components" import { findComponentPath } from "@/helpers/components"
// Smallest possible 1x1 transparent GIF
const ghost = new Image(1, 1)
ghost.src =
"data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="
let searchString let searchString
let searchRef let searchRef
let selectedIndex let selectedIndex
@ -217,7 +222,8 @@
} }
}) })
const onDragStart = component => { const onDragStart = (e, component) => {
e.dataTransfer.setDragImage(ghost, 0, 0)
previewStore.startDrag(component) previewStore.startDrag(component)
} }
@ -250,13 +256,12 @@
{#each category.children as component} {#each category.children as component}
<div <div
draggable="true" draggable="true"
on:dragstart={() => onDragStart(component.component)} on:dragstart={e => onDragStart(e, component.component)}
on:dragend={onDragEnd} on:dragend={onDragEnd}
class="component" class="component"
class:selected={selectedIndex === orderMap[component.component]} class:selected={selectedIndex === orderMap[component.component]}
on:click={() => addComponent(component.component)} on:click={() => addComponent(component.component)}
on:mouseover={() => (selectedIndex = null)} on:mouseenter={() => (selectedIndex = null)}
on:focus
> >
<Icon name={component.icon} /> <Icon name={component.icon} />
<Body size="XS">{component.name}</Body> <Body size="XS">{component.name}</Body>
@ -308,7 +313,6 @@
} }
.component:hover { .component:hover {
background: var(--spectrum-global-color-gray-300); background: var(--spectrum-global-color-gray-300);
cursor: pointer;
} }
.component :global(.spectrum-Body) { .component :global(.spectrum-Body) {
line-height: 1.2 !important; line-height: 1.2 !important;

View File

@ -189,8 +189,8 @@
} else if (type === "reload-plugin") { } else if (type === "reload-plugin") {
await componentStore.refreshDefinitions() await componentStore.refreshDefinitions()
} else if (type === "drop-new-component") { } else if (type === "drop-new-component") {
const { component, parent, index } = data const { component, parent, index, props } = data
await componentStore.create(component, null, parent, index) await componentStore.create(component, props, parent, index)
} else if (type === "add-parent-component") { } else if (type === "add-parent-component") {
const { componentId, parentType } = data const { componentId, parentType } = data
await componentStore.addParent(componentId, parentType) await componentStore.addParent(componentId, parentType)

View File

@ -39,7 +39,7 @@ interface AppMetaState {
appInstance: { _id: string } | null appInstance: { _id: string } | null
initialised: boolean initialised: boolean
hasAppPackage: boolean hasAppPackage: boolean
usedPlugins: Plugin[] | null usedPlugins: Plugin[]
automations: AutomationSettings automations: AutomationSettings
routes: { [key: string]: any } routes: { [key: string]: any }
version?: string version?: string
@ -76,7 +76,7 @@ export const INITIAL_APP_META_STATE: AppMetaState = {
appInstance: null, appInstance: null,
initialised: false, initialised: false,
hasAppPackage: false, hasAppPackage: false,
usedPlugins: null, usedPlugins: [],
automations: {}, automations: {},
routes: {}, routes: {},
} }
@ -109,7 +109,7 @@ export class AppMetaStore extends BudiStore<AppMetaState> {
appInstance: app.instance, appInstance: app.instance,
revertableVersion: app.revertableVersion, revertableVersion: app.revertableVersion,
upgradableVersion: app.upgradableVersion, upgradableVersion: app.upgradableVersion,
usedPlugins: app.usedPlugins || null, usedPlugins: app.usedPlugins || [],
icon: app.icon, icon: app.icon,
features: { features: {
...INITIAL_APP_META_STATE.features, ...INITIAL_APP_META_STATE.features,

View File

@ -11,6 +11,7 @@ import {
findComponentParent, findComponentParent,
findAllMatchingComponents, findAllMatchingComponents,
makeComponentUnique, makeComponentUnique,
findComponentType,
} from "@/helpers/components" } from "@/helpers/components"
import { getComponentFieldOptions } from "@/helpers/formFields" import { getComponentFieldOptions } from "@/helpers/formFields"
import { selectedScreen } from "./screens" import { selectedScreen } from "./screens"
@ -139,10 +140,6 @@ export class ComponentStore extends BudiStore<ComponentState> {
/** /**
* Retrieve the component definition object * Retrieve the component definition object
* @param {string} componentType
* @example
* '@budibase/standard-components/container'
* @returns {object}
*/ */
getDefinition(componentType: string) { getDefinition(componentType: string) {
if (!componentType) { if (!componentType) {
@ -151,10 +148,6 @@ export class ComponentStore extends BudiStore<ComponentState> {
return get(this.store).components[componentType] return get(this.store).components[componentType]
} }
/**
*
* @returns {object}
*/
getDefaultDatasource() { getDefaultDatasource() {
// Ignore users table // Ignore users table
const validTables = get(tables).list.filter(x => x._id !== "ta_users") const validTables = get(tables).list.filter(x => x._id !== "ta_users")
@ -188,8 +181,6 @@ export class ComponentStore extends BudiStore<ComponentState> {
/** /**
* Takes an enriched component instance and applies any required migration * Takes an enriched component instance and applies any required migration
* logic * logic
* @param {object} enrichedComponent
* @returns {object} migrated Component
*/ */
migrateSettings(enrichedComponent: Component) { migrateSettings(enrichedComponent: Component) {
const componentPrefix = "@budibase/standard-components" const componentPrefix = "@budibase/standard-components"
@ -230,22 +221,15 @@ export class ComponentStore extends BudiStore<ComponentState> {
for (let setting of filterableTypes || []) { for (let setting of filterableTypes || []) {
const isLegacy = Array.isArray(enrichedComponent[setting.key]) const isLegacy = Array.isArray(enrichedComponent[setting.key])
if (isLegacy) { if (isLegacy) {
const processedSetting = utils.processSearchFilters( enrichedComponent[setting.key] = utils.processSearchFilters(
enrichedComponent[setting.key] enrichedComponent[setting.key]
) )
enrichedComponent[setting.key] = processedSetting
migrated = true migrated = true
} }
} }
return migrated return migrated
} }
/**
*
* @param {object} component
* @param {object} opts
* @returns
*/
enrichEmptySettings( enrichEmptySettings(
component: Component, component: Component,
opts: { screen?: Screen; parent?: Component; useDefaultValues?: boolean } opts: { screen?: Screen; parent?: Component; useDefaultValues?: boolean }
@ -280,14 +264,25 @@ export class ComponentStore extends BudiStore<ComponentState> {
type: "table", type: "table",
} }
} else if (setting.type === "dataProvider") { } else if (setting.type === "dataProvider") {
// Pick closest data provider where required let providerId
// Pick closest parent data provider if one exists
const path = findComponentPath(screen.props, treeId) const path = findComponentPath(screen.props, treeId)
const providers = path.filter((component: Component) => const providers = path.filter((component: Component) =>
component._component?.endsWith("/dataprovider") component._component?.endsWith("/dataprovider")
) )
if (providers.length) { providerId = providers[providers.length - 1]?._id
const id = providers[providers.length - 1]?._id
component[setting.key] = `{{ literal ${safe(id)} }}` // If none in our direct path, select the first one the screen
if (!providerId) {
providerId = findComponentType(
screen.props,
"@budibase/standard-components/dataprovider"
)?._id
}
if (providerId) {
component[setting.key] = `{{ literal ${safe(providerId)} }}`
} }
} else if (setting.type.startsWith("field/")) { } else if (setting.type.startsWith("field/")) {
// Autofill form field names // Autofill form field names
@ -316,6 +311,8 @@ export class ComponentStore extends BudiStore<ComponentState> {
component[setting.key] = fieldOptions[0] component[setting.key] = fieldOptions[0]
component.label = fieldOptions[0] component.label = fieldOptions[0]
} }
} else if (setting.type === "icon") {
component[setting.key] = "ri-star-fill"
} else if (useDefaultValues && setting.defaultValue !== undefined) { } else if (useDefaultValues && setting.defaultValue !== undefined) {
// Use default value where required // Use default value where required
component[setting.key] = setting.defaultValue component[setting.key] = setting.defaultValue
@ -427,17 +424,10 @@ export class ComponentStore extends BudiStore<ComponentState> {
} }
} }
/**
*
* @param {string} componentName
* @param {object} presetProps
* @param {object} parent
* @returns
*/
createInstance( createInstance(
componentType: string, componentType: string,
presetProps: any, presetProps?: Record<string, any>,
parent: any parent?: Component
): Component | null { ): Component | null {
const screen = get(selectedScreen) const screen = get(selectedScreen)
if (!screen) { if (!screen) {
@ -463,7 +453,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
_id: Helpers.uuid(), _id: Helpers.uuid(),
_component: definition.component, _component: definition.component,
_styles: { _styles: {
normal: {}, normal: { ...presetProps?._styles?.normal },
hover: {}, hover: {},
active: {}, active: {},
}, },
@ -512,19 +502,11 @@ export class ComponentStore extends BudiStore<ComponentState> {
} }
} }
/**
*
* @param {string} componentName
* @param {object} presetProps
* @param {object} parent
* @param {number} index
* @returns
*/
async create( async create(
componentType: string, componentType: string,
presetProps: any, presetProps?: Record<string, any>,
parent: Component, parent?: Component,
index: number index?: number
) { ) {
const state = get(this.store) const state = get(this.store)
const componentInstance = this.createInstance( const componentInstance = this.createInstance(
@ -611,13 +593,6 @@ export class ComponentStore extends BudiStore<ComponentState> {
return componentInstance return componentInstance
} }
/**
*
* @param {function} patchFn
* @param {string} componentId
* @param {string} screenId
* @returns
*/
async patch( async patch(
patchFn: (component: Component, screen: Screen) => any, patchFn: (component: Component, screen: Screen) => any,
componentId?: string, componentId?: string,
@ -652,11 +627,6 @@ export class ComponentStore extends BudiStore<ComponentState> {
await screenStore.patch(patchScreen, screenId) await screenStore.patch(patchScreen, screenId)
} }
/**
*
* @param {object} component
* @returns
*/
async delete(component: Component) { async delete(component: Component) {
if (!component) { if (!component) {
return return
@ -737,13 +707,6 @@ export class ComponentStore extends BudiStore<ComponentState> {
}) })
} }
/**
*
* @param {object} targetComponent
* @param {string} mode
* @param {object} targetScreen
* @returns
*/
async paste( async paste(
targetComponent: Component, targetComponent: Component,
mode: string, mode: string,
@ -1101,6 +1064,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
async updateStyles(styles: Record<string, string>, id: string) { async updateStyles(styles: Record<string, string>, id: string) {
const patchFn = (component: Component) => { const patchFn = (component: Component) => {
delete component._placeholder
component._styles.normal = { component._styles.normal = {
...component._styles.normal, ...component._styles.normal,
...styles, ...styles,
@ -1231,7 +1195,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
} }
// Create new parent instance // Create new parent instance
const newParentDefinition = this.createInstance(parentType, null, parent) const newParentDefinition = this.createInstance(parentType)
if (!newParentDefinition) { if (!newParentDefinition) {
return return
} }
@ -1267,10 +1231,6 @@ export class ComponentStore extends BudiStore<ComponentState> {
/** /**
* Check if the components settings have been cached * Check if the components settings have been cached
* @param {string} componentType
* @example
* '@budibase/standard-components/container'
* @returns {boolean}
*/ */
isCached(componentType: string) { isCached(componentType: string) {
const settings = get(this.store).settingsCache const settings = get(this.store).settingsCache
@ -1279,11 +1239,6 @@ export class ComponentStore extends BudiStore<ComponentState> {
/** /**
* Cache component settings * Cache component settings
* @param {string} componentType
* @param {object} definition
* @example
* '@budibase/standard-components/container'
* @returns {array} the settings
*/ */
cacheSettings(componentType: string, definition: ComponentDefinition | null) { cacheSettings(componentType: string, definition: ComponentDefinition | null) {
let settings: ComponentSetting[] = [] let settings: ComponentSetting[] = []
@ -1313,12 +1268,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
/** /**
* Retrieve an array of the component settings. * Retrieve an array of the component settings.
* These settings are cached because they cannot change at run time. * These settings are cached because they cannot change at run time.
*
* Searches a component's definition for a setting matching a certain predicate. * Searches a component's definition for a setting matching a certain predicate.
* @param {string} componentType
* @example
* '@budibase/standard-components/container'
* @returns {Array<object>}
*/ */
getComponentSettings(componentType: string) { getComponentSettings(componentType: string) {
if (!componentType) { if (!componentType) {

View File

@ -4,15 +4,13 @@ import { appStore } from "@/stores/builder"
import { BudiStore } from "../BudiStore" import { BudiStore } from "../BudiStore"
import { AppNavigation, AppNavigationLink, UIObject } from "@budibase/types" import { AppNavigation, AppNavigationLink, UIObject } from "@budibase/types"
interface BuilderNavigationStore extends AppNavigation {}
export const INITIAL_NAVIGATION_STATE = { export const INITIAL_NAVIGATION_STATE = {
navigation: "Top", navigation: "Top",
links: [], links: [],
textAlign: "Left", textAlign: "Left",
} }
export class NavigationStore extends BudiStore<BuilderNavigationStore> { export class NavigationStore extends BudiStore<AppNavigation> {
constructor() { constructor() {
super(INITIAL_NAVIGATION_STATE) super(INITIAL_NAVIGATION_STATE)
} }

View File

@ -54,7 +54,7 @@ export class PreviewStore extends BudiStore<PreviewState> {
})) }))
} }
startDrag(component: any) { async startDrag(component: string) {
this.sendEvent("dragging-new-component", { this.sendEvent("dragging-new-component", {
dragging: true, dragging: true,
component, component,

View File

@ -5,7 +5,7 @@
"module": "dist/budibase-client.js", "module": "dist/budibase-client.js",
"main": "dist/budibase-client.js", "main": "dist/budibase-client.js",
"type": "module", "type": "module",
"svelte": "src/index.js", "svelte": "src/index.ts",
"exports": { "exports": {
".": { ".": {
"import": "./dist/budibase-client.js", "import": "./dist/budibase-client.js",

View File

@ -1,10 +1,6 @@
import { createAPIClient } from "@budibase/frontend-core" import { createAPIClient } from "@budibase/frontend-core"
import { authStore } from "../stores/auth" import { authStore } from "../stores/auth"
import { import { notificationStore, devToolsEnabled, devToolsStore } from "../stores"
notificationStore,
devToolsEnabled,
devToolsStore,
} from "../stores/index"
import { get } from "svelte/store" import { get } from "svelte/store"
export const API = createAPIClient({ export const API = createAPIClient({

View File

@ -1,7 +1,7 @@
<script> <script>
import { getContext, onDestroy, onMount, setContext } from "svelte" import { getContext, onDestroy, onMount, setContext } from "svelte"
import { builderStore } from "stores/builder" import { builderStore } from "@/stores/builder"
import { blockStore } from "stores/blocks" import { blockStore } from "@/stores/blocks"
const component = getContext("component") const component = getContext("component")
const { styleable } = getContext("sdk") const { styleable } = getContext("sdk")

View File

@ -2,7 +2,7 @@
import { getContext, onDestroy } from "svelte" import { getContext, onDestroy } from "svelte"
import { generate } from "shortid" import { generate } from "shortid"
import { builderStore } from "../stores/builder" import { builderStore } from "../stores/builder"
import Component from "components/Component.svelte" import Component from "@/components/Component.svelte"
export let type export let type
export let props export let props

View File

@ -6,7 +6,7 @@
import { Constants, CookieUtils } from "@budibase/frontend-core" import { Constants, CookieUtils } from "@budibase/frontend-core"
import { getThemeClassNames } from "@budibase/shared-core" import { getThemeClassNames } from "@budibase/shared-core"
import Component from "./Component.svelte" import Component from "./Component.svelte"
import SDK from "sdk" import SDK from "@/sdk"
import { import {
featuresStore, featuresStore,
createContextStore, createContextStore,
@ -22,28 +22,29 @@
environmentStore, environmentStore,
sidePanelStore, sidePanelStore,
modalStore, modalStore,
} from "stores" } from "@/stores"
import NotificationDisplay from "components/overlay/NotificationDisplay.svelte" import NotificationDisplay from "./overlay/NotificationDisplay.svelte"
import ConfirmationDisplay from "components/overlay/ConfirmationDisplay.svelte" import ConfirmationDisplay from "./overlay/ConfirmationDisplay.svelte"
import PeekScreenDisplay from "components/overlay/PeekScreenDisplay.svelte" import PeekScreenDisplay from "./overlay/PeekScreenDisplay.svelte"
import UserBindingsProvider from "components/context/UserBindingsProvider.svelte" import UserBindingsProvider from "./context/UserBindingsProvider.svelte"
import DeviceBindingsProvider from "components/context/DeviceBindingsProvider.svelte" import DeviceBindingsProvider from "./context/DeviceBindingsProvider.svelte"
import StateBindingsProvider from "components/context/StateBindingsProvider.svelte" import StateBindingsProvider from "./context/StateBindingsProvider.svelte"
import RowSelectionProvider from "components/context/RowSelectionProvider.svelte" import RowSelectionProvider from "./context/RowSelectionProvider.svelte"
import QueryParamsProvider from "components/context/QueryParamsProvider.svelte" import QueryParamsProvider from "./context/QueryParamsProvider.svelte"
import SettingsBar from "components/preview/SettingsBar.svelte" import SettingsBar from "./preview/SettingsBar.svelte"
import SelectionIndicator from "components/preview/SelectionIndicator.svelte" import SelectionIndicator from "./preview/SelectionIndicator.svelte"
import HoverIndicator from "components/preview/HoverIndicator.svelte" import HoverIndicator from "./preview/HoverIndicator.svelte"
import CustomThemeWrapper from "./CustomThemeWrapper.svelte" import CustomThemeWrapper from "./CustomThemeWrapper.svelte"
import DNDHandler from "components/preview/DNDHandler.svelte" import DNDHandler from "./preview/DNDHandler.svelte"
import GridDNDHandler from "components/preview/GridDNDHandler.svelte" import GridDNDHandler from "./preview/GridDNDHandler.svelte"
import KeyboardManager from "components/preview/KeyboardManager.svelte" import KeyboardManager from "./preview/KeyboardManager.svelte"
import DevToolsHeader from "components/devtools/DevToolsHeader.svelte" import DevToolsHeader from "./devtools/DevToolsHeader.svelte"
import DevTools from "components/devtools/DevTools.svelte" import DevTools from "./devtools/DevTools.svelte"
import FreeFooter from "components/FreeFooter.svelte" import FreeFooter from "./FreeFooter.svelte"
import MaintenanceScreen from "components/MaintenanceScreen.svelte" import MaintenanceScreen from "./MaintenanceScreen.svelte"
import SnippetsProvider from "./context/SnippetsProvider.svelte" import SnippetsProvider from "./context/SnippetsProvider.svelte"
import EmbedProvider from "./context/EmbedProvider.svelte" import EmbedProvider from "./context/EmbedProvider.svelte"
import DNDSelectionIndicators from "./preview/DNDSelectionIndicators.svelte"
// Provide contexts // Provide contexts
setContext("sdk", SDK) setContext("sdk", SDK)
@ -266,6 +267,7 @@
{#if $builderStore.inBuilder} {#if $builderStore.inBuilder}
<DNDHandler /> <DNDHandler />
<GridDNDHandler /> <GridDNDHandler />
<DNDSelectionIndicators />
{/if} {/if}
</div> </div>
</SnippetsProvider> </SnippetsProvider>

View File

@ -11,7 +11,7 @@
<script> <script>
import { getContext, setContext, onMount } from "svelte" import { getContext, setContext, onMount } from "svelte"
import { writable, get } from "svelte/store" import { writable, get } from "svelte/store"
import { enrichProps, propsAreSame } from "utils/componentProps" import { enrichProps, propsAreSame } from "@/utils/componentProps"
import { getSettingsDefinition } from "@budibase/frontend-core" import { getSettingsDefinition } from "@budibase/frontend-core"
import { import {
builderStore, builderStore,
@ -20,12 +20,15 @@
appStore, appStore,
dndComponentPath, dndComponentPath,
dndIsDragging, dndIsDragging,
} from "stores" } from "@/stores"
import { Helpers } from "@budibase/bbui" import { Helpers } from "@budibase/bbui"
import { getActiveConditions, reduceConditionActions } from "utils/conditions" import {
import EmptyPlaceholder from "components/app/EmptyPlaceholder.svelte" getActiveConditions,
import ScreenPlaceholder from "components/app/ScreenPlaceholder.svelte" reduceConditionActions,
import ComponentErrorState from "components/error-states/ComponentErrorState.svelte" } from "@/utils/conditions"
import EmptyPlaceholder from "@/components/app/EmptyPlaceholder.svelte"
import ScreenPlaceholder from "@/components/app/ScreenPlaceholder.svelte"
import ComponentErrorState from "@/components/error-states/ComponentErrorState.svelte"
import { import {
decodeJSBinding, decodeJSBinding,
findHBSBlocks, findHBSBlocks,
@ -35,7 +38,7 @@
getActionContextKey, getActionContextKey,
getActionDependentContextKeys, getActionDependentContextKeys,
} from "../utils/buttonActions.js" } from "../utils/buttonActions.js"
import { gridLayout } from "utils/grid" import { gridLayout } from "@/utils/grid"
export let instance = {} export let instance = {}
export let parent = null export let parent = null
@ -120,7 +123,7 @@
$: children = instance._children || [] $: children = instance._children || []
$: id = instance._id $: id = instance._id
$: name = isRoot ? "Screen" : instance._instanceName $: name = isRoot ? "Screen" : instance._instanceName
$: icon = definition?.icon $: icon = instance._icon || definition?.icon
// Determine if the component is selected or is part of the critical path // Determine if the component is selected or is part of the critical path
// leading to the selected component // leading to the selected component

View File

@ -1,5 +1,5 @@
<script> <script>
import { themeStore } from "stores" import { themeStore } from "@/stores"
import { setContext } from "svelte" import { setContext } from "svelte"
import { Context } from "@budibase/bbui" import { Context } from "@budibase/bbui"

View File

@ -1,7 +1,7 @@
<script> <script>
import { setContext, getContext, onMount } from "svelte" import { setContext, getContext, onMount } from "svelte"
import Router, { querystring } from "svelte-spa-router" import Router, { querystring } from "svelte-spa-router"
import { routeStore, stateStore } from "stores" import { routeStore, stateStore } from "@/stores"
import Screen from "./Screen.svelte" import Screen from "./Screen.svelte"
import { get } from "svelte/store" import { get } from "svelte/store"

View File

@ -1,5 +1,5 @@
<script> <script>
import { screenStore, routeStore, builderStore } from "stores" import { screenStore, routeStore, builderStore } from "@/stores"
import { get } from "svelte/store" import { get } from "svelte/store"
import Component from "./Component.svelte" import Component from "./Component.svelte"
import Provider from "./context/Provider.svelte" import Provider from "./context/Provider.svelte"

View File

@ -3,7 +3,7 @@
// because it functions similarly to one // because it functions similarly to one
import { getContext, onMount } from "svelte" import { getContext, onMount } from "svelte"
import { get, derived, readable } from "svelte/store" import { get, derived, readable } from "svelte/store"
import { featuresStore } from "stores" import { featuresStore } from "@/stores"
import { Grid } from "@budibase/frontend-core" import { Grid } from "@budibase/frontend-core"
// import { processStringSync } from "@budibase/string-templates" // import { processStringSync } from "@budibase/string-templates"

View File

@ -1,9 +1,9 @@
<script> <script>
import { getContext } from "svelte" import { getContext } from "svelte"
import Block from "components/Block.svelte" import Block from "@/components/Block.svelte"
import BlockComponent from "components/BlockComponent.svelte" import BlockComponent from "@/components/BlockComponent.svelte"
import { makePropSafe as safe } from "@budibase/string-templates" import { makePropSafe as safe } from "@budibase/string-templates"
import { enrichSearchColumns, enrichFilter } from "utils/blocks" import { enrichSearchColumns, enrichFilter } from "@/utils/blocks"
import { get } from "svelte/store" import { get } from "svelte/store"
export let title export let title

View File

@ -1,6 +1,6 @@
<script> <script>
import Block from "components/Block.svelte" import Block from "@/components/Block.svelte"
import BlockComponent from "components/BlockComponent.svelte" import BlockComponent from "@/components/BlockComponent.svelte"
import { makePropSafe as safe } from "@budibase/string-templates" import { makePropSafe as safe } from "@budibase/string-templates"
// Datasource // Datasource

View File

@ -1,5 +1,5 @@
<script> <script>
import BlockComponent from "components/BlockComponent.svelte" import BlockComponent from "@/components/BlockComponent.svelte"
import { FieldType } from "@budibase/types" import { FieldType } from "@budibase/types"
export let field export let field

View File

@ -1,8 +1,8 @@
<script> <script>
import BlockComponent from "components/BlockComponent.svelte" import BlockComponent from "@/components/BlockComponent.svelte"
import { Helpers } from "@budibase/bbui" import { Helpers } from "@budibase/bbui"
import { getContext, setContext } from "svelte" import { getContext, setContext } from "svelte"
import { builderStore } from "stores" import { builderStore } from "@/stores"
import { Utils } from "@budibase/frontend-core" import { Utils } from "@budibase/frontend-core"
import FormBlockWrapper from "./form/FormBlockWrapper.svelte" import FormBlockWrapper from "./form/FormBlockWrapper.svelte"
import { get, writable } from "svelte/store" import { get, writable } from "svelte/store"

View File

@ -1,7 +1,7 @@
<script> <script>
import BlockComponent from "components/BlockComponent.svelte" import BlockComponent from "@/components/BlockComponent.svelte"
import Block from "components/Block.svelte" import Block from "@/components/Block.svelte"
import Placeholder from "components/app/Placeholder.svelte" import Placeholder from "@/components/app/Placeholder.svelte"
import { getContext } from "svelte" import { getContext } from "svelte"
import { makePropSafe as safe } from "@budibase/string-templates" import { makePropSafe as safe } from "@budibase/string-templates"
import { get } from "svelte/store" import { get } from "svelte/store"

View File

@ -1,6 +1,6 @@
<script> <script>
import Block from "components/Block.svelte" import Block from "@/components/Block.svelte"
import BlockComponent from "components/BlockComponent.svelte" import BlockComponent from "@/components/BlockComponent.svelte"
import { makePropSafe as safe } from "@budibase/string-templates" import { makePropSafe as safe } from "@budibase/string-templates"
import { generate } from "shortid" import { generate } from "shortid"
import { get } from "svelte/store" import { get } from "svelte/store"

View File

@ -1,6 +1,6 @@
<script> <script>
import BlockComponent from "components/BlockComponent.svelte" import BlockComponent from "@/components/BlockComponent.svelte"
import Block from "components/Block.svelte" import Block from "@/components/Block.svelte"
import { makePropSafe as safe } from "@budibase/string-templates" import { makePropSafe as safe } from "@budibase/string-templates"
import { getContext } from "svelte" import { getContext } from "svelte"

View File

@ -1,6 +1,6 @@
<script> <script>
import BlockComponent from "components/BlockComponent.svelte" import BlockComponent from "@/components/BlockComponent.svelte"
import Placeholder from "components/app/Placeholder.svelte" import Placeholder from "@/components/app/Placeholder.svelte"
import { getContext } from "svelte" import { getContext } from "svelte"
import FormBlockComponent from "../FormBlockComponent.svelte" import FormBlockComponent from "../FormBlockComponent.svelte"

View File

@ -1,7 +1,7 @@
<script> <script>
import { getContext, onMount } from "svelte" import { getContext, onMount } from "svelte"
import { writable } from "svelte/store" import { writable } from "svelte/store"
import { GridRowHeight, GridColumns } from "constants" import { GridRowHeight, GridColumns } from "@/constants"
import { memo } from "@budibase/frontend-core" import { memo } from "@budibase/frontend-core"
export let onClick export let onClick

View File

@ -2,10 +2,10 @@
import { getContext } from "svelte" import { getContext } from "svelte"
import { get } from "svelte/store" import { get } from "svelte/store"
import { generate } from "shortid" import { generate } from "shortid"
import Block from "components/Block.svelte" import Block from "@/components/Block.svelte"
import BlockComponent from "components/BlockComponent.svelte" import BlockComponent from "@/components/BlockComponent.svelte"
import { makePropSafe as safe } from "@budibase/string-templates" import { makePropSafe as safe } from "@budibase/string-templates"
import { enrichSearchColumns, enrichFilter } from "utils/blocks" import { enrichSearchColumns, enrichFilter } from "@/utils/blocks"
import { Utils } from "@budibase/frontend-core" import { Utils } from "@budibase/frontend-core"
export let title export let title

View File

@ -3,7 +3,7 @@
import { Table } from "@budibase/bbui" import { Table } from "@budibase/bbui"
import SlotRenderer from "./SlotRenderer.svelte" import SlotRenderer from "./SlotRenderer.svelte"
import { canBeSortColumn } from "@budibase/frontend-core" import { canBeSortColumn } from "@budibase/frontend-core"
import Provider from "components/context/Provider.svelte" import Provider from "@/components/context/Provider.svelte"
export let dataProvider export let dataProvider
export let columns export let columns

View File

@ -2,7 +2,7 @@
import Field from "./Field.svelte" import Field from "./Field.svelte"
import { CoreDropzone, ProgressCircle, Helpers } from "@budibase/bbui" import { CoreDropzone, ProgressCircle, Helpers } from "@budibase/bbui"
import { getContext, onMount, onDestroy } from "svelte" import { getContext, onMount, onDestroy } from "svelte"
import { builderStore } from "stores/builder" import { builderStore } from "@/stores/builder"
import { processStringSync } from "@budibase/string-templates" import { processStringSync } from "@budibase/string-templates"
export let datasourceId export let datasourceId

View File

@ -1,7 +1,7 @@
<script> <script>
import Provider from "./Provider.svelte" import Provider from "./Provider.svelte"
import { onMount, onDestroy } from "svelte" import { onMount, onDestroy } from "svelte"
import { themeStore } from "stores" import { themeStore } from "@/stores"
let width = window.innerWidth let width = window.innerWidth
let height = window.innerHeight let height = window.innerHeight

View File

@ -1,7 +1,7 @@
<script> <script>
import { getContext, setContext, onDestroy } from "svelte" import { getContext, setContext, onDestroy } from "svelte"
import { dataSourceStore, createContextStore } from "stores" import { dataSourceStore, createContextStore } from "@/stores"
import { ActionTypes } from "constants" import { ActionTypes } from "@/constants"
import { generate } from "shortid" import { generate } from "shortid"
const { ContextScopes } = getContext("sdk") const { ContextScopes } = getContext("sdk")

View File

@ -1,6 +1,6 @@
<script> <script>
import Provider from "./Provider.svelte" import Provider from "./Provider.svelte"
import { routeStore } from "stores" import { routeStore } from "@/stores"
</script> </script>
<Provider key="query" data={$routeStore.queryParams}> <Provider key="query" data={$routeStore.queryParams}>

View File

@ -1,6 +1,6 @@
<script> <script>
import Provider from "./Provider.svelte" import Provider from "./Provider.svelte"
import { rowSelectionStore } from "stores" import { rowSelectionStore } from "@/stores"
</script> </script>
<Provider key="rowSelection" data={$rowSelectionStore}> <Provider key="rowSelection" data={$rowSelectionStore}>

View File

@ -1,6 +1,6 @@
<script> <script>
import Provider from "./Provider.svelte" import Provider from "./Provider.svelte"
import { snippets } from "stores" import { snippets } from "@/stores"
</script> </script>
<Provider key="snippets" data={$snippets}> <Provider key="snippets" data={$snippets}>

View File

@ -1,6 +1,6 @@
<script> <script>
import Provider from "./Provider.svelte" import Provider from "./Provider.svelte"
import { stateStore } from "stores" import { stateStore } from "@/stores"
</script> </script>
<Provider key="state" data={$stateStore}> <Provider key="state" data={$stateStore}>

View File

@ -1,7 +1,7 @@
<script> <script>
import Provider from "./Provider.svelte" import Provider from "./Provider.svelte"
import { authStore, currentRole } from "stores" import { authStore, currentRole } from "@/stores"
import { ActionTypes } from "constants" import { ActionTypes } from "@/constants"
import { Constants } from "@budibase/frontend-core" import { Constants } from "@budibase/frontend-core"
// Register this as a refreshable datasource so that user changes cause // Register this as a refreshable datasource so that user changes cause

View File

@ -3,7 +3,7 @@
import { Layout, Heading, Tabs, Tab, Icon } from "@budibase/bbui" import { Layout, Heading, Tabs, Tab, Icon } from "@budibase/bbui"
import DevToolsStatsTab from "./DevToolsStatsTab.svelte" import DevToolsStatsTab from "./DevToolsStatsTab.svelte"
import DevToolsComponentTab from "./DevToolsComponentTab.svelte" import DevToolsComponentTab from "./DevToolsComponentTab.svelte"
import { devToolsStore } from "stores" import { devToolsStore } from "@/stores"
const context = getContext("context") const context = getContext("context")
</script> </script>

View File

@ -1,6 +1,6 @@
<script> <script>
import { Layout, Select, Body } from "@budibase/bbui" import { Layout, Select, Body } from "@budibase/bbui"
import { componentStore } from "stores/index.js" import { componentStore } from "@/stores"
import DevToolsStat from "./DevToolsStat.svelte" import DevToolsStat from "./DevToolsStat.svelte"
const ReadableBindingMap = { const ReadableBindingMap = {

View File

@ -2,7 +2,7 @@
import { Layout, Toggle } from "@budibase/bbui" import { Layout, Toggle } from "@budibase/bbui"
import { getSettingsDefinition } from "@budibase/frontend-core" import { getSettingsDefinition } from "@budibase/frontend-core"
import DevToolsStat from "./DevToolsStat.svelte" import DevToolsStat from "./DevToolsStat.svelte"
import { componentStore } from "stores/index.js" import { componentStore } from "@/stores"
let showEnrichedSettings = true let showEnrichedSettings = true

View File

@ -1,6 +1,6 @@
<script> <script>
import { Body, Layout, Heading, Button, Tabs, Tab } from "@budibase/bbui" import { Body, Layout, Heading, Button, Tabs, Tab } from "@budibase/bbui"
import { builderStore, devToolsStore, componentStore } from "stores" import { builderStore, devToolsStore, componentStore } from "@/stores"
import DevToolsStat from "./DevToolsStat.svelte" import DevToolsStat from "./DevToolsStat.svelte"
import DevToolsComponentSettingsTab from "./DevToolsComponentSettingsTab.svelte" import DevToolsComponentSettingsTab from "./DevToolsComponentSettingsTab.svelte"
import DevToolsComponentContextTab from "./DevToolsComponentContextTab.svelte" import DevToolsComponentContextTab from "./DevToolsComponentContextTab.svelte"

View File

@ -1,8 +1,8 @@
<script> <script>
import { Heading, Select, ActionButton } from "@budibase/bbui" import { Heading, Select, ActionButton } from "@budibase/bbui"
import { devToolsStore, appStore } from "../../stores" import { devToolsStore, appStore } from "@/stores"
import { getContext, onMount } from "svelte" import { getContext, onMount } from "svelte"
import { API } from "api" import { API } from "@/api"
const context = getContext("context") const context = getContext("context")
const SELF_ROLE = "self" const SELF_ROLE = "self"

View File

@ -1,6 +1,6 @@
<script> <script>
import { Helpers } from "@budibase/bbui" import { Helpers } from "@budibase/bbui"
import { notificationStore } from "stores" import { notificationStore } from "@/stores"
export let label export let label
export let value export let value

View File

@ -1,6 +1,6 @@
<script> <script>
import { Layout } from "@budibase/bbui" import { Layout } from "@budibase/bbui"
import { authStore, appStore, screenStore, componentStore } from "stores" import { authStore, appStore, screenStore, componentStore } from "@/stores"
import DevToolsStat from "./DevToolsStat.svelte" import DevToolsStat from "./DevToolsStat.svelte"
</script> </script>

View File

@ -1,5 +1,5 @@
<script> <script>
import { confirmationStore } from "stores" import { confirmationStore } from "@/stores"
import { Modal, ModalContent } from "@budibase/bbui" import { Modal, ModalContent } from "@budibase/bbui"
</script> </script>

View File

@ -1,5 +1,5 @@
<script> <script>
import { notificationStore } from "stores" import { notificationStore } from "@/stores"
import { Notification } from "@budibase/bbui" import { Notification } from "@budibase/bbui"
import { fly } from "svelte/transition" import { fly } from "svelte/transition"
</script> </script>

View File

@ -5,7 +5,7 @@
notificationStore, notificationStore,
routeStore, routeStore,
stateStore, stateStore,
} from "stores" } from "@/stores"
import { Modal, ModalContent, ActionButton } from "@budibase/bbui" import { Modal, ModalContent, ActionButton } from "@budibase/bbui"
import { onDestroy } from "svelte" import { onDestroy } from "svelte"

View File

@ -1,19 +1,22 @@
<script> <script lang="ts">
import { onMount, onDestroy } from "svelte" import { onMount, onDestroy } from "svelte"
import { get } from "svelte/store" import { get } from "svelte/store"
import IndicatorSet from "./IndicatorSet.svelte" import { builderStore, screenStore, dndStore, isGridScreen } from "@/stores"
import {
builderStore,
screenStore,
dndStore,
dndParent,
dndIsDragging,
} from "stores"
import DNDPlaceholderOverlay from "./DNDPlaceholderOverlay.svelte"
import { Utils } from "@budibase/frontend-core" import { Utils } from "@budibase/frontend-core"
import { findComponentById } from "utils/components.js" import { findComponentById } from "@/utils/components.js"
import { DNDPlaceholderID } from "constants" import { isGridEvent } from "@/utils/grid"
import { isGridEvent } from "utils/grid" import { DNDPlaceholderID } from "@/constants"
import { Component } from "@budibase/types"
type ChildCoords = {
placeholder: boolean
centerX: number
centerY: number
left: number
right: number
top: number
bottom: number
}
const ThrottleRate = 130 const ThrottleRate = 130
@ -22,17 +25,19 @@
$: source = $dndStore.source $: source = $dndStore.source
$: target = $dndStore.target $: target = $dndStore.target
$: drop = $dndStore.drop $: drop = $dndStore.drop
$: gridScreen = $isGridScreen
// Local flag for whether we are awaiting an async drop event // Local flag for whether we are awaiting an async drop event
let dropping = false let dropping = false
// Util to get the inner DOM node by a component ID // Util to get the inner DOM element by a component ID
const getDOMNode = id => { const getDOMElement = (id: string): HTMLElement | undefined => {
return document.getElementsByClassName(`${id}-dom`)[0] const el = document.getElementsByClassName(`${id}-dom`)[0]
return el instanceof HTMLElement ? el : undefined
} }
// Util to calculate the variance of a set of data // Util to calculate the variance of a set of data
const variance = arr => { const variance = (arr: number[]) => {
const mean = arr.reduce((a, b) => a + b, 0) / arr.length const mean = arr.reduce((a, b) => a + b, 0) / arr.length
let squareSum = 0 let squareSum = 0
arr.forEach(value => { arr.forEach(value => {
@ -61,36 +66,43 @@
} }
// Callback when initially starting a drag on a draggable component // Callback when initially starting a drag on a draggable component
const onDragStart = e => { const onDragStart = (e: DragEvent) => {
if (isGridEvent(e)) { if (isGridEvent(e)) {
return return
} }
if (!(e.target instanceof HTMLElement)) {
return
}
const component = e.target.closest(".component") const component = e.target.closest(".component")
if (!component?.classList.contains("draggable")) { if (
!(component instanceof HTMLElement) ||
!component.classList.contains("draggable")
) {
return return
} }
// Hide drag ghost image // Hide drag ghost image
e.dataTransfer.setDragImage(new Image(), 0, 0) e.dataTransfer?.setDragImage(new Image(), 0, 0)
// Add event handler to clear all drag state when dragging ends // Add event handler to clear all drag state when dragging ends
component.addEventListener("dragend", stopDragging) component.addEventListener("dragend", stopDragging)
// Update state // Update state
const id = component.dataset.id const id = component.dataset.id!
const parentId = component.dataset.parent const parentId = component.dataset.parent!
const parent = findComponentById( const parent: Component = findComponentById(
get(screenStore).activeScreen?.props, get(screenStore).activeScreen?.props,
parentId parentId
) )
const index = parent._children.findIndex( const index = parent._children!.findIndex(child => child._id === id)
x => x._id === component.dataset.id
)
dndStore.actions.startDraggingExistingComponent({ dndStore.actions.startDraggingExistingComponent({
id, id,
bounds: component.children[0].getBoundingClientRect(), bounds: component.children[0].getBoundingClientRect(),
parent: parentId, parent: parentId,
index, index,
name: component.dataset.name,
icon: component.dataset.icon,
type: parent._children![index]!._component,
}) })
builderStore.actions.selectComponent(id) builderStore.actions.selectComponent(id)
@ -109,18 +121,18 @@
// Core logic for handling drop events and determining where to render the // Core logic for handling drop events and determining where to render the
// drop target placeholder // drop target placeholder
const processEvent = Utils.throttle((mouseX, mouseY) => { const processEvent = Utils.throttle((mouseX: number, mouseY: number) => {
if (!target) { if (!target) {
return return
} }
let { id, parent, node, acceptsChildren, empty } = target let { id, parent, element, acceptsChildren, empty } = target
// If we're over something that does not accept children then we go up a // If we're over something that does not accept children then we go up a
// level and consider the mouse position relative to the parent // level and consider the mouse position relative to the parent
if (!acceptsChildren) { if (!acceptsChildren) {
id = parent id = parent
empty = false empty = false
node = getDOMNode(parent) element = getDOMElement(parent)
} }
// We're now hovering over something which does accept children. // We're now hovering over something which does accept children.
@ -133,31 +145,33 @@
return return
} }
// As the first DOM node in a component may not necessarily contain the // As the first DOM element in a component may not necessarily contain the
// child components, we can find to try the parent of the first child // child components, we can find to try the parent of the first child
// component and use that as the real parent DOM node // component and use that as the real parent DOM node
const childNode = node.getElementsByClassName("component")[0] const childElement = element?.getElementsByClassName("component")[0]
if (childNode?.parentNode) { if (childElement?.parentNode instanceof HTMLElement) {
node = childNode.parentNode element = childElement.parentNode
} }
// Append an ephemeral div to allow us to determine layout if only one // Append an ephemeral div to allow us to determine layout if only one
// child exists // child exists
let ephemeralDiv let ephemeralDiv
if (node.children.length === 1) { if (element?.children.length === 1) {
ephemeralDiv = document.createElement("div") ephemeralDiv = document.createElement("div")
ephemeralDiv.dataset.id = DNDPlaceholderID ephemeralDiv.dataset.id = DNDPlaceholderID
node.appendChild(ephemeralDiv) element.appendChild(ephemeralDiv)
} }
// We're now hovering over something which accepts children and is not // We're now hovering over something which accepts children and is not
// empty, so we need to work out where to inside the placeholder // empty, so we need to work out where to inside the placeholder
// Calculate the coordinates of various locations on each child. // Calculate the coordinates of various locations on each child.
const childCoords = [...(node.children || [])].map(node => { const childCoords: ChildCoords[] = [...(element?.children || [])]
const child = node.children?.[0] || node .filter(el => el instanceof HTMLElement)
.map(el => {
const child = el.children?.[0] || el
const bounds = child.getBoundingClientRect() const bounds = child.getBoundingClientRect()
return { return {
placeholder: node.dataset.id === DNDPlaceholderID, placeholder: el.dataset.id === DNDPlaceholderID,
centerX: bounds.left + bounds.width / 2, centerX: bounds.left + bounds.width / 2,
centerY: bounds.top + bounds.height / 2, centerY: bounds.top + bounds.height / 2,
left: bounds.left, left: bounds.left,
@ -170,14 +184,15 @@
// Now that we've calculated the position of the children, we no longer need // Now that we've calculated the position of the children, we no longer need
// the ephemeral div // the ephemeral div
if (ephemeralDiv) { if (ephemeralDiv) {
node.removeChild(ephemeralDiv) element?.removeChild(ephemeralDiv)
} }
// Calculate the variance between each set of positions on the children // Calculate the variance between each set of positions on the children
const variances = Object.keys(childCoords[0]) const variances = Object.keys(childCoords[0] || {})
.filter(x => x !== "placeholder") .filter(key => key !== "placeholder")
.map(key => { .map(key => {
const coords = childCoords.map(x => x[key]) const numericalKey = key as keyof Omit<ChildCoords, "placeholder">
const coords = childCoords.map(x => x[numericalKey])
return { return {
variance: variance(coords), variance: variance(coords),
side: key, side: key,
@ -189,13 +204,13 @@
variances.sort((a, b) => { variances.sort((a, b) => {
return a.variance < b.variance ? -1 : 1 return a.variance < b.variance ? -1 : 1
}) })
const column = ["centerX", "left", "right"].includes(variances[0].side) const column = ["centerX", "left", "right"].includes(variances[0]?.side)
// Calculate breakpoints between child components so we can determine the // Calculate breakpoints between child components so we can determine the
// index to drop the component in. // index to drop the component in.
// We want to ignore the placeholder from this calculation as it should not // We want to ignore the placeholder from this calculation as it should not
// be considered a real child of the parent. // be considered a real child of the parent.
let breakpoints = childCoords const breakpoints = childCoords
.filter(x => !x.placeholder) .filter(x => !x.placeholder)
.map(x => { .map(x => {
return column ? x.centerY : x.centerX return column ? x.centerY : x.centerX
@ -213,38 +228,39 @@
}) })
}, ThrottleRate) }, ThrottleRate)
const handleEvent = e => { const handleEvent = (e: DragEvent) => {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
processEvent(e.clientX, e.clientY) processEvent(e.clientX, e.clientY)
} }
// Callback when on top of a component. // Callback when on top of a component
const onDragOver = e => { const onDragOver = (e: DragEvent) => {
if (!source || !target) { if (!source || !target || gridScreen) {
return return
} }
handleEvent(e) handleEvent(e)
} }
// Callback when entering a potential drop target // Callback when entering a potential drop target
const onDragEnter = e => { const onDragEnter = async (e: DragEvent) => {
if (!source) { if (!source || gridScreen || !(e.target instanceof HTMLElement)) {
return return
} }
// Find the next valid component to consider dropping over, ignoring nested // Find the next valid component to consider dropping over, ignoring nested
// block components // block components
const component = e.target?.closest?.( let comp = e.target.closest?.(`.component:not(.block):not(.${source.id})`)
`.component:not(.block):not(.${source.id})` if (!(comp instanceof HTMLElement)) {
) return
if (component && component.classList.contains("droppable")) { }
if (comp?.classList.contains("droppable")) {
dndStore.actions.updateTarget({ dndStore.actions.updateTarget({
id: component.dataset.id, id: comp.dataset.id!,
parent: component.dataset.parent, parent: comp.dataset.parent!,
node: getDOMNode(component.dataset.id), element: getDOMElement(comp.dataset.id!),
empty: component.classList.contains("empty"), empty: comp.classList.contains("empty"),
acceptsChildren: component.classList.contains("parent"), acceptsChildren: comp.classList.contains("parent"),
}) })
handleEvent(e) handleEvent(e)
} }
@ -257,12 +273,13 @@
} }
// Check if we're adding a new component rather than moving one // Check if we're adding a new component rather than moving one
if (source.newComponentType) { if (source.isNew) {
dropping = true dropping = true
builderStore.actions.dropNewComponent( builderStore.actions.dropNewComponent(
source.newComponentType, source.type,
drop.parent, drop.parent,
drop.index drop.index,
$dndStore.meta?.props
) )
dropping = false dropping = false
stopDragging() stopDragging()
@ -271,7 +288,7 @@
// Convert parent + index into target + mode // Convert parent + index into target + mode
let legacyDropTarget, legacyDropMode let legacyDropTarget, legacyDropMode
const parent = findComponentById( const parent: Component | null = findComponentById(
get(screenStore).activeScreen?.props, get(screenStore).activeScreen?.props,
drop.parent drop.parent
) )
@ -333,14 +350,3 @@
document.removeEventListener("drop", onDrop, false) document.removeEventListener("drop", onDrop, false)
}) })
</script> </script>
<IndicatorSet
componentId={$dndParent}
color="var(--spectrum-global-color-static-green-500)"
zIndex={920}
prefix="Inside"
/>
{#if $dndIsDragging}
<DNDPlaceholderOverlay />
{/if}

View File

@ -1,51 +0,0 @@
<script>
import { onMount } from "svelte"
import { DNDPlaceholderID } from "constants"
import { Utils } from "@budibase/frontend-core"
let left, top, height, width
const updatePosition = () => {
let node = document.getElementsByClassName(DNDPlaceholderID)[0]
const insideGrid = node?.dataset.insideGrid === "true"
if (!insideGrid) {
node = document.getElementsByClassName(`${DNDPlaceholderID}-dom`)[0]
}
if (!node) {
height = 0
width = 0
} else {
const bounds = node.getBoundingClientRect()
left = bounds.left
top = bounds.top
height = bounds.height
width = bounds.width
}
}
const debouncedUpdate = Utils.domDebounce(updatePosition)
onMount(() => {
const interval = setInterval(debouncedUpdate, 100)
return () => {
clearInterval(interval)
}
})
</script>
{#if left != null && top != null && width && height}
<div
class="overlay"
style="left: {left}px; top: {top}px; width: {width}px; height: {height}px;"
/>
{/if}
<style>
.overlay {
position: fixed;
z-index: 800;
background: hsl(160, 64%, 90%);
border-radius: 4px;
transition: all 130ms ease-out;
border: 2px solid var(--spectrum-global-color-static-green-500);
}
</style>

View File

@ -0,0 +1,40 @@
<script lang="ts">
import {
isGridScreen,
dndParent,
dndSource,
dndIsDragging,
dndStore,
} from "@/stores"
import { DNDPlaceholderID } from "@/constants"
import IndicatorSet from "./IndicatorSet.svelte"
// On grid screens, don't draw the indicator until we've dragged over the
// screen. When this happens, the dndSource props will be set as we will have
// attached grid metadata styles.
$: waitingForGrid = $isGridScreen && !$dndStore.meta?.props
</script>
{#if $dndIsDragging}
{#if !$isGridScreen && $dndParent}
<IndicatorSet
componentId={$dndParent}
color="var(--spectrum-global-color-static-green-400)"
zIndex={920}
prefix="Inside"
/>
{/if}
{#if !waitingForGrid}
<IndicatorSet
componentId={DNDPlaceholderID}
color="var(--spectrum-global-color-static-green-500)"
zIndex={930}
allowResizeAnchors={false}
background="hsl(160, 64%, 90%)"
animate={!$isGridScreen}
text={$dndSource?.name}
icon={$dndSource?.icon}
/>
{/if}
{/if}

View File

@ -1,15 +1,50 @@
<script> <script lang="ts">
import { onMount, onDestroy, getContext } from "svelte" import { onMount, onDestroy, getContext } from "svelte"
import { builderStore, componentStore } from "stores" import {
builderStore,
componentStore,
dndIsDragging,
dndStore,
dndSource,
isGridScreen,
} from "@/stores"
import { Utils, memo } from "@budibase/frontend-core" import { Utils, memo } from "@budibase/frontend-core"
import { GridRowHeight } from "constants" import { DNDPlaceholderID, GridRowHeight } from "@/constants"
import { import {
isGridEvent, isGridEvent,
GridParams, GridParams,
getGridVar, getGridVar,
Devices, Devices,
GridDragModes, GridDragMode,
} from "utils/grid" } from "@/utils/grid"
type GridDragSide =
| "top"
| "right"
| "bottom"
| "left"
| "top-left"
| "top-right"
| "bottom-left"
| "bottom-right"
interface GridDragInfo {
mode: GridDragMode
side?: GridDragSide
domTarget?: HTMLElement
domComponent: HTMLElement
domGrid: HTMLElement
id: string
gridId: string
grid: {
startX: number
startY: number
rowStart: number
rowEnd: number
colStart: number
colEnd: number
}
}
const context = getContext("context") const context = getContext("context")
@ -18,11 +53,12 @@
ghost.src = ghost.src =
"data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="
let dragInfo let scrollElement: HTMLElement
let styles = memo() let dragInfo: GridDragInfo | undefined
let styles = memo<Record<string, number> | undefined>()
// Grid CSS variables // Grid CSS variables
$: device = $context.device.mobile ? Devices.Mobile : Devices.Desktop $: device = $context.device?.mobile ? Devices.Mobile : Devices.Desktop
$: vars = { $: vars = {
colStart: getGridVar(device, GridParams.ColStart), colStart: getGridVar(device, GridParams.ColStart),
colEnd: getGridVar(device, GridParams.ColEnd), colEnd: getGridVar(device, GridParams.ColEnd),
@ -35,27 +71,54 @@
// Set ephemeral styles // Set ephemeral styles
$: instance = componentStore.actions.getComponentInstance(id) $: instance = componentStore.actions.getComponentInstance(id)
$: $instance?.setEphemeralStyles($styles) $: applyStyles($instance, $styles)
// Reset when not dragging new components
$: !$dndIsDragging && stopDragging()
const scrollOffset = () => scrollElement?.scrollTop || 0
const applyStyles = async (
instance: any,
styles: Record<string, number> | undefined
) => {
instance?.setEphemeralStyles(styles)
// If dragging a new component on to a grid screen, tick to allow the
// real component to render in the new position before updating the DND
// store, preventing the green DND overlay from being out of position
if ($dndSource?.isNew && styles) {
dndStore.actions.updateNewComponentProps({
_styles: {
normal: styles,
},
})
}
}
// Sugar for a combination of both min and max // Sugar for a combination of both min and max
const minMax = (value, min, max) => Math.min(max, Math.max(min, value)) const minMax = (value: number, min: number, max: number) =>
Math.min(max, Math.max(min, value))
const processEvent = Utils.domDebounce((mouseX, mouseY) => { const processEvent = Utils.domDebounce((mouseX: number, mouseY: number) => {
if (!dragInfo?.grid) { if (!dragInfo?.grid) {
return return
} }
const { mode, side, grid, domGrid } = dragInfo const { mode, grid, domGrid } = dragInfo
const { startX, startY, rowStart, rowEnd, colStart, colEnd } = grid const { startX, startY, rowStart, rowEnd, colStart, colEnd } = grid
if (!domGrid) { if (!domGrid) {
return return
} }
const cols = parseInt(domGrid.dataset.cols) const cols = parseInt(domGrid.dataset.cols || "")
const colSize = parseInt(domGrid.dataset.colSize) const colSize = parseInt(domGrid.dataset.colSize || "")
if (isNaN(cols) || isNaN(colSize)) {
throw "DOM grid missing required dataset attributes"
}
const diffX = mouseX - startX const diffX = mouseX - startX
let deltaX = Math.round(diffX / colSize) let deltaX = Math.round(diffX / colSize)
const diffY = mouseY - startY const diffY = mouseY - startY + scrollOffset()
let deltaY = Math.round(diffY / GridRowHeight) let deltaY = Math.round(diffY / GridRowHeight)
if (mode === GridDragModes.Move) { if (mode === GridDragMode.Move) {
deltaX = minMax(deltaX, 1 - colStart, cols + 1 - colEnd) deltaX = minMax(deltaX, 1 - colStart, cols + 1 - colEnd)
deltaY = Math.max(deltaY, 1 - rowStart) deltaY = Math.max(deltaY, 1 - rowStart)
const newStyles = { const newStyles = {
@ -65,8 +128,9 @@
[vars.rowEnd]: rowEnd + deltaY, [vars.rowEnd]: rowEnd + deltaY,
} }
styles.set(newStyles) styles.set(newStyles)
} else if (mode === GridDragModes.Resize) { } else if (mode === GridDragMode.Resize) {
let newStyles = {} const { side } = dragInfo
let newStyles: Record<string, number> = {}
if (side === "right") { if (side === "right") {
newStyles[vars.colEnd] = Math.max(colEnd + deltaX, colStart + 1) newStyles[vars.colEnd] = Math.max(colEnd + deltaX, colStart + 1)
} else if (side === "left") { } else if (side === "left") {
@ -92,14 +156,50 @@
} }
}) })
const handleEvent = e => { const handleEvent = (e: DragEvent) => {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
processEvent(e.clientX, e.clientY) processEvent(e.clientX, e.clientY)
} }
// Callback when dragging a new component over the preview iframe in a valid
// position for the first time
const startDraggingPlaceholder = () => {
const domComponent = document.getElementsByClassName(DNDPlaceholderID)[0]
const domGrid = domComponent?.closest(".grid")
if (
!(domComponent instanceof HTMLElement) ||
!(domGrid instanceof HTMLElement)
) {
return
}
const styles = getComputedStyle(domComponent)
const bounds = domComponent.getBoundingClientRect()
// Show as active
domComponent.classList.add("dragging")
domGrid.classList.add("highlight")
// Update state
dragInfo = {
domComponent,
domGrid,
id: DNDPlaceholderID,
gridId: domGrid.parentElement!.dataset.id!,
mode: GridDragMode.Move,
grid: {
startX: bounds.left + bounds.width / 2,
startY: bounds.top + bounds.height / 2 + scrollOffset(),
rowStart: parseInt(styles.gridRowStart),
rowEnd: parseInt(styles.gridRowEnd),
colStart: parseInt(styles.gridColumnStart),
colEnd: parseInt(styles.gridColumnEnd),
},
}
}
// Callback when initially starting a drag on a draggable component // Callback when initially starting a drag on a draggable component
const onDragStart = e => { const onDragStart = (e: DragEvent) => {
if (!isGridEvent(e)) { if (!isGridEvent(e)) {
return return
} }
@ -108,27 +208,30 @@
e.dataTransfer.setDragImage(ghost, 0, 0) e.dataTransfer.setDragImage(ghost, 0, 0)
// Extract state // Extract state
let mode, id, side let mode: GridDragMode, id: string, side
if (e.target.dataset.indicator === "true") { if (e.target.dataset.indicator === "true") {
mode = e.target.dataset.dragMode mode = e.target.dataset.dragMode as GridDragMode
id = e.target.dataset.id id = e.target.dataset.id!
side = e.target.dataset.side side = e.target.dataset.side as GridDragSide
} else { } else {
// Handle move // Handle move
mode = GridDragModes.Move mode = GridDragMode.Move
const component = e.target.closest(".component") const component = e.target.closest(".component") as HTMLElement
id = component.dataset.id id = component.dataset.id!
} }
// If holding ctrl/cmd then leave behind a duplicate of this component // If holding ctrl/cmd then leave behind a duplicate of this component
if (mode === GridDragModes.Move && (e.ctrlKey || e.metaKey)) { if (mode === GridDragMode.Move && (e.ctrlKey || e.metaKey)) {
builderStore.actions.duplicateComponent(id, "above", false) builderStore.actions.duplicateComponent(id, "above", false)
} }
// Find grid parent and read from DOM // Find grid parent and read from DOM
const domComponent = document.getElementsByClassName(id)[0] const domComponent = document.getElementsByClassName(id)[0]
const domGrid = domComponent?.closest(".grid") const domGrid = domComponent?.closest(".grid")
if (!domGrid) { if (
!(domComponent instanceof HTMLElement) ||
!(domGrid instanceof HTMLElement)
) {
return return
} }
const styles = getComputedStyle(domComponent) const styles = getComputedStyle(domComponent)
@ -144,25 +247,29 @@
domComponent, domComponent,
domGrid, domGrid,
id, id,
gridId: domGrid.parentNode.dataset.id, gridId: domGrid.parentElement!.dataset.id!,
mode, mode,
side, side,
grid: { grid: {
startX: e.clientX, startX: e.clientX,
startY: e.clientY, startY: e.clientY + scrollOffset(),
rowStart: parseInt(styles["grid-row-start"]), rowStart: parseInt(styles.gridRowStart),
rowEnd: parseInt(styles["grid-row-end"]), rowEnd: parseInt(styles.gridRowEnd),
colStart: parseInt(styles["grid-column-start"]), colStart: parseInt(styles.gridColumnStart),
colEnd: parseInt(styles["grid-column-end"]), colEnd: parseInt(styles.gridColumnEnd),
}, },
} }
// Add event handler to clear all drag state when dragging ends // Add event handler to clear all drag state when dragging ends
dragInfo.domTarget.addEventListener("dragend", stopDragging) dragInfo.domTarget!.addEventListener("dragend", stopDragging, false)
} }
const onDragOver = e => { const onDragOver = (e: DragEvent) => {
if (!dragInfo) { if (!dragInfo) {
// Check if we're dragging a new component
if ($dndIsDragging && $dndSource?.isNew && $isGridScreen) {
startDraggingPlaceholder()
}
return return
} }
handleEvent(e) handleEvent(e)
@ -178,7 +285,7 @@
// Reset DOM // Reset DOM
domComponent.classList.remove("dragging") domComponent.classList.remove("dragging")
domGrid.classList.remove("highlight") domGrid.classList.remove("highlight")
domTarget.removeEventListener("dragend", stopDragging) domTarget?.removeEventListener("dragend", stopDragging)
// Save changes // Save changes
if ($styles) { if ($styles) {
@ -186,17 +293,22 @@
} }
// Reset state // Reset state
dragInfo = null dragInfo = undefined
styles.set(null) styles.set(undefined)
} }
onMount(() => { onMount(() => {
scrollElement = document.getElementsByClassName(
"screen-wrapper"
)[0] as HTMLElement
document.addEventListener("dragstart", onDragStart, false) document.addEventListener("dragstart", onDragStart, false)
document.addEventListener("dragover", onDragOver, false) document.addEventListener("dragover", onDragOver, false)
document.addEventListener("scroll", processEvent)
}) })
onDestroy(() => { onDestroy(() => {
document.removeEventListener("dragstart", onDragStart, false) document.removeEventListener("dragstart", onDragStart, false)
document.removeEventListener("dragover", onDragOver, false) document.removeEventListener("dragover", onDragOver, false)
document.removeEventListener("scroll", processEvent)
}) })
</script> </script>

View File

@ -1,6 +1,6 @@
<script> <script>
import { Icon } from "@budibase/bbui" import { Icon } from "@budibase/bbui"
import { builderStore } from "stores" import { builderStore } from "@/stores"
export let style export let style
export let value export let value

View File

@ -1,26 +1,30 @@
<script> <script lang="ts">
import { onMount, onDestroy } from "svelte" import { onMount, onDestroy } from "svelte"
import IndicatorSet from "./IndicatorSet.svelte" import IndicatorSet from "./IndicatorSet.svelte"
import { dndIsDragging, hoverStore, builderStore } from "stores" import { dndIsDragging, hoverStore, builderStore } from "@/stores"
$: componentId = $hoverStore.hoveredComponentId $: componentId = $hoverStore.hoveredComponentId
$: selectedComponentId = $builderStore.selectedComponentId $: selectedComponentId = $builderStore.selectedComponentId
$: selected = componentId === selectedComponentId $: selected = componentId === selectedComponentId
const onMouseOver = e => { const onMouseOver = (e: MouseEvent) => {
const target = e.target as HTMLElement
// Ignore if dragging // Ignore if dragging
if (e.buttons > 0) { if (e.buttons > 0) {
return return
} }
let newId let newId
if (e.target.classList.contains("anchor")) { if (target.classList.contains("anchor")) {
// Handle resize anchors // Handle resize anchors
newId = e.target.dataset.id newId = target.dataset.id
} else { } else {
// Handle normal components // Handle normal components
const element = e.target.closest(".interactive.component:not(.root)") const element = target.closest(".interactive.component:not(.root)")
newId = element?.dataset?.id if (element instanceof HTMLElement) {
newId = element.dataset?.id
}
} }
if (newId !== componentId) { if (newId !== componentId) {
@ -43,9 +47,11 @@
}) })
</script> </script>
{#if !$dndIsDragging && componentId}
<IndicatorSet <IndicatorSet
componentId={$dndIsDragging ? null : componentId} {componentId}
color="var(--spectrum-global-color-static-blue-200)" color="var(--spectrum-global-color-static-blue-200)"
zIndex={selected ? 890 : 910} zIndex={selected ? 890 : 910}
allowResizeAnchors allowResizeAnchors
/> />
{/if}}

View File

@ -1,19 +1,21 @@
<script> <script lang="ts">
import { Icon } from "@budibase/bbui" import { Icon } from "@budibase/bbui"
import { GridDragModes } from "utils/grid" import { GridDragMode } from "@/utils/grid"
export let top export let top: number
export let left export let left: number
export let width export let width: number
export let height export let height: number
export let text export let text: string | undefined
export let icon export let icon: string | undefined
export let color export let color: string
export let zIndex export let zIndex: number
export let componentId export let componentId: string
export let line = false export let line = false
export let alignRight = false export let alignRight = false
export let showResizeAnchors = false export let showResizeAnchors = false
export let background: string | undefined
export let animate = false
const AnchorSides = [ const AnchorSides = [
"right", "right",
@ -33,10 +35,12 @@
class="indicator" class="indicator"
class:flipped class:flipped
class:line class:line
style="top: {top}px; left: {left}px; width: {width}px; height: {height}px; --color: {color}; --zIndex: {zIndex};" style="top: {top}px; left: {left}px; width: {width}px; height: {height}px; --color: {color}; --zIndex: {zIndex}; --bg: {background ||
'none'};"
class:withText={!!text} class:withText={!!text}
class:vCompact={height < 40} class:vCompact={height < 40}
class:hCompact={width < 40} class:hCompact={width < 40}
class:animate
> >
{#if text || icon} {#if text || icon}
<div <div
@ -46,7 +50,7 @@
class:right={alignRight} class:right={alignRight}
draggable="true" draggable="true"
data-indicator="true" data-indicator="true"
data-drag-mode={GridDragModes.Move} data-drag-mode={GridDragMode.Move}
data-id={componentId} data-id={componentId}
> >
{#if icon} {#if icon}
@ -65,7 +69,7 @@
class="anchor {side}" class="anchor {side}"
draggable="true" draggable="true"
data-indicator="true" data-indicator="true"
data-drag-mode={GridDragModes.Resize} data-drag-mode={GridDragMode.Resize}
data-side={side} data-side={side}
data-id={componentId} data-id={componentId}
> >
@ -84,6 +88,7 @@
border: 2px solid var(--color); border: 2px solid var(--color);
pointer-events: none; pointer-events: none;
border-radius: 4px; border-radius: 4px;
background: var(--bg);
} }
.indicator.withText { .indicator.withText {
border-top-left-radius: 0; border-top-left-radius: 0;
@ -94,6 +99,9 @@
.indicator.line { .indicator.line {
border-radius: 4px !important; border-radius: 4px !important;
} }
.indicator.animate {
transition: all 130ms ease-out;
}
/* Label styles */ /* Label styles */
.label { .label {

View File

@ -1,42 +1,68 @@
<script> <script lang="ts">
import { onMount, onDestroy } from "svelte" import { onMount, onDestroy } from "svelte"
import Indicator from "./Indicator.svelte" import Indicator from "./Indicator.svelte"
import { builderStore } from "stores" import { builderStore } from "@/stores"
import { memo, Utils } from "@budibase/frontend-core" import { memo, Utils } from "@budibase/frontend-core"
export let componentId = null export let componentId: string
export let color = null export let color: string
export let zIndex = 900 export let zIndex: number = 900
export let prefix = null export let prefix: string | undefined = undefined
export let allowResizeAnchors = false export let allowResizeAnchors: boolean = false
export let background: string | undefined = undefined
export let animate: boolean = false
export let text: string | undefined = undefined
export let icon: string | undefined = undefined
interface IndicatorState {
visible: boolean
insideModal: boolean
insideSidePanel: boolean
top: number
left: number
width: number
height: number
}
interface IndicatorSetState {
// Cached props
componentId: string
color: string
zIndex: number
prefix?: string
allowResizeAnchors: boolean
// Computed state
indicators: IndicatorState[]
text?: string
icon?: string
insideGrid: boolean
error: boolean
}
// Offset = 6 (clip-root padding) - 1 (half the border thickness)
const config = memo($$props) const config = memo($$props)
const errorColor = "var(--spectrum-global-color-static-red-600)" const errorColor = "var(--spectrum-global-color-static-red-600)"
const mutationObserver = new MutationObserver(() => debouncedUpdate()) const mutationObserver = new MutationObserver(() => debouncedUpdate())
const defaultState = () => ({ const defaultState = (): IndicatorSetState => ({
// Cached props
componentId, componentId,
color, color,
zIndex, zIndex,
prefix, prefix,
allowResizeAnchors, allowResizeAnchors,
// Computed state
indicators: [], indicators: [],
text: null, text,
icon: null, icon,
insideGrid: false, insideGrid: false,
error: false, error: false,
}) })
let interval let interval: ReturnType<typeof setInterval>
let state = defaultState() let state = defaultState()
let observingMutations = false let observingMutations = false
let updating = false let updating = false
let intersectionObservers = [] let intersectionObservers: IntersectionObserver[] = []
let callbackCount = 0 let callbackCount = 0
let nextState let nextState: ReturnType<typeof defaultState>
$: componentId, reset() $: componentId, reset()
$: visibleIndicators = state.indicators.filter(x => x.visible) $: visibleIndicators = state.indicators.filter(x => x.visible)
@ -58,15 +84,22 @@
updating = false updating = false
} }
const observeMutations = element => { const getElements = (className: string): HTMLElement[] => {
mutationObserver.observe(element, { return [...document.getElementsByClassName(className)]
.filter(el => el instanceof HTMLElement)
.slice(0, 100)
}
const observeMutations = (node: Node) => {
mutationObserver.observe(node, {
attributes: true, attributes: true,
attributeFilter: ["style"], attributeFilter: ["style"],
}) })
observingMutations = true observingMutations = true
} }
const createIntersectionCallback = idx => entries => { const createIntersectionCallback =
(idx: number) => (entries: IntersectionObserverEntry[]) => {
if (callbackCount >= intersectionObservers.length) { if (callbackCount >= intersectionObservers.length) {
return return
} }
@ -86,11 +119,7 @@
} }
// Sanity check // Sanity check
if (!componentId) { let elements = getElements(componentId)
state = defaultState()
return
}
let elements = document.getElementsByClassName(componentId)
if (!elements.length) { if (!elements.length) {
state = defaultState() state = defaultState()
return return
@ -108,18 +137,20 @@
} }
// Check if we're inside a grid // Check if we're inside a grid
if (allowResizeAnchors) {
nextState.insideGrid = elements[0]?.dataset.insideGrid === "true" nextState.insideGrid = elements[0]?.dataset.insideGrid === "true"
}
// Get text to display // Get text and icon to display
if (!text) {
nextState.text = elements[0].dataset.name nextState.text = elements[0].dataset.name
if (nextState.prefix) { if (nextState.prefix) {
nextState.text = `${nextState.prefix} ${nextState.text}` nextState.text = `${nextState.prefix} ${nextState.text}`
} }
}
if (!icon) {
if (elements[0].dataset.icon) { if (elements[0].dataset.icon) {
nextState.icon = elements[0].dataset.icon nextState.icon = elements[0].dataset.icon
} }
}
nextState.error = elements[0].classList.contains("error") nextState.error = elements[0].classList.contains("error")
// Batch reads to minimize reflow // Batch reads to minimize reflow
@ -129,11 +160,8 @@
// Extract valid children // Extract valid children
// Sanity limit of active indicators // Sanity limit of active indicators
if (!nextState.insideGrid) { if (!nextState.insideGrid) {
elements = document.getElementsByClassName(`${componentId}-dom`) elements = getElements(`${componentId}-dom`)
} }
elements = Array.from(elements)
.filter(x => x != null)
.slice(0, 100)
const multi = elements.length > 1 const multi = elements.length > 1
// If there aren't any nodes then reset // If there aren't any nodes then reset
@ -143,15 +171,20 @@
} }
const device = document.getElementById("app-root") const device = document.getElementById("app-root")
if (!device) {
throw "app-root node not found"
}
const deviceBounds = device.getBoundingClientRect() const deviceBounds = device.getBoundingClientRect()
nextState.indicators = elements.map((element, idx) => { nextState.indicators = elements.map((element, idx) => {
const elBounds = element.getBoundingClientRect() const elBounds = element.getBoundingClientRect()
let indicator = { let indicator: IndicatorState = {
top: Math.round(elBounds.top + scrollY - deviceBounds.top + offset), top: Math.round(elBounds.top + scrollY - deviceBounds.top + offset),
left: Math.round(elBounds.left + scrollX - deviceBounds.left + offset), left: Math.round(elBounds.left + scrollX - deviceBounds.left + offset),
width: Math.round(elBounds.width + 2), width: Math.round(elBounds.width + 2),
height: Math.round(elBounds.height + 2), height: Math.round(elBounds.height + 2),
visible: true, visible: true,
insideModal: false,
insideSidePanel: false,
} }
// If observing more than one node then we need to use an intersection // If observing more than one node then we need to use an intersection
@ -199,11 +232,13 @@
left={indicator.left} left={indicator.left}
width={indicator.width} width={indicator.width}
height={indicator.height} height={indicator.height}
text={idx === 0 ? state.text : null} text={idx === 0 ? state.text : undefined}
icon={idx === 0 ? state.icon : null} icon={idx === 0 ? state.icon : undefined}
showResizeAnchors={state.allowResizeAnchors && state.insideGrid} showResizeAnchors={state.allowResizeAnchors && state.insideGrid}
color={state.error ? errorColor : state.color} color={state.error ? errorColor : state.color}
componentId={state.componentId} componentId={state.componentId}
zIndex={state.zIndex} zIndex={state.zIndex}
{background}
{animate}
/> />
{/each} {/each}

View File

@ -1,7 +1,7 @@
<script> <script>
import { onMount, onDestroy } from "svelte" import { onMount, onDestroy } from "svelte"
import { get } from "svelte/store" import { get } from "svelte/store"
import { builderStore } from "stores" import { builderStore } from "@/stores"
onMount(() => { onMount(() => {
if (get(builderStore).inBuilder) { if (get(builderStore).inBuilder) {

View File

@ -1,5 +1,5 @@
<script> <script lang="ts">
import { builderStore } from "stores" import { dndIsDragging, builderStore } from "@/stores"
import IndicatorSet from "./IndicatorSet.svelte" import IndicatorSet from "./IndicatorSet.svelte"
$: color = $builderStore.editMode $: color = $builderStore.editMode
@ -7,9 +7,11 @@
: "var(--spectrum-global-color-static-blue-600)" : "var(--spectrum-global-color-static-blue-600)"
</script> </script>
{#if !$dndIsDragging && $builderStore.selectedComponentId}
<IndicatorSet <IndicatorSet
componentId={$builderStore.selectedComponentId} componentId={$builderStore.selectedComponentId}
{color} {color}
zIndex={900} zIndex={900}
allowResizeAnchors allowResizeAnchors
/> />
{/if}

View File

@ -4,9 +4,9 @@
import GridStylesButton from "./GridStylesButton.svelte" import GridStylesButton from "./GridStylesButton.svelte"
import SettingsColorPicker from "./SettingsColorPicker.svelte" import SettingsColorPicker from "./SettingsColorPicker.svelte"
import SettingsPicker from "./SettingsPicker.svelte" import SettingsPicker from "./SettingsPicker.svelte"
import { builderStore, componentStore, dndIsDragging } from "stores" import { builderStore, componentStore, dndIsDragging } from "@/stores"
import { Utils, shouldDisplaySetting } from "@budibase/frontend-core" import { Utils, shouldDisplaySetting } from "@budibase/frontend-core"
import { getGridVar, GridParams, Devices } from "utils/grid" import { getGridVar, GridParams, Devices } from "@/utils/grid"
const context = getContext("context") const context = getContext("context")
const verticalOffset = 36 const verticalOffset = 36

View File

@ -1,6 +1,6 @@
<script> <script>
import { Icon } from "@budibase/bbui" import { Icon } from "@budibase/bbui"
import { builderStore } from "stores" import { builderStore } from "@/stores"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
export let prop export let prop

View File

@ -1,6 +1,6 @@
<script> <script>
import { ColorPicker } from "@budibase/bbui" import { ColorPicker } from "@budibase/bbui"
import { builderStore } from "stores" import { builderStore } from "@/stores"
export let prop export let prop
export let component export let component

View File

@ -1,6 +1,6 @@
<script> <script>
import { Select } from "@budibase/bbui" import { Select } from "@budibase/bbui"
import { builderStore } from "stores" import { builderStore } from "@/stores"
export let prop export let prop
export let options export let options

View File

@ -1,5 +0,0 @@
interface Window {
"##BUDIBASE_APP_ID##": string
"##BUDIBASE_IN_BUILDER##": string
MIGRATING_APP: boolean
}

View File

@ -1,138 +0,0 @@
import ClientApp from "./components/ClientApp.svelte"
import UpdatingApp from "./components/UpdatingApp.svelte"
import {
builderStore,
appStore,
blockStore,
componentStore,
environmentStore,
dndStore,
eventStore,
hoverStore,
stateStore,
} from "./stores"
import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-vite.js"
import { get } from "svelte/store"
import { initWebsocket } from "./websocket.js"
// Provide svelte and svelte/internal as globals for custom components
import * as svelte from "svelte"
import * as internal from "svelte/internal"
window.svelte_internal = internal
window.svelte = svelte
// Initialise spectrum icons
loadSpectrumIcons()
let app
const loadBudibase = async () => {
// Update builder store with any builder flags
builderStore.set({
...get(builderStore),
inBuilder: !!window["##BUDIBASE_IN_BUILDER##"],
layout: window["##BUDIBASE_PREVIEW_LAYOUT##"],
screen: window["##BUDIBASE_PREVIEW_SCREEN##"],
selectedComponentId: window["##BUDIBASE_SELECTED_COMPONENT_ID##"],
previewId: window["##BUDIBASE_PREVIEW_ID##"],
theme: window["##BUDIBASE_PREVIEW_THEME##"],
customTheme: window["##BUDIBASE_PREVIEW_CUSTOM_THEME##"],
previewDevice: window["##BUDIBASE_PREVIEW_DEVICE##"],
navigation: window["##BUDIBASE_PREVIEW_NAVIGATION##"],
hiddenComponentIds: window["##BUDIBASE_HIDDEN_COMPONENT_IDS##"],
usedPlugins: window["##BUDIBASE_USED_PLUGINS##"],
location: window["##BUDIBASE_LOCATION##"],
snippets: window["##BUDIBASE_SNIPPETS##"],
componentErrors: window["##BUDIBASE_COMPONENT_ERRORS##"],
})
// Set app ID - this window flag is set by both the preview and the real
// server rendered app HTML
appStore.actions.setAppId(window["##BUDIBASE_APP_ID##"])
// Set the flag used to determine if the app is being loaded via an iframe
appStore.actions.setAppEmbedded(
window["##BUDIBASE_APP_EMBEDDED##"] === "true"
)
if (window.MIGRATING_APP) {
new UpdatingApp({
target: window.document.body,
})
return
}
// Fetch environment info
if (!get(environmentStore)?.loaded) {
await environmentStore.actions.fetchEnvironment()
}
// Register handler for runtime events from the builder
window.handleBuilderRuntimeEvent = (type, data) => {
if (!window["##BUDIBASE_IN_BUILDER##"]) {
return
}
if (type === "event-completed") {
eventStore.actions.resolveEvent(data)
} else if (type === "eject-block") {
const block = blockStore.actions.getBlock(data)
block?.eject()
} else if (type === "dragging-new-component") {
const { dragging, component } = data
if (dragging) {
const definition =
componentStore.actions.getComponentDefinition(component)
dndStore.actions.startDraggingNewComponent({ component, definition })
} else {
dndStore.actions.reset()
}
} else if (type === "request-context") {
const { selectedComponentInstance, screenslotInstance } =
get(componentStore)
const instance = selectedComponentInstance || screenslotInstance
const context = instance?.getDataContext()
let stringifiedContext = null
try {
stringifiedContext = JSON.stringify(context)
} catch (error) {
// Ignore - invalid context
}
eventStore.actions.dispatchEvent("provide-context", {
context: stringifiedContext,
})
} else if (type === "hover-component") {
hoverStore.actions.hoverComponent(data, false)
} else if (type === "builder-meta") {
builderStore.actions.setMetadata(data)
} else if (type === "builder-state") {
const [[key, value]] = Object.entries(data)
stateStore.actions.setValue(key, value)
}
}
// Register any custom components
if (window["##BUDIBASE_CUSTOM_COMPONENTS##"]) {
window["##BUDIBASE_CUSTOM_COMPONENTS##"].forEach(component => {
componentStore.actions.registerCustomComponent(component)
})
}
// Make a callback available for custom component bundles to register
// themselves at runtime
window.registerCustomComponent =
componentStore.actions.registerCustomComponent
// Initialise websocket
initWebsocket()
// Create app if one hasn't been created yet
if (!app) {
app = new ClientApp({
target: window.document.body,
})
}
}
// Attach to window so the HTML template can call this when it loads
window.loadBudibase = loadBudibase

View File

@ -1,6 +1,83 @@
import ClientApp from "./components/ClientApp.svelte"
import UpdatingApp from "./components/UpdatingApp.svelte"
import {
builderStore,
appStore,
blockStore,
componentStore,
environmentStore,
dndStore,
eventStore,
hoverStore,
stateStore,
} from "@/stores"
import { get } from "svelte/store"
import { initWebsocket } from "@/websocket"
import { APIClient } from "@budibase/frontend-core" import { APIClient } from "@budibase/frontend-core"
import type { ActionTypes } from "./constants" import type { ActionTypes } from "@/constants"
import { Readable } from "svelte/store" import { Readable } from "svelte/store"
import {
Screen,
Layout,
Theme,
AppCustomTheme,
PreviewDevice,
AppNavigation,
Plugin,
Snippet,
UIComponentError,
CustomComponent,
} from "@budibase/types"
// Provide svelte and svelte/internal as globals for custom components
import * as svelte from "svelte"
// @ts-ignore
import * as internal from "svelte/internal"
window.svelte_internal = internal
window.svelte = svelte
// Initialise spectrum icons
// eslint-disable-next-line local-rules/no-budibase-imports
import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-vite.js"
loadSpectrumIcons()
// Extend global window scope
declare global {
interface Window {
// Data from builder
"##BUDIBASE_APP_ID##"?: string
"##BUDIBASE_IN_BUILDER##"?: true
"##BUDIBASE_PREVIEW_LAYOUT##"?: Layout
"##BUDIBASE_PREVIEW_SCREEN##"?: Screen
"##BUDIBASE_SELECTED_COMPONENT_ID##"?: string
"##BUDIBASE_PREVIEW_ID##"?: number
"##BUDIBASE_PREVIEW_THEME##"?: Theme
"##BUDIBASE_PREVIEW_CUSTOM_THEME##"?: AppCustomTheme
"##BUDIBASE_PREVIEW_DEVICE##"?: PreviewDevice
"##BUDIBASE_APP_EMBEDDED##"?: string // This is a bool wrapped in a string
"##BUDIBASE_PREVIEW_NAVIGATION##"?: AppNavigation
"##BUDIBASE_HIDDEN_COMPONENT_IDS##"?: string[]
"##BUDIBASE_USED_PLUGINS##"?: Plugin[]
"##BUDIBASE_LOCATION##"?: {
protocol: string
hostname: string
port: string
}
"##BUDIBASE_SNIPPETS##"?: Snippet[]
"##BUDIBASE_COMPONENT_ERRORS##"?: Record<string, UIComponentError>[]
"##BUDIBASE_CUSTOM_COMPONENTS##"?: CustomComponent[]
// Other flags
MIGRATING_APP: boolean
// Client additions
handleBuilderRuntimeEvent: (type: string, data: any) => void
registerCustomComponent: typeof componentStore.actions.registerCustomComponent
loadBudibase: typeof loadBudibase
svelte: typeof svelte
svelte_internal: typeof internal
}
}
export interface SDK { export interface SDK {
API: APIClient API: APIClient
@ -28,4 +105,114 @@ export type Component = Readable<{
errorState: boolean errorState: boolean
}> }>
export type Context = Readable<{}> export type Context = Readable<Record<string, any>>
let app: ClientApp
const loadBudibase = async () => {
// Update builder store with any builder flags
builderStore.set({
...get(builderStore),
inBuilder: !!window["##BUDIBASE_IN_BUILDER##"],
layout: window["##BUDIBASE_PREVIEW_LAYOUT##"],
screen: window["##BUDIBASE_PREVIEW_SCREEN##"],
selectedComponentId: window["##BUDIBASE_SELECTED_COMPONENT_ID##"],
previewId: window["##BUDIBASE_PREVIEW_ID##"],
theme: window["##BUDIBASE_PREVIEW_THEME##"],
customTheme: window["##BUDIBASE_PREVIEW_CUSTOM_THEME##"],
previewDevice: window["##BUDIBASE_PREVIEW_DEVICE##"],
navigation: window["##BUDIBASE_PREVIEW_NAVIGATION##"],
hiddenComponentIds: window["##BUDIBASE_HIDDEN_COMPONENT_IDS##"],
usedPlugins: window["##BUDIBASE_USED_PLUGINS##"],
location: window["##BUDIBASE_LOCATION##"],
snippets: window["##BUDIBASE_SNIPPETS##"],
componentErrors: window["##BUDIBASE_COMPONENT_ERRORS##"],
})
// Set app ID - this window flag is set by both the preview and the real
// server rendered app HTML
appStore.actions.setAppId(window["##BUDIBASE_APP_ID##"])
// Set the flag used to determine if the app is being loaded via an iframe
appStore.actions.setAppEmbedded(
window["##BUDIBASE_APP_EMBEDDED##"] === "true"
)
if (window.MIGRATING_APP) {
new UpdatingApp({
target: window.document.body,
})
return
}
// Fetch environment info
if (!get(environmentStore)?.loaded) {
await environmentStore.actions.fetchEnvironment()
}
// Register handler for runtime events from the builder
window.handleBuilderRuntimeEvent = (type, data) => {
if (!window["##BUDIBASE_IN_BUILDER##"]) {
return
}
if (type === "event-completed") {
eventStore.actions.resolveEvent(data)
} else if (type === "eject-block") {
const block = blockStore.actions.getBlock(data)
block?.eject()
} else if (type === "dragging-new-component") {
const { dragging, component } = data
if (dragging) {
dndStore.actions.startDraggingNewComponent(component)
} else {
dndStore.actions.reset()
}
} else if (type === "request-context") {
const { selectedComponentInstance, screenslotInstance } =
get(componentStore)
const instance = selectedComponentInstance || screenslotInstance
const context = instance?.getDataContext()
let stringifiedContext = null
try {
stringifiedContext = JSON.stringify(context)
} catch (error) {
// Ignore - invalid context
}
eventStore.actions.dispatchEvent("provide-context", {
context: stringifiedContext,
})
} else if (type === "hover-component") {
hoverStore.actions.hoverComponent(data, false)
} else if (type === "builder-meta") {
builderStore.actions.setMetadata(data)
} else if (type === "builder-state") {
const [[key, value]] = Object.entries(data)
stateStore.actions.setValue(key, value)
}
}
// Register any custom components
if (window["##BUDIBASE_CUSTOM_COMPONENTS##"]) {
window["##BUDIBASE_CUSTOM_COMPONENTS##"].forEach(component => {
componentStore.actions.registerCustomComponent(component)
})
}
// Make a callback available for custom component bundles to register
// themselves at runtime
window.registerCustomComponent =
componentStore.actions.registerCustomComponent
// Initialise websocket
initWebsocket()
// Create app if one hasn't been created yet
if (!app) {
app = new ClientApp({
target: window.document.body,
})
}
}
// Attach to window so the HTML template can call this when it loads
window.loadBudibase = loadBudibase

View File

@ -1,4 +1,4 @@
import { API } from "api" import { API } from "@/api"
import { import {
authStore, authStore,
notificationStore, notificationStore,
@ -18,13 +18,13 @@ import {
appStore, appStore,
stateStore, stateStore,
createContextStore, createContextStore,
} from "stores" } from "@/stores"
import { styleable } from "utils/styleable" import { styleable } from "@/utils/styleable"
import { linkable } from "utils/linkable" import { linkable } from "@/utils/linkable"
import { getAction } from "utils/getAction" import { getAction } from "@/utils/getAction"
import Provider from "components/context/Provider.svelte" import Provider from "@/components/context/Provider.svelte"
import Block from "components/Block.svelte" import Block from "@/components/Block.svelte"
import BlockComponent from "components/BlockComponent.svelte" import BlockComponent from "@/components/BlockComponent.svelte"
import { ActionTypes } from "./constants" import { ActionTypes } from "./constants"
import { import {
fetchDatasourceSchema, fetchDatasourceSchema,
@ -41,7 +41,7 @@ import {
memo, memo,
derivedMemo, derivedMemo,
} from "@budibase/frontend-core" } from "@budibase/frontend-core"
import { createValidatorFromConstraints } from "components/app/forms/validation" import { createValidatorFromConstraints } from "@/components/app/forms/validation"
export default { export default {
API, API,

View File

@ -1,4 +1,4 @@
import { API } from "api" import { API } from "@/api"
import { get, writable, derived } from "svelte/store" import { get, writable, derived } from "svelte/store"
const initialState = { const initialState = {

View File

@ -1,4 +1,4 @@
import { API } from "api" import { API } from "@/api"
import { writable } from "svelte/store" import { writable } from "svelte/store"
const createAuthStore = () => { const createAuthStore = () => {

View File

@ -1,5 +1,5 @@
import { writable, get } from "svelte/store" import { writable, get } from "svelte/store"
import { API } from "api" import { API } from "@/api"
import { devToolsStore } from "./devTools.js" import { devToolsStore } from "./devTools.js"
import { eventStore } from "./events.js" import { eventStore } from "./events.js"
import { import {
@ -111,11 +111,17 @@ const createBuilderStore = () => {
mode, mode,
}) })
}, },
dropNewComponent: (component: string, parent: string, index: number) => { dropNewComponent: (
component: string,
parent: string,
index: number,
props: Record<string, any>
) => {
eventStore.actions.dispatchEvent("drop-new-component", { eventStore.actions.dispatchEvent("drop-new-component", {
component, component,
parent, parent,
index, index,
props,
}) })
}, },
setEditMode: (enabled: boolean) => { setEditMode: (enabled: boolean) => {

View File

@ -1,5 +1,5 @@
import { writable, get } from "svelte/store" import { writable, get } from "svelte/store"
import { API } from "api" import { API } from "@/api"
import { FieldTypes } from "../constants" import { FieldTypes } from "../constants"
import { routeStore } from "./routes" import { routeStore } from "./routes"

View File

@ -1,6 +1,6 @@
import { derived } from "svelte/store" import { derived } from "svelte/store"
import { findComponentPathById } from "utils/components.js" import { findComponentPathById } from "@/utils/components.js"
import { dndParent } from "../dnd.js" import { dndParent } from "../dnd.ts"
import { screenStore } from "../screens.js" import { screenStore } from "../screens.js"
export const dndComponentPath = derived( export const dndComponentPath = derived(

View File

@ -1,88 +0,0 @@
import { writable } from "svelte/store"
import { derivedMemo } from "@budibase/frontend-core"
const createDndStore = () => {
const initialState = {
// Info about the dragged component
source: null,
// Info about the target component being hovered over
target: null,
// Info about where the component would be dropped
drop: null,
}
const store = writable(initialState)
const startDraggingExistingComponent = ({ id, parent, bounds, index }) => {
store.set({
...initialState,
source: { id, parent, bounds, index },
})
}
const startDraggingNewComponent = ({ component, definition }) => {
if (!component) {
return
}
// Get size of new component so we can show a properly sized placeholder
const width = definition?.size?.width || 128
const height = definition?.size?.height || 64
store.set({
...initialState,
source: {
id: null,
parent: null,
bounds: { height, width },
index: null,
newComponentType: component,
},
})
}
const updateTarget = ({ id, parent, node, empty, acceptsChildren }) => {
store.update(state => {
state.target = { id, parent, node, empty, acceptsChildren }
return state
})
}
const updateDrop = ({ parent, index }) => {
store.update(state => {
state.drop = { parent, index }
return state
})
}
const reset = () => {
store.set(initialState)
}
return {
subscribe: store.subscribe,
actions: {
startDraggingExistingComponent,
startDraggingNewComponent,
updateTarget,
updateDrop,
reset,
},
}
}
export const dndStore = createDndStore()
// The DND store is updated extremely frequently, so we can greatly improve
// performance by deriving any state that needs to be externally observed.
// By doing this and using primitives, we can avoid invalidating other stores
// or components which depend on DND state unless values actually change.
export const dndParent = derivedMemo(dndStore, x => x.drop?.parent)
export const dndIndex = derivedMemo(dndStore, x => x.drop?.index)
export const dndBounds = derivedMemo(dndStore, x => x.source?.bounds)
export const dndIsDragging = derivedMemo(dndStore, x => !!x.source)
export const dndIsNewComponent = derivedMemo(
dndStore,
x => x.source?.newComponentType != null
)

View File

@ -0,0 +1,147 @@
import { writable, get } from "svelte/store"
import { derivedMemo } from "@budibase/frontend-core"
import { screenStore, isGridScreen, componentStore } from "@/stores"
import { ScreenslotID } from "@/constants"
import { ComponentDefinition } from "@budibase/types"
interface DNDSource {
id?: string
parent?: string
index?: number
bounds: {
height: number
width: number
}
name?: string
icon?: string
type: string
isNew: boolean
}
interface DNDTarget {
id: string
parent: string
empty: boolean
acceptsChildren: boolean
element?: HTMLElement
}
interface DNDDrop {
parent: string
index: number
}
interface DNDMeta {
props?: Record<string, any>
}
interface DNDState {
source?: DNDSource
target?: DNDTarget
drop?: DNDDrop
meta?: DNDMeta
}
const createDndStore = () => {
const store = writable<DNDState>({})
const startDraggingExistingComponent = (source: Omit<DNDSource, "isNew">) => {
store.set({
source: {
...source,
isNew: false,
},
})
}
const startDraggingNewComponent = (type: string) => {
// On grid screens, we already know exactly where to insert the component
let target: DNDTarget | undefined = undefined
let drop: DNDDrop | undefined = undefined
if (get(isGridScreen)) {
const screen = get(screenStore)?.activeScreen
const id = screen.props._id
target = {
id,
parent: ScreenslotID,
empty: false,
acceptsChildren: true,
}
drop = {
parent: id,
index: screen?.props?._children?.length,
}
}
// Get size of new component so we can show a properly sized placeholder
const definition: ComponentDefinition =
componentStore.actions.getComponentDefinition(type)
const width = definition?.size?.width || 128
const height = definition?.size?.height || 64
store.set({
source: {
bounds: { height, width },
type,
isNew: true,
name: `New ${definition?.name || "component"}`,
icon: definition?.icon || "Selection",
},
target,
drop,
})
}
const updateTarget = (target: DNDTarget) => {
store.update(state => {
state.target = target
return state
})
}
const updateDrop = (drop: DNDDrop) => {
store.update(state => {
state.drop = drop
return state
})
}
const reset = () => {
store.set({})
}
const updateNewComponentProps = (props: Record<string, any>) => {
store.update(state => {
return {
...state,
meta: {
...state.meta,
props,
},
}
})
}
return {
subscribe: store.subscribe,
actions: {
startDraggingExistingComponent,
startDraggingNewComponent,
updateTarget,
updateDrop,
reset,
updateNewComponentProps,
},
}
}
export const dndStore = createDndStore()
// The DND store is updated extremely frequently, so we can greatly improve
// performance by deriving any state that needs to be externally observed.
// By doing this and using primitives, we can avoid invalidating other stores
// or components which depend on DND state unless values actually change.
export const dndParent = derivedMemo(dndStore, x => x.drop?.parent)
export const dndIndex = derivedMemo(dndStore, x => x.drop?.index)
export const dndSource = derivedMemo(dndStore, x => x.source)
export const dndIsDragging = derivedMemo(dndStore, x => !!x.source)

View File

@ -1,4 +1,4 @@
import { API } from "api" import { API } from "@/api"
import { writable } from "svelte/store" import { writable } from "svelte/store"
const initialState = { const initialState = {

View File

@ -2,7 +2,7 @@ export { authStore } from "./auth"
export { appStore } from "./app" export { appStore } from "./app"
export { notificationStore } from "./notification" export { notificationStore } from "./notification"
export { routeStore } from "./routes" export { routeStore } from "./routes"
export { screenStore } from "./screens" export { screenStore, isGridScreen } from "./screens"
export { builderStore } from "./builder" export { builderStore } from "./builder"
export { dataSourceStore } from "./dataSource" export { dataSourceStore } from "./dataSource"
export { confirmationStore } from "./confirmation" export { confirmationStore } from "./confirmation"
@ -18,14 +18,7 @@ export { environmentStore } from "./environment"
export { eventStore } from "./events" export { eventStore } from "./events"
export { orgStore } from "./org" export { orgStore } from "./org"
export { roleStore } from "./roles" export { roleStore } from "./roles"
export { export { dndStore, dndIndex, dndParent, dndIsDragging, dndSource } from "./dnd"
dndStore,
dndIndex,
dndParent,
dndBounds,
dndIsNewComponent,
dndIsDragging,
} from "./dnd"
export { sidePanelStore } from "./sidePanel" export { sidePanelStore } from "./sidePanel"
export { modalStore } from "./modal" export { modalStore } from "./modal"
export { hoverStore } from "./hover" export { hoverStore } from "./hover"

View File

@ -1,4 +1,4 @@
import { API } from "api" import { API } from "@/api"
import { writable, get } from "svelte/store" import { writable, get } from "svelte/store"
import { appStore } from "./app" import { appStore } from "./app"

View File

@ -1,4 +1,4 @@
import { API } from "api" import { API } from "@/api"
import { writable } from "svelte/store" import { writable } from "svelte/store"
import { currentRole } from "./derived" import { currentRole } from "./derived"

View File

@ -1,6 +1,6 @@
import { get, writable } from "svelte/store" import { get, writable } from "svelte/store"
import { push } from "svelte-spa-router" import { push } from "svelte-spa-router"
import { API } from "api" import { API } from "@/api"
import { peekStore } from "./peek" import { peekStore } from "./peek"
import { builderStore } from "./builder" import { builderStore } from "./builder"

View File

@ -3,11 +3,11 @@ import { routeStore } from "./routes"
import { builderStore } from "./builder" import { builderStore } from "./builder"
import { appStore } from "./app" import { appStore } from "./app"
import { orgStore } from "./org" import { orgStore } from "./org"
import { dndIndex, dndParent, dndIsNewComponent, dndBounds } from "./dnd.js" import { dndIndex, dndParent, dndSource } from "./dnd.ts"
import { RoleUtils } from "@budibase/frontend-core" import { RoleUtils } from "@budibase/frontend-core"
import { findComponentById, findComponentParent } from "../utils/components.js" import { findComponentById, findComponentParent } from "../utils/components.js"
import { Helpers } from "@budibase/bbui" import { Helpers } from "@budibase/bbui"
import { DNDPlaceholderID, ScreenslotID, ScreenslotType } from "constants" import { DNDPlaceholderID, ScreenslotID, ScreenslotType } from "@/constants"
const createScreenStore = () => { const createScreenStore = () => {
const store = derived( const store = derived(
@ -18,8 +18,7 @@ const createScreenStore = () => {
orgStore, orgStore,
dndParent, dndParent,
dndIndex, dndIndex,
dndIsNewComponent, dndSource,
dndBounds,
], ],
([ ([
$appStore, $appStore,
@ -28,8 +27,7 @@ const createScreenStore = () => {
$orgStore, $orgStore,
$dndParent, $dndParent,
$dndIndex, $dndIndex,
$dndIsNewComponent, $dndSource,
$dndBounds,
]) => { ]) => {
let activeLayout, activeScreen let activeLayout, activeScreen
let screens let screens
@ -85,7 +83,7 @@ const createScreenStore = () => {
// Remove selected component from tree if we are moving an existing // Remove selected component from tree if we are moving an existing
// component // component
if (!$dndIsNewComponent && selectedParent) { if (!$dndSource.isNew && selectedParent) {
selectedParent._children = selectedParent._children?.filter( selectedParent._children = selectedParent._children?.filter(
x => x._id !== selectedComponentId x => x._id !== selectedComponentId
) )
@ -97,11 +95,11 @@ const createScreenStore = () => {
_id: DNDPlaceholderID, _id: DNDPlaceholderID,
_styles: { _styles: {
normal: { normal: {
width: `${$dndBounds?.width || 400}px`, width: `${$dndSource?.bounds?.width || 400}px`,
height: `${$dndBounds?.height || 200}px`, height: `${$dndSource?.bounds?.height || 200}px`,
opacity: 0, opacity: 0,
"--default-width": $dndBounds?.width || 400, "--default-width": $dndSource?.bounds?.width || 400,
"--default-height": $dndBounds?.height || 200, "--default-height": $dndSource?.bounds?.height || 200,
}, },
}, },
static: true, static: true,
@ -198,3 +196,7 @@ const createScreenStore = () => {
} }
export const screenStore = createScreenStore() export const screenStore = createScreenStore()
export const isGridScreen = derived(screenStore, $screenStore => {
return $screenStore.activeScreen?.props?.layout === "grid"
})

View File

@ -1,4 +1,4 @@
import { API } from "api" import { API } from "@/api"
export const getAPIKey = async () => { export const getAPIKey = async () => {
const { apiKey } = await API.fetchDeveloperInfo() const { apiKey } = await API.fetchDeveloperInfo()

View File

@ -13,9 +13,9 @@ import {
rowSelectionStore, rowSelectionStore,
sidePanelStore, sidePanelStore,
modalStore, modalStore,
} from "stores" } from "@/stores"
import { API } from "api" import { API } from "@/api"
import { ActionTypes } from "constants" import { ActionTypes } from "@/constants"
import { enrichDataBindings } from "./enrichDataBinding" import { enrichDataBindings } from "./enrichDataBinding"
import { Helpers } from "@budibase/bbui" import { Helpers } from "@budibase/bbui"

View File

@ -1,6 +1,6 @@
import { GridSpacing, GridRowHeight } from "@/constants" import { GridSpacing, GridRowHeight } from "@/constants"
import { builderStore } from "stores" import { builderStore } from "@/stores"
import { buildStyleString } from "utils/styleable.js" import { buildStyleString } from "@/utils/styleable.js"
interface GridMetadata { interface GridMetadata {
id: string id: string
@ -37,46 +37,54 @@ interface GridMetadata {
*/ */
// Enum representing the different CSS variables we use for grid metadata // Enum representing the different CSS variables we use for grid metadata
export const GridParams = { export enum GridParams {
HAlign: "h-align", HAlign = "h-align",
VAlign: "v-align", VAlign = "v-align",
ColStart: "col-start", ColStart = "col-start",
ColEnd: "col-end", ColEnd = "col-end",
RowStart: "row-start", RowStart = "row-start",
RowEnd: "row-end", RowEnd = "row-end",
} }
// Classes used in selectors inside grid containers to control child styles // Classes used in selectors inside grid containers to control child styles
export const GridClasses = { export enum GridClasses {
DesktopFill: "grid-desktop-grow", DesktopFill = "grid-desktop-grow",
MobileFill: "grid-mobile-grow", MobileFill = "grid-mobile-grow",
} }
// Enum for device preview type, included in grid CSS variables // Enum for device preview type, included in grid CSS variables
export const Devices = { export enum Devices {
Desktop: "desktop", Desktop = "desktop",
Mobile: "mobile", Mobile = "mobile",
} }
export const GridDragModes = { export enum GridDragMode {
Resize: "resize", Resize = "resize",
Move: "move", Move = "move",
} }
// Builds a CSS variable name for a certain piece of grid metadata // Builds a CSS variable name for a certain piece of grid metadata
export const getGridVar = (device: string, param: string) => export const getGridVar = (device: string, param: string) =>
`--grid-${device}-${param}` `--grid-${device}-${param}`
export interface GridEvent extends DragEvent {
target: HTMLElement
dataTransfer: DataTransfer
}
// Determines whether a JS event originated from immediately within a grid // Determines whether a JS event originated from immediately within a grid
export const isGridEvent = (e: Event & { target: HTMLElement }): boolean => { export const isGridEvent = (e: Event): e is GridEvent => {
if (!(e.target instanceof HTMLElement)) {
return false
}
const componentParent = e.target.closest?.(".component")?.parentNode as
| HTMLElement
| undefined
const gridDOMCandidate = componentParent?.closest(".component")
?.childNodes[0] as HTMLElement | undefined
return ( return (
e.target.dataset?.indicator === "true" || e.target.dataset?.indicator === "true" ||
// @ts-expect-error: api is not properly typed !!gridDOMCandidate?.classList?.contains("grid")
e.target
.closest?.(".component")
// @ts-expect-error
?.parentNode.closest(".component")
?.childNodes[0]?.classList?.contains("grid")
) )
} }

View File

@ -1,6 +1,6 @@
import { get } from "svelte/store" import { get } from "svelte/store"
import { link, LinkActionOpts } from "svelte-spa-router" import { link, LinkActionOpts } from "svelte-spa-router"
import { builderStore } from "stores" import { builderStore } from "@/stores"
export const linkable = (node: HTMLElement, href?: LinkActionOpts) => { export const linkable = (node: HTMLElement, href?: LinkActionOpts) => {
if (get(builderStore).inBuilder) { if (get(builderStore).inBuilder) {

View File

@ -1,4 +1,4 @@
import { API } from "api" import { API } from "@/api"
import { DataFetchMap, DataFetchType } from "@budibase/frontend-core" import { DataFetchMap, DataFetchType } from "@budibase/frontend-core"
import { FieldType, TableSchema } from "@budibase/types" import { FieldType, TableSchema } from "@budibase/types"

View File

@ -1,4 +1,4 @@
import { builderStore } from "stores" import { builderStore } from "@/stores"
/** /**
* Helper to build a CSS string from a style object. * Helper to build a CSS string from a style object.

View File

@ -1,8 +1,4 @@
import { import { builderStore, environmentStore, notificationStore } from "@/stores"
builderStore,
environmentStore,
notificationStore,
} from "./stores/index.js"
import { get } from "svelte/store" import { get } from "svelte/store"
import { createWebsocket } from "@budibase/frontend-core" import { createWebsocket } from "@budibase/frontend-core"

View File

@ -20,7 +20,7 @@ export default defineConfig(({ mode }) => {
}, },
build: { build: {
lib: { lib: {
entry: "src/index.js", entry: "src/index.ts",
formats: ["iife"], formats: ["iife"],
outDir: "dist", outDir: "dist",
name: "budibase_client", name: "budibase_client",
@ -47,34 +47,6 @@ export default defineConfig(({ mode }) => {
find: "manifest.json", find: "manifest.json",
replacement: path.resolve("./manifest.json"), replacement: path.resolve("./manifest.json"),
}, },
{
find: "api",
replacement: path.resolve("./src/api"),
},
{
find: "components",
replacement: path.resolve("./src/components"),
},
{
find: "stores",
replacement: path.resolve("./src/stores"),
},
{
find: "utils",
replacement: path.resolve("./src/utils"),
},
{
find: "constants",
replacement: path.resolve("./src/constants"),
},
{
find: "@/constants",
replacement: path.resolve("./src/constants"),
},
{
find: "sdk",
replacement: path.resolve("./src/sdk"),
},
{ {
find: "@budibase/types", find: "@budibase/types",
replacement: path.resolve("../types/src"), replacement: path.resolve("../types/src"),
@ -87,6 +59,10 @@ export default defineConfig(({ mode }) => {
find: "@budibase/bbui", find: "@budibase/bbui",
replacement: path.resolve("../bbui/src"), replacement: path.resolve("../bbui/src"),
}, },
{
find: "@",
replacement: path.resolve(__dirname, "src"),
},
], ],
}, },
} }

View File

@ -1,7 +1,7 @@
import { Readable, Writable } from "svelte/store" import { Readable, Writable } from "svelte/store"
declare module "./memo" { declare module "./memo" {
export function memo<T>(value: T): Writable<T> export function memo<T>(value?: T): Writable<T>
export function derivedMemo<TStore, TResult>( export function derivedMemo<TStore, TResult>(
store: Readable<TStore>, store: Readable<TStore>,

View File

@ -21,20 +21,7 @@ const baseConfig: Config.InitialProjectOptions = {
transform: { transform: {
"^.+\\.ts?$": "@swc/jest", "^.+\\.ts?$": "@swc/jest",
"^.+\\.js?$": "@swc/jest", "^.+\\.js?$": "@swc/jest",
"^.+\\.svelte$": [ "^.+\\.svelte?$": "<rootDir>/scripts/svelteTransformer.js",
"jest-chain-transform", // https://github.com/svelteness/svelte-jester/issues/166
{
transformers: [
[
"svelte-jester",
{
preprocess: true,
},
],
"@swc/jest",
],
},
],
}, },
transformIgnorePatterns: ["/node_modules/(?!svelte/).*"], transformIgnorePatterns: ["/node_modules/(?!svelte/).*"],
moduleNameMapper: { moduleNameMapper: {

View File

@ -139,7 +139,6 @@
"@babel/core": "^7.22.5", "@babel/core": "^7.22.5",
"@babel/preset-env": "7.16.11", "@babel/preset-env": "7.16.11",
"@jest/types": "^29.6.3", "@jest/types": "^29.6.3",
"@sveltejs/vite-plugin-svelte": "1.4.0",
"@swc/core": "1.3.71", "@swc/core": "1.3.71",
"@swc/jest": "0.2.27", "@swc/jest": "0.2.27",
"@types/archiver": "6.0.2", "@types/archiver": "6.0.2",
@ -165,7 +164,6 @@
"docker-compose": "0.23.17", "docker-compose": "0.23.17",
"ioredis-mock": "8.9.0", "ioredis-mock": "8.9.0",
"jest": "29.7.0", "jest": "29.7.0",
"jest-chain-transform": "^0.0.8",
"jest-extended": "^4.0.2", "jest-extended": "^4.0.2",
"jest-openapi": "0.14.2", "jest-openapi": "0.14.2",
"nock": "13.5.4", "nock": "13.5.4",

View File

@ -0,0 +1,11 @@
const { compile } = require("svelte/compiler")
const { transformSync } = require("@babel/core")
module.exports = {
process(sourceText) {
const { js } = compile(sourceText, { css: "injected", generate: "ssr" })
const { code } = transformSync(js.code, { babelrc: true })
return { code: code }
},
}

View File

@ -1,8 +1,9 @@
<script lang="ts"> <script>
import ClientAppSkeleton from "@budibase/frontend-core/src/components/ClientAppSkeleton.svelte" import ClientAppSkeleton from "@budibase/frontend-core/src/components/ClientAppSkeleton.svelte"
import type { BudibaseAppProps } from "@budibase/types"
export let props: BudibaseAppProps /** @type {BudibaseAppProps} this receives all the props in one structure, following
* the type from @budibase/types */
export let props
</script> </script>
<svelte:head> <svelte:head>

View File

@ -1,5 +0,0 @@
const { vitePreprocess } = require("@sveltejs/vite-plugin-svelte")
module.exports = {
preprocess: vitePreprocess(),
}

View File

@ -33,6 +33,7 @@
"@rollup/plugin-json": "^4.1.0", "@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-typescript": "8.3.0", "@rollup/plugin-typescript": "8.3.0",
"@types/doctrine": "^0.0.9",
"doctrine": "^3.0.0", "doctrine": "^3.0.0",
"jest": "29.7.0", "jest": "29.7.0",
"marked": "^4.0.10", "marked": "^4.0.10",

View File

@ -7,18 +7,30 @@ import { marked } from "marked"
import { join, dirname } from "path" import { join, dirname } from "path"
const helpers = require("@budibase/handlebars-helpers") const helpers = require("@budibase/handlebars-helpers")
const doctrine = require("doctrine") import doctrine, { Annotation } from "doctrine"
type HelperInfo = { type BudibaseAnnotation = Annotation & {
example?: string
acceptsInline?: boolean acceptsInline?: boolean
acceptsBlock?: boolean acceptsBlock?: boolean
}
type Helper = {
args: string[]
numArgs: number
example?: string example?: string
description: string description: string
tags?: any[] requiresBlock?: boolean
}
type Manifest = {
[category: string]: {
[helper: string]: Helper
}
} }
const FILENAME = join(__dirname, "..", "src", "manifest.json") const FILENAME = join(__dirname, "..", "src", "manifest.json")
const outputJSON: any = {} const outputJSON: Manifest = {}
const ADDED_HELPERS = { const ADDED_HELPERS = {
date: { date: {
date: { date: {
@ -38,11 +50,10 @@ const ADDED_HELPERS = {
}, },
} }
function fixSpecialCases(name: string, obj: any) { function fixSpecialCases(name: string, obj: Helper) {
const args = obj.args
if (name === "ifNth") { if (name === "ifNth") {
args[0] = "a" obj.args = ["a", "b", "options"]
args[1] = "b" obj.numArgs = 3
} }
if (name === "eachIndex") { if (name === "eachIndex") {
obj.description = "Iterates the array, listing an item and the index of it." obj.description = "Iterates the array, listing an item and the index of it."
@ -66,10 +77,10 @@ function lookForward(lines: string[], funcLines: string[], idx: number) {
return true return true
} }
function getCommentInfo(file: string, func: string): HelperInfo { function getCommentInfo(file: string, func: string): BudibaseAnnotation {
const lines = file.split("\n") const lines = file.split("\n")
const funcLines = func.split("\n") const funcLines = func.split("\n")
let comment = null let comment: string | null = null
for (let idx = 0; idx < lines.length; ++idx) { for (let idx = 0; idx < lines.length; ++idx) {
// from here work back until we have the comment // from here work back until we have the comment
if (lookForward(lines, funcLines, idx)) { if (lookForward(lines, funcLines, idx)) {
@ -91,15 +102,9 @@ function getCommentInfo(file: string, func: string): HelperInfo {
} }
} }
if (comment == null) { if (comment == null) {
return { description: "" } return { description: "", tags: [] }
} }
const docs: { const docs: BudibaseAnnotation = doctrine.parse(comment, { unwrap: true })
acceptsInline?: boolean
acceptsBlock?: boolean
example: string
description: string
tags: any[]
} = doctrine.parse(comment, { unwrap: true })
// some hacky fixes // some hacky fixes
docs.description = docs.description.replace(/\n/g, " ") docs.description = docs.description.replace(/\n/g, " ")
docs.description = docs.description.replace(/[ ]{2,}/g, " ") docs.description = docs.description.replace(/[ ]{2,}/g, " ")
@ -135,7 +140,7 @@ function run() {
)}/lib/${collection}.js`, )}/lib/${collection}.js`,
"utf8" "utf8"
) )
const collectionInfo: any = {} const collectionInfo: { [name: string]: Helper } = {}
// collect information about helper // collect information about helper
let hbsHelperInfo = helpers[collection]() let hbsHelperInfo = helpers[collection]()
for (let entry of Object.entries(hbsHelperInfo)) { for (let entry of Object.entries(hbsHelperInfo)) {
@ -154,11 +159,8 @@ function run() {
const jsDocInfo = getCommentInfo(collectionFile, fnc) const jsDocInfo = getCommentInfo(collectionFile, fnc)
let args = jsDocInfo.tags let args = jsDocInfo.tags
.filter(tag => tag.title === "param") .filter(tag => tag.title === "param")
.map( .filter(tag => tag.description)
tag => .map(tag => tag.description!.replace(/`/g, "").split(" ")[0].trim())
tag.description &&
tag.description.replace(/`/g, "").split(" ")[0].trim()
)
collectionInfo[name] = fixSpecialCases(name, { collectionInfo[name] = fixSpecialCases(name, {
args, args,
numArgs: args.length, numArgs: args.length,

View File

@ -2,6 +2,16 @@ export * from "./sidepanel"
export * from "./codeEditor" export * from "./codeEditor"
export * from "./errors" export * from "./errors"
export interface CustomComponent {
Component: any
schema: {
type: "component"
metadata: Record<string, any>
schema: ComponentDefinition
}
version: string
}
export interface ComponentDefinition { export interface ComponentDefinition {
component: string component: string
name: string name: string
@ -13,6 +23,11 @@ export interface ComponentDefinition {
legalDirectChildren: string[] legalDirectChildren: string[]
requiredAncestors?: string[] requiredAncestors?: string[]
illegalChildren: string[] illegalChildren: string[]
icon?: string
size?: {
width: number
height: number
}
} }
export type DependsOnComponentSetting = export type DependsOnComponentSetting =

View File

@ -6456,6 +6456,11 @@
"@types/node" "*" "@types/node" "*"
"@types/ssh2" "*" "@types/ssh2" "*"
"@types/doctrine@^0.0.9":
version "0.0.9"
resolved "https://registry.yarnpkg.com/@types/doctrine/-/doctrine-0.0.9.tgz#d86a5f452a15e3e3113b99e39616a9baa0f9863f"
integrity sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==
"@types/estree@*", "@types/estree@1.0.5", "@types/estree@^1.0.0", "@types/estree@^1.0.1": "@types/estree@*", "@types/estree@1.0.5", "@types/estree@^1.0.0", "@types/estree@^1.0.1":
version "1.0.5" version "1.0.5"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4"
@ -13739,11 +13744,6 @@ jake@^10.8.5:
filelist "^1.0.1" filelist "^1.0.1"
minimatch "^3.0.4" minimatch "^3.0.4"
jest-chain-transform@^0.0.8:
version "0.0.8"
resolved "https://registry.yarnpkg.com/jest-chain-transform/-/jest-chain-transform-0.0.8.tgz#cbb4d3aef8d02678b1852968a9b0c861f75eef5a"
integrity sha512-AELTTzYJ34WrmQKAbxUGT+xqnAHu0/XJZhahYNGvBVUhnAayjm1QmT45DQjwEbQPQp7gn6CXzu6rZA03riwBuw==
jest-changed-files@^29.7.0: jest-changed-files@^29.7.0:
version "29.7.0" version "29.7.0"
resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.7.0.tgz#1c06d07e77c78e1585d020424dedc10d6e17ac3a" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.7.0.tgz#1c06d07e77c78e1585d020424dedc10d6e17ac3a"