Merge pull request #13327 from Budibase/cheeks-fixes

Various fixes and improvements
This commit is contained in:
Andrew Kingston 2024-03-22 12:57:25 +00:00 committed by GitHub
commit cc02d759d5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 157 additions and 116 deletions

View File

@ -79,7 +79,8 @@ const removeHandler = id => {
export default (element, opts) => { export default (element, opts) => {
const id = Math.random() const id = Math.random()
const update = newOpts => { const update = newOpts => {
const callback = newOpts?.callback || newOpts const callback =
newOpts?.callback || (typeof newOpts === "function" ? newOpts : null)
const anchor = newOpts?.anchor || element const anchor = newOpts?.anchor || element
const allowedType = newOpts?.allowedType || "click" const allowedType = newOpts?.allowedType || "click"
updateHandler(id, element, anchor, callback, allowedType) updateHandler(id, element, anchor, callback, allowedType)

View File

@ -42,7 +42,6 @@
.main { .main {
height: 100%; height: 100%;
overflow: auto; overflow: auto;
overflow-x: hidden;
} }
.padding .main { .padding .main {
padding: var(--spacing-xl); padding: var(--spacing-xl);

View File

@ -12,6 +12,7 @@
export let schema export let schema
export let value export let value
export let customRenderers = [] export let customRenderers = []
export let snippets
let renderer let renderer
const typeMap = { const typeMap = {
@ -44,7 +45,7 @@
if (!template) { if (!template) {
return value return value
} }
return processStringSync(template, { value }) return processStringSync(template, { value, snippets })
} }
</script> </script>

View File

@ -42,6 +42,7 @@
export let customPlaceholder = false export let customPlaceholder = false
export let showHeaderBorder = true export let showHeaderBorder = true
export let placeholderText = "No rows found" export let placeholderText = "No rows found"
export let snippets = []
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -425,6 +426,7 @@
<CellRenderer <CellRenderer
{customRenderers} {customRenderers}
{row} {row}
{snippets}
schema={schema[field]} schema={schema[field]}
value={deepGet(row, field)} value={deepGet(row, field)}
on:clickrelationship on:clickrelationship

View File

@ -313,7 +313,7 @@ export const bindingsToCompletions = (bindings, mode) => {
...bindingByCategory[catKey].reduce((acc, binding) => { ...bindingByCategory[catKey].reduce((acc, binding) => {
let displayType = binding.fieldSchema?.type || binding.display?.type let displayType = binding.fieldSchema?.type || binding.display?.type
acc.push({ acc.push({
label: binding.display?.name || "NO NAME", label: binding.display?.name || binding.readableBinding || "NO NAME",
info: completion => { info: completion => {
return buildBindingInfoNode(completion, binding) return buildBindingInfoNode(completion, binding)
}, },

View File

@ -8,6 +8,7 @@
export let allowJS = false export let allowJS = false
export let allowHelpers = true export let allowHelpers = true
export let autofocusEditor = false export let autofocusEditor = false
export let context = null
$: enrichedBindings = enrichBindings(bindings) $: enrichedBindings = enrichBindings(bindings)
@ -27,7 +28,7 @@
<BindingPanel <BindingPanel
bindings={enrichedBindings} bindings={enrichedBindings}
context={$previewStore.selectedComponentContext} context={{ ...$previewStore.selectedComponentContext, ...context }}
snippets={$snippets} snippets={$snippets}
{value} {value}
{allowJS} {allowJS}

View File

@ -7,10 +7,13 @@
Layout, Layout,
Label, Label,
} from "@budibase/bbui" } from "@budibase/bbui"
import { themeStore } from "stores/builder" import { themeStore, previewStore } from "stores/builder"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte" import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
export let column export let column
$: columnValue =
$previewStore.selectedComponentContext?.eventContext?.row?.[column.name]
</script> </script>
<DrawerContent> <DrawerContent>
@ -41,6 +44,9 @@
icon: "TableColumnMerge", icon: "TableColumnMerge",
}, },
]} ]}
context={{
value: columnValue,
}}
/> />
<Layout noPadding gap="XS"> <Layout noPadding gap="XS">
<Label>Background color</Label> <Label>Background color</Label>

View File

@ -14,7 +14,7 @@
{ {
"name": "Layout", "name": "Layout",
"icon": "ClassicGridView", "icon": "ClassicGridView",
"children": ["container", "section", "grid", "sidepanel"] "children": ["container", "section", "sidepanel"]
}, },
{ {
"name": "Data", "name": "Data",

View File

@ -279,12 +279,10 @@ export class ComponentStore extends BudiStore {
else { else {
if (setting.type === "dataProvider") { if (setting.type === "dataProvider") {
// Validate data provider exists, or else clear it // Validate data provider exists, or else clear it
const treeId = parent?._id || component._id const providers = findAllMatchingComponents(
const path = findComponentPath(screen?.props, treeId) screen?.props,
const providers = path.filter(component => x => x._component === "@budibase/standard-components/dataprovider"
component._component?.endsWith("/dataprovider")
) )
// Validate non-empty values
const valid = providers?.some(dp => value.includes?.(dp._id)) const valid = providers?.some(dp => value.includes?.(dp._id))
if (!valid) { if (!valid) {
if (providers.length) { if (providers.length) {

View File

@ -7,12 +7,25 @@ export const INITIAL_HOVER_STATE = {
} }
export class HoverStore extends BudiStore { export class HoverStore extends BudiStore {
hoverTimeout
constructor() { constructor() {
super({ ...INITIAL_HOVER_STATE }) super({ ...INITIAL_HOVER_STATE })
this.hover = this.hover.bind(this) this.hover = this.hover.bind(this)
} }
hover(componentId, notifyClient = true) { hover(componentId, notifyClient = true) {
clearTimeout(this.hoverTimeout)
if (componentId) {
this.processHover(componentId, notifyClient)
} else {
this.hoverTimeout = setTimeout(() => {
this.processHover(componentId, notifyClient)
}, 10)
}
}
processHover(componentId, notifyClient) {
if (componentId === get(this.store).componentId) { if (componentId === get(this.store).componentId) {
return return
} }

View File

@ -26,9 +26,12 @@
let schema let schema
$: id = $component.id
$: selected = $component.selected
$: builderStep = $builderStore.metadata?.step
$: fetchSchema(dataSource) $: fetchSchema(dataSource)
$: enrichedSteps = enrichSteps(steps, schema, $component.id, $currentStep) $: enrichedSteps = enrichSteps(steps, schema, id)
$: updateCurrentStep(enrichedSteps, $builderStore, $component) $: updateCurrentStep(enrichedSteps, selected, builderStep)
// Provide additional data context for live binding eval // Provide additional data context for live binding eval
export const getAdditionalDataContext = () => { export const getAdditionalDataContext = () => {
@ -40,30 +43,22 @@
} }
} }
const updateCurrentStep = (steps, builderStore, component) => { const updateCurrentStep = (steps, selected, builderStep) => {
const { componentId, step } = builderStore.metadata || {} // If we aren't selected in the builder then just allowing the normal form
// to take control.
// If we aren't in the builder or aren't selected then don't update the step if (!selected) {
// context at all, allowing the normal form to take control.
if (
!component.selected ||
!builderStore.inBuilder ||
componentId !== component.id
) {
return return
} }
// Ensure we have a valid step selected // Ensure we have a valid step selected
let newStep = Math.min(step || 0, steps.length - 1) let newStep = Math.min(builderStep || 0, steps.length - 1)
// Sanity check
newStep = Math.max(newStep, 0) newStep = Math.max(newStep, 0)
// Add 1 because the form component expects 1 indexed rather than 0 indexed // Add 1 because the form component expects 1 indexed rather than 0 indexed
currentStep.set(newStep + 1) currentStep.set(newStep + 1)
} }
const fetchSchema = async () => { const fetchSchema = async dataSource => {
schema = (await fetchDatasourceSchema(dataSource)) || {} schema = (await fetchDatasourceSchema(dataSource)) || {}
} }

View File

@ -34,6 +34,7 @@
$: formattedFields = convertOldFieldFormat(fields) $: formattedFields = convertOldFieldFormat(fields)
$: fieldsOrDefault = getDefaultFields(formattedFields, schema) $: fieldsOrDefault = getDefaultFields(formattedFields, schema)
$: fetchSchema(dataSource) $: fetchSchema(dataSource)
$: id = $component.id
// We could simply spread $$props into the inner form and append our // We could simply spread $$props into the inner form and append our
// additions, but that would create svelte warnings about unused props and // additions, but that would create svelte warnings about unused props and
// make maintenance in future more confusing as we typically always have a // make maintenance in future more confusing as we typically always have a
@ -53,7 +54,7 @@
buttons: buttons:
buttons || buttons ||
Utils.buildFormBlockButtonConfig({ Utils.buildFormBlockButtonConfig({
_id: $component.id, _id: id,
showDeleteButton, showDeleteButton,
showSaveButton, showSaveButton,
saveButtonLabel, saveButtonLabel,

View File

@ -50,6 +50,7 @@
let schemaLoaded = false let schemaLoaded = false
$: deleteLabel = setDeleteLabel(sidePanelDeleteLabel, sidePanelShowDelete) $: deleteLabel = setDeleteLabel(sidePanelDeleteLabel, sidePanelShowDelete)
$: id = $component.id
$: isDSPlus = dataSource?.type === "table" || dataSource?.type === "viewV2" $: isDSPlus = dataSource?.type === "table" || dataSource?.type === "viewV2"
$: fetchSchema(dataSource) $: fetchSchema(dataSource)
$: enrichSearchColumns(searchColumns, schema).then( $: enrichSearchColumns(searchColumns, schema).then(
@ -279,7 +280,7 @@
dataSource, dataSource,
buttonPosition: "top", buttonPosition: "top",
buttons: Utils.buildFormBlockButtonConfig({ buttons: Utils.buildFormBlockButtonConfig({
_id: $component.id + "-form-edit", _id: id + "-form-edit",
showDeleteButton: deleteLabel !== "", showDeleteButton: deleteLabel !== "",
showSaveButton: true, showSaveButton: true,
saveButtonLabel: sidePanelSaveLabel || "Save", saveButtonLabel: sidePanelSaveLabel || "Save",
@ -313,7 +314,7 @@
dataSource, dataSource,
buttonPosition: "top", buttonPosition: "top",
buttons: Utils.buildFormBlockButtonConfig({ buttons: Utils.buildFormBlockButtonConfig({
_id: $component.id + "-form-new", _id: id + "-form-new",
showDeleteButton: false, showDeleteButton: false,
showSaveButton: true, showSaveButton: true,
saveButtonLabel: "Save", saveButtonLabel: "Save",

View File

@ -16,8 +16,15 @@
export let noRowsMessage export let noRowsMessage
const component = getContext("component") const component = getContext("component")
const { styleable, getAction, ActionTypes, rowSelectionStore } = const context = getContext("context")
getContext("sdk") const {
styleable,
getAction,
ActionTypes,
rowSelectionStore,
generateGoldenSample,
} = getContext("sdk")
const customColumnKey = `custom-${Math.random()}` const customColumnKey = `custom-${Math.random()}`
const customRenderers = [ const customRenderers = [
{ {
@ -28,6 +35,7 @@
let selectedRows = [] let selectedRows = []
$: snippets = $context.snippets
$: hasChildren = $component.children $: hasChildren = $component.children
$: loading = dataProvider?.loading ?? false $: loading = dataProvider?.loading ?? false
$: data = dataProvider?.rows || [] $: data = dataProvider?.rows || []
@ -61,6 +69,16 @@
selectedRows, selectedRows,
} }
// Provide additional data context for live binding eval
export const getAdditionalDataContext = () => {
const goldenRow = generateGoldenSample(data)
return {
eventContext: {
row: goldenRow,
},
}
}
const getFields = ( const getFields = (
schema, schema,
customColumns, customColumns,
@ -178,6 +196,7 @@
{quiet} {quiet}
{compact} {compact}
{customRenderers} {customRenderers}
{snippets}
allowSelectRows={allowSelectRows && table} allowSelectRows={allowSelectRows && table}
bind:selectedRows bind:selectedRows
allowEditRows={false} allowEditRows={false}

View File

@ -6,36 +6,24 @@ export const getOptions = (
valueColumn, valueColumn,
customOptions customOptions
) => { ) => {
const isArray = fieldSchema?.type === "array"
// Take options from schema // Take options from schema
if (optionsSource == null || optionsSource === "schema") { if (optionsSource == null || optionsSource === "schema") {
return fieldSchema?.constraints?.inclusion ?? [] return fieldSchema?.constraints?.inclusion ?? []
} }
if (optionsSource === "provider" && isArray) {
let optionsSet = {}
dataProvider?.rows?.forEach(row => {
const value = row?.[valueColumn]
if (value != null) {
const label = row[labelColumn] || value
optionsSet[value] = { value, label }
}
})
return Object.values(optionsSet)
}
// Extract options from data provider // Extract options from data provider
if (optionsSource === "provider" && valueColumn) { if (optionsSource === "provider" && valueColumn) {
let optionsSet = {} let valueCache = {}
let options = []
dataProvider?.rows?.forEach(row => { dataProvider?.rows?.forEach(row => {
const value = row?.[valueColumn] const value = row?.[valueColumn]
if (value != null) { if (value != null && !valueCache[value]) {
valueCache[value] = true
const label = row[labelColumn] || value const label = row[labelColumn] || value
optionsSet[value] = { value, label } options.push({ value, label })
} }
}) })
return Object.values(optionsSet) return options
} }
// Extract custom options // Extract custom options

View File

@ -345,8 +345,7 @@
<IndicatorSet <IndicatorSet
componentId={$dndParent} componentId={$dndParent}
color="var(--spectrum-global-color-static-green-500)" color="var(--spectrum-global-color-static-green-500)"
zIndex="930" zIndex={920}
transition
prefix="Inside" prefix="Inside"
/> />

View File

@ -1,10 +1,11 @@
<script> <script>
import { onMount, onDestroy } from "svelte" import { onMount, onDestroy } from "svelte"
import IndicatorSet from "./IndicatorSet.svelte" import IndicatorSet from "./IndicatorSet.svelte"
import { builderStore, dndIsDragging, hoverStore } from "stores" import { dndIsDragging, hoverStore, builderStore } from "stores"
$: componentId = $hoverStore.hoveredComponentId $: componentId = $hoverStore.hoveredComponentId
$: zIndex = componentId === $builderStore.selectedComponentId ? 900 : 920 $: selectedComponentId = $builderStore.selectedComponentId
$: selected = componentId === selectedComponentId
const onMouseOver = e => { const onMouseOver = e => {
// Ignore if dragging // Ignore if dragging
@ -45,7 +46,6 @@
<IndicatorSet <IndicatorSet
componentId={$dndIsDragging ? null : componentId} componentId={$dndIsDragging ? null : componentId}
color="var(--spectrum-global-color-static-blue-200)" color="var(--spectrum-global-color-static-blue-200)"
transition zIndex={selected ? 890 : 910}
{zIndex}
allowResizeAnchors allowResizeAnchors
/> />

View File

@ -1,5 +1,4 @@
<script> <script>
import { fade } from "svelte/transition"
import { Icon } from "@budibase/bbui" import { Icon } from "@budibase/bbui"
export let top export let top
@ -11,7 +10,6 @@
export let color export let color
export let zIndex export let zIndex
export let componentId export let componentId
export let transition = false
export let line = false export let line = false
export let alignRight = false export let alignRight = false
export let showResizeAnchors = false export let showResizeAnchors = false
@ -31,10 +29,6 @@
</script> </script>
<div <div
in:fade={{
delay: transition ? 100 : 0,
duration: transition ? 100 : 0,
}}
class="indicator" class="indicator"
class:flipped class:flipped
class:line class:line
@ -127,10 +121,6 @@
font-weight: 600; font-weight: 600;
} }
/* Icon styles */
.label :global(.spectrum-Icon + .text) {
}
/* Anchor */ /* Anchor */
.anchor { .anchor {
--size: 24px; --size: 24px;

View File

@ -4,27 +4,39 @@
import { domDebounce } from "utils/domDebounce" import { domDebounce } from "utils/domDebounce"
import { builderStore } from "stores" import { builderStore } from "stores"
export let componentId export let componentId = null
export let color export let color = null
export let transition export let zIndex = 900
export let zIndex
export let prefix = null export let prefix = null
export let allowResizeAnchors = false export let allowResizeAnchors = false
let indicators = [] const errorColor = "var(--spectrum-global-color-static-red-600)"
const defaultState = () => ({
// Cached props
componentId,
color,
zIndex,
prefix,
allowResizeAnchors,
// Computed state
indicators: [],
text: null,
icon: null,
insideGrid: false,
error: false,
})
let interval let interval
let text let state = defaultState()
let icon let nextState = null
let insideGrid = false
let errorState = false
$: visibleIndicators = indicators.filter(x => x.visible)
$: offset = $builderStore.inBuilder ? 0 : 2
let updating = false let updating = false
let observers = [] let observers = []
let callbackCount = 0 let callbackCount = 0
let nextIndicators = []
$: visibleIndicators = state.indicators.filter(x => x.visible)
$: offset = $builderStore.inBuilder ? 0 : 2
$: $$props, debouncedUpdate()
const checkInsideGrid = id => { const checkInsideGrid = id => {
const component = document.getElementsByClassName(id)[0] const component = document.getElementsByClassName(id)[0]
@ -44,10 +56,10 @@
if (callbackCount >= observers.length) { if (callbackCount >= observers.length) {
return return
} }
nextIndicators[idx].visible = nextState.indicators[idx].visible =
nextIndicators[idx].insideSidePanel || entries[0].isIntersecting nextState.indicators[idx].insideSidePanel || entries[0].isIntersecting
if (++callbackCount === observers.length) { if (++callbackCount === observers.length) {
indicators = nextIndicators state = nextState
updating = false updating = false
} }
} }
@ -59,7 +71,7 @@
// Sanity check // Sanity check
if (!componentId) { if (!componentId) {
indicators = [] state = defaultState()
return return
} }
@ -68,25 +80,25 @@
callbackCount = 0 callbackCount = 0
observers.forEach(o => o.disconnect()) observers.forEach(o => o.disconnect())
observers = [] observers = []
nextIndicators = [] nextState = defaultState()
// Check if we're inside a grid // Check if we're inside a grid
if (allowResizeAnchors) { if (allowResizeAnchors) {
insideGrid = checkInsideGrid(componentId) nextState.insideGrid = checkInsideGrid(componentId)
} }
// Determine next set of indicators // Determine next set of indicators
const parents = document.getElementsByClassName(componentId) const parents = document.getElementsByClassName(componentId)
if (parents.length) { if (parents.length) {
text = parents[0].dataset.name nextState.text = parents[0].dataset.name
if (prefix) { if (nextState.prefix) {
text = `${prefix} ${text}` nextState.text = `${nextState.prefix} ${nextState.text}`
} }
if (parents[0].dataset.icon) { if (parents[0].dataset.icon) {
icon = parents[0].dataset.icon nextState.icon = parents[0].dataset.icon
} }
} }
errorState = parents?.[0]?.classList.contains("error") nextState.error = parents?.[0]?.classList.contains("error")
// Batch reads to minimize reflow // Batch reads to minimize reflow
const scrollX = window.scrollX const scrollX = window.scrollX
@ -102,8 +114,9 @@
// If there aren't any nodes then reset // If there aren't any nodes then reset
if (!children.length) { if (!children.length) {
indicators = [] state = defaultState()
updating = false updating = false
return
} }
const device = document.getElementById("app-root") const device = document.getElementById("app-root")
@ -119,7 +132,7 @@
observers.push(observer) observers.push(observer)
const elBounds = child.getBoundingClientRect() const elBounds = child.getBoundingClientRect()
nextIndicators.push({ nextState.indicators.push({
top: elBounds.top + scrollY - deviceBounds.top - offset, top: elBounds.top + scrollY - deviceBounds.top - offset,
left: elBounds.left + scrollX - deviceBounds.left - offset, left: elBounds.left + scrollX - deviceBounds.left - offset,
width: elBounds.width + 4, width: elBounds.width + 4,
@ -144,20 +157,17 @@
}) })
</script> </script>
{#key componentId} {#each visibleIndicators as indicator, idx}
{#each visibleIndicators as indicator, idx} <Indicator
<Indicator top={indicator.top}
top={indicator.top} 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 ? text : null} icon={idx === 0 ? state.icon : null}
icon={idx === 0 ? icon : null} showResizeAnchors={state.allowResizeAnchors && state.insideGrid}
showResizeAnchors={allowResizeAnchors && insideGrid} color={state.error ? errorColor : state.color}
color={errorState ? "var(--spectrum-global-color-static-red-600)" : color} componentId={state.componentId}
{componentId} zIndex={state.zIndex}
{transition} />
{zIndex} {/each}
/>
{/each}
{/key}

View File

@ -10,7 +10,6 @@
<IndicatorSet <IndicatorSet
componentId={$builderStore.selectedComponentId} componentId={$builderStore.selectedComponentId}
{color} {color}
zIndex="910" zIndex={900}
transition
allowResizeAnchors allowResizeAnchors
/> />

View File

@ -98,7 +98,7 @@ const loadBudibase = async () => {
context: stringifiedContext, context: stringifiedContext,
}) })
} else if (type === "hover-component") { } else if (type === "hover-component") {
hoverStore.actions.hoverComponent(data) hoverStore.actions.hoverComponent(data, false)
} else if (type === "builder-meta") { } else if (type === "builder-meta") {
builderStore.actions.setMetadata(data) builderStore.actions.setMetadata(data)
} }

View File

@ -34,6 +34,8 @@ import {
LuceneUtils, LuceneUtils,
Constants, Constants,
RowUtils, RowUtils,
memo,
derivedMemo,
} from "@budibase/frontend-core" } from "@budibase/frontend-core"
export default { export default {
@ -71,6 +73,8 @@ export default {
makePropSafe, makePropSafe,
createContextStore, createContextStore,
generateGoldenSample: RowUtils.generateGoldenSample, generateGoldenSample: RowUtils.generateGoldenSample,
memo,
derivedMemo,
// Components // Components
Provider, Provider,

View File

@ -5,13 +5,27 @@ const createHoverStore = () => {
const store = writable({ const store = writable({
hoveredComponentId: null, hoveredComponentId: null,
}) })
let hoverTimeout
const hoverComponent = id => { const hoverComponent = (id, notifyBuilder = true) => {
clearTimeout(hoverTimeout)
if (id) {
processHover(id, notifyBuilder)
} else {
hoverTimeout = setTimeout(() => {
processHover(id, notifyBuilder)
}, 10)
}
}
const processHover = (id, notifyBuilder = true) => {
if (id === get(store).hoveredComponentId) { if (id === get(store).hoveredComponentId) {
return return
} }
store.set({ hoveredComponentId: id }) store.set({ hoveredComponentId: id })
eventStore.actions.dispatchEvent("hover-component", { id }) if (notifyBuilder) {
eventStore.actions.dispatchEvent("hover-component", { id })
}
} }
return { return {