Merge branch 'frontend-core-ts-2' of github.com:Budibase/budibase into ts-portal-admin-store
This commit is contained in:
commit
5319fb34ac
|
@ -4,8 +4,8 @@ import {
|
||||||
getContainerRuntimeClient,
|
getContainerRuntimeClient,
|
||||||
} from "testcontainers"
|
} from "testcontainers"
|
||||||
import { ContainerInfo } from "dockerode"
|
import { ContainerInfo } from "dockerode"
|
||||||
import path from "path"
|
import * as path from "path"
|
||||||
import lockfile from "proper-lockfile"
|
import * as lockfile from "proper-lockfile"
|
||||||
import { execSync } from "child_process"
|
import { execSync } from "child_process"
|
||||||
|
|
||||||
interface DockerContext {
|
interface DockerContext {
|
||||||
|
@ -29,8 +29,8 @@ function getCurrentDockerContext(): DockerContext {
|
||||||
|
|
||||||
async function getBudibaseContainers() {
|
async function getBudibaseContainers() {
|
||||||
const client = await getContainerRuntimeClient()
|
const client = await getContainerRuntimeClient()
|
||||||
const conatiners = await client.container.list()
|
const containers = await client.container.list()
|
||||||
return conatiners.filter(
|
return containers.filter(
|
||||||
container =>
|
container =>
|
||||||
container.Labels["com.budibase"] === "true" &&
|
container.Labels["com.budibase"] === "true" &&
|
||||||
container.Labels["org.testcontainers"] === "true"
|
container.Labels["org.testcontainers"] === "true"
|
||||||
|
|
|
@ -59,11 +59,15 @@ export function isExternalTable(table: Table) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildExternalTableId(datasourceId: string, tableName: string) {
|
export function buildExternalTableId(datasourceId: string, tableName: string) {
|
||||||
// encode spaces
|
return `${datasourceId}${DOUBLE_SEPARATOR}${encodeURIComponent(tableName)}`
|
||||||
if (tableName.includes(" ")) {
|
}
|
||||||
tableName = 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) {
|
export function breakExternalTableId(tableId: string) {
|
||||||
|
|
|
@ -7,19 +7,31 @@
|
||||||
onDestroy,
|
onDestroy,
|
||||||
tick,
|
tick,
|
||||||
} from "svelte"
|
} 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"
|
import { selectedAutomation, automationStore } from "stores/builder"
|
||||||
|
|
||||||
export function zoomIn() {
|
// CSS classes that, on mouse down, will trigger the view drag behaviour
|
||||||
const scale = Number(Math.min($view.scale + 0.1, 1.5).toFixed(2))
|
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 => ({
|
view.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
scale,
|
scale: newScale,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function zoomOut() {
|
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 => ({
|
view.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
scale,
|
scale,
|
||||||
|
@ -32,12 +44,13 @@
|
||||||
w: contentDims.original.w,
|
w: contentDims.original.w,
|
||||||
h: contentDims.original.h,
|
h: contentDims.original.h,
|
||||||
}
|
}
|
||||||
dragOffset = []
|
|
||||||
contentPos.update(state => ({
|
contentPos.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
}))
|
}))
|
||||||
|
offsetX = 0
|
||||||
|
offsetY = 0
|
||||||
view.update(state => ({
|
view.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
scale: 1,
|
scale: 1,
|
||||||
|
@ -48,18 +61,23 @@
|
||||||
const { width: wViewPort, height: hViewPort } =
|
const { width: wViewPort, height: hViewPort } =
|
||||||
viewPort.getBoundingClientRect()
|
viewPort.getBoundingClientRect()
|
||||||
|
|
||||||
const scaleTarget = Math.min(
|
const scaleTarget = parseFloat(
|
||||||
|
Math.min(
|
||||||
wViewPort / contentDims.original.w,
|
wViewPort / contentDims.original.w,
|
||||||
hViewPort / contentDims.original.h
|
hViewPort / contentDims.original.h
|
||||||
|
).toFixed(2)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Skip behaviour if the scale target scale is the current scale
|
||||||
|
if ($view.scale !== scaleTarget) {
|
||||||
// Smallest ratio determines which dimension needs squeezed
|
// Smallest ratio determines which dimension needs squeezed
|
||||||
view.update(state => ({
|
view.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
scale: scaleTarget,
|
scale: parseFloat(scaleTarget.toFixed(2)),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
await tick()
|
await tick()
|
||||||
|
}
|
||||||
|
|
||||||
const adjustedY = (hViewPort - contentDims.original.h) / 2
|
const adjustedY = (hViewPort - contentDims.original.h) / 2
|
||||||
|
|
||||||
|
@ -67,7 +85,10 @@
|
||||||
...state,
|
...state,
|
||||||
x: 0,
|
x: 0,
|
||||||
y: parseInt(0 + adjustedY),
|
y: parseInt(0 + adjustedY),
|
||||||
|
scrollX: 0,
|
||||||
|
scrollY: 0,
|
||||||
}))
|
}))
|
||||||
|
offsetY = parseInt(0 + adjustedY)
|
||||||
}
|
}
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
@ -79,7 +100,6 @@
|
||||||
dragSpot: null,
|
dragSpot: null,
|
||||||
scale: 1,
|
scale: 1,
|
||||||
dropzones: {},
|
dropzones: {},
|
||||||
//focus - node to center on?
|
|
||||||
})
|
})
|
||||||
|
|
||||||
setContext("draggableView", view)
|
setContext("draggableView", view)
|
||||||
|
@ -89,7 +109,12 @@
|
||||||
setContext("viewPos", internalPos)
|
setContext("viewPos", internalPos)
|
||||||
|
|
||||||
// Content pos tracking
|
// 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)
|
setContext("contentPos", contentPos)
|
||||||
|
|
||||||
// Elements
|
// Elements
|
||||||
|
@ -101,7 +126,7 @@
|
||||||
let down = false
|
let down = false
|
||||||
|
|
||||||
// Monitor the size of the viewPort
|
// Monitor the size of the viewPort
|
||||||
let observer
|
let viewObserver
|
||||||
|
|
||||||
// Size of the core display content
|
// Size of the core display content
|
||||||
let contentDims = {}
|
let contentDims = {}
|
||||||
|
@ -109,24 +134,51 @@
|
||||||
// Size of the view port
|
// Size of the view port
|
||||||
let viewDims = {}
|
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
|
// Edge around the draggable content
|
||||||
let contentDragPadding = 200
|
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 () => {
|
const onScale = async () => {
|
||||||
dispatch("zoom", $view.scale)
|
dispatch("zoom", $view.scale)
|
||||||
await getDims()
|
await getDims()
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDims = async () => {
|
const getDims = async (forceRefresh = false) => {
|
||||||
if (!mainContent) return
|
if (!mainContent || !viewPort) {
|
||||||
|
return
|
||||||
if (!contentDims.original) {
|
}
|
||||||
|
if (!contentDims.original || (contentDims.original && forceRefresh)) {
|
||||||
contentDims.original = {
|
contentDims.original = {
|
||||||
w: parseInt(mainContent.getBoundingClientRect().width),
|
w: parseInt(mainContent.getBoundingClientRect().width),
|
||||||
h: parseInt(mainContent.getBoundingClientRect().height),
|
h: parseInt(mainContent.getBoundingClientRect().height),
|
||||||
|
@ -153,7 +205,11 @@
|
||||||
const buildWrapStyles = (pos, scale, dims) => {
|
const buildWrapStyles = (pos, scale, dims) => {
|
||||||
const { x, y } = pos
|
const { x, y } = pos
|
||||||
const { w, h } = dims
|
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 => {
|
const onViewScroll = e => {
|
||||||
|
@ -177,18 +233,20 @@
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
offsetX = offsetX - xBump
|
||||||
} else if (e.ctrlKey || e.metaKey) {
|
} else if (e.ctrlKey || e.metaKey) {
|
||||||
// Scale the content on scrolling
|
// Scale the content on scrolling
|
||||||
let updatedScale
|
let updatedScale
|
||||||
if (e.deltaY < 0) {
|
if (e.deltaY < 0) {
|
||||||
updatedScale = Math.min(1, currentScale + 0.05)
|
updatedScale = Math.min(1, currentScale + 0.05)
|
||||||
} else if (e.deltaY > 0) {
|
} else if (e.deltaY > 0) {
|
||||||
updatedScale = Math.max(0, currentScale - 0.05)
|
updatedScale = Math.max(0.1, currentScale - 0.05)
|
||||||
}
|
}
|
||||||
|
|
||||||
view.update(state => ({
|
view.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
scale: Number(updatedScale.toFixed(2)),
|
scale: parseFloat(updatedScale.toFixed(2)),
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
yBump = scrollIncrement * (e.deltaY < 0 ? -1 : 1)
|
yBump = scrollIncrement * (e.deltaY < 0 ? -1 : 1)
|
||||||
|
@ -203,6 +261,7 @@
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
}))
|
}))
|
||||||
|
offsetY = offsetY - yBump
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -219,18 +278,80 @@
|
||||||
y,
|
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 => ({
|
contentPos.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
x: x - dragOffset[0],
|
x: startPos[0] + viewDragOffset[0],
|
||||||
y: y - dragOffset[1],
|
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 = () => {
|
const onViewDragEnd = () => {
|
||||||
down = false
|
down = false
|
||||||
dragOffset = [0, 0]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDragDrop = () => {
|
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 = () => {
|
const onMouseUp = () => {
|
||||||
if ($view.droptarget) {
|
if ($view.droptarget && $view.dragging) {
|
||||||
handleDragDrop()
|
handleDragDrop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -273,8 +426,6 @@
|
||||||
if (!viewPort) {
|
if (!viewPort) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Update viewDims to get the latest viewport dimensions
|
|
||||||
viewDims = viewPort.getBoundingClientRect()
|
|
||||||
|
|
||||||
if ($view.moveStep && $view.dragging === false) {
|
if ($view.moveStep && $view.dragging === false) {
|
||||||
view.update(state => ({
|
view.update(state => ({
|
||||||
|
@ -329,18 +480,13 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onMoveContent = e => {
|
const isDraggable = e => {
|
||||||
if (down || !viewPort) {
|
const draggable = ["draggable-view", ...draggableClasses]
|
||||||
return
|
return draggable.some(cls => e.target.classList.contains(cls))
|
||||||
}
|
|
||||||
const { x, y } = eleXY(e, viewPort)
|
|
||||||
|
|
||||||
dragOffset = [Math.abs(x - $contentPos.x), Math.abs(y - $contentPos.y)]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const focusOnLoad = () => {
|
const viewToFocusEle = () => {
|
||||||
if ($view.focusEle && !loaded) {
|
if ($focusElement) {
|
||||||
const focusEleDims = $view.focusEle
|
|
||||||
const viewWidth = viewDims.width
|
const viewWidth = viewDims.width
|
||||||
|
|
||||||
// The amount to shift the content in order to center the trigger on load.
|
// 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.
|
// The sidebar offset factors into the left positioning of the content here.
|
||||||
const targetX =
|
const targetX =
|
||||||
contentWrap.getBoundingClientRect().x -
|
contentWrap.getBoundingClientRect().x -
|
||||||
focusEleDims.x +
|
$focusElement.x +
|
||||||
(viewWidth / 2 - focusEleDims.width / 2)
|
(viewWidth / 2 - $focusElement.width / 2)
|
||||||
|
|
||||||
// Update the content position state
|
// Update the content position state
|
||||||
// Shift the content up slightly to accommodate the padding
|
// Shift the content up slightly to accommodate the padding
|
||||||
|
@ -358,21 +504,19 @@
|
||||||
x: targetX,
|
x: targetX,
|
||||||
y: -(contentDragPadding / 2),
|
y: -(contentDragPadding / 2),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
loaded = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update dims after scaling
|
// Update dims after scaling
|
||||||
$: {
|
$: viewScale = $view.scale
|
||||||
$view.scale
|
$: if (viewScale && mainContent) {
|
||||||
onScale()
|
onScale()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Focus on a registered element
|
// Focus on a registered element
|
||||||
$: {
|
$: {
|
||||||
$view.focusEle
|
$focusElement
|
||||||
focusOnLoad()
|
viewToFocusEle()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Content mouse pos and scale to css variables.
|
// Content mouse pos and scale to css variables.
|
||||||
|
@ -380,12 +524,21 @@
|
||||||
$: wrapStyles = buildWrapStyles($contentPos, $view.scale, contentDims)
|
$: wrapStyles = buildWrapStyles($contentPos, $view.scale, contentDims)
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
observer = new ResizeObserver(getDims)
|
// As the view/browser resizes, ensure the stored view is up to date
|
||||||
observer.observe(viewPort)
|
viewObserver = new ResizeObserver(
|
||||||
|
Utils.domDebounce(() => {
|
||||||
|
getDims()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
viewObserver.observe(viewPort)
|
||||||
|
|
||||||
|
// Global mouse observer
|
||||||
|
document.addEventListener("mouseup", globalMouseUp)
|
||||||
})
|
})
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
observer.disconnect()
|
viewObserver.disconnect()
|
||||||
|
document.removeEventListener("mouseup", globalMouseUp)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -399,56 +552,45 @@
|
||||||
on:mousemove={Utils.domDebounce(onMouseMove)}
|
on:mousemove={Utils.domDebounce(onMouseMove)}
|
||||||
style={`--dragPadding: ${contentDragPadding}px;`}
|
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
|
<div
|
||||||
class="draggable-view"
|
class="draggable-view"
|
||||||
|
class:dragging={down}
|
||||||
bind:this={viewPort}
|
bind:this={viewPort}
|
||||||
on:wheel={Utils.domDebounce(onViewScroll)}
|
on:wheel={Utils.domDebounce(onViewScroll)}
|
||||||
on:mousemove={Utils.domDebounce(onViewMouseMove)}
|
on:mousemove={Utils.domDebounce(onViewMouseMove)}
|
||||||
on:mouseup={onViewDragEnd}
|
|
||||||
on:mouseleave={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 => {
|
on:mousedown={e => {
|
||||||
if (e.which === 1 || e.button === 0) {
|
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
|
down = true
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
on:mouseup={e => {
|
on:mouseup={e => {
|
||||||
if (e.which === 1 || e.button === 0) {
|
viewDragOffset = [0, 0]
|
||||||
|
if ((e.which === 1 || e.button === 0) && isDraggable(e)) {
|
||||||
down = false
|
down = false
|
||||||
}
|
}
|
||||||
}}
|
onViewDragEnd()
|
||||||
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" />
|
<slot name="content" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -465,7 +607,19 @@
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
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 {
|
.content-wrap {
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -473,25 +627,25 @@
|
||||||
left: 0;
|
left: 0;
|
||||||
width: var(--wrapW);
|
width: var(--wrapW);
|
||||||
height: var(--wrapH);
|
height: var(--wrapH);
|
||||||
cursor: grab;
|
|
||||||
transform: translate(var(--posX), var(--posY));
|
transform: translate(var(--posX), var(--posY));
|
||||||
}
|
}
|
||||||
.content {
|
|
||||||
transform: scale(var(--scale));
|
.draggable-background {
|
||||||
user-select: none;
|
position: absolute;
|
||||||
padding: var(--dragPadding);
|
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 {
|
.draggable-background :global(svg g path) {
|
||||||
cursor: grabbing;
|
fill: #91919a;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* .debug {
|
.draggable-background :global(svg g) {
|
||||||
display: flex;
|
transform: scale(var(--dotSize));
|
||||||
align-items: center;
|
}
|
||||||
gap: 8px;
|
|
||||||
position: fixed;
|
|
||||||
padding: 8px;
|
|
||||||
z-index: 2;
|
|
||||||
} */
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -222,6 +222,7 @@
|
||||||
.branch-actions {
|
.branch-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--spacing-l);
|
gap: var(--spacing-l);
|
||||||
|
cursor: default;
|
||||||
}
|
}
|
||||||
.footer {
|
.footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -260,6 +261,7 @@
|
||||||
background-color: var(--background);
|
background-color: var(--background);
|
||||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||||
border-radius: 4px 4px 4px 4px;
|
border-radius: 4px 4px 4px 4px;
|
||||||
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.blockSection {
|
.blockSection {
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import TestDataModal from "./TestDataModal.svelte"
|
import TestDataModal from "./TestDataModal.svelte"
|
||||||
import {
|
import {
|
||||||
Icon,
|
|
||||||
notifications,
|
notifications,
|
||||||
Modal,
|
Modal,
|
||||||
Toggle,
|
Toggle,
|
||||||
|
@ -44,19 +43,8 @@
|
||||||
$: isRowAction = sdk.automations.isRowAction($memoAutomation)
|
$: isRowAction = sdk.automations.isRowAction($memoAutomation)
|
||||||
|
|
||||||
const refresh = () => {
|
const refresh = () => {
|
||||||
// Build global automation bindings.
|
|
||||||
const environmentBindings =
|
|
||||||
automationStore.actions.buildEnvironmentBindings()
|
|
||||||
|
|
||||||
// Get all processed block references
|
// Get all processed block references
|
||||||
blockRefs = $selectedAutomation.blockRefs
|
blockRefs = $selectedAutomation.blockRefs
|
||||||
|
|
||||||
automationStore.update(state => {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
bindings: [...environmentBindings],
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getBlocks = automation => {
|
const getBlocks = automation => {
|
||||||
|
@ -77,8 +65,6 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
||||||
<div class="header" class:scrolling>
|
<div class="header" class:scrolling>
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<UndoRedoControl store={automationHistoryStore} showButtonGroup />
|
<UndoRedoControl store={automationHistoryStore} showButtonGroup />
|
||||||
|
@ -111,15 +97,18 @@
|
||||||
Run test
|
Run test
|
||||||
</Button>
|
</Button>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<Icon disabled={!$automationStore.testResults} size="M" name="Multiple" />
|
{#if !$automationStore.showTestPanel && $automationStore.testResults}
|
||||||
<div
|
<Button
|
||||||
class:disabled={!$automationStore.testResults}
|
secondary
|
||||||
|
icon={"Multiple"}
|
||||||
|
disabled={!$automationStore.testResults}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
$automationStore.showTestPanel = true
|
$automationStore.showTestPanel = true
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Test details
|
Test details
|
||||||
</div>
|
</Button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if !isRowAction}
|
{#if !isRowAction}
|
||||||
<div class="toggle-active setting-spacing">
|
<div class="toggle-active setting-spacing">
|
||||||
|
@ -138,7 +127,18 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="root" bind:this={treeEle}>
|
<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">
|
<span class="main-content" slot="content">
|
||||||
{#if Object.keys(blockRefs).length}
|
{#if Object.keys(blockRefs).length}
|
||||||
{#each blocks as block, idx (block.id)}
|
{#each blocks as block, idx (block.id)}
|
||||||
|
@ -179,9 +179,6 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
max-height: 100%;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-left {
|
.header-left {
|
||||||
|
@ -221,15 +218,26 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-left: var(--spacing-l);
|
padding: var(--spacing-l);
|
||||||
transition: background 130ms ease-out;
|
|
||||||
flex: 0 0 60px;
|
flex: 0 0 60px;
|
||||||
padding-right: var(--spacing-xl);
|
padding-right: var(--spacing-xl);
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header > * {
|
||||||
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls {
|
.controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--spacing-xl);
|
gap: var(--spacing-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls .toggle-active :global(.spectrum-Switch-label) {
|
||||||
|
margin-right: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttons {
|
.buttons {
|
||||||
|
@ -243,11 +251,6 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.disabled {
|
|
||||||
pointer-events: none;
|
|
||||||
color: var(--spectrum-global-color-gray-500) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group {
|
.group {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -51,8 +51,13 @@
|
||||||
if (!blockEle) {
|
if (!blockEle) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const { width, height } = blockEle.getBoundingClientRect()
|
const { width, height, top, left } = blockEle.getBoundingClientRect()
|
||||||
blockDims = { width: width / $view.scale, height: height / $view.scale }
|
blockDims = {
|
||||||
|
width: width / $view.scale,
|
||||||
|
height: height / $view.scale,
|
||||||
|
top,
|
||||||
|
left,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadSteps = blockRef => {
|
const loadSteps = blockRef => {
|
||||||
|
@ -174,12 +179,21 @@
|
||||||
|
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|
||||||
|
updateBlockDims()
|
||||||
|
|
||||||
|
const { clientX, clientY } = e
|
||||||
view.update(state => ({
|
view.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
moveStep: {
|
moveStep: {
|
||||||
id: block.id,
|
id: block.id,
|
||||||
offsetX: $pos.x,
|
offsetX: $pos.x,
|
||||||
offsetY: $pos.y,
|
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;
|
width: 480px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
cursor: default;
|
||||||
}
|
}
|
||||||
.block .wrap {
|
.block .wrap {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -47,5 +47,6 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--spacing-m);
|
gap: var(--spacing-m);
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
|
cursor: default;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -50,9 +50,12 @@
|
||||||
// Register the trigger as the focus element for the automation
|
// Register the trigger as the focus element for the automation
|
||||||
// Onload, the canvas will use the dimensions to center the step
|
// Onload, the canvas will use the dimensions to center the step
|
||||||
if (stepEle && step.type === "TRIGGER" && !$view.focusEle) {
|
if (stepEle && step.type === "TRIGGER" && !$view.focusEle) {
|
||||||
|
const { width, height, left, right, top, bottom, x, y } =
|
||||||
|
stepEle.getBoundingClientRect()
|
||||||
|
|
||||||
view.update(state => ({
|
view.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
focusEle: stepEle.getBoundingClientRect(),
|
focusEle: { width, height, left, right, top, bottom, x, y },
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -78,6 +78,27 @@ const automationActions = store => ({
|
||||||
* @param {Object} automation the automaton to be mutated
|
* @param {Object} automation the automaton to be mutated
|
||||||
*/
|
*/
|
||||||
moveBlock: async (sourcePath, destPath, automation) => {
|
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
|
// Use core delete to remove and return the deleted block
|
||||||
// from the automation
|
// from the automation
|
||||||
const { deleted, newAutomation } = store.actions.deleteBlock(
|
const { deleted, newAutomation } = store.actions.deleteBlock(
|
||||||
|
@ -90,9 +111,6 @@ const automationActions = store => ({
|
||||||
const newRefs = {}
|
const newRefs = {}
|
||||||
store.actions.traverse(newRefs, newAutomation)
|
store.actions.traverse(newRefs, newAutomation)
|
||||||
|
|
||||||
// The last part of the destination node address, containing the id.
|
|
||||||
const pathEnd = destPath.at(-1)
|
|
||||||
|
|
||||||
let finalPath
|
let finalPath
|
||||||
// If dropping in a branch-step dropzone you need to find
|
// If dropping in a branch-step dropzone you need to find
|
||||||
// the updated parent step route then add the branch details again
|
// the updated parent step route then add the branch details again
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { writable, get } from "svelte/store"
|
import { writable, get } from "svelte/store"
|
||||||
import { setContext, onMount } from "svelte"
|
import { setContext, onMount } from "svelte"
|
||||||
import { Layout, Heading, Body } from "@budibase/bbui"
|
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 { 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"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import {
|
import {
|
||||||
FetchBuiltinPermissionsRequest,
|
FetchBuiltinPermissionsResponse,
|
||||||
FetchIntegrationsResponse,
|
FetchIntegrationsResponse,
|
||||||
GetEnvironmentResponse,
|
GetEnvironmentResponse,
|
||||||
GetVersionResponse,
|
GetVersionResponse,
|
||||||
|
@ -11,7 +11,7 @@ export interface OtherEndpoints {
|
||||||
getSystemStatus: () => Promise<SystemStatusResponse>
|
getSystemStatus: () => Promise<SystemStatusResponse>
|
||||||
getBudibaseVersion: () => Promise<string>
|
getBudibaseVersion: () => Promise<string>
|
||||||
getIntegrations: () => Promise<FetchIntegrationsResponse>
|
getIntegrations: () => Promise<FetchIntegrationsResponse>
|
||||||
getBasePermissions: () => Promise<FetchBuiltinPermissionsRequest>
|
getBasePermissions: () => Promise<FetchBuiltinPermissionsResponse>
|
||||||
getEnvironment: () => Promise<GetEnvironmentResponse>
|
getEnvironment: () => Promise<GetEnvironmentResponse>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {
|
||||||
RemovePermissionRequest,
|
RemovePermissionRequest,
|
||||||
RemovePermissionResponse,
|
RemovePermissionResponse,
|
||||||
FetchResourcePermissionInfoResponse,
|
FetchResourcePermissionInfoResponse,
|
||||||
FetchBuiltinPermissionsRequest,
|
FetchBuiltinPermissionsResponse,
|
||||||
FetchPermissionLevelsRequest,
|
FetchPermissionLevelsRequest,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import {
|
import {
|
||||||
|
@ -22,7 +22,7 @@ import { PermissionUpdateType } from "../../sdk/app/permissions"
|
||||||
const SUPPORTED_LEVELS = CURRENTLY_SUPPORTED_LEVELS
|
const SUPPORTED_LEVELS = CURRENTLY_SUPPORTED_LEVELS
|
||||||
|
|
||||||
export function fetchBuiltin(
|
export function fetchBuiltin(
|
||||||
ctx: UserCtx<void, FetchBuiltinPermissionsRequest>
|
ctx: UserCtx<void, FetchBuiltinPermissionsResponse>
|
||||||
) {
|
) {
|
||||||
ctx.body = Object.values(permissions.getBuiltinPermissions())
|
ctx.body = Object.values(permissions.getBuiltinPermissions())
|
||||||
}
|
}
|
||||||
|
|
|
@ -288,19 +288,21 @@ function replaceTableNamesInFilters(
|
||||||
for (const key of Object.keys(filter)) {
|
for (const key of Object.keys(filter)) {
|
||||||
const matches = key.match(`^(?<relation>.+)\\.(?<field>.+)`)
|
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"]
|
const field = matches?.groups?.["field"]
|
||||||
|
|
||||||
if (!relation || !field) {
|
if (!relatedTableName || !field) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const table = allTables.find(r => r._id === tableId)!
|
const table = allTables.find(r => r._id === tableId)
|
||||||
if (Object.values(table.schema).some(f => f.name === relation)) {
|
const isColumnName = !!table?.schema[relatedTableName]
|
||||||
|
if (!table || isColumnName) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const matchedTable = allTables.find(t => t.name === relation)
|
const matchedTable = allTables.find(t => t.name === relatedTableName)
|
||||||
const relationship = Object.values(table.schema).find(
|
const relationship = Object.values(table.schema).find(
|
||||||
f => isRelationshipField(f) && f.tableId === matchedTable?._id
|
f => isRelationshipField(f) && f.tableId === matchedTable?._id
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as utils from "../../../../db/utils"
|
import * as utils from "../../../../db/utils"
|
||||||
|
|
||||||
import { docIds } from "@budibase/backend-core"
|
import { docIds, sql } from "@budibase/backend-core"
|
||||||
import {
|
import {
|
||||||
Ctx,
|
Ctx,
|
||||||
DatasourcePlusQueryResponse,
|
DatasourcePlusQueryResponse,
|
||||||
|
@ -69,15 +69,15 @@ export function getSourceId(ctx: Ctx): { tableId: string; viewId?: string } {
|
||||||
viewId: sourceId,
|
viewId: sourceId,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { tableId: ctx.params.sourceId }
|
return { tableId: sql.utils.encodeTableId(ctx.params.sourceId) }
|
||||||
}
|
}
|
||||||
// now check for old way of specifying table ID
|
// now check for old way of specifying table ID
|
||||||
if (ctx.params?.tableId) {
|
if (ctx.params?.tableId) {
|
||||||
return { tableId: ctx.params.tableId }
|
return { tableId: sql.utils.encodeTableId(ctx.params.tableId) }
|
||||||
}
|
}
|
||||||
// check body for a table ID
|
// check body for a table ID
|
||||||
if (ctx.request.body?.tableId) {
|
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")
|
throw new Error("Unable to find table ID in request")
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,18 +71,27 @@ if (descriptions.length) {
|
||||||
let tableOrViewId: string
|
let tableOrViewId: string
|
||||||
let rows: Row[]
|
let rows: Row[]
|
||||||
|
|
||||||
async function basicRelationshipTables(type: RelationshipType) {
|
async function basicRelationshipTables(
|
||||||
|
type: RelationshipType,
|
||||||
|
opts?: {
|
||||||
|
tableName?: string
|
||||||
|
primaryColumn?: string
|
||||||
|
otherColumn?: string
|
||||||
|
}
|
||||||
|
) {
|
||||||
const relatedTable = await createTable({
|
const relatedTable = await createTable({
|
||||||
name: { name: "name", type: FieldType.STRING },
|
name: { name: opts?.tableName || "name", type: FieldType.STRING },
|
||||||
})
|
})
|
||||||
const tableId = await createTable({
|
|
||||||
name: { name: "name", type: FieldType.STRING },
|
const columnName = opts?.primaryColumn || "productCat"
|
||||||
//@ts-ignore - API accepts this structure, will build out rest of definition
|
//@ts-ignore - API accepts this structure, will build out rest of definition
|
||||||
productCat: {
|
const tableId = await createTable({
|
||||||
|
name: { name: opts?.tableName || "name", type: FieldType.STRING },
|
||||||
|
[columnName]: {
|
||||||
type: FieldType.LINK,
|
type: FieldType.LINK,
|
||||||
relationshipType: type,
|
relationshipType: type,
|
||||||
name: "productCat",
|
name: columnName,
|
||||||
fieldName: "product",
|
fieldName: opts?.otherColumn || "product",
|
||||||
tableId: relatedTable,
|
tableId: relatedTable,
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "array",
|
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 &&
|
isSql &&
|
||||||
describe.each([
|
describe.each([
|
||||||
RelationshipType.MANY_TO_ONE,
|
RelationshipType.MANY_TO_ONE,
|
||||||
|
|
|
@ -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 {
|
import {
|
||||||
DatabaseQueryOpts,
|
DatabaseQueryOpts,
|
||||||
Datasource,
|
Datasource,
|
||||||
|
@ -328,7 +334,7 @@ export function extractViewInfoFromID(viewId: string) {
|
||||||
const regex = new RegExp(`^(?<tableId>.+)${SEPARATOR}([^${SEPARATOR}]+)$`)
|
const regex = new RegExp(`^(?<tableId>.+)${SEPARATOR}([^${SEPARATOR}]+)$`)
|
||||||
const res = regex.exec(viewId)
|
const res = regex.exec(viewId)
|
||||||
return {
|
return {
|
||||||
tableId: res!.groups!["tableId"],
|
tableId: sql.utils.encodeTableId(res!.groups!["tableId"]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { BuiltinPermission, PermissionLevel } from "../../../sdk"
|
import { BuiltinPermission, PermissionLevel } from "../../../sdk"
|
||||||
|
|
||||||
export type FetchBuiltinPermissionsRequest = BuiltinPermission[]
|
export type FetchBuiltinPermissionsResponse = BuiltinPermission[]
|
||||||
|
|
||||||
export type FetchPermissionLevelsRequest = string[]
|
export type FetchPermissionLevelsRequest = string[]
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { DevInfo, User } from "../../../documents"
|
import { DevInfo, User } from "../../../documents"
|
||||||
|
|
||||||
export interface GenerateAPIKeyRequest {
|
export interface GenerateAPIKeyRequest {
|
||||||
userId: string
|
userId?: string
|
||||||
}
|
}
|
||||||
export interface GenerateAPIKeyResponse extends DevInfo {}
|
export interface GenerateAPIKeyResponse extends DevInfo {}
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,7 @@ export interface App extends Document {
|
||||||
upgradableVersion?: string
|
upgradableVersion?: string
|
||||||
snippets?: Snippet[]
|
snippets?: Snippet[]
|
||||||
creationVersion?: string
|
creationVersion?: string
|
||||||
|
updatedBy?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppInstance {
|
export interface AppInstance {
|
||||||
|
|
Loading…
Reference in New Issue