Merge branch 'develop' into grid-mobile-scrolling
This commit is contained in:
commit
0303027754
|
@ -20,6 +20,7 @@ env:
|
||||||
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||||
NX_BASE_BRANCH: origin/${{ github.base_ref }}
|
NX_BASE_BRANCH: origin/${{ github.base_ref }}
|
||||||
USE_NX_AFFECTED: ${{ github.event_name == 'pull_request' && github.base_ref != 'master'}}
|
USE_NX_AFFECTED: ${{ github.event_name == 'pull_request' && github.base_ref != 'master'}}
|
||||||
|
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
|
|
|
@ -14,7 +14,7 @@ jobs:
|
||||||
- uses: passeidireto/trigger-external-workflow-action@main
|
- uses: passeidireto/trigger-external-workflow-action@main
|
||||||
env:
|
env:
|
||||||
PAYLOAD_BRANCH: ${{ github.head_ref }}
|
PAYLOAD_BRANCH: ${{ github.head_ref }}
|
||||||
PAYLOAD_PR_NUMBER: ${{ github.ref }}
|
PAYLOAD_PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
with:
|
with:
|
||||||
repository: budibase/budibase-deploys
|
repository: budibase/budibase-deploys
|
||||||
event: featurebranch-qa-close
|
event: featurebranch-qa-close
|
||||||
|
|
|
@ -4,6 +4,7 @@ on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- develop
|
- develop
|
||||||
|
- master
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
|
@ -13,7 +14,7 @@ jobs:
|
||||||
- uses: passeidireto/trigger-external-workflow-action@main
|
- uses: passeidireto/trigger-external-workflow-action@main
|
||||||
env:
|
env:
|
||||||
PAYLOAD_BRANCH: ${{ github.head_ref }}
|
PAYLOAD_BRANCH: ${{ github.head_ref }}
|
||||||
PAYLOAD_PR_NUMBER: ${{ github.ref }}
|
PAYLOAD_PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
with:
|
with:
|
||||||
repository: budibase/budibase-deploys
|
repository: budibase/budibase-deploys
|
||||||
event: featurebranch-qa-deploy
|
event: featurebranch-qa-deploy
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.10.16-alpha.11",
|
"version": "2.11.15-alpha.0",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
AutomationStepIdArray,
|
AutomationStepIdArray,
|
||||||
AutomationIOType,
|
AutomationIOType,
|
||||||
AutomationCustomIOType,
|
AutomationCustomIOType,
|
||||||
|
DatasourceFeature,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import joi from "joi"
|
import joi from "joi"
|
||||||
|
|
||||||
|
@ -67,9 +68,27 @@ function validateDatasource(schema: any) {
|
||||||
version: joi.string().optional(),
|
version: joi.string().optional(),
|
||||||
schema: joi.object({
|
schema: joi.object({
|
||||||
docs: joi.string(),
|
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(),
|
friendlyName: joi.string().required(),
|
||||||
type: joi.string().allow(...DATASOURCE_TYPES),
|
type: joi.string().allow(...DATASOURCE_TYPES),
|
||||||
description: joi.string().required(),
|
|
||||||
datasource: joi.object().pattern(joi.string(), fieldValidator).required(),
|
datasource: joi.object().pattern(joi.string(), fieldValidator).required(),
|
||||||
query: joi
|
query: joi
|
||||||
.object()
|
.object()
|
||||||
|
|
|
@ -21,14 +21,6 @@
|
||||||
"hsla(240, 90%, 75%, 0.3)",
|
"hsla(240, 90%, 75%, 0.3)",
|
||||||
"hsla(320, 90%, 75%, 0.3)",
|
"hsla(320, 90%, 75%, 0.3)",
|
||||||
]
|
]
|
||||||
$: {
|
|
||||||
if (constraints.inclusion.length) {
|
|
||||||
options = constraints.inclusion.map(value => ({
|
|
||||||
name: value,
|
|
||||||
id: Math.random(),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const removeInput = idx => {
|
const removeInput = idx => {
|
||||||
delete optionColors[options[idx].name]
|
delete optionColors[options[idx].name]
|
||||||
constraints.inclusion = constraints.inclusion.filter((e, i) => i !== idx)
|
constraints.inclusion = constraints.inclusion.filter((e, i) => i !== idx)
|
||||||
|
@ -80,6 +72,11 @@
|
||||||
// Initialize anchor arrays on mount, assuming 'options' is already populated
|
// Initialize anchor arrays on mount, assuming 'options' is already populated
|
||||||
colorPopovers = constraints.inclusion.map(() => undefined)
|
colorPopovers = constraints.inclusion.map(() => undefined)
|
||||||
anchors = constraints.inclusion.map(() => undefined)
|
anchors = constraints.inclusion.map(() => undefined)
|
||||||
|
|
||||||
|
options = constraints.inclusion.map(value => ({
|
||||||
|
name: value,
|
||||||
|
id: Math.random(),
|
||||||
|
}))
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -221,18 +221,6 @@ const automationActions = store => ({
|
||||||
newAutomation.definition.steps.splice(blockIdx, 0, block)
|
newAutomation.definition.steps.splice(blockIdx, 0, block)
|
||||||
await store.actions.save(newAutomation)
|
await store.actions.save(newAutomation)
|
||||||
},
|
},
|
||||||
/**
|
|
||||||
* "rowControl" appears to be the name of the flag used to determine whether
|
|
||||||
* a certain automation block uses values or bindings as inputs
|
|
||||||
*/
|
|
||||||
toggleRowControl: async (block, rowControl) => {
|
|
||||||
const newBlock = { ...block, rowControl }
|
|
||||||
const newAutomation = store.actions.getUpdatedDefinition(
|
|
||||||
get(selectedAutomation),
|
|
||||||
newBlock
|
|
||||||
)
|
|
||||||
await store.actions.save(newAutomation)
|
|
||||||
},
|
|
||||||
deleteAutomationBlock: async block => {
|
deleteAutomationBlock: async block => {
|
||||||
const automation = get(selectedAutomation)
|
const automation = get(selectedAutomation)
|
||||||
let newAutomation = cloneDeep(automation)
|
let newAutomation = cloneDeep(automation)
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
Detail,
|
Detail,
|
||||||
Modal,
|
Modal,
|
||||||
Button,
|
Button,
|
||||||
Select,
|
|
||||||
ActionButton,
|
ActionButton,
|
||||||
notifications,
|
notifications,
|
||||||
Label,
|
Label,
|
||||||
|
@ -39,9 +38,6 @@
|
||||||
step => step.stepId === ActionStepID.COLLECT
|
step => step.stepId === ActionStepID.COLLECT
|
||||||
)
|
)
|
||||||
$: automationId = $selectedAutomation?._id
|
$: automationId = $selectedAutomation?._id
|
||||||
$: showBindingPicker =
|
|
||||||
block.stepId === ActionStepID.CREATE_ROW ||
|
|
||||||
block.stepId === ActionStepID.UPDATE_ROW
|
|
||||||
$: isTrigger = block.type === "TRIGGER"
|
$: isTrigger = block.type === "TRIGGER"
|
||||||
$: steps = $selectedAutomation?.definition?.steps ?? []
|
$: steps = $selectedAutomation?.definition?.steps ?? []
|
||||||
$: blockIdx = steps.findIndex(step => step.id === block.id)
|
$: blockIdx = steps.findIndex(step => step.id === block.id)
|
||||||
|
@ -96,15 +92,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* "rowControl" appears to be the name of the flag used to determine whether
|
|
||||||
* a certain automation block uses values or bindings as inputs
|
|
||||||
*/
|
|
||||||
function toggleRowControl(evt) {
|
|
||||||
const rowControl = evt.detail !== "Use values"
|
|
||||||
automationStore.actions.toggleRowControl(block, rowControl)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addLooping() {
|
async function addLooping() {
|
||||||
const loopDefinition = $automationStore.blockDefinitions.ACTION.LOOP
|
const loopDefinition = $automationStore.blockDefinitions.ACTION.LOOP
|
||||||
const loopBlock = automationStore.actions.constructBlock(
|
const loopBlock = automationStore.actions.constructBlock(
|
||||||
|
@ -189,16 +176,6 @@
|
||||||
Add Looping
|
Add Looping
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
{/if}
|
{/if}
|
||||||
{#if showBindingPicker}
|
|
||||||
<Select
|
|
||||||
on:change={toggleRowControl}
|
|
||||||
defaultValue="Use values"
|
|
||||||
autoWidth
|
|
||||||
value={block.rowControl ? "Use bindings" : "Use values"}
|
|
||||||
options={["Use values", "Use bindings"]}
|
|
||||||
placeholder={null}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
<ActionButton
|
<ActionButton
|
||||||
on:click={() => deleteStep()}
|
on:click={() => deleteStep()}
|
||||||
icon="DeleteOutline"
|
icon="DeleteOutline"
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
import { environment, licensing } from "stores/portal"
|
import { environment, licensing } from "stores/portal"
|
||||||
import WebhookDisplay from "../Shared/WebhookDisplay.svelte"
|
import WebhookDisplay from "../Shared/WebhookDisplay.svelte"
|
||||||
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
|
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
|
||||||
|
import DrawerBindableSlot from "../../common/bindings/DrawerBindableSlot.svelte"
|
||||||
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
||||||
import CodeEditorModal from "./CodeEditorModal.svelte"
|
import CodeEditorModal from "./CodeEditorModal.svelte"
|
||||||
import QuerySelector from "./QuerySelector.svelte"
|
import QuerySelector from "./QuerySelector.svelte"
|
||||||
|
@ -82,33 +83,6 @@
|
||||||
? [hbAutocomplete([...bindingsToCompletions(bindings, codeMode)])]
|
? [hbAutocomplete([...bindingsToCompletions(bindings, codeMode)])]
|
||||||
: []
|
: []
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO - Remove after November 2023
|
|
||||||
* *******************************
|
|
||||||
* Code added to provide backwards compatibility between Values 1,2,3,4,5
|
|
||||||
* and the new JSON body.
|
|
||||||
*/
|
|
||||||
let deprecatedSchemaProperties
|
|
||||||
$: {
|
|
||||||
if (block?.stepId === "integromat" || block?.stepId === "zapier") {
|
|
||||||
deprecatedSchemaProperties = schemaProperties.filter(
|
|
||||||
prop => !prop[0].startsWith("value")
|
|
||||||
)
|
|
||||||
if (!deprecatedSchemaProperties.map(entry => entry[0]).includes("body")) {
|
|
||||||
deprecatedSchemaProperties.push([
|
|
||||||
"body",
|
|
||||||
{
|
|
||||||
title: "Payload",
|
|
||||||
type: "json",
|
|
||||||
},
|
|
||||||
])
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
deprecatedSchemaProperties = schemaProperties
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/****************************************************/
|
|
||||||
|
|
||||||
const getInputData = (testData, blockInputs) => {
|
const getInputData = (testData, blockInputs) => {
|
||||||
// Test data is not cloned for reactivity
|
// Test data is not cloned for reactivity
|
||||||
let newInputData = testData || cloneDeep(blockInputs)
|
let newInputData = testData || cloneDeep(blockInputs)
|
||||||
|
@ -118,30 +92,6 @@
|
||||||
newInputData = cloneDeep(blockInputs)
|
newInputData = cloneDeep(blockInputs)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO - Remove after November 2023
|
|
||||||
* *******************************
|
|
||||||
* Code added to provide backwards compatibility between Values 1,2,3,4,5
|
|
||||||
* and the new JSON body.
|
|
||||||
*/
|
|
||||||
if (
|
|
||||||
(block?.stepId === "integromat" || block?.stepId === "zapier") &&
|
|
||||||
!newInputData?.body?.value
|
|
||||||
) {
|
|
||||||
let deprecatedValues = {
|
|
||||||
...newInputData,
|
|
||||||
}
|
|
||||||
delete deprecatedValues.url
|
|
||||||
delete deprecatedValues.body
|
|
||||||
newInputData = {
|
|
||||||
url: newInputData.url,
|
|
||||||
body: {
|
|
||||||
value: JSON.stringify(deprecatedValues),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**********************************/
|
|
||||||
|
|
||||||
inputData = newInputData
|
inputData = newInputData
|
||||||
setDefaultEnumValues()
|
setDefaultEnumValues()
|
||||||
}
|
}
|
||||||
|
@ -337,7 +287,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
{#each deprecatedSchemaProperties as [key, value]}
|
{#each schemaProperties as [key, value]}
|
||||||
{#if canShowField(key, value)}
|
{#if canShowField(key, value)}
|
||||||
<div class="block-field">
|
<div class="block-field">
|
||||||
{#if key !== "fields" && value.type !== "boolean"}
|
{#if key !== "fields" && value.type !== "boolean"}
|
||||||
|
@ -362,18 +312,6 @@
|
||||||
mode="json"
|
mode="json"
|
||||||
value={inputData[key]?.value}
|
value={inputData[key]?.value}
|
||||||
on:change={e => {
|
on:change={e => {
|
||||||
/**
|
|
||||||
* TODO - Remove after November 2023
|
|
||||||
* *******************************
|
|
||||||
* Code added to provide backwards compatibility between Values 1,2,3,4,5
|
|
||||||
* and the new JSON body.
|
|
||||||
*/
|
|
||||||
delete inputData.value1
|
|
||||||
delete inputData.value2
|
|
||||||
delete inputData.value3
|
|
||||||
delete inputData.value4
|
|
||||||
delete inputData.value5
|
|
||||||
/***********************/
|
|
||||||
onChange(e, key)
|
onChange(e, key)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -386,10 +324,23 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else if value.type === "date"}
|
{:else if value.type === "date"}
|
||||||
<DatePicker
|
<DrawerBindableSlot
|
||||||
|
fillWidth
|
||||||
|
title={value.title}
|
||||||
|
panel={AutomationBindingPanel}
|
||||||
|
type={"date"}
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange(e, key)}
|
||||||
/>
|
{bindings}
|
||||||
|
allowJS={true}
|
||||||
|
updateOnChange={false}
|
||||||
|
drawerLeft="260px"
|
||||||
|
>
|
||||||
|
<DatePicker
|
||||||
|
value={inputData[key]}
|
||||||
|
on:change={e => onChange(e, key)}
|
||||||
|
/>
|
||||||
|
</DrawerBindableSlot>
|
||||||
{:else if value.customType === "column"}
|
{:else if value.customType === "column"}
|
||||||
<Select
|
<Select
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange(e, key)}
|
||||||
|
@ -469,7 +420,6 @@
|
||||||
/>
|
/>
|
||||||
{:else if value.customType === "row"}
|
{:else if value.customType === "row"}
|
||||||
<RowSelector
|
<RowSelector
|
||||||
{block}
|
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
meta={inputData["meta"] || {}}
|
meta={inputData["meta"] || {}}
|
||||||
on:change={e => {
|
on:change={e => {
|
||||||
|
|
|
@ -1,18 +1,16 @@
|
||||||
<script>
|
<script>
|
||||||
import { tables } from "stores/backend"
|
import { tables } from "stores/backend"
|
||||||
import { Select, Checkbox } from "@budibase/bbui"
|
import { Select, Checkbox } from "@budibase/bbui"
|
||||||
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
|
|
||||||
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import RowSelectorTypes from "./RowSelectorTypes.svelte"
|
import RowSelectorTypes from "./RowSelectorTypes.svelte"
|
||||||
import ModalBindableInput from "../../common/bindings/ModalBindableInput.svelte"
|
import DrawerBindableSlot from "../../common/bindings/DrawerBindableSlot.svelte"
|
||||||
|
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
export let meta
|
export let meta
|
||||||
export let bindings
|
export let bindings
|
||||||
export let block
|
|
||||||
export let isTestModal
|
export let isTestModal
|
||||||
export let isUpdateRow
|
export let isUpdateRow
|
||||||
|
|
||||||
|
@ -25,16 +23,6 @@
|
||||||
let table
|
let table
|
||||||
let schemaFields
|
let schemaFields
|
||||||
|
|
||||||
let placeholders = {
|
|
||||||
number: 10,
|
|
||||||
boolean: "true",
|
|
||||||
datetime: "2022-02-16T12:00:00.000Z ",
|
|
||||||
options: "1",
|
|
||||||
array: "1 2 3 4",
|
|
||||||
link: "ro_ta_123_456",
|
|
||||||
longform: "long form text",
|
|
||||||
}
|
|
||||||
$: rowControl = block.rowControl
|
|
||||||
$: {
|
$: {
|
||||||
table = $tables.list.find(table => table._id === value?.tableId)
|
table = $tables.list.find(table => table._id === value?.tableId)
|
||||||
schemaFields = Object.entries(table?.schema ?? {})
|
schemaFields = Object.entries(table?.schema ?? {})
|
||||||
|
@ -57,19 +45,13 @@
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === "boolean") {
|
|
||||||
if (typeof value === "boolean") {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
return value === "true"
|
|
||||||
}
|
|
||||||
if (type === "number") {
|
if (type === "number") {
|
||||||
if (typeof value === "number") {
|
if (typeof value === "number") {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
return Number(value)
|
return Number(value)
|
||||||
}
|
}
|
||||||
if (type === "options") {
|
if (type === "options" || type === "boolean") {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
if (type === "array") {
|
if (type === "array") {
|
||||||
|
@ -127,9 +109,31 @@
|
||||||
{#if schemaFields.length}
|
{#if schemaFields.length}
|
||||||
<div class="schema-fields">
|
<div class="schema-fields">
|
||||||
{#each schemaFields as [field, schema]}
|
{#each schemaFields as [field, schema]}
|
||||||
{#if !schema.autocolumn}
|
{#if !schema.autocolumn && schema.type !== "attachment"}
|
||||||
{#if schema.type !== "attachment"}
|
{#if isTestModal}
|
||||||
{#if !rowControl}
|
<RowSelectorTypes
|
||||||
|
{isTestModal}
|
||||||
|
{field}
|
||||||
|
{schema}
|
||||||
|
bindings={parsedBindings}
|
||||||
|
{value}
|
||||||
|
{onChange}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<DrawerBindableSlot
|
||||||
|
fillWidth
|
||||||
|
title={value.title}
|
||||||
|
label={field}
|
||||||
|
panel={AutomationBindingPanel}
|
||||||
|
type={schema.type}
|
||||||
|
{schema}
|
||||||
|
value={value[field]}
|
||||||
|
on:change={e => onChange(e, field)}
|
||||||
|
{bindings}
|
||||||
|
allowJS={true}
|
||||||
|
updateOnChange={false}
|
||||||
|
drawerLeft="260px"
|
||||||
|
>
|
||||||
<RowSelectorTypes
|
<RowSelectorTypes
|
||||||
{isTestModal}
|
{isTestModal}
|
||||||
{field}
|
{field}
|
||||||
|
@ -138,37 +142,19 @@
|
||||||
{value}
|
{value}
|
||||||
{onChange}
|
{onChange}
|
||||||
/>
|
/>
|
||||||
{:else}
|
</DrawerBindableSlot>
|
||||||
<div>
|
|
||||||
<svelte:component
|
|
||||||
this={isTestModal ? ModalBindableInput : DrawerBindableInput}
|
|
||||||
placeholder={placeholders[schema.type]}
|
|
||||||
panel={AutomationBindingPanel}
|
|
||||||
value={Array.isArray(value[field])
|
|
||||||
? value[field].join(",")
|
|
||||||
: value[field]}
|
|
||||||
on:change={e => onChange(e, field, schema.type)}
|
|
||||||
label={field}
|
|
||||||
type="string"
|
|
||||||
bindings={parsedBindings}
|
|
||||||
fillWidth={true}
|
|
||||||
allowJS={true}
|
|
||||||
updateOnChange={false}
|
|
||||||
/>
|
|
||||||
{#if isUpdateRow && schema.type === "link"}
|
|
||||||
<div class="checkbox-field">
|
|
||||||
<Checkbox
|
|
||||||
value={meta.fields?.[field]?.clearRelationships}
|
|
||||||
text={"Clear relationships if empty?"}
|
|
||||||
size={"S"}
|
|
||||||
on:change={e => onChangeSetting(e, field)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if isUpdateRow && schema.type === "link"}
|
||||||
|
<div class="checkbox-field">
|
||||||
|
<Checkbox
|
||||||
|
value={meta.fields?.[field]?.clearRelationships}
|
||||||
|
text={"Clear relationships if empty?"}
|
||||||
|
size={"S"}
|
||||||
|
on:change={e => onChangeSetting(e, field)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
Toggle,
|
|
||||||
DatePicker,
|
DatePicker,
|
||||||
Multiselect,
|
Multiselect,
|
||||||
TextArea,
|
TextArea,
|
||||||
|
@ -45,10 +44,14 @@
|
||||||
on:change={e => onChange(e, field)}
|
on:change={e => onChange(e, field)}
|
||||||
/>
|
/>
|
||||||
{:else if schema.type === "boolean"}
|
{:else if schema.type === "boolean"}
|
||||||
<Toggle
|
<Select
|
||||||
text={field}
|
|
||||||
value={value[field]}
|
|
||||||
on:change={e => onChange(e, field)}
|
on:change={e => onChange(e, field)}
|
||||||
|
label={field}
|
||||||
|
value={value[field]}
|
||||||
|
options={[
|
||||||
|
{ label: "True", value: "true" },
|
||||||
|
{ label: "False", value: "false" },
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
{:else if schema.type === "array"}
|
{:else if schema.type === "array"}
|
||||||
<Multiselect
|
<Multiselect
|
||||||
|
|
|
@ -13,7 +13,13 @@
|
||||||
let modal
|
let modal
|
||||||
|
|
||||||
$: tempValue = filters || []
|
$: tempValue = filters || []
|
||||||
$: schemaFields = Object.values(schema || {})
|
$: schemaFields = Object.entries(schema || {}).map(
|
||||||
|
([fieldName, fieldSchema]) => ({
|
||||||
|
name: fieldName, // Using the key as name if not defined in the schema, for example in some autogenerated columns
|
||||||
|
...fieldSchema,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
$: text = getText(filters)
|
$: text = getText(filters)
|
||||||
$: selected = tempValue.filter(x => !x.onEmptyFilter)?.length > 0
|
$: selected = tempValue.filter(x => !x.onEmptyFilter)?.length > 0
|
||||||
|
|
||||||
|
|
|
@ -85,7 +85,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
let relationshipMap = {
|
let relationshipMap = {
|
||||||
[RelationshipType.MANY_TO_ONE]: {
|
[RelationshipType.ONE_TO_MANY]: {
|
||||||
part1: PrettyRelationshipDefinitions.MANY,
|
part1: PrettyRelationshipDefinitions.MANY,
|
||||||
part2: PrettyRelationshipDefinitions.ONE,
|
part2: PrettyRelationshipDefinitions.ONE,
|
||||||
},
|
},
|
||||||
|
@ -93,7 +93,7 @@
|
||||||
part1: PrettyRelationshipDefinitions.MANY,
|
part1: PrettyRelationshipDefinitions.MANY,
|
||||||
part2: PrettyRelationshipDefinitions.MANY,
|
part2: PrettyRelationshipDefinitions.MANY,
|
||||||
},
|
},
|
||||||
[RelationshipType.ONE_TO_MANY]: {
|
[RelationshipType.MANY_TO_ONE]: {
|
||||||
part1: PrettyRelationshipDefinitions.ONE,
|
part1: PrettyRelationshipDefinitions.ONE,
|
||||||
part2: PrettyRelationshipDefinitions.MANY,
|
part2: PrettyRelationshipDefinitions.MANY,
|
||||||
},
|
},
|
||||||
|
@ -277,10 +277,7 @@
|
||||||
dispatch("updatecolumns")
|
dispatch("updatecolumns")
|
||||||
gridDispatch("close-edit-column")
|
gridDispatch("close-edit-column")
|
||||||
|
|
||||||
if (
|
if (saveColumn.type === LINK_TYPE) {
|
||||||
saveColumn.type === LINK_TYPE &&
|
|
||||||
saveColumn.relationshipType === RelationshipType.MANY_TO_MANY
|
|
||||||
) {
|
|
||||||
// Fetching the new tables
|
// Fetching the new tables
|
||||||
tables.fetch()
|
tables.fetch()
|
||||||
// Fetching the new relationships
|
// Fetching the new relationships
|
||||||
|
@ -312,6 +309,11 @@
|
||||||
confirmDeleteDialog.hide()
|
confirmDeleteDialog.hide()
|
||||||
dispatch("updatecolumns")
|
dispatch("updatecolumns")
|
||||||
gridDispatch("close-edit-column")
|
gridDispatch("close-edit-column")
|
||||||
|
|
||||||
|
if (editableColumn.type === LINK_TYPE) {
|
||||||
|
// Updating the relationships
|
||||||
|
datasources.fetch()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error(`Error deleting column: ${error.message}`)
|
notifications.error(`Error deleting column: ${error.message}`)
|
||||||
|
@ -658,7 +660,8 @@
|
||||||
>Open schema editor</Button
|
>Open schema editor</Button
|
||||||
>
|
>
|
||||||
{:else if editableColumn.type === USER_REFRENCE_TYPE}
|
{:else if editableColumn.type === USER_REFRENCE_TYPE}
|
||||||
<Toggle
|
<!-- Disabled temporally -->
|
||||||
|
<!-- <Toggle
|
||||||
value={editableColumn.relationshipType === RelationshipType.MANY_TO_MANY}
|
value={editableColumn.relationshipType === RelationshipType.MANY_TO_MANY}
|
||||||
on:change={e =>
|
on:change={e =>
|
||||||
(editableColumn.relationshipType = e.detail
|
(editableColumn.relationshipType = e.detail
|
||||||
|
@ -667,7 +670,7 @@
|
||||||
disabled={!isCreating}
|
disabled={!isCreating}
|
||||||
thin
|
thin
|
||||||
text="Allow multiple users"
|
text="Allow multiple users"
|
||||||
/>
|
/> -->
|
||||||
{/if}
|
{/if}
|
||||||
{#if editableColumn.type === AUTO_TYPE || editableColumn.autocolumn}
|
{#if editableColumn.type === AUTO_TYPE || editableColumn.autocolumn}
|
||||||
<Select
|
<Select
|
||||||
|
|
|
@ -57,7 +57,8 @@
|
||||||
label: table.name,
|
label: table.name,
|
||||||
value: table._id,
|
value: table._id,
|
||||||
}))
|
}))
|
||||||
$: valid = getErrorCount(errors) === 0 && allRequiredAttributesSet()
|
$: valid =
|
||||||
|
getErrorCount(errors) === 0 && allRequiredAttributesSet(relationshipType)
|
||||||
$: isManyToMany = relationshipType === RelationshipType.MANY_TO_MANY
|
$: isManyToMany = relationshipType === RelationshipType.MANY_TO_MANY
|
||||||
$: isManyToOne = relationshipType === RelationshipType.MANY_TO_ONE
|
$: isManyToOne = relationshipType === RelationshipType.MANY_TO_ONE
|
||||||
|
|
||||||
|
@ -114,7 +115,7 @@
|
||||||
return Object.entries(errors).filter(entry => !!entry[1]).length
|
return Object.entries(errors).filter(entry => !!entry[1]).length
|
||||||
}
|
}
|
||||||
|
|
||||||
function allRequiredAttributesSet() {
|
function allRequiredAttributesSet(relationshipType) {
|
||||||
const base = getTable(fromId) && getTable(toId) && fromColumn && toColumn
|
const base = getTable(fromId) && getTable(toId) && fromColumn && toColumn
|
||||||
if (relationshipType === RelationshipType.MANY_TO_ONE) {
|
if (relationshipType === RelationshipType.MANY_TO_ONE) {
|
||||||
return base && fromPrimary && fromForeign
|
return base && fromPrimary && fromForeign
|
||||||
|
@ -124,9 +125,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function validate() {
|
function validate() {
|
||||||
if (!allRequiredAttributesSet() && !hasValidated) {
|
if (!allRequiredAttributesSet(relationshipType) && !hasValidated) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
hasValidated = true
|
hasValidated = true
|
||||||
errorChecker.setType(relationshipType)
|
errorChecker.setType(relationshipType)
|
||||||
const fromTable = getTable(fromId),
|
const fromTable = getTable(fromId),
|
||||||
|
|
|
@ -11,11 +11,11 @@
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
let rows = []
|
let rows = []
|
||||||
let linkedIds = (Array.isArray(linkedRows) ? linkedRows : [])?.map(
|
let linkedIds = []
|
||||||
|
|
||||||
|
$: linkedIds = (Array.isArray(linkedRows) ? linkedRows : [])?.map(
|
||||||
row => row?._id || row
|
row => row?._id || row
|
||||||
)
|
)
|
||||||
|
|
||||||
$: linkedRows = linkedIds
|
|
||||||
$: label = capitalise(schema.name)
|
$: label = capitalise(schema.name)
|
||||||
$: linkedTableId = schema.tableId
|
$: linkedTableId = schema.tableId
|
||||||
$: linkedTable = $tables.list.find(table => table._id === linkedTableId)
|
$: linkedTable = $tables.list.find(table => table._id === linkedTableId)
|
||||||
|
|
|
@ -0,0 +1,250 @@
|
||||||
|
<script>
|
||||||
|
import { Icon, Input, Drawer, Button } from "@budibase/bbui"
|
||||||
|
import {
|
||||||
|
readableToRuntimeBinding,
|
||||||
|
runtimeToReadableBinding,
|
||||||
|
} from "builderStore/dataBinding"
|
||||||
|
|
||||||
|
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
||||||
|
import { createEventDispatcher, setContext } from "svelte"
|
||||||
|
import { isJSBinding } from "@budibase/string-templates"
|
||||||
|
|
||||||
|
export let panel = ClientBindingPanel
|
||||||
|
export let value = ""
|
||||||
|
export let bindings = []
|
||||||
|
export let title = "Bindings"
|
||||||
|
export let placeholder
|
||||||
|
export let label
|
||||||
|
export let disabled = false
|
||||||
|
export let fillWidth
|
||||||
|
export let allowJS = true
|
||||||
|
export let allowHelpers = true
|
||||||
|
export let updateOnChange = true
|
||||||
|
export let drawerLeft
|
||||||
|
export let type
|
||||||
|
export let schema
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
let bindingDrawer
|
||||||
|
let valid = true
|
||||||
|
let currentVal = value
|
||||||
|
|
||||||
|
$: readableValue = runtimeToReadableBinding(bindings, value)
|
||||||
|
$: tempValue = readableValue
|
||||||
|
$: isJS = isJSBinding(value)
|
||||||
|
|
||||||
|
const saveBinding = () => {
|
||||||
|
onChange(tempValue)
|
||||||
|
onBlur()
|
||||||
|
bindingDrawer.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
setContext("binding-drawer-actions", {
|
||||||
|
save: saveBinding,
|
||||||
|
})
|
||||||
|
|
||||||
|
const onChange = value => {
|
||||||
|
if (type === "link" && value && hasValidLinks(value)) {
|
||||||
|
currentVal = value.split(",")
|
||||||
|
} else if (type === "array" && value && hasValidOptions(value)) {
|
||||||
|
currentVal = value.split(",")
|
||||||
|
} else {
|
||||||
|
currentVal = readableToRuntimeBinding(bindings, value)
|
||||||
|
}
|
||||||
|
dispatch("change", currentVal)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onBlur = () => {
|
||||||
|
dispatch("blur", currentVal)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValidDate = value => {
|
||||||
|
return !value || !isNaN(new Date(value).valueOf())
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasValidLinks = value => {
|
||||||
|
let links = []
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
links = value
|
||||||
|
} else if (value && typeof value === "string") {
|
||||||
|
links = value.split(",")
|
||||||
|
} else {
|
||||||
|
return !value
|
||||||
|
}
|
||||||
|
|
||||||
|
return links.every(link => link.startsWith("ro_"))
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasValidOptions = value => {
|
||||||
|
let links = []
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
links = value
|
||||||
|
} else if (value && typeof value === "string") {
|
||||||
|
links = value.split(",")
|
||||||
|
} else {
|
||||||
|
return !value
|
||||||
|
}
|
||||||
|
return links.every(link => schema?.constraints?.inclusion?.includes(link))
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValidBoolean = value => {
|
||||||
|
return value === "false" || value === "true" || value == ""
|
||||||
|
}
|
||||||
|
|
||||||
|
const validationMap = {
|
||||||
|
date: isValidDate,
|
||||||
|
datetime: isValidDate,
|
||||||
|
link: hasValidLinks,
|
||||||
|
array: hasValidOptions,
|
||||||
|
longform: value => !isJSBinding(value),
|
||||||
|
json: value => !isJSBinding(value),
|
||||||
|
boolean: isValidBoolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = value => {
|
||||||
|
const validate = validationMap[type]
|
||||||
|
return validate ? validate(value) : true
|
||||||
|
}
|
||||||
|
|
||||||
|
const getIconClass = (value, type) => {
|
||||||
|
if (type === "longform" && !isJSBinding(value)) {
|
||||||
|
return "text-area-slot-icon"
|
||||||
|
}
|
||||||
|
if (type === "json" && !isJSBinding(value)) {
|
||||||
|
return "json-slot-icon"
|
||||||
|
}
|
||||||
|
if (type !== "string" && type !== "number") {
|
||||||
|
return "slot-icon"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="control" class:disabled>
|
||||||
|
{#if !isValid(value)}
|
||||||
|
<Input
|
||||||
|
{label}
|
||||||
|
{disabled}
|
||||||
|
readonly={isJS}
|
||||||
|
value={isJS ? "(JavaScript function)" : readableValue}
|
||||||
|
on:change={event => onChange(event.detail)}
|
||||||
|
on:blur={onBlur}
|
||||||
|
{placeholder}
|
||||||
|
{updateOnChange}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="icon"
|
||||||
|
on:click={() => {
|
||||||
|
if (!isJS) {
|
||||||
|
dispatch("change", "")
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon disabled={isJS} size="S" name="Close" />
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<slot
|
||||||
|
{label}
|
||||||
|
{disabled}
|
||||||
|
readonly={isJS}
|
||||||
|
value={isJS ? "(JavaScript function)" : readableValue}
|
||||||
|
{placeholder}
|
||||||
|
{updateOnChange}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{#if !disabled && type !== "formula"}
|
||||||
|
<div
|
||||||
|
class={`icon ${getIconClass(value, type)}`}
|
||||||
|
on:click={() => {
|
||||||
|
bindingDrawer.show()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon size="S" name="FlashOn" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<Drawer
|
||||||
|
on:drawerHide
|
||||||
|
on:drawerShow
|
||||||
|
{fillWidth}
|
||||||
|
bind:this={bindingDrawer}
|
||||||
|
{title}
|
||||||
|
left={drawerLeft}
|
||||||
|
headless
|
||||||
|
>
|
||||||
|
<svelte:fragment slot="description">
|
||||||
|
Add the objects on the left to enrich your text.
|
||||||
|
</svelte:fragment>
|
||||||
|
<Button cta slot="buttons" disabled={!valid} on:click={saveBinding}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<svelte:component
|
||||||
|
this={panel}
|
||||||
|
slot="body"
|
||||||
|
bind:valid
|
||||||
|
value={readableValue}
|
||||||
|
on:change={event => (tempValue = event.detail)}
|
||||||
|
{bindings}
|
||||||
|
{allowJS}
|
||||||
|
{allowHelpers}
|
||||||
|
/>
|
||||||
|
</Drawer>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.control {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slot-icon {
|
||||||
|
right: 31px !important;
|
||||||
|
border-right: 1px solid var(--spectrum-alias-border-color);
|
||||||
|
border-top-right-radius: 0px !important;
|
||||||
|
border-bottom-right-radius: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-area-slot-icon {
|
||||||
|
border-bottom: 1px solid var(--spectrum-alias-border-color);
|
||||||
|
border-bottom-right-radius: 0px !important;
|
||||||
|
top: 26px !important;
|
||||||
|
}
|
||||||
|
.json-slot-icon {
|
||||||
|
border-bottom: 1px solid var(--spectrum-alias-border-color);
|
||||||
|
border-bottom-right-radius: 0px !important;
|
||||||
|
top: 23px !important;
|
||||||
|
right: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
right: 1px;
|
||||||
|
bottom: 1px;
|
||||||
|
position: absolute;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-left: 1px solid var(--spectrum-alias-border-color);
|
||||||
|
border-top-right-radius: var(--spectrum-alias-border-radius-regular);
|
||||||
|
border-bottom-right-radius: var(--spectrum-alias-border-radius-regular);
|
||||||
|
width: 31px;
|
||||||
|
color: var(--spectrum-alias-text-color);
|
||||||
|
background-color: var(--spectrum-global-color-gray-75);
|
||||||
|
transition: background-color
|
||||||
|
var(--spectrum-global-animation-duration-100, 130ms),
|
||||||
|
box-shadow var(--spectrum-global-animation-duration-100, 130ms),
|
||||||
|
border-color var(--spectrum-global-animation-duration-100, 130ms);
|
||||||
|
height: calc(var(--spectrum-alias-item-height-m) - 2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--spectrum-alias-text-color-hover);
|
||||||
|
background-color: var(--spectrum-global-color-gray-50);
|
||||||
|
border-color: var(--spectrum-alias-border-color-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control:not(.disabled) :global(.spectrum-Textfield-input) {
|
||||||
|
padding-right: 40px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -3,21 +3,24 @@
|
||||||
Body,
|
Body,
|
||||||
Button,
|
Button,
|
||||||
Combobox,
|
Combobox,
|
||||||
Multiselect,
|
|
||||||
DatePicker,
|
DatePicker,
|
||||||
DrawerContent,
|
DrawerContent,
|
||||||
Icon,
|
Icon,
|
||||||
Input,
|
Input,
|
||||||
Layout,
|
|
||||||
Select,
|
|
||||||
Label,
|
Label,
|
||||||
|
Layout,
|
||||||
|
Multiselect,
|
||||||
|
Select,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||||
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
||||||
import { generate } from "shortid"
|
import { generate } from "shortid"
|
||||||
import { LuceneUtils, Constants } from "@budibase/frontend-core"
|
import { Constants, LuceneUtils } from "@budibase/frontend-core"
|
||||||
import { getFields } from "helpers/searchFields"
|
import { getFields } from "helpers/searchFields"
|
||||||
|
import { FieldType } from "@budibase/types"
|
||||||
import { createEventDispatcher, onMount } from "svelte"
|
import { createEventDispatcher, onMount } from "svelte"
|
||||||
|
import FilterUsers from "./FilterUsers.svelte"
|
||||||
|
import { RelationshipType } from "constants/backend"
|
||||||
|
|
||||||
export let schemaFields
|
export let schemaFields
|
||||||
export let filters = []
|
export let filters = []
|
||||||
|
@ -29,7 +32,6 @@
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const { OperatorOptions } = Constants
|
const { OperatorOptions } = Constants
|
||||||
const { getValidOperatorsForType } = LuceneUtils
|
|
||||||
const KeyedFieldRegex = /\d[0-9]*:/g
|
const KeyedFieldRegex = /\d[0-9]*:/g
|
||||||
const behaviourOptions = [
|
const behaviourOptions = [
|
||||||
{ value: "and", label: "Match all filters" },
|
{ value: "and", label: "Match all filters" },
|
||||||
|
@ -120,7 +122,7 @@
|
||||||
return enrichedSchemaFields.find(field => field.name === filter.field)
|
return enrichedSchemaFields.find(field => field.name === filter.field)
|
||||||
}
|
}
|
||||||
|
|
||||||
const santizeTypes = filter => {
|
const sanitizeTypes = filter => {
|
||||||
// Update type based on field
|
// Update type based on field
|
||||||
const fieldSchema = enrichedSchemaFields.find(x => x.name === filter.field)
|
const fieldSchema = enrichedSchemaFields.find(x => x.name === filter.field)
|
||||||
filter.type = fieldSchema?.type
|
filter.type = fieldSchema?.type
|
||||||
|
@ -129,13 +131,9 @@
|
||||||
filter.externalType = getSchema(filter)?.externalType
|
filter.externalType = getSchema(filter)?.externalType
|
||||||
}
|
}
|
||||||
|
|
||||||
const santizeOperator = filter => {
|
const sanitizeOperator = filter => {
|
||||||
// Ensure a valid operator is selected
|
// Ensure a valid operator is selected
|
||||||
const operators = getValidOperatorsForType(
|
const operators = getValidOperatorsForType(filter).map(x => x.value)
|
||||||
filter.type,
|
|
||||||
filter.field,
|
|
||||||
datasource
|
|
||||||
).map(x => x.value)
|
|
||||||
if (!operators.includes(filter.operator)) {
|
if (!operators.includes(filter.operator)) {
|
||||||
filter.operator = operators[0] ?? OperatorOptions.Equals.value
|
filter.operator = operators[0] ?? OperatorOptions.Equals.value
|
||||||
}
|
}
|
||||||
|
@ -148,7 +146,7 @@
|
||||||
filter.noValue = noValueOptions.includes(filter.operator)
|
filter.noValue = noValueOptions.includes(filter.operator)
|
||||||
}
|
}
|
||||||
|
|
||||||
const santizeValue = filter => {
|
const sanitizeValue = (filter, previousType) => {
|
||||||
// Check if the operator allows a value at all
|
// Check if the operator allows a value at all
|
||||||
if (filter.noValue) {
|
if (filter.noValue) {
|
||||||
filter.value = null
|
filter.value = null
|
||||||
|
@ -162,28 +160,47 @@
|
||||||
}
|
}
|
||||||
} else if (filter.type === "array" && filter.valueType === "Value") {
|
} else if (filter.type === "array" && filter.valueType === "Value") {
|
||||||
filter.value = []
|
filter.value = []
|
||||||
|
} else if (
|
||||||
|
previousType !== filter.type &&
|
||||||
|
(previousType === FieldType.BB_REFERENCE ||
|
||||||
|
filter.type === FieldType.BB_REFERENCE)
|
||||||
|
) {
|
||||||
|
filter.value = filter.type === "array" ? [] : null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onFieldChange = filter => {
|
const onFieldChange = filter => {
|
||||||
santizeTypes(filter)
|
const previousType = filter.type
|
||||||
santizeOperator(filter)
|
sanitizeTypes(filter)
|
||||||
santizeValue(filter)
|
sanitizeOperator(filter)
|
||||||
|
sanitizeValue(filter, previousType)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onOperatorChange = filter => {
|
const onOperatorChange = filter => {
|
||||||
santizeOperator(filter)
|
sanitizeOperator(filter)
|
||||||
santizeValue(filter)
|
sanitizeValue(filter, filter.type)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onValueTypeChange = filter => {
|
const onValueTypeChange = filter => {
|
||||||
santizeValue(filter)
|
sanitizeValue(filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFieldOptions = field => {
|
const getFieldOptions = field => {
|
||||||
const schema = enrichedSchemaFields.find(x => x.name === field)
|
const schema = enrichedSchemaFields.find(x => x.name === field)
|
||||||
return schema?.constraints?.inclusion || []
|
return schema?.constraints?.inclusion || []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getValidOperatorsForType = filter => {
|
||||||
|
if (!filter?.field) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return LuceneUtils.getValidOperatorsForType(
|
||||||
|
filter.type,
|
||||||
|
filter.field,
|
||||||
|
datasource
|
||||||
|
)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DrawerContent>
|
<DrawerContent>
|
||||||
|
@ -228,11 +245,7 @@
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
disabled={!filter.field}
|
disabled={!filter.field}
|
||||||
options={getValidOperatorsForType(
|
options={getValidOperatorsForType(filter)}
|
||||||
filter.type,
|
|
||||||
filter.field,
|
|
||||||
datasource
|
|
||||||
)}
|
|
||||||
bind:value={filter.operator}
|
bind:value={filter.operator}
|
||||||
on:change={() => onOperatorChange(filter)}
|
on:change={() => onOperatorChange(filter)}
|
||||||
placeholder={null}
|
placeholder={null}
|
||||||
|
@ -285,6 +298,14 @@
|
||||||
timeOnly={getSchema(filter)?.timeOnly}
|
timeOnly={getSchema(filter)?.timeOnly}
|
||||||
bind:value={filter.value}
|
bind:value={filter.value}
|
||||||
/>
|
/>
|
||||||
|
{:else if filter.type === FieldType.BB_REFERENCE}
|
||||||
|
<FilterUsers
|
||||||
|
bind:value={filter.value}
|
||||||
|
multiselect={getSchema(filter).relationshipType ===
|
||||||
|
RelationshipType.MANY_TO_MANY ||
|
||||||
|
filter.operator === OperatorOptions.In.value}
|
||||||
|
disabled={filter.noValue}
|
||||||
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<DrawerBindableInput disabled />
|
<DrawerBindableInput disabled />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
<script>
|
||||||
|
import { Select, Multiselect } from "@budibase/bbui"
|
||||||
|
import { fetchData } from "@budibase/frontend-core"
|
||||||
|
|
||||||
|
import { API } from "api"
|
||||||
|
|
||||||
|
export let value = null
|
||||||
|
export let disabled
|
||||||
|
export let multiselect = false
|
||||||
|
|
||||||
|
$: fetch = fetchData({
|
||||||
|
API,
|
||||||
|
datasource: {
|
||||||
|
type: "user",
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
limit: 100,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
$: options = $fetch.rows
|
||||||
|
|
||||||
|
$: component = multiselect ? Multiselect : Select
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:component
|
||||||
|
this={component}
|
||||||
|
bind:value
|
||||||
|
autocomplete
|
||||||
|
{options}
|
||||||
|
getOptionLabel={option => option.email}
|
||||||
|
getOptionValue={option => option._id}
|
||||||
|
{disabled}
|
||||||
|
/>
|
|
@ -20,7 +20,9 @@
|
||||||
|
|
||||||
const getSortableFields = schema => {
|
const getSortableFields = schema => {
|
||||||
return Object.entries(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])
|
.map(entry => entry[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -62,7 +62,14 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="truncate">
|
<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>
|
</div>
|
||||||
<div class="right">
|
<div class="right">
|
||||||
|
|
|
@ -21,15 +21,22 @@
|
||||||
function getRelationships(tables) {
|
function getRelationships(tables) {
|
||||||
const relatedColumns = {}
|
const relatedColumns = {}
|
||||||
|
|
||||||
tables.forEach(({ name: tableName, schema }) => {
|
tables.forEach(({ name: tableName, schema, _id: tableId }) => {
|
||||||
Object.values(schema).forEach(column => {
|
Object.values(schema).forEach(column => {
|
||||||
if (column.type !== "link") return
|
if (column.type !== "link") return
|
||||||
|
|
||||||
relatedColumns[column._id] ??= {}
|
const columnId =
|
||||||
relatedColumns[column._id].through =
|
column.through ||
|
||||||
relatedColumns[column._id].through || column.through
|
column._id ||
|
||||||
|
(column.main
|
||||||
|
? `${tableId}_${column.fieldName}__${column.tableId}_${column.foreignKey}`
|
||||||
|
: `${column.tableId}_${column.foreignKey}__${tableId}_${column.fieldName}`)
|
||||||
|
|
||||||
relatedColumns[column._id][column.main ? "from" : "to"] = {
|
relatedColumns[columnId] ??= {}
|
||||||
|
relatedColumns[columnId].through =
|
||||||
|
relatedColumns[columnId].through || column.through
|
||||||
|
|
||||||
|
relatedColumns[columnId][column.main ? "from" : "to"] = {
|
||||||
...column,
|
...column,
|
||||||
tableName,
|
tableName,
|
||||||
}
|
}
|
||||||
|
|
|
@ -136,6 +136,7 @@ export function createDatasourcesStore() {
|
||||||
config,
|
config,
|
||||||
name: `${integration.friendlyName}${nameModifier}`,
|
name: `${integration.friendlyName}${nameModifier}`,
|
||||||
plus: integration.plus && integration.name !== IntegrationTypes.REST,
|
plus: integration.plus && integration.name !== IntegrationTypes.REST,
|
||||||
|
isSQL: integration.isSQL,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await checkDatasourceValidity(integration, datasource)) {
|
if (await checkDatasourceValidity(integration, datasource)) {
|
||||||
|
|
|
@ -57,7 +57,8 @@ export async function checkDockerConfigured() {
|
||||||
"docker/docker-compose has not been installed, please follow instructions at: https://docs.budibase.com/docs/docker-compose"
|
"docker/docker-compose has not been installed, please follow instructions at: https://docs.budibase.com/docs/docker-compose"
|
||||||
const docker = await lookpath("docker")
|
const docker = await lookpath("docker")
|
||||||
const compose = await lookpath("docker-compose")
|
const compose = await lookpath("docker-compose")
|
||||||
if (!docker || !compose) {
|
const composeV2 = await lookpath("docker compose")
|
||||||
|
if (!docker || (!compose && !composeV2)) {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,10 @@ if (!process.argv[0].includes("node")) {
|
||||||
checkForBinaries()
|
checkForBinaries()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function localPrebuildPath() {
|
||||||
|
return join(process.execPath, "..", PREBUILDS)
|
||||||
|
}
|
||||||
|
|
||||||
function checkForBinaries() {
|
function checkForBinaries() {
|
||||||
const readDir = join(__filename, "..", "..", "..", "cli", PREBUILDS, ARCH)
|
const readDir = join(__filename, "..", "..", "..", "cli", PREBUILDS, ARCH)
|
||||||
if (fs.existsSync(PREBUILD_DIR) || !fs.existsSync(readDir)) {
|
if (fs.existsSync(PREBUILD_DIR) || !fs.existsSync(readDir)) {
|
||||||
|
@ -19,17 +23,21 @@ function checkForBinaries() {
|
||||||
}
|
}
|
||||||
const natives = fs.readdirSync(readDir)
|
const natives = fs.readdirSync(readDir)
|
||||||
if (fs.existsSync(readDir)) {
|
if (fs.existsSync(readDir)) {
|
||||||
const writePath = join(process.execPath, PREBUILDS, ARCH)
|
const writePath = join(localPrebuildPath(), ARCH)
|
||||||
fs.mkdirSync(writePath, { recursive: true })
|
fs.mkdirSync(writePath, { recursive: true })
|
||||||
for (let native of natives) {
|
for (let native of natives) {
|
||||||
const filename = `${native.split(".fake")[0]}.node`
|
const filename = `${native.split(".fake")[0]}.node`
|
||||||
fs.cpSync(join(readDir, native), join(writePath, filename))
|
fs.cpSync(join(readDir, native), join(writePath, filename))
|
||||||
}
|
}
|
||||||
console.log("copied something")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanup(evt?: number) {
|
function cleanup(evt?: number) {
|
||||||
|
// cleanup prebuilds first
|
||||||
|
const path = localPrebuildPath()
|
||||||
|
if (fs.existsSync(path)) {
|
||||||
|
fs.rmSync(path, { recursive: true })
|
||||||
|
}
|
||||||
if (evt && !isNaN(evt)) {
|
if (evt && !isNaN(evt)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -41,10 +49,6 @@ function cleanup(evt?: number) {
|
||||||
)
|
)
|
||||||
console.error(error(evt))
|
console.error(error(evt))
|
||||||
}
|
}
|
||||||
const path = join(process.execPath, PREBUILDS)
|
|
||||||
if (fs.existsSync(path)) {
|
|
||||||
fs.rmSync(path, { recursive: true })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const events = ["exit", "SIGINT", "SIGUSR1", "SIGUSR2", "uncaughtException"]
|
const events = ["exit", "SIGINT", "SIGUSR1", "SIGUSR2", "uncaughtException"]
|
||||||
|
|
|
@ -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",
|
"type": "boolean",
|
||||||
"label": "Add rows",
|
"label": "Add rows",
|
||||||
|
|
|
@ -14,12 +14,14 @@
|
||||||
export let initialSortOrder = null
|
export let initialSortOrder = null
|
||||||
export let fixedRowHeight = null
|
export let fixedRowHeight = null
|
||||||
export let columns = null
|
export let columns = null
|
||||||
|
export let onRowClick = null
|
||||||
|
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
const { styleable, API, builderStore, notificationStore } = getContext("sdk")
|
const { styleable, API, builderStore, notificationStore } = getContext("sdk")
|
||||||
|
|
||||||
$: columnWhitelist = columns?.map(col => col.name)
|
$: columnWhitelist = columns?.map(col => col.name)
|
||||||
$: schemaOverrides = getSchemaOverrides(columns)
|
$: schemaOverrides = getSchemaOverrides(columns)
|
||||||
|
$: handleRowClick = allowEditRows ? undefined : onRowClick
|
||||||
|
|
||||||
const getSchemaOverrides = columns => {
|
const getSchemaOverrides = columns => {
|
||||||
let overrides = {}
|
let overrides = {}
|
||||||
|
@ -56,6 +58,7 @@
|
||||||
showControls={false}
|
showControls={false}
|
||||||
notifySuccess={notificationStore.actions.success}
|
notifySuccess={notificationStore.actions.success}
|
||||||
notifyError={notificationStore.actions.error}
|
notifyError={notificationStore.actions.error}
|
||||||
|
on:rowclick={e => handleRowClick?.({ row: e.detail })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
import { writable } from "svelte/store"
|
import { writable } from "svelte/store"
|
||||||
import { Heading, Icon, clickOutside } from "@budibase/bbui"
|
import { Heading, Icon, clickOutside } from "@budibase/bbui"
|
||||||
import { FieldTypes } from "constants"
|
import { FieldTypes } from "constants"
|
||||||
|
import { Constants } from "@budibase/frontend-core"
|
||||||
import active from "svelte-spa-router/active"
|
import active from "svelte-spa-router/active"
|
||||||
|
|
||||||
const sdk = getContext("sdk")
|
const sdk = getContext("sdk")
|
||||||
|
@ -103,7 +104,8 @@
|
||||||
let validLinks = (allLinks || []).filter(link => link.text && link.url)
|
let validLinks = (allLinks || []).filter(link => link.text && link.url)
|
||||||
// Filter to only links allowed by the current role
|
// Filter to only links allowed by the current role
|
||||||
return validLinks.filter(link => {
|
return validLinks.filter(link => {
|
||||||
return userRoleHierarchy?.find(roleId => roleId === link.roleId)
|
const role = link.roleId || Constants.Roles.BASIC
|
||||||
|
return userRoleHierarchy?.find(roleId => roleId === role)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -60,6 +60,12 @@
|
||||||
// even if they are not in the inital fetch results
|
// even if they are not in the inital fetch results
|
||||||
initialValuesProcessed = true
|
initialValuesProcessed = true
|
||||||
optionsObj = (fieldState?.value || []).reduce((accumulator, value) => {
|
optionsObj = (fieldState?.value || []).reduce((accumulator, value) => {
|
||||||
|
// fieldState has to be an array of strings to be valid for an update
|
||||||
|
// therefore we cannot guarantee value will be an object
|
||||||
|
// https://linear.app/budibase/issue/BUDI-7577/refactor-the-relationshipfield-component-to-have-better-support-for
|
||||||
|
if (!value._id) {
|
||||||
|
return accumulator
|
||||||
|
}
|
||||||
accumulator[value._id] = {
|
accumulator[value._id] = {
|
||||||
_id: value._id,
|
_id: value._id,
|
||||||
[primaryDisplay]: value.primaryDisplay,
|
[primaryDisplay]: value.primaryDisplay,
|
||||||
|
|
|
@ -25,7 +25,6 @@
|
||||||
value: roleId,
|
value: roleId,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
devToolsStore.actions.changeRole(SELF_ROLE)
|
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { createLocalStorageStore } from "@budibase/frontend-core"
|
||||||
import { initialise } from "./initialise"
|
import { initialise } from "./initialise"
|
||||||
import { authStore } from "./auth"
|
import { authStore } from "./auth"
|
||||||
import { API } from "../api"
|
import { API } from "../api"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
visible: false,
|
visible: false,
|
||||||
|
@ -27,9 +28,15 @@ const createDevToolStore = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const changeRole = async role => {
|
const changeRole = async role => {
|
||||||
|
if (role === "self") {
|
||||||
|
role = null
|
||||||
|
}
|
||||||
|
if (role === get(store).role) {
|
||||||
|
return
|
||||||
|
}
|
||||||
store.update(state => ({
|
store.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
role: role === "self" ? null : role,
|
role,
|
||||||
}))
|
}))
|
||||||
API.invalidateCache()
|
API.invalidateCache()
|
||||||
await authStore.actions.fetchUser()
|
await authStore.actions.fetchUser()
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
import RelationshipCell from "./RelationshipCell.svelte"
|
import RelationshipCell from "./RelationshipCell.svelte"
|
||||||
import { FieldSubtype } from "@budibase/types"
|
import { FieldSubtype } from "@budibase/types"
|
||||||
|
|
||||||
|
export let api
|
||||||
|
|
||||||
const { API } = getContext("grid")
|
const { API } = getContext("grid")
|
||||||
const { subtype } = $$props.schema
|
const { subtype } = $$props.schema
|
||||||
|
|
||||||
|
@ -17,8 +19,11 @@
|
||||||
throw `Search for '${subtype}' not implemented`
|
throw `Search for '${subtype}' not implemented`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// As we are overriding the search function from RelationshipCell, we want to map one shape to the expected one for the specific API
|
||||||
|
const email = Object.values(searchParams.query.string)[0]
|
||||||
|
|
||||||
const results = await API.searchUsers({
|
const results = await API.searchUsers({
|
||||||
...searchParams,
|
email,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Mapping to the expected data within RelationshipCell
|
// Mapping to the expected data within RelationshipCell
|
||||||
|
@ -31,6 +36,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<RelationshipCell
|
<RelationshipCell
|
||||||
|
bind:api
|
||||||
{...$$props}
|
{...$$props}
|
||||||
{schema}
|
{schema}
|
||||||
{searchFunction}
|
{searchFunction}
|
||||||
|
|
|
@ -17,13 +17,24 @@
|
||||||
const { config, dispatch, selectedRows } = getContext("grid")
|
const { config, dispatch, selectedRows } = getContext("grid")
|
||||||
const svelteDispatch = createEventDispatcher()
|
const svelteDispatch = createEventDispatcher()
|
||||||
|
|
||||||
const select = () => {
|
const select = e => {
|
||||||
|
e.stopPropagation()
|
||||||
svelteDispatch("select")
|
svelteDispatch("select")
|
||||||
const id = row?._id
|
const id = row?._id
|
||||||
if (id) {
|
if (id) {
|
||||||
selectedRows.actions.toggleRow(id)
|
selectedRows.actions.toggleRow(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bulkDelete = e => {
|
||||||
|
e.stopPropagation()
|
||||||
|
dispatch("request-bulk-delete")
|
||||||
|
}
|
||||||
|
|
||||||
|
const expand = e => {
|
||||||
|
e.stopPropagation()
|
||||||
|
svelteDispatch("expand")
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<GridCell
|
<GridCell
|
||||||
|
@ -56,7 +67,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{#if rowSelected && $config.canDeleteRows}
|
{#if rowSelected && $config.canDeleteRows}
|
||||||
<div class="delete" on:click={() => dispatch("request-bulk-delete")}>
|
<div class="delete" on:click={bulkDelete}>
|
||||||
<Icon
|
<Icon
|
||||||
name="Delete"
|
name="Delete"
|
||||||
size="S"
|
size="S"
|
||||||
|
@ -65,12 +76,7 @@
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="expand" class:visible={$config.canExpandRows && expandable}>
|
<div class="expand" class:visible={$config.canExpandRows && expandable}>
|
||||||
<Icon
|
<Icon size="S" name="Maximize" hoverable on:click={expand} />
|
||||||
size="S"
|
|
||||||
name="Maximize"
|
|
||||||
hoverable
|
|
||||||
on:click={() => svelteDispatch("expand")}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
columnHorizontalInversionIndex,
|
columnHorizontalInversionIndex,
|
||||||
contentLines,
|
contentLines,
|
||||||
isDragging,
|
isDragging,
|
||||||
|
dispatch,
|
||||||
} = getContext("grid")
|
} = getContext("grid")
|
||||||
|
|
||||||
$: rowSelected = !!$selectedRows[row._id]
|
$: rowSelected = !!$selectedRows[row._id]
|
||||||
|
@ -30,6 +31,7 @@
|
||||||
on:focus
|
on:focus
|
||||||
on:mouseenter={$isDragging ? null : () => ($hoveredRowId = row._id)}
|
on:mouseenter={$isDragging ? null : () => ($hoveredRowId = row._id)}
|
||||||
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
|
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
|
||||||
|
on:click={() => dispatch("rowclick", row)}
|
||||||
>
|
>
|
||||||
{#each $renderedColumns as column, columnIdx (column.name)}
|
{#each $renderedColumns as column, columnIdx (column.name)}
|
||||||
{@const cellId = `${row._id}-${column.name}`}
|
{@const cellId = `${row._id}-${column.name}`}
|
||||||
|
|
|
@ -74,6 +74,7 @@
|
||||||
class="row"
|
class="row"
|
||||||
on:mouseenter={$isDragging ? null : () => ($hoveredRowId = row._id)}
|
on:mouseenter={$isDragging ? null : () => ($hoveredRowId = row._id)}
|
||||||
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
|
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
|
||||||
|
on:click={() => dispatch("rowclick", row)}
|
||||||
>
|
>
|
||||||
<GutterCell {row} {rowFocused} {rowHovered} {rowSelected} />
|
<GutterCell {row} {rowFocused} {rowHovered} {rowSelected} />
|
||||||
{#if $stickyColumn}
|
{#if $stickyColumn}
|
||||||
|
|
|
@ -19,7 +19,7 @@ docker run --rm \
|
||||||
-v ${PWD}/generated:/generated \
|
-v ${PWD}/generated:/generated \
|
||||||
-v ${PWD}/config.json:/config.json \
|
-v ${PWD}/config.json:/config.json \
|
||||||
-u $(id -u):$(id -g) \
|
-u $(id -u):$(id -g) \
|
||||||
swaggerapi/swagger-codegen-cli-v3 generate \
|
swaggerapi/swagger-codegen-cli-v3:3.0.46 generate \
|
||||||
-i /openapi.yml \
|
-i /openapi.yml \
|
||||||
-l javascript \
|
-l javascript \
|
||||||
-o /generated \
|
-o /generated \
|
||||||
|
|
|
@ -2,7 +2,7 @@ version: "3.8"
|
||||||
services:
|
services:
|
||||||
db:
|
db:
|
||||||
container_name: postgres
|
container_name: postgres
|
||||||
image: postgres
|
image: postgres:15
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: root
|
POSTGRES_USER: root
|
||||||
|
@ -25,4 +25,4 @@ services:
|
||||||
- "5050:80"
|
- "5050:80"
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pg_data:
|
pg_data:
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { quotas } from "@budibase/pro"
|
||||||
import { events, context, utils, constants } from "@budibase/backend-core"
|
import { events, context, utils, constants } from "@budibase/backend-core"
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
import { QueryEvent } from "../../../threads/definitions"
|
import { QueryEvent } from "../../../threads/definitions"
|
||||||
import { Query } from "@budibase/types"
|
import { ConfigType, Query, UserCtx } from "@budibase/types"
|
||||||
import { ValidQueryNameRegex } from "@budibase/shared-core"
|
import { ValidQueryNameRegex } from "@budibase/shared-core"
|
||||||
|
|
||||||
const Runner = new Thread(ThreadType.QUERY, {
|
const Runner = new Thread(ThreadType.QUERY, {
|
||||||
|
@ -28,11 +28,11 @@ function enrichQueries(input: any) {
|
||||||
return wasArray ? queries : queries[0]
|
return wasArray ? queries : queries[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetch(ctx: any) {
|
export async function fetch(ctx: UserCtx) {
|
||||||
ctx.body = await sdk.queries.fetch()
|
ctx.body = await sdk.queries.fetch()
|
||||||
}
|
}
|
||||||
|
|
||||||
const _import = async (ctx: any) => {
|
const _import = async (ctx: UserCtx) => {
|
||||||
const body = ctx.request.body
|
const body = ctx.request.body
|
||||||
const data = body.data
|
const data = body.data
|
||||||
|
|
||||||
|
@ -73,7 +73,7 @@ const _import = async (ctx: any) => {
|
||||||
}
|
}
|
||||||
export { _import as import }
|
export { _import as import }
|
||||||
|
|
||||||
export async function save(ctx: any) {
|
export async function save(ctx: UserCtx) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const query = ctx.request.body
|
const query = ctx.request.body
|
||||||
|
|
||||||
|
@ -100,19 +100,19 @@ export async function save(ctx: any) {
|
||||||
ctx.message = `Query ${query.name} saved successfully.`
|
ctx.message = `Query ${query.name} saved successfully.`
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function find(ctx: any) {
|
export async function find(ctx: UserCtx) {
|
||||||
const queryId = ctx.params.queryId
|
const queryId = ctx.params.queryId
|
||||||
ctx.body = await sdk.queries.find(queryId)
|
ctx.body = await sdk.queries.find(queryId)
|
||||||
}
|
}
|
||||||
|
|
||||||
//Required to discern between OIDC OAuth config entries
|
//Required to discern between OIDC OAuth config entries
|
||||||
function getOAuthConfigCookieId(ctx: any) {
|
function getOAuthConfigCookieId(ctx: UserCtx) {
|
||||||
if (ctx.user.providerType === constants.Config.OIDC) {
|
if (ctx.user.providerType === ConfigType.OIDC) {
|
||||||
return utils.getCookie(ctx, constants.Cookie.OIDC_CONFIG)
|
return utils.getCookie(ctx, constants.Cookie.OIDC_CONFIG)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAuthConfig(ctx: any) {
|
function getAuthConfig(ctx: UserCtx) {
|
||||||
const authCookie = utils.getCookie(ctx, constants.Cookie.Auth)
|
const authCookie = utils.getCookie(ctx, constants.Cookie.Auth)
|
||||||
let authConfigCtx: any = {}
|
let authConfigCtx: any = {}
|
||||||
authConfigCtx["configId"] = getOAuthConfigCookieId(ctx)
|
authConfigCtx["configId"] = getOAuthConfigCookieId(ctx)
|
||||||
|
@ -120,7 +120,7 @@ function getAuthConfig(ctx: any) {
|
||||||
return authConfigCtx
|
return authConfigCtx
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function preview(ctx: any) {
|
export async function preview(ctx: UserCtx) {
|
||||||
const { datasource, envVars } = await sdk.datasources.getWithEnvVars(
|
const { datasource, envVars } = await sdk.datasources.getWithEnvVars(
|
||||||
ctx.request.body.datasourceId
|
ctx.request.body.datasourceId
|
||||||
)
|
)
|
||||||
|
@ -129,6 +129,19 @@ export async function preview(ctx: any) {
|
||||||
// this stops dynamic variables from calling the same query
|
// this stops dynamic variables from calling the same query
|
||||||
const { fields, parameters, queryVerb, transformer, queryId, schema } = query
|
const { fields, parameters, queryVerb, transformer, queryId, schema } = query
|
||||||
|
|
||||||
|
let existingSchema = schema
|
||||||
|
if (queryId && !existingSchema) {
|
||||||
|
try {
|
||||||
|
const db = context.getAppDB()
|
||||||
|
const existing = (await db.get(queryId)) as Query
|
||||||
|
existingSchema = existing.schema
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err.status !== 404) {
|
||||||
|
ctx.throw(500, "Unable to retrieve existing query")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const authConfigCtx: any = getAuthConfig(ctx)
|
const authConfigCtx: any = getAuthConfig(ctx)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -180,6 +193,14 @@ export async function preview(ctx: any) {
|
||||||
schemaFields[key] = fieldType
|
schemaFields[key] = fieldType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// if existing schema, update to include any previous schema keys
|
||||||
|
if (existingSchema) {
|
||||||
|
for (let key of Object.keys(schemaFields)) {
|
||||||
|
if (existingSchema[key]?.type) {
|
||||||
|
schemaFields[key] = existingSchema[key].type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// remove configuration before sending event
|
// remove configuration before sending event
|
||||||
delete datasource.config
|
delete datasource.config
|
||||||
await events.query.previewed(datasource, query)
|
await events.query.previewed(datasource, query)
|
||||||
|
@ -189,13 +210,13 @@ export async function preview(ctx: any) {
|
||||||
info,
|
info,
|
||||||
extra,
|
extra,
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
ctx.throw(400, err)
|
ctx.throw(400, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function execute(
|
async function execute(
|
||||||
ctx: any,
|
ctx: UserCtx,
|
||||||
opts: any = { rowsOnly: false, isAutomation: false }
|
opts: any = { rowsOnly: false, isAutomation: false }
|
||||||
) {
|
) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
|
@ -255,17 +276,17 @@ async function execute(
|
||||||
} else {
|
} else {
|
||||||
ctx.body = { data: rows, pagination, ...extra, ...info }
|
ctx.body = { data: rows, pagination, ...extra, ...info }
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
ctx.throw(400, err)
|
ctx.throw(400, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function executeV1(ctx: any) {
|
export async function executeV1(ctx: UserCtx) {
|
||||||
return execute(ctx, { rowsOnly: true, isAutomation: false })
|
return execute(ctx, { rowsOnly: true, isAutomation: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function executeV2(
|
export async function executeV2(
|
||||||
ctx: any,
|
ctx: UserCtx,
|
||||||
{ isAutomation }: { isAutomation?: boolean } = {}
|
{ isAutomation }: { isAutomation?: boolean } = {}
|
||||||
) {
|
) {
|
||||||
return execute(ctx, { rowsOnly: false, isAutomation })
|
return execute(ctx, { rowsOnly: false, isAutomation })
|
||||||
|
@ -292,7 +313,7 @@ const removeDynamicVariables = async (queryId: any) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function destroy(ctx: any) {
|
export async function destroy(ctx: UserCtx) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const queryId = ctx.params.queryId
|
const queryId = ctx.params.queryId
|
||||||
await removeDynamicVariables(queryId)
|
await removeDynamicVariables(queryId)
|
||||||
|
|
|
@ -340,10 +340,16 @@ export class ExternalRequest<T extends Operation> {
|
||||||
// one to many
|
// one to many
|
||||||
if (isOneSide(field)) {
|
if (isOneSide(field)) {
|
||||||
let id = row[key][0]
|
let id = row[key][0]
|
||||||
if (typeof row[key] === "string") {
|
if (id) {
|
||||||
id = decodeURIComponent(row[key]).match(/\[(.*?)\]/)?.[1]
|
if (typeof row[key] === "string") {
|
||||||
|
id = decodeURIComponent(row[key]).match(/\[(.*?)\]/)?.[1]
|
||||||
|
}
|
||||||
|
newRow[field.foreignKey || linkTablePrimary] = breakRowIdField(id)[0]
|
||||||
|
} else {
|
||||||
|
// Removing from both new and row, as we don't know if it has already been processed
|
||||||
|
row[field.foreignKey || linkTablePrimary] = null
|
||||||
|
newRow[field.foreignKey || linkTablePrimary] = null
|
||||||
}
|
}
|
||||||
newRow[field.foreignKey || linkTablePrimary] = breakRowIdField(id)[0]
|
|
||||||
}
|
}
|
||||||
// many to many
|
// many to many
|
||||||
else if (field.through) {
|
else if (field.through) {
|
||||||
|
@ -830,10 +836,7 @@ export class ExternalRequest<T extends Operation> {
|
||||||
// can't really use response right now
|
// can't really use response right now
|
||||||
const response = await getDatasourceAndQuery(json)
|
const response = await getDatasourceAndQuery(json)
|
||||||
// handle many to many relationships now if we know the ID (could be auto increment)
|
// handle many to many relationships now if we know the ID (could be auto increment)
|
||||||
if (
|
if (operation !== Operation.READ) {
|
||||||
operation !== Operation.READ &&
|
|
||||||
processed.manyRelationships?.length > 0
|
|
||||||
) {
|
|
||||||
await this.handleManyRelationships(
|
await this.handleManyRelationships(
|
||||||
table._id || "",
|
table._id || "",
|
||||||
response[0],
|
response[0],
|
||||||
|
|
|
@ -108,13 +108,11 @@ export async function save(ctx: UserCtx) {
|
||||||
row,
|
row,
|
||||||
})
|
})
|
||||||
|
|
||||||
const responseRow = response as { row: Row }
|
|
||||||
|
|
||||||
if (!isEqual(table, updatedTable)) {
|
if (!isEqual(table, updatedTable)) {
|
||||||
await sdk.tables.saveTable(updatedTable)
|
await sdk.tables.saveTable(updatedTable)
|
||||||
}
|
}
|
||||||
|
|
||||||
const rowId = responseRow.row._id
|
const rowId = response.row._id
|
||||||
if (rowId) {
|
if (rowId) {
|
||||||
const row = await sdk.rows.external.getRow(tableId, rowId, {
|
const row = await sdk.rows.external.getRow(tableId, rowId, {
|
||||||
relationships: true,
|
relationships: true,
|
||||||
|
|
|
@ -14,7 +14,6 @@ import {
|
||||||
Table,
|
Table,
|
||||||
TableResponse,
|
TableResponse,
|
||||||
UserCtx,
|
UserCtx,
|
||||||
Datasource,
|
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
import { jsonFromCsvString } from "../../../utilities/csv"
|
import { jsonFromCsvString } from "../../../utilities/csv"
|
||||||
|
|
|
@ -18,7 +18,6 @@ import {
|
||||||
SortType,
|
SortType,
|
||||||
StaticQuotaName,
|
StaticQuotaName,
|
||||||
Table,
|
Table,
|
||||||
User,
|
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import {
|
import {
|
||||||
expectAnyExternalColsAttributes,
|
expectAnyExternalColsAttributes,
|
||||||
|
@ -1515,9 +1514,82 @@ describe.each([
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("bb reference fields", () => {
|
let o2mTable: Table
|
||||||
|
let m2mTable: Table
|
||||||
|
beforeAll(async () => {
|
||||||
|
o2mTable = await config.createTable(
|
||||||
|
{ ...generateTableConfig(), name: "o2m" },
|
||||||
|
{
|
||||||
|
skipReassigning: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
m2mTable = await config.createTable(
|
||||||
|
{ ...generateTableConfig(), name: "m2m" },
|
||||||
|
{
|
||||||
|
skipReassigning: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe.each([
|
||||||
|
[
|
||||||
|
"relationship fields",
|
||||||
|
() => ({
|
||||||
|
user: {
|
||||||
|
name: "user",
|
||||||
|
relationshipType: RelationshipType.ONE_TO_MANY,
|
||||||
|
type: FieldType.LINK,
|
||||||
|
tableId: o2mTable._id!,
|
||||||
|
fieldName: "fk_o2m",
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
name: "users",
|
||||||
|
relationshipType: RelationshipType.MANY_TO_MANY,
|
||||||
|
type: FieldType.LINK,
|
||||||
|
tableId: m2mTable._id!,
|
||||||
|
fieldName: "fk_m2m",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
(tableId: string) =>
|
||||||
|
config.api.row.save(tableId, {
|
||||||
|
name: generator.word(),
|
||||||
|
description: generator.paragraph(),
|
||||||
|
tableId,
|
||||||
|
}),
|
||||||
|
(row: Row) => ({
|
||||||
|
_id: row._id,
|
||||||
|
primaryDisplay: row.name,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"bb reference fields",
|
||||||
|
() => ({
|
||||||
|
user: {
|
||||||
|
name: "user",
|
||||||
|
relationshipType: RelationshipType.ONE_TO_MANY,
|
||||||
|
type: FieldType.BB_REFERENCE,
|
||||||
|
subtype: FieldTypeSubtypes.BB_REFERENCE.USER,
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
name: "users",
|
||||||
|
type: FieldType.BB_REFERENCE,
|
||||||
|
subtype: FieldTypeSubtypes.BB_REFERENCE.USER,
|
||||||
|
relationshipType: RelationshipType.MANY_TO_MANY,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
() => config.createUser(),
|
||||||
|
(row: Row) => ({
|
||||||
|
_id: row._id,
|
||||||
|
email: row.email,
|
||||||
|
firstName: row.firstName,
|
||||||
|
lastName: row.lastName,
|
||||||
|
primaryDisplay: row.email,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
])("links - %s", (__, relSchema, dataGenerator, resultMapper) => {
|
||||||
let tableId: string
|
let tableId: string
|
||||||
let users: User[]
|
let o2mData: Row[]
|
||||||
|
let m2mData: Row[]
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const tableConfig = generateTableConfig()
|
const tableConfig = generateTableConfig()
|
||||||
|
@ -1532,31 +1604,27 @@ describe.each([
|
||||||
...tableConfig,
|
...tableConfig,
|
||||||
schema: {
|
schema: {
|
||||||
...tableConfig.schema,
|
...tableConfig.schema,
|
||||||
user: {
|
...relSchema(),
|
||||||
name: "user",
|
|
||||||
type: FieldType.BB_REFERENCE,
|
|
||||||
subtype: FieldTypeSubtypes.BB_REFERENCE.USER,
|
|
||||||
relationshipType: RelationshipType.ONE_TO_MANY,
|
|
||||||
},
|
|
||||||
users: {
|
|
||||||
name: "users",
|
|
||||||
type: FieldType.BB_REFERENCE,
|
|
||||||
subtype: FieldTypeSubtypes.BB_REFERENCE.USER,
|
|
||||||
relationshipType: RelationshipType.MANY_TO_MANY,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
tableId = table._id!
|
tableId = table._id!
|
||||||
|
|
||||||
users = [
|
o2mData = [
|
||||||
await config.createUser(),
|
await dataGenerator(o2mTable._id!),
|
||||||
await config.createUser(),
|
await dataGenerator(o2mTable._id!),
|
||||||
await config.createUser(),
|
await dataGenerator(o2mTable._id!),
|
||||||
await config.createUser(),
|
await dataGenerator(o2mTable._id!),
|
||||||
|
]
|
||||||
|
|
||||||
|
m2mData = [
|
||||||
|
await dataGenerator(m2mTable._id!),
|
||||||
|
await dataGenerator(m2mTable._id!),
|
||||||
|
await dataGenerator(m2mTable._id!),
|
||||||
|
await dataGenerator(m2mTable._id!),
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
it("can save a row when BB reference fields are empty", async () => {
|
it("can save a row when relationship fields are empty", async () => {
|
||||||
const rowData = {
|
const rowData = {
|
||||||
...basicRow(tableId),
|
...basicRow(tableId),
|
||||||
name: generator.name(),
|
name: generator.name(),
|
||||||
|
@ -1575,13 +1643,13 @@ describe.each([
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("can save a row with a single BB reference field", async () => {
|
it("can save a row with a single relationship field", async () => {
|
||||||
const user = _.sample(users)!
|
const user = _.sample(o2mData)!
|
||||||
const rowData = {
|
const rowData = {
|
||||||
...basicRow(tableId),
|
...basicRow(tableId),
|
||||||
name: generator.name(),
|
name: generator.name(),
|
||||||
description: generator.name(),
|
description: generator.name(),
|
||||||
user: user,
|
user: [user],
|
||||||
}
|
}
|
||||||
const row = await config.api.row.save(tableId, rowData)
|
const row = await config.api.row.save(tableId, rowData)
|
||||||
|
|
||||||
|
@ -1589,24 +1657,17 @@ describe.each([
|
||||||
name: rowData.name,
|
name: rowData.name,
|
||||||
description: rowData.description,
|
description: rowData.description,
|
||||||
tableId,
|
tableId,
|
||||||
user: [
|
user: [user].map(u => resultMapper(u)),
|
||||||
{
|
|
||||||
_id: user._id,
|
|
||||||
email: user.email,
|
|
||||||
firstName: user.firstName,
|
|
||||||
lastName: user.lastName,
|
|
||||||
primaryDisplay: user.email,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
_id: expect.any(String),
|
_id: expect.any(String),
|
||||||
_rev: expect.any(String),
|
_rev: expect.any(String),
|
||||||
id: isInternal ? undefined : expect.any(Number),
|
id: isInternal ? undefined : expect.any(Number),
|
||||||
type: isInternal ? "row" : undefined,
|
type: isInternal ? "row" : undefined,
|
||||||
|
[`fk_${o2mTable.name}_fk_o2m`]: isInternal ? undefined : user.id,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("can save a row with a multiple BB reference field", async () => {
|
it("can save a row with a multiple relationship field", async () => {
|
||||||
const selectedUsers = _.sampleSize(users, 2)
|
const selectedUsers = _.sampleSize(m2mData, 2)
|
||||||
const rowData = {
|
const rowData = {
|
||||||
...basicRow(tableId),
|
...basicRow(tableId),
|
||||||
name: generator.name(),
|
name: generator.name(),
|
||||||
|
@ -1619,13 +1680,7 @@ describe.each([
|
||||||
name: rowData.name,
|
name: rowData.name,
|
||||||
description: rowData.description,
|
description: rowData.description,
|
||||||
tableId,
|
tableId,
|
||||||
users: selectedUsers.map(u => ({
|
users: expect.arrayContaining(selectedUsers.map(u => resultMapper(u))),
|
||||||
_id: u._id,
|
|
||||||
email: u.email,
|
|
||||||
firstName: u.firstName,
|
|
||||||
lastName: u.lastName,
|
|
||||||
primaryDisplay: u.email,
|
|
||||||
})),
|
|
||||||
_id: expect.any(String),
|
_id: expect.any(String),
|
||||||
_rev: expect.any(String),
|
_rev: expect.any(String),
|
||||||
id: isInternal ? undefined : expect.any(Number),
|
id: isInternal ? undefined : expect.any(Number),
|
||||||
|
@ -1633,7 +1688,7 @@ describe.each([
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("can retrieve rows with no populated BB references", async () => {
|
it("can retrieve rows with no populated relationships", async () => {
|
||||||
const rowData = {
|
const rowData = {
|
||||||
...basicRow(tableId),
|
...basicRow(tableId),
|
||||||
name: generator.name(),
|
name: generator.name(),
|
||||||
|
@ -1655,14 +1710,15 @@ describe.each([
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("can retrieve rows with populated BB references", async () => {
|
it("can retrieve rows with populated relationships", async () => {
|
||||||
const [user1, user2] = _.sampleSize(users, 2)
|
const user1 = _.sample(o2mData)!
|
||||||
|
const [user2, user3] = _.sampleSize(m2mData, 2)
|
||||||
|
|
||||||
const rowData = {
|
const rowData = {
|
||||||
...basicRow(tableId),
|
...basicRow(tableId),
|
||||||
name: generator.name(),
|
name: generator.name(),
|
||||||
description: generator.name(),
|
description: generator.name(),
|
||||||
users: [user1, user2],
|
users: [user2, user3],
|
||||||
user: [user1],
|
user: [user1],
|
||||||
}
|
}
|
||||||
const row = await config.api.row.save(tableId, rowData)
|
const row = await config.api.row.save(tableId, rowData)
|
||||||
|
@ -1672,72 +1728,51 @@ describe.each([
|
||||||
name: rowData.name,
|
name: rowData.name,
|
||||||
description: rowData.description,
|
description: rowData.description,
|
||||||
tableId,
|
tableId,
|
||||||
user: [user1].map(u => ({
|
user: expect.arrayContaining([user1].map(u => resultMapper(u))),
|
||||||
_id: u._id,
|
users: expect.arrayContaining([user2, user3].map(u => resultMapper(u))),
|
||||||
email: u.email,
|
|
||||||
firstName: u.firstName,
|
|
||||||
lastName: u.lastName,
|
|
||||||
primaryDisplay: u.email,
|
|
||||||
})),
|
|
||||||
users: [user1, user2].map(u => ({
|
|
||||||
_id: u._id,
|
|
||||||
email: u.email,
|
|
||||||
firstName: u.firstName,
|
|
||||||
lastName: u.lastName,
|
|
||||||
primaryDisplay: u.email,
|
|
||||||
})),
|
|
||||||
_id: row._id,
|
_id: row._id,
|
||||||
_rev: expect.any(String),
|
_rev: expect.any(String),
|
||||||
id: isInternal ? undefined : expect.any(Number),
|
id: isInternal ? undefined : expect.any(Number),
|
||||||
|
[`fk_${o2mTable.name}_fk_o2m`]: isInternal ? undefined : user1.id,
|
||||||
...defaultRowFields,
|
...defaultRowFields,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("can update an existing populated row", async () => {
|
it("can update an existing populated row", async () => {
|
||||||
const [user1, user2, user3] = _.sampleSize(users, 3)
|
const user = _.sample(o2mData)!
|
||||||
|
const [users1, users2, users3] = _.sampleSize(m2mData, 3)
|
||||||
|
|
||||||
const rowData = {
|
const rowData = {
|
||||||
...basicRow(tableId),
|
...basicRow(tableId),
|
||||||
name: generator.name(),
|
name: generator.name(),
|
||||||
description: generator.name(),
|
description: generator.name(),
|
||||||
users: [user1, user2],
|
users: [users1, users2],
|
||||||
}
|
}
|
||||||
const row = await config.api.row.save(tableId, rowData)
|
const row = await config.api.row.save(tableId, rowData)
|
||||||
|
|
||||||
const updatedRow = await config.api.row.save(tableId, {
|
const updatedRow = await config.api.row.save(tableId, {
|
||||||
...row,
|
...row,
|
||||||
user: [user3],
|
user: [user],
|
||||||
users: [user3, user2],
|
users: [users3, users1],
|
||||||
})
|
})
|
||||||
expect(updatedRow).toEqual({
|
expect(updatedRow).toEqual({
|
||||||
name: rowData.name,
|
name: rowData.name,
|
||||||
description: rowData.description,
|
description: rowData.description,
|
||||||
tableId,
|
tableId,
|
||||||
user: [
|
user: expect.arrayContaining([user].map(u => resultMapper(u))),
|
||||||
{
|
users: expect.arrayContaining(
|
||||||
_id: user3._id,
|
[users3, users1].map(u => resultMapper(u))
|
||||||
email: user3.email,
|
),
|
||||||
firstName: user3.firstName,
|
|
||||||
lastName: user3.lastName,
|
|
||||||
primaryDisplay: user3.email,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
users: [user3, user2].map(u => ({
|
|
||||||
_id: u._id,
|
|
||||||
email: u.email,
|
|
||||||
firstName: u.firstName,
|
|
||||||
lastName: u.lastName,
|
|
||||||
primaryDisplay: u.email,
|
|
||||||
})),
|
|
||||||
_id: row._id,
|
_id: row._id,
|
||||||
_rev: expect.any(String),
|
_rev: expect.any(String),
|
||||||
id: isInternal ? undefined : expect.any(Number),
|
id: isInternal ? undefined : expect.any(Number),
|
||||||
type: isInternal ? "row" : undefined,
|
type: isInternal ? "row" : undefined,
|
||||||
|
[`fk_${o2mTable.name}_fk_o2m`]: isInternal ? undefined : user.id,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("can wipe an existing populated BB references in row", async () => {
|
it("can wipe an existing populated relationships in row", async () => {
|
||||||
const [user1, user2] = _.sampleSize(users, 2)
|
const [user1, user2] = _.sampleSize(m2mData, 2)
|
||||||
|
|
||||||
const rowData = {
|
const rowData = {
|
||||||
...basicRow(tableId),
|
...basicRow(tableId),
|
||||||
|
@ -1756,8 +1791,6 @@ describe.each([
|
||||||
name: rowData.name,
|
name: rowData.name,
|
||||||
description: rowData.description,
|
description: rowData.description,
|
||||||
tableId,
|
tableId,
|
||||||
user: isInternal ? null : undefined,
|
|
||||||
users: isInternal ? null : undefined,
|
|
||||||
_id: row._id,
|
_id: row._id,
|
||||||
_rev: expect.any(String),
|
_rev: expect.any(String),
|
||||||
id: isInternal ? undefined : expect.any(Number),
|
id: isInternal ? undefined : expect.any(Number),
|
||||||
|
@ -1765,34 +1798,35 @@ describe.each([
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("fetch all will populate the BB references", async () => {
|
it("fetch all will populate the relationships", async () => {
|
||||||
const [user1, user2, user3] = _.sampleSize(users, 3)
|
const [user1] = _.sampleSize(o2mData, 1)
|
||||||
|
const [users1, users2, users3] = _.sampleSize(m2mData, 3)
|
||||||
|
|
||||||
const rows: {
|
const rows: {
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
user?: User[]
|
user?: Row[]
|
||||||
users?: User[]
|
users?: Row[]
|
||||||
tableId: string
|
tableId: string
|
||||||
}[] = [
|
}[] = [
|
||||||
{
|
{
|
||||||
...basicRow(tableId),
|
...basicRow(tableId),
|
||||||
name: generator.name(),
|
name: generator.name(),
|
||||||
description: generator.name(),
|
description: generator.name(),
|
||||||
users: [user1, user2],
|
users: [users1, users2],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...basicRow(tableId),
|
...basicRow(tableId),
|
||||||
name: generator.name(),
|
name: generator.name(),
|
||||||
description: generator.name(),
|
description: generator.name(),
|
||||||
user: [user1],
|
user: [user1],
|
||||||
users: [user1, user3],
|
users: [users1, users3],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...basicRow(tableId),
|
...basicRow(tableId),
|
||||||
name: generator.name(),
|
name: generator.name(),
|
||||||
description: generator.name(),
|
description: generator.name(),
|
||||||
users: [user3],
|
users: [users3],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1808,57 +1842,50 @@ describe.each([
|
||||||
name: r.name,
|
name: r.name,
|
||||||
description: r.description,
|
description: r.description,
|
||||||
tableId,
|
tableId,
|
||||||
user: r.user?.map(u => ({
|
user: r.user?.map(u => resultMapper(u)),
|
||||||
_id: u._id,
|
users: r.users?.length
|
||||||
email: u.email,
|
? expect.arrayContaining(r.users?.map(u => resultMapper(u)))
|
||||||
firstName: u.firstName,
|
: undefined,
|
||||||
lastName: u.lastName,
|
|
||||||
primaryDisplay: u.email,
|
|
||||||
})),
|
|
||||||
users: r.users?.map(u => ({
|
|
||||||
_id: u._id,
|
|
||||||
email: u.email,
|
|
||||||
firstName: u.firstName,
|
|
||||||
lastName: u.lastName,
|
|
||||||
primaryDisplay: u.email,
|
|
||||||
})),
|
|
||||||
_id: expect.any(String),
|
_id: expect.any(String),
|
||||||
_rev: expect.any(String),
|
_rev: expect.any(String),
|
||||||
id: isInternal ? undefined : expect.any(Number),
|
id: isInternal ? undefined : expect.any(Number),
|
||||||
|
[`fk_${o2mTable.name}_fk_o2m`]:
|
||||||
|
isInternal || !r.user?.length ? undefined : r.user[0].id,
|
||||||
...defaultRowFields,
|
...defaultRowFields,
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("search all will populate the BB references", async () => {
|
it("search all will populate the relationships", async () => {
|
||||||
const [user1, user2, user3] = _.sampleSize(users, 3)
|
const [user1] = _.sampleSize(o2mData, 1)
|
||||||
|
const [users1, users2, users3] = _.sampleSize(m2mData, 3)
|
||||||
|
|
||||||
const rows: {
|
const rows: {
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
user?: User[]
|
user?: Row[]
|
||||||
users?: User[]
|
users?: Row[]
|
||||||
tableId: string
|
tableId: string
|
||||||
}[] = [
|
}[] = [
|
||||||
{
|
{
|
||||||
...basicRow(tableId),
|
...basicRow(tableId),
|
||||||
name: generator.name(),
|
name: generator.name(),
|
||||||
description: generator.name(),
|
description: generator.name(),
|
||||||
users: [user1, user2],
|
users: [users1, users2],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...basicRow(tableId),
|
...basicRow(tableId),
|
||||||
name: generator.name(),
|
name: generator.name(),
|
||||||
description: generator.name(),
|
description: generator.name(),
|
||||||
user: [user1],
|
user: [user1],
|
||||||
users: [user1, user3],
|
users: [users1, users3],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...basicRow(tableId),
|
...basicRow(tableId),
|
||||||
name: generator.name(),
|
name: generator.name(),
|
||||||
description: generator.name(),
|
description: generator.name(),
|
||||||
users: [user3],
|
users: [users3],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1874,23 +1901,15 @@ describe.each([
|
||||||
name: r.name,
|
name: r.name,
|
||||||
description: r.description,
|
description: r.description,
|
||||||
tableId,
|
tableId,
|
||||||
user: r.user?.map(u => ({
|
user: r.user?.map(u => resultMapper(u)),
|
||||||
_id: u._id,
|
users: r.users?.length
|
||||||
email: u.email,
|
? expect.arrayContaining(r.users?.map(u => resultMapper(u)))
|
||||||
firstName: u.firstName,
|
: undefined,
|
||||||
lastName: u.lastName,
|
|
||||||
primaryDisplay: u.email,
|
|
||||||
})),
|
|
||||||
users: r.users?.map(u => ({
|
|
||||||
_id: u._id,
|
|
||||||
email: u.email,
|
|
||||||
firstName: u.firstName,
|
|
||||||
lastName: u.lastName,
|
|
||||||
primaryDisplay: u.email,
|
|
||||||
})),
|
|
||||||
_id: expect.any(String),
|
_id: expect.any(String),
|
||||||
_rev: expect.any(String),
|
_rev: expect.any(String),
|
||||||
id: isInternal ? undefined : expect.any(Number),
|
id: isInternal ? undefined : expect.any(Number),
|
||||||
|
[`fk_${o2mTable.name}_fk_o2m`]:
|
||||||
|
isInternal || !r.user?.length ? undefined : r.user[0].id,
|
||||||
...defaultRowFields,
|
...defaultRowFields,
|
||||||
}))
|
}))
|
||||||
),
|
),
|
||||||
|
|
|
@ -78,8 +78,7 @@ export const definition: AutomationStepSchema = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function run({ inputs }: AutomationStepInput) {
|
export async function run({ inputs }: AutomationStepInput) {
|
||||||
//TODO - Remove deprecated values 1,2,3,4,5 after November 2023
|
const { url, body } = inputs
|
||||||
const { url, value1, value2, value3, value4, value5, body } = inputs
|
|
||||||
|
|
||||||
let payload = {}
|
let payload = {}
|
||||||
try {
|
try {
|
||||||
|
@ -104,11 +103,6 @@ export async function run({ inputs }: AutomationStepInput) {
|
||||||
response = await fetch(url, {
|
response = await fetch(url, {
|
||||||
method: "post",
|
method: "post",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
value1,
|
|
||||||
value2,
|
|
||||||
value3,
|
|
||||||
value4,
|
|
||||||
value5,
|
|
||||||
...payload,
|
...payload,
|
||||||
}),
|
}),
|
||||||
headers: {
|
headers: {
|
||||||
|
|
|
@ -71,8 +71,7 @@ export const definition: AutomationStepSchema = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function run({ inputs }: AutomationStepInput) {
|
export async function run({ inputs }: AutomationStepInput) {
|
||||||
//TODO - Remove deprecated values 1,2,3,4,5 after November 2023
|
const { url, body } = inputs
|
||||||
const { url, value1, value2, value3, value4, value5, body } = inputs
|
|
||||||
|
|
||||||
let payload = {}
|
let payload = {}
|
||||||
try {
|
try {
|
||||||
|
@ -100,11 +99,6 @@ export async function run({ inputs }: AutomationStepInput) {
|
||||||
method: "post",
|
method: "post",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
platform: "budibase",
|
platform: "budibase",
|
||||||
value1,
|
|
||||||
value2,
|
|
||||||
value3,
|
|
||||||
value4,
|
|
||||||
value5,
|
|
||||||
...payload,
|
...payload,
|
||||||
}),
|
}),
|
||||||
headers: {
|
headers: {
|
||||||
|
|
|
@ -308,12 +308,19 @@ class LinkController {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
// remove schema from other table
|
try {
|
||||||
let linkedTable = await this._db.get<Table>(field.tableId)
|
// remove schema from other table, if it exists
|
||||||
if (field.fieldName) {
|
let linkedTable = await this._db.get<Table>(field.tableId)
|
||||||
delete linkedTable.schema[field.fieldName]
|
if (field.fieldName) {
|
||||||
|
delete linkedTable.schema[field.fieldName]
|
||||||
|
}
|
||||||
|
await this._db.put(linkedTable)
|
||||||
|
} catch (error: any) {
|
||||||
|
// ignore missing to ensure broken relationship columns can be deleted
|
||||||
|
if (error.statusCode !== 404) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await this._db.put(linkedTable)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -233,4 +233,19 @@ describe("test the link controller", () => {
|
||||||
}
|
}
|
||||||
await config.updateTable(table)
|
await config.updateTable(table)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should be able to remove a linked field from a table, even if the linked table does not exist", async () => {
|
||||||
|
await createLinkedRow()
|
||||||
|
await createLinkedRow("link2")
|
||||||
|
table1.schema["link"].tableId = "not_found"
|
||||||
|
const controller = await createLinkController(table1, null, table1)
|
||||||
|
await context.doInAppContext(appId, async () => {
|
||||||
|
let before = await controller.getTableLinkDocs()
|
||||||
|
await controller.removeFieldFromTable("link")
|
||||||
|
let after = await controller.getTableLinkDocs()
|
||||||
|
expect(before.length).toEqual(2)
|
||||||
|
// shouldn't delete the other field
|
||||||
|
expect(after.length).toEqual(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { cleanExportRows } from "../utils"
|
||||||
import { utils } from "@budibase/shared-core"
|
import { utils } from "@budibase/shared-core"
|
||||||
import { ExportRowsParams, ExportRowsResult } from "../search"
|
import { ExportRowsParams, ExportRowsResult } from "../search"
|
||||||
import { HTTPError, db } from "@budibase/backend-core"
|
import { HTTPError, db } from "@budibase/backend-core"
|
||||||
|
import { searchInputMapping } from "./utils"
|
||||||
import pick from "lodash/pick"
|
import pick from "lodash/pick"
|
||||||
import { outputProcessing } from "../../../../utilities/rowProcessor"
|
import { outputProcessing } from "../../../../utilities/rowProcessor"
|
||||||
|
|
||||||
|
@ -50,7 +51,10 @@ export async function search(options: SearchParams) {
|
||||||
[params.sort]: { direction },
|
[params.sort]: { direction },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const table = await sdk.tables.getTable(tableId)
|
||||||
|
options = searchInputMapping(table, options)
|
||||||
let rows = (await handleRequest(Operation.READ, tableId, {
|
let rows = (await handleRequest(Operation.READ, tableId, {
|
||||||
filters: query,
|
filters: query,
|
||||||
sort,
|
sort,
|
||||||
|
@ -76,7 +80,6 @@ export async function search(options: SearchParams) {
|
||||||
rows = rows.map((r: any) => pick(r, fields))
|
rows = rows.map((r: any) => pick(r, fields))
|
||||||
}
|
}
|
||||||
|
|
||||||
const table = await sdk.tables.getTable(tableId)
|
|
||||||
rows = await outputProcessing(table, rows, { preserveLinks: true })
|
rows = await outputProcessing(table, rows, { preserveLinks: true })
|
||||||
|
|
||||||
// need wrapper object for bookmarks etc when paginating
|
// need wrapper object for bookmarks etc when paginating
|
||||||
|
|
|
@ -29,6 +29,7 @@ import {
|
||||||
} from "../../../../api/controllers/view/utils"
|
} from "../../../../api/controllers/view/utils"
|
||||||
import sdk from "../../../../sdk"
|
import sdk from "../../../../sdk"
|
||||||
import { ExportRowsParams, ExportRowsResult } from "../search"
|
import { ExportRowsParams, ExportRowsResult } from "../search"
|
||||||
|
import { searchInputMapping } from "./utils"
|
||||||
import pick from "lodash/pick"
|
import pick from "lodash/pick"
|
||||||
|
|
||||||
export async function search(options: SearchParams) {
|
export async function search(options: SearchParams) {
|
||||||
|
@ -47,9 +48,9 @@ export async function search(options: SearchParams) {
|
||||||
disableEscaping: options.disableEscaping,
|
disableEscaping: options.disableEscaping,
|
||||||
}
|
}
|
||||||
|
|
||||||
let table
|
let table = await sdk.tables.getTable(tableId)
|
||||||
|
options = searchInputMapping(table, options)
|
||||||
if (params.sort && !params.sortType) {
|
if (params.sort && !params.sortType) {
|
||||||
table = await sdk.tables.getTable(tableId)
|
|
||||||
const schema = table.schema
|
const schema = table.schema
|
||||||
const sortField = schema[params.sort]
|
const sortField = schema[params.sort]
|
||||||
params.sortType = sortField.type === "number" ? "number" : "string"
|
params.sortType = sortField.type === "number" ? "number" : "string"
|
||||||
|
@ -68,7 +69,6 @@ export async function search(options: SearchParams) {
|
||||||
if (tableId === InternalTables.USER_METADATA) {
|
if (tableId === InternalTables.USER_METADATA) {
|
||||||
response.rows = await getGlobalUsersFromMetadata(response.rows)
|
response.rows = await getGlobalUsersFromMetadata(response.rows)
|
||||||
}
|
}
|
||||||
table = table || (await sdk.tables.getTable(tableId))
|
|
||||||
|
|
||||||
if (options.fields) {
|
if (options.fields) {
|
||||||
const fields = [...options.fields, ...db.CONSTANT_INTERNAL_ROW_COLS]
|
const fields = [...options.fields, ...db.CONSTANT_INTERNAL_ROW_COLS]
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { searchInputMapping } from "../utils"
|
||||||
|
import { db as dbCore } from "@budibase/backend-core"
|
||||||
|
import {
|
||||||
|
FieldType,
|
||||||
|
FieldTypeSubtypes,
|
||||||
|
Table,
|
||||||
|
SearchParams,
|
||||||
|
} from "@budibase/types"
|
||||||
|
|
||||||
|
const tableId = "ta_a"
|
||||||
|
const tableWithUserCol: Table = {
|
||||||
|
_id: tableId,
|
||||||
|
name: "table",
|
||||||
|
schema: {
|
||||||
|
user: {
|
||||||
|
name: "user",
|
||||||
|
type: FieldType.BB_REFERENCE,
|
||||||
|
subtype: FieldTypeSubtypes.BB_REFERENCE.USER,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("searchInputMapping", () => {
|
||||||
|
const globalUserId = dbCore.generateGlobalUserID()
|
||||||
|
const userMedataId = dbCore.generateUserMetadataID(globalUserId)
|
||||||
|
|
||||||
|
it("should be able to map ro_ to global user IDs", () => {
|
||||||
|
const params: SearchParams = {
|
||||||
|
tableId,
|
||||||
|
query: {
|
||||||
|
equal: {
|
||||||
|
"1:user": userMedataId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const output = searchInputMapping(tableWithUserCol, params)
|
||||||
|
expect(output.query.equal!["1:user"]).toBe(globalUserId)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle array of user IDs", () => {
|
||||||
|
const params: SearchParams = {
|
||||||
|
tableId,
|
||||||
|
query: {
|
||||||
|
oneOf: {
|
||||||
|
"1:user": [userMedataId, globalUserId],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const output = searchInputMapping(tableWithUserCol, params)
|
||||||
|
expect(output.query.oneOf!["1:user"]).toStrictEqual([
|
||||||
|
globalUserId,
|
||||||
|
globalUserId,
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("shouldn't change any other input", () => {
|
||||||
|
const email = "test@test.com"
|
||||||
|
const params: SearchParams = {
|
||||||
|
tableId,
|
||||||
|
query: {
|
||||||
|
equal: {
|
||||||
|
"1:user": email,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const output = searchInputMapping(tableWithUserCol, params)
|
||||||
|
expect(output.query.equal!["1:user"]).toBe(email)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("shouldn't error if no query supplied", () => {
|
||||||
|
const params: any = {
|
||||||
|
tableId,
|
||||||
|
}
|
||||||
|
const output = searchInputMapping(tableWithUserCol, params)
|
||||||
|
expect(output.query).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,76 @@
|
||||||
|
import {
|
||||||
|
FieldType,
|
||||||
|
FieldTypeSubtypes,
|
||||||
|
SearchParams,
|
||||||
|
Table,
|
||||||
|
DocumentType,
|
||||||
|
SEPARATOR,
|
||||||
|
} from "@budibase/types"
|
||||||
|
import { db as dbCore } from "@budibase/backend-core"
|
||||||
|
|
||||||
|
function findColumnInQueries(
|
||||||
|
column: string,
|
||||||
|
options: SearchParams,
|
||||||
|
callback: (filter: any) => any
|
||||||
|
) {
|
||||||
|
if (!options.query) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (let filterBlock of Object.values(options.query)) {
|
||||||
|
if (typeof filterBlock !== "object") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for (let [key, filter] of Object.entries(filterBlock)) {
|
||||||
|
if (key.endsWith(column)) {
|
||||||
|
filterBlock[key] = callback(filter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function userColumnMapping(column: string, options: SearchParams) {
|
||||||
|
findColumnInQueries(column, options, (filterValue: any): any => {
|
||||||
|
const isArray = Array.isArray(filterValue),
|
||||||
|
isString = typeof filterValue === "string"
|
||||||
|
if (!isString && !isArray) {
|
||||||
|
return filterValue
|
||||||
|
}
|
||||||
|
const processString = (input: string) => {
|
||||||
|
const rowPrefix = DocumentType.ROW + SEPARATOR
|
||||||
|
if (input.startsWith(rowPrefix)) {
|
||||||
|
return dbCore.getGlobalIDFromUserMetadataID(input)
|
||||||
|
} else {
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isArray) {
|
||||||
|
return filterValue.map(el => {
|
||||||
|
if (typeof el === "string") {
|
||||||
|
return processString(el)
|
||||||
|
} else {
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return processString(filterValue)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps through the search parameters to check if any of the inputs are invalid
|
||||||
|
// based on the table schema, converts them to something that is valid.
|
||||||
|
export function searchInputMapping(table: Table, options: SearchParams) {
|
||||||
|
if (!table?.schema) {
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
for (let [key, column] of Object.entries(table.schema)) {
|
||||||
|
switch (column.type) {
|
||||||
|
case FieldType.BB_REFERENCE:
|
||||||
|
if (column.subtype === FieldTypeSubtypes.BB_REFERENCE.USER) {
|
||||||
|
userColumnMapping(key, options)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
}
|
|
@ -11,12 +11,7 @@ export interface QueryEvent {
|
||||||
queryId: string
|
queryId: string
|
||||||
environmentVariables?: Record<string, string>
|
environmentVariables?: Record<string, string>
|
||||||
ctx?: any
|
ctx?: any
|
||||||
schema?: {
|
schema?: Record<string, { name?: string; type: string }>
|
||||||
[key: string]: {
|
|
||||||
name: string
|
|
||||||
type: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QueryVariable {
|
export interface QueryVariable {
|
||||||
|
|
|
@ -48,7 +48,7 @@ export async function processOutputBBReferences(
|
||||||
) {
|
) {
|
||||||
if (typeof value !== "string") {
|
if (typeof value !== "string") {
|
||||||
// Already processed or nothing to process
|
// Already processed or nothing to process
|
||||||
return value
|
return value || undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const ids = value.split(",").filter(id => !!id)
|
const ids = value.split(",").filter(id => !!id)
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
processInputBBReferences,
|
processInputBBReferences,
|
||||||
processOutputBBReferences,
|
processOutputBBReferences,
|
||||||
} from "./bbReferenceProcessor"
|
} from "./bbReferenceProcessor"
|
||||||
|
import { isExternalTable } from "../../integrations/utils"
|
||||||
export * from "./utils"
|
export * from "./utils"
|
||||||
|
|
||||||
type AutoColumnProcessingOpts = {
|
type AutoColumnProcessingOpts = {
|
||||||
|
@ -234,9 +235,6 @@ export async function outputProcessing<T extends Row[] | Row>(
|
||||||
}
|
}
|
||||||
} else if (column.type == FieldTypes.BB_REFERENCE) {
|
} else if (column.type == FieldTypes.BB_REFERENCE) {
|
||||||
for (let row of enriched) {
|
for (let row of enriched) {
|
||||||
if (row[property] == null) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
row[property] = await processOutputBBReferences(
|
row[property] = await processOutputBBReferences(
|
||||||
row[property],
|
row[property],
|
||||||
column.subtype as FieldSubtype
|
column.subtype as FieldSubtype
|
||||||
|
@ -250,6 +248,16 @@ export async function outputProcessing<T extends Row[] | Row>(
|
||||||
enriched
|
enriched
|
||||||
)) as Row[]
|
)) as Row[]
|
||||||
}
|
}
|
||||||
|
// remove null properties to match internal API
|
||||||
|
if (isExternalTable(table._id!)) {
|
||||||
|
for (let row of enriched) {
|
||||||
|
for (let key of Object.keys(row)) {
|
||||||
|
if (row[key] === null) {
|
||||||
|
delete row[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return (wasArray ? enriched : enriched[0]) as T
|
return (wasArray ? enriched : enriched[0]) as T
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -66,7 +66,7 @@ describe("rowProcessor - outputProcessing", () => {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("does not fetch bb references when fields are empty", async () => {
|
it("process output even when the field is not empty", async () => {
|
||||||
const table: Table = {
|
const table: Table = {
|
||||||
_id: generator.guid(),
|
_id: generator.guid(),
|
||||||
name: "TestTable",
|
name: "TestTable",
|
||||||
|
@ -100,7 +100,7 @@ describe("rowProcessor - outputProcessing", () => {
|
||||||
|
|
||||||
expect(result).toEqual({ name: "Jack" })
|
expect(result).toEqual({ name: "Jack" })
|
||||||
|
|
||||||
expect(bbReferenceProcessor.processOutputBBReferences).not.toBeCalled()
|
expect(bbReferenceProcessor.processOutputBBReferences).toBeCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("does not fetch bb references when not in the schema", async () => {
|
it("does not fetch bb references when not in the schema", async () => {
|
||||||
|
|
|
@ -14,7 +14,6 @@ const HBS_REGEX = /{{([^{].*?)}}/g
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the valid operator options for a certain data type
|
* Returns the valid operator options for a certain data type
|
||||||
* @param type the data type
|
|
||||||
*/
|
*/
|
||||||
export const getValidOperatorsForType = (
|
export const getValidOperatorsForType = (
|
||||||
type: FieldType,
|
type: FieldType,
|
||||||
|
@ -44,22 +43,24 @@ export const getValidOperatorsForType = (
|
||||||
value: string
|
value: string
|
||||||
label: string
|
label: string
|
||||||
}[] = []
|
}[] = []
|
||||||
if (type === "string") {
|
if (type === FieldType.STRING) {
|
||||||
ops = stringOps
|
ops = stringOps
|
||||||
} else if (type === "number" || type === "bigint") {
|
} else if (type === FieldType.NUMBER || type === FieldType.BIGINT) {
|
||||||
ops = numOps
|
ops = numOps
|
||||||
} else if (type === "options") {
|
} else if (type === FieldType.OPTIONS) {
|
||||||
ops = [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty, Op.In]
|
ops = [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty, Op.In]
|
||||||
} else if (type === "array") {
|
} else if (type === FieldType.ARRAY) {
|
||||||
ops = [Op.Contains, Op.NotContains, Op.Empty, Op.NotEmpty, Op.ContainsAny]
|
ops = [Op.Contains, Op.NotContains, Op.Empty, Op.NotEmpty, Op.ContainsAny]
|
||||||
} else if (type === "boolean") {
|
} else if (type === FieldType.BOOLEAN) {
|
||||||
ops = [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty]
|
ops = [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty]
|
||||||
} else if (type === "longform") {
|
} else if (type === FieldType.LONGFORM) {
|
||||||
ops = stringOps
|
ops = stringOps
|
||||||
} else if (type === "datetime") {
|
} else if (type === FieldType.DATETIME) {
|
||||||
ops = numOps
|
ops = numOps
|
||||||
} else if (type === "formula") {
|
} else if (type === FieldType.FORMULA) {
|
||||||
ops = stringOps.concat([Op.MoreThan, Op.LessThan])
|
ops = stringOps.concat([Op.MoreThan, Op.LessThan])
|
||||||
|
} else if (type === FieldType.BB_REFERENCE) {
|
||||||
|
ops = [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty, Op.In]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only allow equal/not equal for _id in SQL tables
|
// Only allow equal/not equal for _id in SQL tables
|
||||||
|
|
|
@ -14,5 +14,5 @@ export function isSQL(datasource: Datasource): boolean {
|
||||||
SourceName.MYSQL,
|
SourceName.MYSQL,
|
||||||
SourceName.ORACLE,
|
SourceName.ORACLE,
|
||||||
]
|
]
|
||||||
return SQL.indexOf(datasource.source) !== -1
|
return SQL.indexOf(datasource.source) !== -1 || datasource.isSQL === true
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ export interface Datasource extends Document {
|
||||||
// the config is defined by the schema
|
// the config is defined by the schema
|
||||||
config?: Record<string, any>
|
config?: Record<string, any>
|
||||||
plus?: boolean
|
plus?: boolean
|
||||||
|
isSQL?: boolean
|
||||||
entities?: {
|
entities?: {
|
||||||
[key: string]: Table
|
[key: string]: Table
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ export interface Query extends Document {
|
||||||
parameters: QueryParameter[]
|
parameters: QueryParameter[]
|
||||||
fields: RestQueryFields | any
|
fields: RestQueryFields | any
|
||||||
transformer: string | null
|
transformer: string | null
|
||||||
schema: any
|
schema: Record<string, { name?: string; type: string }>
|
||||||
readable: boolean
|
readable: boolean
|
||||||
queryVerb: string
|
queryVerb: string
|
||||||
}
|
}
|
||||||
|
|
|
@ -140,6 +140,7 @@ export interface DatasourceConfig {
|
||||||
export interface Integration {
|
export interface Integration {
|
||||||
docs: string
|
docs: string
|
||||||
plus?: boolean
|
plus?: boolean
|
||||||
|
isSQL?: boolean
|
||||||
auth?: { type: string }
|
auth?: { type: string }
|
||||||
features?: Partial<Record<DatasourceFeature, boolean>>
|
features?: Partial<Record<DatasourceFeature, boolean>>
|
||||||
relationships?: boolean
|
relationships?: boolean
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Context, Request } from "koa"
|
import { Context, Request } from "koa"
|
||||||
import { User, Role, UserRoles, Account } from "../documents"
|
import { User, Role, UserRoles, Account, ConfigType } from "../documents"
|
||||||
import { FeatureFlag, License } from "../sdk"
|
import { FeatureFlag, License } from "../sdk"
|
||||||
import { Files } from "formidable"
|
import { Files } from "formidable"
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ export interface ContextUser extends Omit<User, "roles"> {
|
||||||
csrfToken?: string
|
csrfToken?: string
|
||||||
featureFlags?: FeatureFlag[]
|
featureFlags?: FeatureFlag[]
|
||||||
accountPortalAccess?: boolean
|
accountPortalAccess?: boolean
|
||||||
|
providerType?: ConfigType
|
||||||
account?: Account
|
account?: Account
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { TestConfiguration } from "../../../../tests"
|
||||||
import { events } from "@budibase/backend-core"
|
import { events } from "@budibase/backend-core"
|
||||||
|
|
||||||
// this test can 409 - retries reduce issues with this
|
// this test can 409 - retries reduce issues with this
|
||||||
jest.retryTimes(2)
|
jest.retryTimes(2, { logErrorsBeforeRetry: true })
|
||||||
jest.setTimeout(30000)
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
mocks.licenses.useScimIntegration()
|
mocks.licenses.useScimIntegration()
|
||||||
|
|
Loading…
Reference in New Issue