1
0
Fork 0
mirror of synced 2024-07-07 07:15:43 +12:00

Merge branch 'master' of github.com:budibase/budibase into test-race-condition

This commit is contained in:
Sam Rose 2024-05-03 14:22:37 +01:00
commit 49fad46025
No known key found for this signature in database
17 changed files with 205 additions and 123 deletions

View file

@ -1,4 +1,5 @@
{
"root": true,
"env": {
"browser": true,
"es6": true,

View file

@ -12,4 +12,5 @@ packages/pro/coverage
packages/account-portal/packages/ui/build
packages/account-portal/packages/ui/.routify
packages/account-portal/packages/server/build
packages/account-portal/packages/server/coverage
**/*.ivm.bundle.js

View file

@ -1,22 +1,25 @@
// These class names will never trigger a callback if clicked, no matter what
const ignoredClasses = [
".download-js-link",
".spectrum-Menu",
".date-time-popover",
]
// These class names will only trigger a callback when clicked if the registered
// component is not nested inside them. For example, clicking inside a modal
// will not close the modal, or clicking inside a popover will not close the
// popover.
const conditionallyIgnoredClasses = [
".spectrum-Underlay",
".drawer-wrapper",
".spectrum-Popover",
]
let clickHandlers = []
let candidateTarget
/**
* Handle a body click event
*/
// Processes a "click outside" event and invokes callbacks if our source element
// is valid
const handleClick = event => {
// Treat right clicks (context menu events) as normal clicks
const eventType = event.type === "contextmenu" ? "click" : event.type
// Ignore click if this is an ignored class
if (event.target.closest('[data-ignore-click-outside="true"]')) {
return
@ -29,11 +32,6 @@ const handleClick = event => {
// Process handlers
clickHandlers.forEach(handler => {
// Check that we're the right kind of click event
if (handler.allowedType && eventType !== handler.allowedType) {
return
}
// Check that the click isn't inside the target
if (handler.element.contains(event.target)) {
return
@ -51,17 +49,43 @@ const handleClick = event => {
handler.callback?.(event)
})
}
document.documentElement.addEventListener("click", handleClick, true)
document.documentElement.addEventListener("mousedown", handleClick, true)
document.documentElement.addEventListener("contextmenu", handleClick, true)
// On mouse up we only trigger a "click outside" callback if we targetted the
// same element that we did on mouse down. This fixes all sorts of issues where
// we get annoying callbacks firing when we drag to select text.
const handleMouseUp = e => {
if (candidateTarget === e.target) {
handleClick(e)
}
candidateTarget = null
}
// On mouse down we store which element was targetted for comparison later
const handleMouseDown = e => {
// Only handle the primary mouse button here.
// We handle context menu (right click) events in another handler.
if (e.button !== 0) {
return
}
candidateTarget = e.target
// Clear any previous listeners in case of multiple down events, and register
// a single mouse up listener
document.removeEventListener("mouseup", handleMouseUp)
document.addEventListener("mouseup", handleMouseUp, true)
}
// Global singleton listeners for our events
document.addEventListener("mousedown", handleMouseDown)
document.addEventListener("contextmenu", handleClick)
/**
* Adds or updates a click handler
*/
const updateHandler = (id, element, anchor, callback, allowedType) => {
const updateHandler = (id, element, anchor, callback) => {
let existingHandler = clickHandlers.find(x => x.id === id)
if (!existingHandler) {
clickHandlers.push({ id, element, anchor, callback, allowedType })
clickHandlers.push({ id, element, anchor, callback })
} else {
existingHandler.callback = callback
}
@ -88,8 +112,7 @@ export default (element, opts) => {
const callback =
newOpts?.callback || (typeof newOpts === "function" ? newOpts : null)
const anchor = newOpts?.anchor || element
const allowedType = newOpts?.allowedType || "click"
updateHandler(id, element, anchor, callback, allowedType)
updateHandler(id, element, anchor, callback)
}
update(opts)
return {

View file

@ -1,8 +1,10 @@
<script>
import { Body, Label, Input } from "@budibase/bbui"
import { Body, Label } from "@budibase/bbui"
import { onMount } from "svelte"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
export let parameters
export let bindings
onMount(() => {
if (!parameters.confirm) {
@ -15,11 +17,18 @@
<Body size="S">Enter the message you wish to display to the user.</Body>
<div class="params">
<Label small>Title</Label>
<Input placeholder="Prompt User" bind:value={parameters.customTitleText} />
<DrawerBindableInput
placeholder="Title"
value={parameters.customTitleText}
on:change={e => (parameters.customTitleText = e.detail)}
{bindings}
/>
<Label small>Message</Label>
<Input
<DrawerBindableInput
placeholder="Are you sure you want to continue?"
bind:value={parameters.confirmText}
value={parameters.confirmText}
on:change={e => (parameters.confirmText = e.detail)}
{bindings}
/>
</div>
</div>

View file

@ -21,26 +21,24 @@
const currentStep = derived(multiStepStore, state => state.currentStep)
const componentType = "@budibase/standard-components/multistepformblockstep"
setContext("multi-step-form-block", multiStepStore)
let cachedValue
let cachedInstance = {}
$: if (!isEqual(cachedValue, value)) {
cachedValue = value
}
$: if (!isEqual(componentInstance, cachedInstance)) {
cachedInstance = componentInstance
}
setContext("multi-step-form-block", multiStepStore)
$: stepCount = cachedValue?.length || 0
$: updateStore(stepCount)
$: dataSource = getDatasourceForProvider($selectedScreen, cachedInstance)
$: emitCurrentStep($currentStep)
$: stepLabel = getStepLabel($multiStepStore)
$: stepDef = getDefinition(stepLabel)
$: stepSettings = cachedValue?.[$currentStep] || {}
$: savedInstance = cachedValue?.[$currentStep] || {}
$: defaults = Utils.buildMultiStepFormBlockDefaultProps({
_id: cachedInstance._id,
stepCount: $multiStepStore.stepCount,
@ -48,14 +46,16 @@
actionType: cachedInstance.actionType,
dataSource: cachedInstance.dataSource,
})
// For backwards compatibility we need to sometimes manually set base
// properties like _id and _component as we didn't used to save these
$: stepInstance = {
_id: Helpers.uuid(),
_component: componentType,
_id: savedInstance._id || Helpers.uuid(),
_component: savedInstance._component || componentType,
_instanceName: `Step ${currentStep + 1}`,
title: stepSettings.title ?? defaults?.title,
buttons: stepSettings.buttons || defaults?.buttons,
fields: stepSettings.fields,
desc: stepSettings.desc,
title: savedInstance.title ?? defaults?.title,
buttons: savedInstance.buttons || defaults?.buttons,
fields: savedInstance.fields,
desc: savedInstance.desc,
// Needed for field configuration
dataSource,
@ -92,7 +92,8 @@
}
const addStep = () => {
value = value.toSpliced($currentStep + 1, 0, {})
const newInstance = componentStore.createInstance(componentType)
value = value.toSpliced($currentStep + 1, 0, newInstance)
dispatch("change", value)
multiStepStore.update(state => ({
...state,

View file

@ -1,6 +1,6 @@
<script>
import { createEventDispatcher, getContext } from "svelte"
import { ActionButton } from "@budibase/bbui"
import { ActionButton, AbsTooltip } from "@budibase/bbui"
const multiStepStore = getContext("multi-step-form-block")
const dispatch = createEventDispatcher()
@ -28,45 +28,49 @@
</div>
{:else}
<div class="step-actions">
<ActionButton
size="S"
secondary
icon="ChevronLeft"
disabled={currentStep === 0}
on:click={() => {
stepAction("previousStep")
}}
tooltip={"Previous step"}
/>
<ActionButton
size="S"
secondary
disabled={currentStep === stepCount - 1}
icon="ChevronRight"
on:click={() => {
stepAction("nextStep")
}}
tooltip={"Next step"}
/>
<ActionButton
size="S"
secondary
icon="Close"
disabled={stepCount === 1}
on:click={() => {
stepAction("removeStep")
}}
tooltip={"Remove step"}
/>
<ActionButton
size="S"
secondary
icon="MultipleAdd"
on:click={() => {
stepAction("addStep")
}}
tooltip={"Add step"}
/>
<AbsTooltip text="Previous step" noWrap>
<ActionButton
size="S"
secondary
icon="ChevronLeft"
disabled={currentStep === 0}
on:click={() => {
stepAction("previousStep")
}}
/>
</AbsTooltip>
<AbsTooltip text="Next step" noWrap>
<ActionButton
size="S"
secondary
disabled={currentStep === stepCount - 1}
icon="ChevronRight"
on:click={() => {
stepAction("nextStep")
}}
/>
</AbsTooltip>
<AbsTooltip text="Remove step" noWrap>
<ActionButton
size="S"
secondary
icon="Close"
disabled={stepCount === 1}
on:click={() => {
stepAction("removeStep")
}}
/>
</AbsTooltip>
<AbsTooltip text="Add step" noWrap>
<ActionButton
size="S"
secondary
icon="MultipleAdd"
on:click={() => {
stepAction("addStep")
}}
/>
</AbsTooltip>
</div>
{/if}

View file

@ -75,6 +75,7 @@ const toDraggableListFormat = (gridFormatColumns, createComponent, schema) => {
return createComponent(
"@budibase/standard-components/labelfield",
{
_id: column.field,
_instanceName: column.field,
active: column.active,
field: column.field,

View file

@ -65,6 +65,7 @@ describe("getColumns", () => {
it("returns the selected and unselected fields in the modern format, respecting the original order", ctx => {
expect(ctx.columns.sortable).toEqual([
{
_id: "three",
_instanceName: "three",
active: true,
columnType: "foo",
@ -73,6 +74,7 @@ describe("getColumns", () => {
label: "three label",
},
{
_id: "two",
_instanceName: "two",
active: true,
columnType: "foo",
@ -81,6 +83,7 @@ describe("getColumns", () => {
label: "two label",
},
{
_id: "one",
_instanceName: "one",
active: false,
columnType: "foo",
@ -91,6 +94,7 @@ describe("getColumns", () => {
])
expect(ctx.columns.primary).toEqual({
_id: "four",
_instanceName: "four",
active: true,
columnType: "foo",
@ -115,6 +119,7 @@ describe("getColumns", () => {
it("returns all columns, with non-hidden columns automatically selected", ctx => {
expect(ctx.columns.sortable).toEqual([
{
_id: "two",
_instanceName: "two",
active: true,
columnType: "foo",
@ -123,6 +128,7 @@ describe("getColumns", () => {
label: "two",
},
{
_id: "three",
_instanceName: "three",
active: true,
columnType: "foo",
@ -131,6 +137,7 @@ describe("getColumns", () => {
label: "three",
},
{
_id: "one",
_instanceName: "one",
active: false,
columnType: "foo",
@ -141,6 +148,7 @@ describe("getColumns", () => {
])
expect(ctx.columns.primary).toEqual({
_id: "four",
_instanceName: "four",
active: true,
columnType: "foo",
@ -173,6 +181,7 @@ describe("getColumns", () => {
it("returns all columns, including those missing from the initial data", ctx => {
expect(ctx.columns.sortable).toEqual([
{
_id: "three",
_instanceName: "three",
active: true,
columnType: "foo",
@ -181,6 +190,7 @@ describe("getColumns", () => {
label: "three label",
},
{
_id: "two",
_instanceName: "two",
active: false,
columnType: "foo",
@ -189,6 +199,7 @@ describe("getColumns", () => {
label: "two",
},
{
_id: "one",
_instanceName: "one",
active: false,
columnType: "foo",
@ -199,6 +210,7 @@ describe("getColumns", () => {
])
expect(ctx.columns.primary).toEqual({
_id: "four",
_instanceName: "four",
active: true,
columnType: "foo",
@ -228,6 +240,7 @@ describe("getColumns", () => {
it("returns all valid columns, excluding those that aren't valid for the schema", ctx => {
expect(ctx.columns.sortable).toEqual([
{
_id: "three",
_instanceName: "three",
active: true,
columnType: "foo",
@ -236,6 +249,7 @@ describe("getColumns", () => {
label: "three label",
},
{
_id: "two",
_instanceName: "two",
active: false,
columnType: "foo",
@ -244,6 +258,7 @@ describe("getColumns", () => {
label: "two",
},
{
_id: "one",
_instanceName: "one",
active: false,
columnType: "foo",
@ -254,6 +269,7 @@ describe("getColumns", () => {
])
expect(ctx.columns.primary).toEqual({
_id: "four",
_instanceName: "four",
active: true,
columnType: "foo",
@ -318,6 +334,7 @@ describe("getColumns", () => {
beforeEach(ctx => {
ctx.updateSortable([
{
_id: "three",
_instanceName: "three",
active: true,
columnType: "foo",
@ -326,6 +343,7 @@ describe("getColumns", () => {
label: "three",
},
{
_id: "one",
_instanceName: "one",
active: true,
columnType: "foo",
@ -334,6 +352,7 @@ describe("getColumns", () => {
label: "one",
},
{
_id: "two",
_instanceName: "two",
active: false,
columnType: "foo",

View file

@ -1106,50 +1106,51 @@ export const getAllStateVariables = () => {
getAllAssets().forEach(asset => {
findAllMatchingComponents(asset.props, component => {
const settings = componentStore.getComponentSettings(component._component)
const nestedTypes = [
"buttonConfiguration",
"fieldConfiguration",
"stepConfiguration",
]
// Extracts all event settings from a component instance.
// Recurses into nested types to find all event-like settings at any
// depth.
const parseEventSettings = (settings, comp) => {
if (!settings?.length) {
return
}
// Extract top level event settings
settings
.filter(setting => setting.type === "event")
.forEach(setting => {
eventSettings.push(comp[setting.key])
})
}
const parseComponentSettings = (settings, component) => {
// Parse the nested button configurations
// Recurse into any nested instance types
settings
.filter(setting => setting.type === "buttonConfiguration")
.filter(setting => nestedTypes.includes(setting.type))
.forEach(setting => {
const buttonConfig = component[setting.key]
const instances = comp[setting.key]
if (Array.isArray(instances) && instances.length) {
instances.forEach(instance => {
let type = instance?._component
if (Array.isArray(buttonConfig)) {
buttonConfig.forEach(button => {
const nestedSettings = componentStore.getComponentSettings(
button._component
)
parseEventSettings(nestedSettings, button)
// Backwards compatibility for multi-step from blocks which
// didn't set a proper component type previously.
if (setting.type === "stepConfiguration" && !type) {
type = "@budibase/standard-components/multistepformblockstep"
}
// Parsed nested component instances inside this setting
const nestedSettings = componentStore.getComponentSettings(type)
parseEventSettings(nestedSettings, instance)
})
}
})
parseEventSettings(settings, component)
}
// Parse the base component settings
parseComponentSettings(settings, component)
// Parse step configuration
const stepSetting = settings.find(
setting => setting.type === "stepConfiguration"
)
const steps = stepSetting ? component[stepSetting.key] : []
const stepDefinition = componentStore.getComponentSettings(
"@budibase/standard-components/multistepformblockstep"
)
steps?.forEach(step => {
parseComponentSettings(stepDefinition, step)
})
parseEventSettings(settings, component)
})
})

View file

@ -324,10 +324,7 @@
<div
id="side-panel-container"
class:open={$sidePanelStore.open}
use:clickOutside={{
callback: autoCloseSidePanel ? sidePanelStore.actions.close : null,
allowedType: "mousedown",
}}
use:clickOutside={autoCloseSidePanel ? sidePanelStore.actions.close : null}
class:builder={$builderStore.inBuilder}
>
<div class="side-panel-header">

View file

@ -542,16 +542,22 @@ export const enrichButtonActions = (actions, context) => {
// then execute the rest of the actions in the chain
const result = await callback()
if (result !== false) {
// Generate a new total context to pass into the next enrichment
// Generate a new total context for the next enrichment
buttonContext.push(result)
const newContext = { ...context, actions: buttonContext }
// Enrich and call the next button action if there is more than one action remaining
// Enrich and call the next button action if there is more
// than one action remaining
const next = enrichButtonActions(
actions.slice(i + 1),
newContext
)
resolve(typeof next === "function" ? await next() : true)
if (typeof next === "function") {
// Pass the event context back into the new action chain
resolve(await next(eventContext))
} else {
resolve(true)
}
} else {
resolve(false)
}

View file

@ -124,6 +124,7 @@
const fieldSchema = schemaFields.find(x => x.name === filter.field)
filter.type = fieldSchema?.type
filter.subtype = fieldSchema?.subtype
filter.formulaType = fieldSchema?.formulaType
// Update external type based on field
filter.externalType = getSchema(filter)?.externalType

View file

@ -121,8 +121,14 @@
const onContextMenu = e => {
e.preventDefault()
ui.actions.blur()
open = !open
// The timeout allows time for clickoutside to close other open popvers
// before we show this one. Without the timeout, this popover closes again
// before it's even visible as clickoutside closes it.
setTimeout(() => {
ui.actions.blur()
open = !open
}, 10)
}
const sortAscending = () => {

View file

@ -18,7 +18,7 @@
focusedCellAPI,
focusedRowId,
notifications,
isDatasourcePlus,
hasBudibaseIdentifiers,
} = getContext("grid")
let anchor
@ -82,7 +82,7 @@
</MenuItem>
<MenuItem
icon="Copy"
disabled={isNewRow || !$focusedRow?._id || !$isDatasourcePlus}
disabled={isNewRow || !$focusedRow?._id || !$hasBudibaseIdentifiers}
on:click={() => copyToClipboard($focusedRow?._id)}
on:click={menu.actions.close}
>
@ -90,7 +90,7 @@
</MenuItem>
<MenuItem
icon="Copy"
disabled={isNewRow || !$focusedRow?._rev}
disabled={isNewRow || !$focusedRow?._rev || !$hasBudibaseIdentifiers}
on:click={() => copyToClipboard($focusedRow?._rev)}
on:click={menu.actions.close}
>

View file

@ -75,14 +75,18 @@ export const deriveStores = context => {
}
)
const isDatasourcePlus = derived(datasource, $datasource => {
return ["table", "viewV2"].includes($datasource?.type)
const hasBudibaseIdentifiers = derived(datasource, $datasource => {
let type = $datasource?.type
if (type === "provider") {
type = $datasource.value?.datasource?.type
}
return ["table", "viewV2", "link"].includes(type)
})
return {
schema,
enrichedSchema,
isDatasourcePlus,
hasBudibaseIdentifiers,
}
}

View file

@ -17,6 +17,7 @@ export const createActions = context => {
const open = (cellId, e) => {
e.preventDefault()
e.stopPropagation()
// Get DOM node for grid data wrapper to compute relative position to
const gridNode = document.getElementById(gridID)

View file

@ -83,7 +83,7 @@ export const createActions = context => {
error,
notifications,
fetch,
isDatasourcePlus,
hasBudibaseIdentifiers,
refreshing,
} = context
const instanceLoaded = writable(false)
@ -196,9 +196,16 @@ export const createActions = context => {
// Handles validation errors from the rows API and updates local validation
// state, storing error messages against relevant cells
const handleValidationError = (rowId, error) => {
let errorString
if (typeof error === "string") {
errorString = error
} else if (typeof error?.message === "string") {
errorString = error.message
}
// If the server doesn't reply with a valid error, assume that the source
// of the error is the focused cell's column
if (!error?.json?.validationErrors && error?.message) {
if (!error?.json?.validationErrors && errorString) {
const focusedColumn = get(focusedCellId)?.split("-")[1]
if (focusedColumn) {
error = {
@ -261,7 +268,7 @@ export const createActions = context => {
focusedCellId.set(`${rowId}-${erroredColumns[0]}`)
}
} else {
get(notifications).error(error?.message || "An unknown error occurred")
get(notifications).error(errorString || "An unknown error occurred")
}
}
@ -458,14 +465,14 @@ export const createActions = context => {
}
let rowsToAppend = []
let newRow
const $isDatasourcePlus = get(isDatasourcePlus)
const $hasBudibaseIdentifiers = get(hasBudibaseIdentifiers)
for (let i = 0; i < newRows.length; i++) {
newRow = newRows[i]
// Ensure we have a unique _id.
// This means generating one for non DS+, overwriting any that may already
// exist as we cannot allow duplicates.
if (!$isDatasourcePlus) {
if (!$hasBudibaseIdentifiers) {
newRow._id = Helpers.uuid()
}
@ -510,7 +517,7 @@ export const createActions = context => {
const cleanRow = row => {
let clone = { ...row }
delete clone.__idx
if (!get(isDatasourcePlus)) {
if (!get(hasBudibaseIdentifiers)) {
delete clone._id
}
return clone