Merge branch 'frontend-core-ts-2' of github.com:Budibase/budibase into ts-portal-admin-store

This commit is contained in:
Andrew Kingston 2024-12-13 09:14:13 +00:00
commit 5319fb34ac
No known key found for this signature in database
19 changed files with 446 additions and 192 deletions

View File

@ -4,8 +4,8 @@ import {
getContainerRuntimeClient,
} from "testcontainers"
import { ContainerInfo } from "dockerode"
import path from "path"
import lockfile from "proper-lockfile"
import * as path from "path"
import * as lockfile from "proper-lockfile"
import { execSync } from "child_process"
interface DockerContext {
@ -29,8 +29,8 @@ function getCurrentDockerContext(): DockerContext {
async function getBudibaseContainers() {
const client = await getContainerRuntimeClient()
const conatiners = await client.container.list()
return conatiners.filter(
const containers = await client.container.list()
return containers.filter(
container =>
container.Labels["com.budibase"] === "true" &&
container.Labels["org.testcontainers"] === "true"

View File

@ -59,11 +59,15 @@ export function isExternalTable(table: Table) {
}
export function buildExternalTableId(datasourceId: string, tableName: string) {
// encode spaces
if (tableName.includes(" ")) {
tableName = encodeURIComponent(tableName)
return `${datasourceId}${DOUBLE_SEPARATOR}${encodeURIComponent(tableName)}`
}
export function encodeTableId(tableId: string) {
if (isExternalTableID(tableId)) {
return encodeURIComponent(tableId)
} else {
return tableId
}
return `${datasourceId}${DOUBLE_SEPARATOR}${tableName}`
}
export function breakExternalTableId(tableId: string) {

View File

@ -7,19 +7,31 @@
onDestroy,
tick,
} from "svelte"
import { Utils } from "@budibase/frontend-core"
import Logo from "assets/bb-emblem.svg?raw"
import { Utils, memo } from "@budibase/frontend-core"
import { selectedAutomation, automationStore } from "stores/builder"
export function zoomIn() {
const scale = Number(Math.min($view.scale + 0.1, 1.5).toFixed(2))
// CSS classes that, on mouse down, will trigger the view drag behaviour
export let draggableClasses = []
export function toFocus() {
viewToFocusEle()
}
export async function zoomIn() {
const newScale = parseFloat(Math.min($view.scale + 0.1, 1.5).toFixed(2))
if ($view.scale === 1.5) return
view.update(state => ({
...state,
scale,
scale: newScale,
}))
}
export function zoomOut() {
const scale = Number(Math.max($view.scale - 0.1, 0).toFixed(2))
const scale = parseFloat(Math.max($view.scale - 0.1, 0.1).toFixed(2))
if ($view.scale === 0.1) return
view.update(state => ({
...state,
scale,
@ -32,12 +44,13 @@
w: contentDims.original.w,
h: contentDims.original.h,
}
dragOffset = []
contentPos.update(state => ({
...state,
x: 0,
y: 0,
}))
offsetX = 0
offsetY = 0
view.update(state => ({
...state,
scale: 1,
@ -48,18 +61,23 @@
const { width: wViewPort, height: hViewPort } =
viewPort.getBoundingClientRect()
const scaleTarget = Math.min(
wViewPort / contentDims.original.w,
hViewPort / contentDims.original.h
const scaleTarget = parseFloat(
Math.min(
wViewPort / contentDims.original.w,
hViewPort / contentDims.original.h
).toFixed(2)
)
// Smallest ratio determines which dimension needs squeezed
view.update(state => ({
...state,
scale: scaleTarget,
}))
// Skip behaviour if the scale target scale is the current scale
if ($view.scale !== scaleTarget) {
// Smallest ratio determines which dimension needs squeezed
view.update(state => ({
...state,
scale: parseFloat(scaleTarget.toFixed(2)),
}))
await tick()
await tick()
}
const adjustedY = (hViewPort - contentDims.original.h) / 2
@ -67,7 +85,10 @@
...state,
x: 0,
y: parseInt(0 + adjustedY),
scrollX: 0,
scrollY: 0,
}))
offsetY = parseInt(0 + adjustedY)
}
const dispatch = createEventDispatcher()
@ -79,7 +100,6 @@
dragSpot: null,
scale: 1,
dropzones: {},
//focus - node to center on?
})
setContext("draggableView", view)
@ -89,7 +109,12 @@
setContext("viewPos", internalPos)
// Content pos tracking
const contentPos = writable({ x: 0, y: 0, scrollX: 0, scrollY: 0 })
const contentPos = writable({
x: 0,
y: 0,
scrollX: 0,
scrollY: 0,
})
setContext("contentPos", contentPos)
// Elements
@ -101,7 +126,7 @@
let down = false
// Monitor the size of the viewPort
let observer
let viewObserver
// Size of the core display content
let contentDims = {}
@ -109,24 +134,51 @@
// Size of the view port
let viewDims = {}
// When dragging the content, maintain the drag start offset
let dragOffset
// Used when focusing the UI on trigger
let loaded = false
// Edge around the draggable content
let contentDragPadding = 200
// Auto scroll
let scrollInterval
// Used to track where the draggable item is scrolling into
let scrollZones
// Used to track the movements of the dragged content
// This allows things like the background to have their own starting coords
let offsetX = 0
let offsetY = 0
// Focus element details. Used to move the viewport
let focusElement = memo()
// Memo Focus
$: focusElement.set($view.focusEle)
// Background pattern
let bgDim = 24
// Scale prop for the icon
let dotDefault = 0.006
let viewDragStart = { x: 0, y: 0 }
let viewDragOffset = [0, 0]
let startPos = [0, 0]
$: bgSize = Math.max(bgDim * $view.scale, 10)
$: bgWidth = bgSize
$: bgHeight = bgSize
$: dotSize = Math.max(dotDefault * $view.scale, dotDefault)
const onScale = async () => {
dispatch("zoom", $view.scale)
await getDims()
}
const getDims = async () => {
if (!mainContent) return
if (!contentDims.original) {
const getDims = async (forceRefresh = false) => {
if (!mainContent || !viewPort) {
return
}
if (!contentDims.original || (contentDims.original && forceRefresh)) {
contentDims.original = {
w: parseInt(mainContent.getBoundingClientRect().width),
h: parseInt(mainContent.getBoundingClientRect().height),
@ -153,7 +205,11 @@
const buildWrapStyles = (pos, scale, dims) => {
const { x, y } = pos
const { w, h } = dims
return `--posX: ${x}px; --posY: ${y}px; --scale: ${scale}; --wrapH: ${h}px; --wrapW: ${w}px`
return `
--posX: ${x}px; --posY: ${y}px;
--scale: ${scale};
--wrapH: ${h}px; --wrapW: ${w}px;
`
}
const onViewScroll = e => {
@ -177,18 +233,20 @@
}
: {}),
}))
offsetX = offsetX - xBump
} else if (e.ctrlKey || e.metaKey) {
// Scale the content on scrolling
let updatedScale
if (e.deltaY < 0) {
updatedScale = Math.min(1, currentScale + 0.05)
} else if (e.deltaY > 0) {
updatedScale = Math.max(0, currentScale - 0.05)
updatedScale = Math.max(0.1, currentScale - 0.05)
}
view.update(state => ({
...state,
scale: Number(updatedScale.toFixed(2)),
scale: parseFloat(updatedScale.toFixed(2)),
}))
} else {
yBump = scrollIncrement * (e.deltaY < 0 ? -1 : 1)
@ -203,6 +261,7 @@
}
: {}),
}))
offsetY = offsetY - yBump
}
}
@ -219,18 +278,80 @@
y,
}))
if (down && !$view.dragging && dragOffset) {
if (down && !$view.dragging) {
// Determine how much the view has moved since
viewDragOffset = [x - viewDragStart.x, y - viewDragStart.y]
contentPos.update(state => ({
...state,
x: x - dragOffset[0],
y: y - dragOffset[1],
x: startPos[0] + viewDragOffset[0],
y: startPos[1] + viewDragOffset[1],
}))
offsetX = startPos[0] + viewDragOffset[0]
offsetY = startPos[1] + viewDragOffset[1]
}
const clearScrollInterval = () => {
if (scrollInterval) {
clearInterval(scrollInterval)
scrollInterval = undefined
}
}
if ($view.dragging) {
// Static, default buffer centered around the mouse
const dragBuffer = 100
scrollZones = {
top: y < dragBuffer,
bottom: y > viewDims.height - dragBuffer,
left: x < dragBuffer,
// An exception for the right side as the drag handle is on the extreme left
right: x > viewDims.width - $view.moveStep.w,
}
// Determine which zones are currently in play
const dragOutEntries = Object.entries(scrollZones).filter(e => e[1])
if (dragOutEntries.length) {
if (!scrollInterval) {
const autoScroll = () => {
const bump = 30
return () => {
const dragOutEntries = Object.entries(scrollZones).filter(
e => e[1]
)
const dragOut = Object.fromEntries(dragOutEntries)
// Depending on the zone, you want to move the content
// in the opposite direction
const xInterval = dragOut.right ? -bump : dragOut.left ? bump : 0
const yInterval = dragOut.bottom ? -bump : dragOut.top ? bump : 0
contentPos.update(state => ({
...state,
x: (state.x || 0) + xInterval,
y: (state.y || 0) + yInterval,
scrollX: state.scrollX + xInterval,
scrollY: state.scrollY + yInterval,
}))
offsetX = offsetX + xInterval
offsetY = offsetY + yInterval
}
}
scrollInterval = setInterval(autoScroll(), 30)
}
} else {
clearScrollInterval()
}
} else {
clearScrollInterval()
}
}
const onViewDragEnd = () => {
down = false
dragOffset = [0, 0]
}
const handleDragDrop = () => {
@ -247,8 +368,40 @@
)
}
// Reset state on mouse up
const globalMouseUp = () => {
down = false
viewDragStart = { x: 0, y: 0 }
viewDragOffset = [0, 0]
if ($view.dragging) {
view.update(state => ({
...state,
dragging: false,
moveStep: null,
dragSpot: null,
dropzones: {},
droptarget: null,
}))
if (scrollInterval) {
clearInterval(scrollInterval)
scrollInterval = undefined
scrollZones = {}
}
// Clear the scroll offset for dragging
contentPos.update(state => ({
...state,
scrollY: 0,
scrollX: 0,
}))
}
}
const onMouseUp = () => {
if ($view.droptarget) {
if ($view.droptarget && $view.dragging) {
handleDragDrop()
}
@ -273,8 +426,6 @@
if (!viewPort) {
return
}
// Update viewDims to get the latest viewport dimensions
viewDims = viewPort.getBoundingClientRect()
if ($view.moveStep && $view.dragging === false) {
view.update(state => ({
@ -329,18 +480,13 @@
}
}
const onMoveContent = e => {
if (down || !viewPort) {
return
}
const { x, y } = eleXY(e, viewPort)
dragOffset = [Math.abs(x - $contentPos.x), Math.abs(y - $contentPos.y)]
const isDraggable = e => {
const draggable = ["draggable-view", ...draggableClasses]
return draggable.some(cls => e.target.classList.contains(cls))
}
const focusOnLoad = () => {
if ($view.focusEle && !loaded) {
const focusEleDims = $view.focusEle
const viewToFocusEle = () => {
if ($focusElement) {
const viewWidth = viewDims.width
// The amount to shift the content in order to center the trigger on load.
@ -348,8 +494,8 @@
// The sidebar offset factors into the left positioning of the content here.
const targetX =
contentWrap.getBoundingClientRect().x -
focusEleDims.x +
(viewWidth / 2 - focusEleDims.width / 2)
$focusElement.x +
(viewWidth / 2 - $focusElement.width / 2)
// Update the content position state
// Shift the content up slightly to accommodate the padding
@ -358,21 +504,19 @@
x: targetX,
y: -(contentDragPadding / 2),
}))
loaded = true
}
}
// Update dims after scaling
$: {
$view.scale
$: viewScale = $view.scale
$: if (viewScale && mainContent) {
onScale()
}
// Focus on a registered element
$: {
$view.focusEle
focusOnLoad()
$focusElement
viewToFocusEle()
}
// Content mouse pos and scale to css variables.
@ -380,12 +524,21 @@
$: wrapStyles = buildWrapStyles($contentPos, $view.scale, contentDims)
onMount(() => {
observer = new ResizeObserver(getDims)
observer.observe(viewPort)
// As the view/browser resizes, ensure the stored view is up to date
viewObserver = new ResizeObserver(
Utils.domDebounce(() => {
getDims()
})
)
viewObserver.observe(viewPort)
// Global mouse observer
document.addEventListener("mouseup", globalMouseUp)
})
onDestroy(() => {
observer.disconnect()
viewObserver.disconnect()
document.removeEventListener("mouseup", globalMouseUp)
})
</script>
@ -399,56 +552,45 @@
on:mousemove={Utils.domDebounce(onMouseMove)}
style={`--dragPadding: ${contentDragPadding}px;`}
>
<svg class="draggable-background" style={`--dotSize: ${dotSize};`}>
<!-- Small 2px offset to tuck the points under the viewport on load-->
<pattern
id="dot-pattern"
width={bgWidth}
height={bgHeight}
patternUnits="userSpaceOnUse"
patternTransform={`translate(${offsetX - 2}, ${offsetY - 2})`}
>
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
{@html Logo}
</pattern>
<rect x="0" y="0" width="100%" height="100%" fill="url(#dot-pattern)" />
</svg>
<div
class="draggable-view"
class:dragging={down}
bind:this={viewPort}
on:wheel={Utils.domDebounce(onViewScroll)}
on:mousemove={Utils.domDebounce(onViewMouseMove)}
on:mouseup={onViewDragEnd}
on:mouseleave={onViewDragEnd}
on:mousedown={e => {
if ((e.which === 1 || e.button === 0) && isDraggable(e)) {
const { x, y } = eleXY(e, viewPort)
viewDragStart = { x, y }
startPos = [$contentPos.x, $contentPos.y]
down = true
}
}}
on:mouseup={e => {
viewDragOffset = [0, 0]
if ((e.which === 1 || e.button === 0) && isDraggable(e)) {
down = false
}
onViewDragEnd()
}}
>
<!-- <div class="debug">
<span>
View Pos [{$internalPos.x}, {$internalPos.y}]
</span>
<span>View Dims [{viewDims.width}, {viewDims.height}]</span>
<span>Mouse Down [{down}]</span>
<span>Drag [{$view.dragging}]</span>
<span>Dragging [{$view?.moveStep?.id || "no"}]</span>
<span>Scale [{$view.scale}]</span>
<span>Content [{JSON.stringify($contentPos)}]</span>
</div> -->
<div
class="content-wrap"
style={wrapStyles}
bind:this={contentWrap}
class:dragging={down}
>
<div
class="content"
bind:this={mainContent}
on:mousemove={Utils.domDebounce(onMoveContent)}
on:mousedown={e => {
if (e.which === 1 || e.button === 0) {
down = true
}
}}
on:mouseup={e => {
if (e.which === 1 || e.button === 0) {
down = false
}
}}
on:mouseleave={() => {
down = false
view.update(state => ({
...state,
dragging: false,
moveStep: null,
dragSpot: null,
dropzones: {},
}))
}}
>
<div class="content-wrap" style={wrapStyles} bind:this={contentWrap}>
<div class="content" bind:this={mainContent}>
<slot name="content" />
</div>
</div>
@ -465,7 +607,19 @@
height: 100%;
overflow: hidden;
position: relative;
cursor: grab;
}
.draggable-view.dragging {
cursor: grabbing;
}
.content {
transform-origin: 50% 50%;
transform: scale(var(--scale));
user-select: none;
padding: var(--dragPadding);
}
.content-wrap {
min-width: 100%;
position: absolute;
@ -473,25 +627,25 @@
left: 0;
width: var(--wrapW);
height: var(--wrapH);
cursor: grab;
transform: translate(var(--posX), var(--posY));
}
.content {
transform: scale(var(--scale));
user-select: none;
padding: var(--dragPadding);
.draggable-background {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
pointer-events: none;
z-index: -1;
background-color: var(--spectrum-global-color-gray-50);
}
.content-wrap.dragging {
cursor: grabbing;
.draggable-background :global(svg g path) {
fill: #91919a;
}
/* .debug {
display: flex;
align-items: center;
gap: 8px;
position: fixed;
padding: 8px;
z-index: 2;
} */
.draggable-background :global(svg g) {
transform: scale(var(--dotSize));
}
</style>

View File

@ -222,6 +222,7 @@
.branch-actions {
display: flex;
gap: var(--spacing-l);
cursor: default;
}
.footer {
display: flex;
@ -260,6 +261,7 @@
background-color: var(--background);
border: 1px solid var(--spectrum-global-color-gray-300);
border-radius: 4px 4px 4px 4px;
cursor: default;
}
.blockSection {

View File

@ -7,7 +7,6 @@
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import TestDataModal from "./TestDataModal.svelte"
import {
Icon,
notifications,
Modal,
Toggle,
@ -44,19 +43,8 @@
$: isRowAction = sdk.automations.isRowAction($memoAutomation)
const refresh = () => {
// Build global automation bindings.
const environmentBindings =
automationStore.actions.buildEnvironmentBindings()
// Get all processed block references
blockRefs = $selectedAutomation.blockRefs
automationStore.update(state => {
return {
...state,
bindings: [...environmentBindings],
}
})
}
const getBlocks = automation => {
@ -77,8 +65,6 @@
}
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="header" class:scrolling>
<div class="header-left">
<UndoRedoControl store={automationHistoryStore} showButtonGroup />
@ -111,15 +97,18 @@
Run test
</Button>
<div class="buttons">
<Icon disabled={!$automationStore.testResults} size="M" name="Multiple" />
<div
class:disabled={!$automationStore.testResults}
on:click={() => {
$automationStore.showTestPanel = true
}}
>
Test details
</div>
{#if !$automationStore.showTestPanel && $automationStore.testResults}
<Button
secondary
icon={"Multiple"}
disabled={!$automationStore.testResults}
on:click={() => {
$automationStore.showTestPanel = true
}}
>
Test details
</Button>
{/if}
</div>
{#if !isRowAction}
<div class="toggle-active setting-spacing">
@ -138,7 +127,18 @@
</div>
<div class="root" bind:this={treeEle}>
<DraggableCanvas bind:this={draggable}>
<DraggableCanvas
bind:this={draggable}
draggableClasses={[
"main-content",
"content",
"block",
"branched",
"branch",
"flow-item",
"branch-wrap",
]}
>
<span class="main-content" slot="content">
{#if Object.keys(blockRefs).length}
{#each blocks as block, idx (block.id)}
@ -179,9 +179,6 @@
display: flex;
flex-direction: column;
align-items: center;
max-height: 100%;
height: 100%;
width: 100%;
}
.header-left {
@ -221,15 +218,26 @@
display: flex;
justify-content: space-between;
align-items: center;
padding-left: var(--spacing-l);
transition: background 130ms ease-out;
padding: var(--spacing-l);
flex: 0 0 60px;
padding-right: var(--spacing-xl);
position: absolute;
width: 100%;
box-sizing: border-box;
pointer-events: none;
}
.header > * {
pointer-events: auto;
}
.controls {
display: flex;
gap: var(--spacing-xl);
gap: var(--spacing-l);
}
.controls .toggle-active :global(.spectrum-Switch-label) {
margin-right: 0px;
}
.buttons {
@ -243,11 +251,6 @@
cursor: pointer;
}
.disabled {
pointer-events: none;
color: var(--spectrum-global-color-gray-500) !important;
}
.group {
border-radius: 4px;
display: flex;

View File

@ -51,8 +51,13 @@
if (!blockEle) {
return
}
const { width, height } = blockEle.getBoundingClientRect()
blockDims = { width: width / $view.scale, height: height / $view.scale }
const { width, height, top, left } = blockEle.getBoundingClientRect()
blockDims = {
width: width / $view.scale,
height: height / $view.scale,
top,
left,
}
}
const loadSteps = blockRef => {
@ -174,12 +179,21 @@
e.stopPropagation()
updateBlockDims()
const { clientX, clientY } = e
view.update(state => ({
...state,
moveStep: {
id: block.id,
offsetX: $pos.x,
offsetY: $pos.y,
w: blockDims.width,
h: blockDims.height,
mouse: {
x: Math.max(Math.round(clientX - blockDims.left), 0),
y: Math.max(Math.round(clientY - blockDims.top), 0),
},
},
}))
}
@ -386,6 +400,7 @@
width: 480px;
font-size: 16px;
border-radius: 4px;
cursor: default;
}
.block .wrap {
width: 100%;

View File

@ -47,5 +47,6 @@
display: flex;
gap: var(--spacing-m);
padding: 8px 12px;
cursor: default;
}
</style>

View File

@ -50,9 +50,12 @@
// Register the trigger as the focus element for the automation
// Onload, the canvas will use the dimensions to center the step
if (stepEle && step.type === "TRIGGER" && !$view.focusEle) {
const { width, height, left, right, top, bottom, x, y } =
stepEle.getBoundingClientRect()
view.update(state => ({
...state,
focusEle: stepEle.getBoundingClientRect(),
focusEle: { width, height, left, right, top, bottom, x, y },
}))
}
})

View File

@ -78,6 +78,27 @@ const automationActions = store => ({
* @param {Object} automation the automaton to be mutated
*/
moveBlock: async (sourcePath, destPath, automation) => {
// The last part of the source node address, containing the id.
const pathSource = sourcePath.at(-1)
// The last part of the destination node address, containing the id.
const pathEnd = destPath.at(-1)
// Check if dragging a step into its own drag zone
const isOwnDragzone = pathSource.id === pathEnd.id
// Check if dragging the first branch step into the branch node drag zone
const isFirstBranchStep =
pathEnd.branchStepId &&
pathEnd.branchIdx === pathSource.branchIdx &&
pathSource.stepIdx === 0
// If dragging into an area that will not affect the tree structure
// Ignore the drag and drop.
if (isOwnDragzone || isFirstBranchStep) {
return
}
// Use core delete to remove and return the deleted block
// from the automation
const { deleted, newAutomation } = store.actions.deleteBlock(
@ -90,9 +111,6 @@ const automationActions = store => ({
const newRefs = {}
store.actions.traverse(newRefs, newAutomation)
// The last part of the destination node address, containing the id.
const pathEnd = destPath.at(-1)
let finalPath
// If dropping in a branch-step dropzone you need to find
// the updated parent step route then add the branch details again

View File

@ -2,7 +2,7 @@
import { writable, get } from "svelte/store"
import { setContext, onMount } from "svelte"
import { Layout, Heading, Body } from "@budibase/bbui"
import ErrorSVG from "@budibase/frontend-core/assets/error.svg"
import ErrorSVG from "@budibase/frontend-core/assets/error.svg?raw"
import { Constants, CookieUtils } from "@budibase/frontend-core"
import { getThemeClassNames } from "@budibase/shared-core"
import Component from "./Component.svelte"

View File

@ -1,5 +1,5 @@
import {
FetchBuiltinPermissionsRequest,
FetchBuiltinPermissionsResponse,
FetchIntegrationsResponse,
GetEnvironmentResponse,
GetVersionResponse,
@ -11,7 +11,7 @@ export interface OtherEndpoints {
getSystemStatus: () => Promise<SystemStatusResponse>
getBudibaseVersion: () => Promise<string>
getIntegrations: () => Promise<FetchIntegrationsResponse>
getBasePermissions: () => Promise<FetchBuiltinPermissionsRequest>
getBasePermissions: () => Promise<FetchBuiltinPermissionsResponse>
getEnvironment: () => Promise<GetEnvironmentResponse>
}

View File

@ -9,7 +9,7 @@ import {
RemovePermissionRequest,
RemovePermissionResponse,
FetchResourcePermissionInfoResponse,
FetchBuiltinPermissionsRequest,
FetchBuiltinPermissionsResponse,
FetchPermissionLevelsRequest,
} from "@budibase/types"
import {
@ -22,7 +22,7 @@ import { PermissionUpdateType } from "../../sdk/app/permissions"
const SUPPORTED_LEVELS = CURRENTLY_SUPPORTED_LEVELS
export function fetchBuiltin(
ctx: UserCtx<void, FetchBuiltinPermissionsRequest>
ctx: UserCtx<void, FetchBuiltinPermissionsResponse>
) {
ctx.body = Object.values(permissions.getBuiltinPermissions())
}

View File

@ -288,19 +288,21 @@ function replaceTableNamesInFilters(
for (const key of Object.keys(filter)) {
const matches = key.match(`^(?<relation>.+)\\.(?<field>.+)`)
const relation = matches?.groups?.["relation"]
// this is the possible table name which we need to check if it needs to be converted
const relatedTableName = matches?.groups?.["relation"]
const field = matches?.groups?.["field"]
if (!relation || !field) {
if (!relatedTableName || !field) {
continue
}
const table = allTables.find(r => r._id === tableId)!
if (Object.values(table.schema).some(f => f.name === relation)) {
const table = allTables.find(r => r._id === tableId)
const isColumnName = !!table?.schema[relatedTableName]
if (!table || isColumnName) {
continue
}
const matchedTable = allTables.find(t => t.name === relation)
const matchedTable = allTables.find(t => t.name === relatedTableName)
const relationship = Object.values(table.schema).find(
f => isRelationshipField(f) && f.tableId === matchedTable?._id
)

View File

@ -1,6 +1,6 @@
import * as utils from "../../../../db/utils"
import { docIds } from "@budibase/backend-core"
import { docIds, sql } from "@budibase/backend-core"
import {
Ctx,
DatasourcePlusQueryResponse,
@ -69,15 +69,15 @@ export function getSourceId(ctx: Ctx): { tableId: string; viewId?: string } {
viewId: sourceId,
}
}
return { tableId: ctx.params.sourceId }
return { tableId: sql.utils.encodeTableId(ctx.params.sourceId) }
}
// now check for old way of specifying table ID
if (ctx.params?.tableId) {
return { tableId: ctx.params.tableId }
return { tableId: sql.utils.encodeTableId(ctx.params.tableId) }
}
// check body for a table ID
if (ctx.request.body?.tableId) {
return { tableId: ctx.request.body.tableId }
return { tableId: sql.utils.encodeTableId(ctx.request.body.tableId) }
}
throw new Error("Unable to find table ID in request")
}

View File

@ -71,18 +71,27 @@ if (descriptions.length) {
let tableOrViewId: string
let rows: Row[]
async function basicRelationshipTables(type: RelationshipType) {
async function basicRelationshipTables(
type: RelationshipType,
opts?: {
tableName?: string
primaryColumn?: string
otherColumn?: string
}
) {
const relatedTable = await createTable({
name: { name: "name", type: FieldType.STRING },
name: { name: opts?.tableName || "name", type: FieldType.STRING },
})
const columnName = opts?.primaryColumn || "productCat"
//@ts-ignore - API accepts this structure, will build out rest of definition
const tableId = await createTable({
name: { name: "name", type: FieldType.STRING },
//@ts-ignore - API accepts this structure, will build out rest of definition
productCat: {
name: { name: opts?.tableName || "name", type: FieldType.STRING },
[columnName]: {
type: FieldType.LINK,
relationshipType: type,
name: "productCat",
fieldName: "product",
name: columnName,
fieldName: opts?.otherColumn || "product",
tableId: relatedTable,
constraints: {
type: "array",
@ -2776,6 +2785,42 @@ if (descriptions.length) {
})
})
isSql &&
describe("relationship - table with spaces", () => {
let primaryTable: Table, row: Row
beforeAll(async () => {
const { relatedTable, tableId } =
await basicRelationshipTables(
RelationshipType.ONE_TO_MANY,
{
tableName: "table with spaces",
primaryColumn: "related",
otherColumn: "related",
}
)
tableOrViewId = tableId
primaryTable = relatedTable
row = await config.api.row.save(primaryTable._id!, {
name: "foo",
})
await config.api.row.save(tableOrViewId, {
name: "foo",
related: [row._id],
})
})
it("should be able to search by table name with spaces", async () => {
await expectQuery({
equal: {
["table with spaces.name"]: "foo",
},
}).toContain([{ name: "foo" }])
})
})
isSql &&
describe.each([
RelationshipType.MANY_TO_ONE,

View File

@ -1,4 +1,10 @@
import { context, db as dbCore, docIds, utils } from "@budibase/backend-core"
import {
context,
db as dbCore,
docIds,
utils,
sql,
} from "@budibase/backend-core"
import {
DatabaseQueryOpts,
Datasource,
@ -328,7 +334,7 @@ export function extractViewInfoFromID(viewId: string) {
const regex = new RegExp(`^(?<tableId>.+)${SEPARATOR}([^${SEPARATOR}]+)$`)
const res = regex.exec(viewId)
return {
tableId: res!.groups!["tableId"],
tableId: sql.utils.encodeTableId(res!.groups!["tableId"]),
}
}

View File

@ -1,6 +1,6 @@
import { BuiltinPermission, PermissionLevel } from "../../../sdk"
export type FetchBuiltinPermissionsRequest = BuiltinPermission[]
export type FetchBuiltinPermissionsResponse = BuiltinPermission[]
export type FetchPermissionLevelsRequest = string[]

View File

@ -1,7 +1,7 @@
import { DevInfo, User } from "../../../documents"
export interface GenerateAPIKeyRequest {
userId: string
userId?: string
}
export interface GenerateAPIKeyResponse extends DevInfo {}

View File

@ -28,6 +28,7 @@ export interface App extends Document {
upgradableVersion?: string
snippets?: Snippet[]
creationVersion?: string
updatedBy?: string
}
export interface AppInstance {