Merge branch 'develop' of github.com:Budibase/budibase into backmerge-20231011

This commit is contained in:
mike12345567 2023-10-11 14:02:48 +01:00
commit d14a773925
24 changed files with 235 additions and 175 deletions

View File

@ -14,7 +14,7 @@ jobs:
- uses: passeidireto/trigger-external-workflow-action@main
env:
PAYLOAD_BRANCH: ${{ github.head_ref }}
PAYLOAD_PR_NUMBER: ${{ github.ref }}
PAYLOAD_PR_NUMBER: ${{ github.event.pull_request.number }}
with:
repository: budibase/budibase-deploys
event: featurebranch-qa-close

View File

@ -138,6 +138,8 @@ To develop the Budibase platform you'll need [Docker](https://www.docker.com/) a
`yarn setup` will check that all necessary components are installed and setup the repo for usage.
If you have access to the `@budibase/pro` submodule then please follow the Pro section of this guide before running the above command.
##### Manual method
The following commands can be executed to manually get Budibase up and running (assuming Docker/Docker Compose has been installed).
@ -146,6 +148,8 @@ The following commands can be executed to manually get Budibase up and running (
`yarn build` will build all budibase packages.
If you have access to the `@budibase/pro` submodule then please follow the Pro section of this guide before running the above commands.
#### 4. Running
To run the budibase server and builder in dev mode (i.e. with live reloading):

View File

@ -6,6 +6,7 @@ import {
AutomationStepIdArray,
AutomationIOType,
AutomationCustomIOType,
DatasourceFeature,
} from "@budibase/types"
import joi from "joi"
@ -67,9 +68,27 @@ function validateDatasource(schema: any) {
version: joi.string().optional(),
schema: joi.object({
docs: joi.string(),
plus: joi.boolean().optional(),
isSQL: joi.boolean().optional(),
auth: joi
.object({
type: joi.string().required(),
})
.optional(),
features: joi
.object(
Object.fromEntries(
Object.values(DatasourceFeature).map(key => [
key,
joi.boolean().optional(),
])
)
)
.optional(),
relationships: joi.boolean().optional(),
description: joi.string().required(),
friendlyName: joi.string().required(),
type: joi.string().allow(...DATASOURCE_TYPES),
description: joi.string().required(),
datasource: joi.object().pattern(joi.string(), fieldValidator).required(),
query: joi
.object()

View File

@ -13,6 +13,8 @@
import { Helpers } from "@budibase/bbui"
import { RelationshipErrorChecker } from "./relationshipErrors"
import { onMount } from "svelte"
import RelationshipSelector from "components/common/RelationshipSelector.svelte"
import { PrettyRelationshipDefinitions } from "constants/backend"
export let save
export let datasource
@ -22,16 +24,21 @@
export let selectedFromTable
export let close
const relationshipTypes = [
{
label: "One to Many",
value: RelationshipType.MANY_TO_ONE,
let relationshipMap = {
[RelationshipType.MANY_TO_MANY]: {
part1: PrettyRelationshipDefinitions.MANY,
part2: PrettyRelationshipDefinitions.MANY,
},
{
label: "Many to Many",
value: RelationshipType.MANY_TO_MANY,
[RelationshipType.MANY_TO_ONE]: {
part1: PrettyRelationshipDefinitions.ONE,
part2: PrettyRelationshipDefinitions.MANY,
},
]
}
let relationshipOpts1 = Object.values(PrettyRelationshipDefinitions)
let relationshipOpts2 = Object.values(PrettyRelationshipDefinitions)
let relationshipPart1 = PrettyRelationshipDefinitions.MANY
let relationshipPart2 = PrettyRelationshipDefinitions.ONE
let originalFromColumnName = toRelationship.name,
originalToColumnName = fromRelationship.name
@ -49,14 +56,32 @@
)
let errors = {}
let fromPrimary, fromForeign, fromColumn, toColumn
let fromId, toId, throughId, throughToKey, throughFromKey
let throughId, throughToKey, throughFromKey
let isManyToMany, isManyToOne, relationshipType
let hasValidated = false
$: fromId = null
$: toId = null
$: tableOptions = plusTables.map(table => ({
label: table.name,
value: table._id,
name: table.name,
_id: table._id,
}))
$: {
// Determine the relationship type based on the selected values of both parts
relationshipType = Object.entries(relationshipMap).find(
([_, parts]) =>
parts.part1 === relationshipPart1 && parts.part2 === relationshipPart2
)?.[0]
changed(() => {
hasValidated = false
})
}
$: valid =
getErrorCount(errors) === 0 && allRequiredAttributesSet(relationshipType)
$: isManyToMany = relationshipType === RelationshipType.MANY_TO_MANY
@ -338,33 +363,34 @@
onConfirm={saveRelationship}
disabled={!valid}
>
<Select
label="Relationship type"
options={relationshipTypes}
bind:value={relationshipType}
bind:error={errors.relationshipType}
on:change={() =>
changed(() => {
hasValidated = false
})}
/>
<div class="headings">
<Detail>Tables</Detail>
</div>
{#if !selectedFromTable}
<Select
label="Select from table"
options={tableOptions}
bind:value={fromId}
bind:error={errors.fromTable}
on:change={e =>
<RelationshipSelector
bind:relationshipPart1
bind:relationshipPart2
bind:relationshipTableIdPrimary={fromId}
bind:relationshipTableIdSecondary={toId}
{relationshipOpts1}
{relationshipOpts2}
{tableOptions}
{errors}
primaryDisabled={selectedFromTable}
primaryTableChanged={e =>
changed(() => {
const table = plusTables.find(tbl => tbl._id === e.detail)
fromColumn = table?.name || ""
fromPrimary = table?.primary?.[0]
})}
secondaryTableChanged={e =>
changed(() => {
const table = plusTables.find(tbl => tbl._id === e.detail)
toColumn = table.name || ""
fromForeign = null
})}
/>
{/if}
{#if isManyToOne && fromId}
<Select
label={`Primary Key (${getTable(fromId).name})`}
@ -374,18 +400,6 @@
on:change={changed}
/>
{/if}
<Select
label={"Select to table"}
options={tableOptions}
bind:value={toId}
bind:error={errors.toTable}
on:change={e =>
changed(() => {
const table = plusTables.find(tbl => tbl._id === e.detail)
toColumn = table.name || ""
fromForeign = null
})}
/>
{#if isManyToMany}
<Select
label={"Through"}

View File

@ -6,11 +6,14 @@
export let relationshipTableIdPrimary
export let relationshipTableIdSecondary
export let editableColumn
export let linkEditDisabled
export let linkEditDisabled = false
export let tableOptions
export let errors
export let relationshipOpts1
export let relationshipOpts2
export let primaryTableChanged
export let secondaryTableChanged
export let primaryDisabled = true
</script>
<div class="relationship-container">
@ -19,16 +22,19 @@
disabled={linkEditDisabled}
bind:value={relationshipPart1}
options={relationshipOpts1}
bind:error={errors.relationshipType}
/>
</div>
<div class="relationship-label">in</div>
<div class="relationship-part">
<Select
disabled
disabled={primaryDisabled}
options={tableOptions}
getOptionLabel={table => table.name}
getOptionValue={table => table._id}
bind:value={relationshipTableIdPrimary}
on:change={primaryTableChanged}
bind:error={errors.fromTable}
/>
</div>
</div>
@ -46,20 +52,24 @@
<Select
disabled={linkEditDisabled}
bind:value={relationshipTableIdSecondary}
bind:error={errors.toTable}
options={tableOptions.filter(
table => table._id !== relationshipTableIdPrimary
)}
getOptionLabel={table => table.name}
getOptionValue={table => table._id}
on:change={secondaryTableChanged}
/>
</div>
</div>
{#if editableColumn}
<Input
disabled={linkEditDisabled}
label={`Column name in other table`}
bind:value={editableColumn.fieldName}
error={errors.relatedName}
/>
{/if}
<style>
.relationship-container {

View File

@ -1,91 +0,0 @@
<script>
import { Button, ActionButton, Drawer } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import ColumnDrawer from "./ColumnEditor/ColumnDrawer.svelte"
import { cloneDeep } from "lodash/fp"
import {
getDatasourceForProvider,
getSchemaForDatasource,
} from "builderStore/dataBinding"
import { currentAsset } from "builderStore"
import { getFields } from "helpers/searchFields"
export let componentInstance
export let value = []
export let allowCellEditing = true
export let subject = "Table"
const dispatch = createEventDispatcher()
let drawer
let boundValue
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
$: schema = getSchema($currentAsset, datasource)
$: options = allowCellEditing
? Object.keys(schema || {})
: enrichedSchemaFields?.map(field => field.name)
$: sanitisedValue = getValidColumns(value, options)
$: updateBoundValue(sanitisedValue)
$: enrichedSchemaFields = getFields(Object.values(schema || {}), {
allowLinks: true,
})
const getSchema = (asset, datasource) => {
const schema = getSchemaForDatasource(asset, datasource).schema
// Don't show ID and rev in tables
if (schema) {
delete schema._id
delete schema._rev
}
return schema
}
const updateBoundValue = value => {
boundValue = cloneDeep(value)
}
const getValidColumns = (columns, options) => {
if (!Array.isArray(columns) || !columns.length) {
return []
}
// We need to account for legacy configs which would just be an array
// of strings
if (typeof columns[0] === "string") {
columns = columns.map(col => ({
name: col,
displayName: col,
}))
}
return columns.filter(column => {
return options.includes(column.name)
})
}
const open = () => {
updateBoundValue(sanitisedValue)
drawer.show()
}
const save = () => {
dispatch("change", getValidColumns(boundValue, options))
drawer.hide()
}
</script>
<ActionButton on:click={open}>Configure columns</ActionButton>
<Drawer bind:this={drawer} title="{subject} Columns">
<svelte:fragment slot="description">
Configure the columns in your {subject.toLowerCase()}.
</svelte:fragment>
<Button cta slot="buttons" on:click={save}>Save</Button>
<ColumnDrawer
slot="body"
bind:columns={boundValue}
{options}
{schema}
{allowCellEditing}
/>
</Drawer>

View File

@ -20,7 +20,9 @@
const getSortableFields = schema => {
return Object.entries(schema || {})
.filter(entry => !UNSORTABLE_TYPES.includes(entry[1].type))
.filter(
entry => !UNSORTABLE_TYPES.includes(entry[1].type) && entry[1].sortable
)
.map(entry => entry[0])
}

View File

@ -62,7 +62,14 @@
</div>
{/if}
<div class="truncate">
<Body>{getSubtitle(datasource)}</Body>
<Body>
{@const subtitle = getSubtitle(datasource)}
{#if subtitle}
{subtitle}
{:else}
{Object.values(datasource.config).join(" / ")}
{/if}
</Body>
</div>
</div>
<div class="right">

View File

@ -136,6 +136,7 @@ export function createDatasourcesStore() {
config,
name: `${integration.friendlyName}${nameModifier}`,
plus: integration.plus && integration.name !== IntegrationTypes.REST,
isSQL: integration.isSQL,
}
if (await checkDatasourceValidity(integration, datasource)) {

View File

@ -5598,6 +5598,21 @@
}
]
},
{
"type": "event",
"label": "On row click",
"key": "onRowClick",
"context": [
{
"label": "Clicked row",
"key": "row"
}
],
"dependsOn": {
"setting": "allowEditRows",
"value": false
}
},
{
"type": "boolean",
"label": "Add rows",

View File

@ -14,12 +14,14 @@
export let initialSortOrder = null
export let fixedRowHeight = null
export let columns = null
export let onRowClick = null
const component = getContext("component")
const { styleable, API, builderStore, notificationStore } = getContext("sdk")
$: columnWhitelist = columns?.map(col => col.name)
$: schemaOverrides = getSchemaOverrides(columns)
$: handleRowClick = allowEditRows ? undefined : onRowClick
const getSchemaOverrides = columns => {
let overrides = {}
@ -56,6 +58,7 @@
showControls={false}
notifySuccess={notificationStore.actions.success}
notifyError={notificationStore.actions.error}
on:rowclick={e => handleRowClick?.({ row: e.detail })}
/>
</div>

View File

@ -17,13 +17,24 @@
const { config, dispatch, selectedRows } = getContext("grid")
const svelteDispatch = createEventDispatcher()
const select = () => {
const select = e => {
e.stopPropagation()
svelteDispatch("select")
const id = row?._id
if (id) {
selectedRows.actions.toggleRow(id)
}
}
const bulkDelete = e => {
e.stopPropagation()
dispatch("request-bulk-delete")
}
const expand = e => {
e.stopPropagation()
svelteDispatch("expand")
}
</script>
<GridCell
@ -56,7 +67,7 @@
{/if}
{/if}
{#if rowSelected && $config.canDeleteRows}
<div class="delete" on:click={() => dispatch("request-bulk-delete")}>
<div class="delete" on:click={bulkDelete}>
<Icon
name="Delete"
size="S"
@ -65,12 +76,7 @@
</div>
{:else}
<div class="expand" class:visible={$config.canExpandRows && expandable}>
<Icon
size="S"
name="Maximize"
hoverable
on:click={() => svelteDispatch("expand")}
/>
<Icon size="S" name="Maximize" hoverable on:click={expand} />
</div>
{/if}
</div>

View File

@ -35,7 +35,7 @@
</script>
<div bind:this={body} class="grid-body">
<GridScrollWrapper scrollHorizontally scrollVertically wheelInteractive>
<GridScrollWrapper scrollHorizontally scrollVertically attachHandlers>
{#each $renderedRows as row, idx}
<GridRow
{row}

View File

@ -17,6 +17,7 @@
columnHorizontalInversionIndex,
contentLines,
isDragging,
dispatch,
} = getContext("grid")
$: rowSelected = !!$selectedRows[row._id]
@ -30,6 +31,7 @@
on:focus
on:mouseenter={$isDragging ? null : () => ($hoveredRowId = row._id)}
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
on:click={() => dispatch("rowclick", row)}
>
{#each $renderedColumns as column, columnIdx (column.name)}
{@const cellId = `${row._id}-${column.name}`}

View File

@ -17,7 +17,11 @@
export let scrollVertically = false
export let scrollHorizontally = false
export let wheelInteractive = false
export let attachHandlers = false
// Used for tracking touch events
let initialTouchX
let initialTouchY
$: style = generateStyle($scroll, $rowHeight, $hiddenColumnsWidth)
@ -27,17 +31,47 @@
return `transform: translate3d(${offsetX}px, ${offsetY}px, 0);`
}
// Handles a wheel even and updates the scroll offsets
// Handles a mouse wheel event and updates scroll state
const handleWheel = e => {
e.preventDefault()
debouncedHandleWheel(e.deltaX, e.deltaY, e.clientY)
updateScroll(e.deltaX, e.deltaY, e.clientY)
// If a context menu was visible, hide it
if ($menu.visible) {
menu.actions.close()
}
}
const debouncedHandleWheel = domDebounce((deltaX, deltaY, clientY) => {
// Handles touch start events
const handleTouchStart = e => {
if (!e.touches?.[0]) return
initialTouchX = e.touches[0].clientX
initialTouchY = e.touches[0].clientY
}
// Handles touch move events and updates scroll state
const handleTouchMove = e => {
if (!e.touches?.[0]) return
e.preventDefault()
// Compute delta from previous event, and update scroll
const deltaX = initialTouchX - e.touches[0].clientX
const deltaY = initialTouchY - e.touches[0].clientY
updateScroll(deltaX, deltaY)
// Store position to reference in next event
initialTouchX = e.touches[0].clientX
initialTouchY = e.touches[0].clientY
// If a context menu was visible, hide it
if ($menu.visible) {
menu.actions.close()
}
}
// Updates the scroll offset by a certain delta, and ensure scrolling
// stays within sensible bounds. Debounced for performance.
const updateScroll = domDebounce((deltaX, deltaY, clientY) => {
const { top, left } = $scroll
// Calculate new scroll top
@ -55,15 +89,19 @@
})
// Hover row under cursor
if (clientY != null) {
const y = clientY - $bounds.top + (newScrollTop % $rowHeight)
const hoveredRow = $renderedRows[Math.floor(y / $rowHeight)]
hoveredRowId.set(hoveredRow?._id)
}
})
</script>
<div
class="outer"
on:wheel={wheelInteractive ? handleWheel : null}
on:wheel={attachHandlers ? handleWheel : null}
on:touchstart={attachHandlers ? handleTouchStart : null}
on:touchmove={attachHandlers ? handleTouchMove : null}
on:click|self={() => ($focusedCellId = null)}
>
<div {style} class="inner">

View File

@ -205,7 +205,7 @@
{/if}
</div>
<div class="normal-columns" transition:fade|local={{ duration: 130 }}>
<GridScrollWrapper scrollHorizontally wheelInteractive>
<GridScrollWrapper scrollHorizontally attachHandlers>
<div class="row">
{#each $renderedColumns as column, columnIdx}
{@const cellId = `new-${column.name}`}

View File

@ -64,7 +64,7 @@
</div>
<div class="content" on:mouseleave={() => ($hoveredRowId = null)}>
<GridScrollWrapper scrollVertically wheelInteractive>
<GridScrollWrapper scrollVertically attachHandlers>
{#each $renderedRows as row, idx}
{@const rowSelected = !!$selectedRows[row._id]}
{@const rowHovered = $hoveredRowId === row._id}
@ -74,6 +74,7 @@
class="row"
on:mouseenter={$isDragging ? null : () => ($hoveredRowId = row._id)}
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
on:click={() => dispatch("rowclick", row)}
>
<GutterCell {row} {rowFocused} {rowHovered} {rowSelected} />
{#if $stickyColumn}

View File

@ -53,18 +53,27 @@
}
}
const getLocation = e => {
return {
y: e.touches?.[0]?.clientY ?? e.clientY,
x: e.touches?.[0]?.clientX ?? e.clientX,
}
}
// V scrollbar drag handlers
const startVDragging = e => {
e.preventDefault()
initialMouse = e.clientY
initialMouse = getLocation(e).y
initialScroll = $scrollTop
document.addEventListener("mousemove", moveVDragging)
document.addEventListener("touchmove", moveVDragging)
document.addEventListener("mouseup", stopVDragging)
document.addEventListener("touchend", stopVDragging)
isDraggingV = true
closeMenu()
}
const moveVDragging = domDebounce(e => {
const delta = e.clientY - initialMouse
const delta = getLocation(e).y - initialMouse
const weight = delta / availHeight
const newScrollTop = initialScroll + weight * $maxScrollTop
scroll.update(state => ({
@ -74,22 +83,26 @@
})
const stopVDragging = () => {
document.removeEventListener("mousemove", moveVDragging)
document.removeEventListener("touchmove", moveVDragging)
document.removeEventListener("mouseup", stopVDragging)
document.removeEventListener("touchend", stopVDragging)
isDraggingV = false
}
// H scrollbar drag handlers
const startHDragging = e => {
e.preventDefault()
initialMouse = e.clientX
initialMouse = getLocation(e).x
initialScroll = $scrollLeft
document.addEventListener("mousemove", moveHDragging)
document.addEventListener("touchmove", moveHDragging)
document.addEventListener("mouseup", stopHDragging)
document.addEventListener("touchend", stopHDragging)
isDraggingH = true
closeMenu()
}
const moveHDragging = domDebounce(e => {
const delta = e.clientX - initialMouse
const delta = getLocation(e).x - initialMouse
const weight = delta / availWidth
const newScrollLeft = initialScroll + weight * $maxScrollLeft
scroll.update(state => ({
@ -99,7 +112,9 @@
})
const stopHDragging = () => {
document.removeEventListener("mousemove", moveHDragging)
document.removeEventListener("touchmove", moveHDragging)
document.removeEventListener("mouseup", stopHDragging)
document.removeEventListener("touchend", stopHDragging)
isDraggingH = false
}
</script>
@ -109,6 +124,7 @@
class="v-scrollbar"
style="--size:{ScrollBarSize}px; top:{barTop}px; height:{barHeight}px;"
on:mousedown={startVDragging}
on:touchstart={startVDragging}
class:dragging={isDraggingV}
/>
{/if}
@ -117,6 +133,7 @@
class="h-scrollbar"
style="--size:{ScrollBarSize}px; left:{barLeft}px; width:{barWidth}px;"
on:mousedown={startHDragging}
on:touchstart={startHDragging}
class:dragging={isDraggingH}
/>
{/if}

View File

@ -1,4 +1,5 @@
import { writable, get } from "svelte/store"
import { Helpers } from "@budibase/bbui"
export const createStores = () => {
const copiedCell = writable(null)
@ -12,7 +13,16 @@ export const createActions = context => {
const { copiedCell, focusedCellAPI } = context
const copy = () => {
copiedCell.set(get(focusedCellAPI)?.getValue())
const value = get(focusedCellAPI)?.getValue()
copiedCell.set(value)
// Also copy a stringified version to the clipboard
let stringified = ""
if (value != null && value !== "") {
// Only conditionally stringify to avoid redundant quotes around text
stringified = typeof value === "object" ? JSON.stringify(value) : value
}
Helpers.copyToClipboard(stringified)
}
const paste = () => {

View File

@ -14,5 +14,5 @@ export function isSQL(datasource: Datasource): boolean {
SourceName.MYSQL,
SourceName.ORACLE,
]
return SQL.indexOf(datasource.source) !== -1
return SQL.indexOf(datasource.source) !== -1 || datasource.isSQL === true
}

View File

@ -9,6 +9,7 @@ export interface Datasource extends Document {
// the config is defined by the schema
config?: Record<string, any>
plus?: boolean
isSQL?: boolean
entities?: {
[key: string]: Table
}

View File

@ -140,6 +140,7 @@ export interface DatasourceConfig {
export interface Integration {
docs: string
plus?: boolean
isSQL?: boolean
auth?: { type: string }
features?: Partial<Record<DatasourceFeature, boolean>>
relationships?: boolean

View File

@ -11,7 +11,7 @@ import { TestConfiguration } from "../../../../tests"
import { events } from "@budibase/backend-core"
// this test can 409 - retries reduce issues with this
jest.retryTimes(2)
jest.retryTimes(2, { logErrorsBeforeRetry: true })
jest.setTimeout(30000)
mocks.licenses.useScimIntegration()