Merge branch 'develop' into feat/relationship-configuration
This commit is contained in:
commit
5937f2139a
|
@ -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:
|
||||||
|
|
|
@ -4,6 +4,7 @@ on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- develop
|
- develop
|
||||||
|
- master
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
|
|
|
@ -138,6 +138,8 @@ To develop the Budibase platform you'll need [Docker](https://www.docker.com/) a
|
||||||
|
|
||||||
`yarn setup` will check that all necessary components are installed and setup the repo for usage.
|
`yarn setup` will check that all necessary components are installed and setup the repo for usage.
|
||||||
|
|
||||||
|
If you have access to the `@budibase/pro` submodule then please follow the Pro section of this guide before running the above command.
|
||||||
|
|
||||||
##### Manual method
|
##### Manual method
|
||||||
|
|
||||||
The following commands can be executed to manually get Budibase up and running (assuming Docker/Docker Compose has been installed).
|
The following commands can be executed to manually get Budibase up and running (assuming Docker/Docker Compose has been installed).
|
||||||
|
@ -146,6 +148,8 @@ The following commands can be executed to manually get Budibase up and running (
|
||||||
|
|
||||||
`yarn build` will build all budibase packages.
|
`yarn build` will build all budibase packages.
|
||||||
|
|
||||||
|
If you have access to the `@budibase/pro` submodule then please follow the Pro section of this guide before running the above commands.
|
||||||
|
|
||||||
#### 4. Running
|
#### 4. Running
|
||||||
|
|
||||||
To run the budibase server and builder in dev mode (i.e. with live reloading):
|
To run the budibase server and builder in dev mode (i.e. with live reloading):
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.11.5-alpha.2",
|
"version": "2.11.15-alpha.2",
|
||||||
"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>
|
||||||
|
|
||||||
|
|
|
@ -110,20 +110,7 @@
|
||||||
<div class="schema-fields">
|
<div class="schema-fields">
|
||||||
{#each schemaFields as [field, schema]}
|
{#each schemaFields as [field, schema]}
|
||||||
{#if !schema.autocolumn && schema.type !== "attachment"}
|
{#if !schema.autocolumn && schema.type !== "attachment"}
|
||||||
<DrawerBindableSlot
|
{#if isTestModal}
|
||||||
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}
|
||||||
|
@ -132,7 +119,31 @@
|
||||||
{value}
|
{value}
|
||||||
{onChange}
|
{onChange}
|
||||||
/>
|
/>
|
||||||
</DrawerBindableSlot>
|
{: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
|
||||||
|
{isTestModal}
|
||||||
|
{field}
|
||||||
|
{schema}
|
||||||
|
bindings={parsedBindings}
|
||||||
|
{value}
|
||||||
|
{onChange}
|
||||||
|
/>
|
||||||
|
</DrawerBindableSlot>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{#if isUpdateRow && schema.type === "link"}
|
{#if isUpdateRow && schema.type === "link"}
|
||||||
<div class="checkbox-field">
|
<div class="checkbox-field">
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -660,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
|
||||||
|
@ -669,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
|
||||||
|
|
|
@ -1,91 +0,0 @@
|
||||||
<script>
|
|
||||||
import { Button, ActionButton, Drawer } from "@budibase/bbui"
|
|
||||||
import { createEventDispatcher } from "svelte"
|
|
||||||
import ColumnDrawer from "./ColumnEditor/ColumnDrawer.svelte"
|
|
||||||
import { cloneDeep } from "lodash/fp"
|
|
||||||
import {
|
|
||||||
getDatasourceForProvider,
|
|
||||||
getSchemaForDatasource,
|
|
||||||
} from "builderStore/dataBinding"
|
|
||||||
import { currentAsset } from "builderStore"
|
|
||||||
import { getFields } from "helpers/searchFields"
|
|
||||||
|
|
||||||
export let componentInstance
|
|
||||||
export let value = []
|
|
||||||
export let allowCellEditing = true
|
|
||||||
export let subject = "Table"
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
|
||||||
|
|
||||||
let drawer
|
|
||||||
let boundValue
|
|
||||||
|
|
||||||
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
|
|
||||||
$: schema = getSchema($currentAsset, datasource)
|
|
||||||
$: options = allowCellEditing
|
|
||||||
? Object.keys(schema || {})
|
|
||||||
: enrichedSchemaFields?.map(field => field.name)
|
|
||||||
$: sanitisedValue = getValidColumns(value, options)
|
|
||||||
$: updateBoundValue(sanitisedValue)
|
|
||||||
$: enrichedSchemaFields = getFields(Object.values(schema || {}), {
|
|
||||||
allowLinks: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
const getSchema = (asset, datasource) => {
|
|
||||||
const schema = getSchemaForDatasource(asset, datasource).schema
|
|
||||||
|
|
||||||
// Don't show ID and rev in tables
|
|
||||||
if (schema) {
|
|
||||||
delete schema._id
|
|
||||||
delete schema._rev
|
|
||||||
}
|
|
||||||
|
|
||||||
return schema
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateBoundValue = value => {
|
|
||||||
boundValue = cloneDeep(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getValidColumns = (columns, options) => {
|
|
||||||
if (!Array.isArray(columns) || !columns.length) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
// We need to account for legacy configs which would just be an array
|
|
||||||
// of strings
|
|
||||||
if (typeof columns[0] === "string") {
|
|
||||||
columns = columns.map(col => ({
|
|
||||||
name: col,
|
|
||||||
displayName: col,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
return columns.filter(column => {
|
|
||||||
return options.includes(column.name)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const open = () => {
|
|
||||||
updateBoundValue(sanitisedValue)
|
|
||||||
drawer.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
const save = () => {
|
|
||||||
dispatch("change", getValidColumns(boundValue, options))
|
|
||||||
drawer.hide()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ActionButton on:click={open}>Configure columns</ActionButton>
|
|
||||||
<Drawer bind:this={drawer} title="{subject} Columns">
|
|
||||||
<svelte:fragment slot="description">
|
|
||||||
Configure the columns in your {subject.toLowerCase()}.
|
|
||||||
</svelte:fragment>
|
|
||||||
<Button cta slot="buttons" on:click={save}>Save</Button>
|
|
||||||
<ColumnDrawer
|
|
||||||
slot="body"
|
|
||||||
bind:columns={boundValue}
|
|
||||||
{options}
|
|
||||||
{schema}
|
|
||||||
{allowCellEditing}
|
|
||||||
/>
|
|
||||||
</Drawer>
|
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={body} class="grid-body">
|
<div bind:this={body} class="grid-body">
|
||||||
<GridScrollWrapper scrollHorizontally scrollVertically wheelInteractive>
|
<GridScrollWrapper scrollHorizontally scrollVertically attachHandlers>
|
||||||
{#each $renderedRows as row, idx}
|
{#each $renderedRows as row, idx}
|
||||||
<GridRow
|
<GridRow
|
||||||
{row}
|
{row}
|
||||||
|
|
|
@ -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}`}
|
||||||
|
|
|
@ -17,7 +17,11 @@
|
||||||
|
|
||||||
export let scrollVertically = false
|
export let scrollVertically = false
|
||||||
export let scrollHorizontally = false
|
export let scrollHorizontally = false
|
||||||
export let wheelInteractive = false
|
export let attachHandlers = false
|
||||||
|
|
||||||
|
// Used for tracking touch events
|
||||||
|
let initialTouchX
|
||||||
|
let initialTouchY
|
||||||
|
|
||||||
$: style = generateStyle($scroll, $rowHeight, $hiddenColumnsWidth)
|
$: style = generateStyle($scroll, $rowHeight, $hiddenColumnsWidth)
|
||||||
|
|
||||||
|
@ -27,17 +31,47 @@
|
||||||
return `transform: translate3d(${offsetX}px, ${offsetY}px, 0);`
|
return `transform: translate3d(${offsetX}px, ${offsetY}px, 0);`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handles a wheel even and updates the scroll offsets
|
// Handles a mouse wheel event and updates scroll state
|
||||||
const handleWheel = e => {
|
const handleWheel = e => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
debouncedHandleWheel(e.deltaX, e.deltaY, e.clientY)
|
updateScroll(e.deltaX, e.deltaY, e.clientY)
|
||||||
|
|
||||||
// If a context menu was visible, hide it
|
// If a context menu was visible, hide it
|
||||||
if ($menu.visible) {
|
if ($menu.visible) {
|
||||||
menu.actions.close()
|
menu.actions.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const debouncedHandleWheel = domDebounce((deltaX, deltaY, clientY) => {
|
|
||||||
|
// Handles touch start events
|
||||||
|
const handleTouchStart = e => {
|
||||||
|
if (!e.touches?.[0]) return
|
||||||
|
initialTouchX = e.touches[0].clientX
|
||||||
|
initialTouchY = e.touches[0].clientY
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles touch move events and updates scroll state
|
||||||
|
const handleTouchMove = e => {
|
||||||
|
if (!e.touches?.[0]) return
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
// Compute delta from previous event, and update scroll
|
||||||
|
const deltaX = initialTouchX - e.touches[0].clientX
|
||||||
|
const deltaY = initialTouchY - e.touches[0].clientY
|
||||||
|
updateScroll(deltaX, deltaY)
|
||||||
|
|
||||||
|
// Store position to reference in next event
|
||||||
|
initialTouchX = e.touches[0].clientX
|
||||||
|
initialTouchY = e.touches[0].clientY
|
||||||
|
|
||||||
|
// If a context menu was visible, hide it
|
||||||
|
if ($menu.visible) {
|
||||||
|
menu.actions.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updates the scroll offset by a certain delta, and ensure scrolling
|
||||||
|
// stays within sensible bounds. Debounced for performance.
|
||||||
|
const updateScroll = domDebounce((deltaX, deltaY, clientY) => {
|
||||||
const { top, left } = $scroll
|
const { top, left } = $scroll
|
||||||
|
|
||||||
// Calculate new scroll top
|
// Calculate new scroll top
|
||||||
|
@ -55,15 +89,19 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
// Hover row under cursor
|
// Hover row under cursor
|
||||||
const y = clientY - $bounds.top + (newScrollTop % $rowHeight)
|
if (clientY != null) {
|
||||||
const hoveredRow = $renderedRows[Math.floor(y / $rowHeight)]
|
const y = clientY - $bounds.top + (newScrollTop % $rowHeight)
|
||||||
hoveredRowId.set(hoveredRow?._id)
|
const hoveredRow = $renderedRows[Math.floor(y / $rowHeight)]
|
||||||
|
hoveredRowId.set(hoveredRow?._id)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="outer"
|
class="outer"
|
||||||
on:wheel={wheelInteractive ? handleWheel : null}
|
on:wheel={attachHandlers ? handleWheel : null}
|
||||||
|
on:touchstart={attachHandlers ? handleTouchStart : null}
|
||||||
|
on:touchmove={attachHandlers ? handleTouchMove : null}
|
||||||
on:click|self={() => ($focusedCellId = null)}
|
on:click|self={() => ($focusedCellId = null)}
|
||||||
>
|
>
|
||||||
<div {style} class="inner">
|
<div {style} class="inner">
|
||||||
|
|
|
@ -205,7 +205,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="normal-columns" transition:fade|local={{ duration: 130 }}>
|
<div class="normal-columns" transition:fade|local={{ duration: 130 }}>
|
||||||
<GridScrollWrapper scrollHorizontally wheelInteractive>
|
<GridScrollWrapper scrollHorizontally attachHandlers>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{#each $renderedColumns as column, columnIdx}
|
{#each $renderedColumns as column, columnIdx}
|
||||||
{@const cellId = `new-${column.name}`}
|
{@const cellId = `new-${column.name}`}
|
||||||
|
|
|
@ -64,7 +64,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content" on:mouseleave={() => ($hoveredRowId = null)}>
|
<div class="content" on:mouseleave={() => ($hoveredRowId = null)}>
|
||||||
<GridScrollWrapper scrollVertically wheelInteractive>
|
<GridScrollWrapper scrollVertically attachHandlers>
|
||||||
{#each $renderedRows as row, idx}
|
{#each $renderedRows as row, idx}
|
||||||
{@const rowSelected = !!$selectedRows[row._id]}
|
{@const rowSelected = !!$selectedRows[row._id]}
|
||||||
{@const rowHovered = $hoveredRowId === row._id}
|
{@const rowHovered = $hoveredRowId === row._id}
|
||||||
|
@ -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}
|
||||||
|
|
|
@ -53,18 +53,27 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getLocation = e => {
|
||||||
|
return {
|
||||||
|
y: e.touches?.[0]?.clientY ?? e.clientY,
|
||||||
|
x: e.touches?.[0]?.clientX ?? e.clientX,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// V scrollbar drag handlers
|
// V scrollbar drag handlers
|
||||||
const startVDragging = e => {
|
const startVDragging = e => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
initialMouse = e.clientY
|
initialMouse = getLocation(e).y
|
||||||
initialScroll = $scrollTop
|
initialScroll = $scrollTop
|
||||||
document.addEventListener("mousemove", moveVDragging)
|
document.addEventListener("mousemove", moveVDragging)
|
||||||
|
document.addEventListener("touchmove", moveVDragging)
|
||||||
document.addEventListener("mouseup", stopVDragging)
|
document.addEventListener("mouseup", stopVDragging)
|
||||||
|
document.addEventListener("touchend", stopVDragging)
|
||||||
isDraggingV = true
|
isDraggingV = true
|
||||||
closeMenu()
|
closeMenu()
|
||||||
}
|
}
|
||||||
const moveVDragging = domDebounce(e => {
|
const moveVDragging = domDebounce(e => {
|
||||||
const delta = e.clientY - initialMouse
|
const delta = getLocation(e).y - initialMouse
|
||||||
const weight = delta / availHeight
|
const weight = delta / availHeight
|
||||||
const newScrollTop = initialScroll + weight * $maxScrollTop
|
const newScrollTop = initialScroll + weight * $maxScrollTop
|
||||||
scroll.update(state => ({
|
scroll.update(state => ({
|
||||||
|
@ -74,22 +83,26 @@
|
||||||
})
|
})
|
||||||
const stopVDragging = () => {
|
const stopVDragging = () => {
|
||||||
document.removeEventListener("mousemove", moveVDragging)
|
document.removeEventListener("mousemove", moveVDragging)
|
||||||
|
document.removeEventListener("touchmove", moveVDragging)
|
||||||
document.removeEventListener("mouseup", stopVDragging)
|
document.removeEventListener("mouseup", stopVDragging)
|
||||||
|
document.removeEventListener("touchend", stopVDragging)
|
||||||
isDraggingV = false
|
isDraggingV = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// H scrollbar drag handlers
|
// H scrollbar drag handlers
|
||||||
const startHDragging = e => {
|
const startHDragging = e => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
initialMouse = e.clientX
|
initialMouse = getLocation(e).x
|
||||||
initialScroll = $scrollLeft
|
initialScroll = $scrollLeft
|
||||||
document.addEventListener("mousemove", moveHDragging)
|
document.addEventListener("mousemove", moveHDragging)
|
||||||
|
document.addEventListener("touchmove", moveHDragging)
|
||||||
document.addEventListener("mouseup", stopHDragging)
|
document.addEventListener("mouseup", stopHDragging)
|
||||||
|
document.addEventListener("touchend", stopHDragging)
|
||||||
isDraggingH = true
|
isDraggingH = true
|
||||||
closeMenu()
|
closeMenu()
|
||||||
}
|
}
|
||||||
const moveHDragging = domDebounce(e => {
|
const moveHDragging = domDebounce(e => {
|
||||||
const delta = e.clientX - initialMouse
|
const delta = getLocation(e).x - initialMouse
|
||||||
const weight = delta / availWidth
|
const weight = delta / availWidth
|
||||||
const newScrollLeft = initialScroll + weight * $maxScrollLeft
|
const newScrollLeft = initialScroll + weight * $maxScrollLeft
|
||||||
scroll.update(state => ({
|
scroll.update(state => ({
|
||||||
|
@ -99,7 +112,9 @@
|
||||||
})
|
})
|
||||||
const stopHDragging = () => {
|
const stopHDragging = () => {
|
||||||
document.removeEventListener("mousemove", moveHDragging)
|
document.removeEventListener("mousemove", moveHDragging)
|
||||||
|
document.removeEventListener("touchmove", moveHDragging)
|
||||||
document.removeEventListener("mouseup", stopHDragging)
|
document.removeEventListener("mouseup", stopHDragging)
|
||||||
|
document.removeEventListener("touchend", stopHDragging)
|
||||||
isDraggingH = false
|
isDraggingH = false
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -109,6 +124,7 @@
|
||||||
class="v-scrollbar"
|
class="v-scrollbar"
|
||||||
style="--size:{ScrollBarSize}px; top:{barTop}px; height:{barHeight}px;"
|
style="--size:{ScrollBarSize}px; top:{barTop}px; height:{barHeight}px;"
|
||||||
on:mousedown={startVDragging}
|
on:mousedown={startVDragging}
|
||||||
|
on:touchstart={startVDragging}
|
||||||
class:dragging={isDraggingV}
|
class:dragging={isDraggingV}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -117,6 +133,7 @@
|
||||||
class="h-scrollbar"
|
class="h-scrollbar"
|
||||||
style="--size:{ScrollBarSize}px; left:{barLeft}px; width:{barWidth}px;"
|
style="--size:{ScrollBarSize}px; left:{barLeft}px; width:{barWidth}px;"
|
||||||
on:mousedown={startHDragging}
|
on:mousedown={startHDragging}
|
||||||
|
on:touchstart={startHDragging}
|
||||||
class:dragging={isDraggingH}
|
class:dragging={isDraggingH}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { writable, get } from "svelte/store"
|
import { writable, get } from "svelte/store"
|
||||||
|
import { Helpers } from "@budibase/bbui"
|
||||||
|
|
||||||
export const createStores = () => {
|
export const createStores = () => {
|
||||||
const copiedCell = writable(null)
|
const copiedCell = writable(null)
|
||||||
|
@ -12,7 +13,16 @@ export const createActions = context => {
|
||||||
const { copiedCell, focusedCellAPI } = context
|
const { copiedCell, focusedCellAPI } = context
|
||||||
|
|
||||||
const copy = () => {
|
const copy = () => {
|
||||||
copiedCell.set(get(focusedCellAPI)?.getValue())
|
const value = get(focusedCellAPI)?.getValue()
|
||||||
|
copiedCell.set(value)
|
||||||
|
|
||||||
|
// Also copy a stringified version to the clipboard
|
||||||
|
let stringified = ""
|
||||||
|
if (value != null && value !== "") {
|
||||||
|
// Only conditionally stringify to avoid redundant quotes around text
|
||||||
|
stringified = typeof value === "object" ? JSON.stringify(value) : value
|
||||||
|
}
|
||||||
|
Helpers.copyToClipboard(stringified)
|
||||||
}
|
}
|
||||||
|
|
||||||
const paste = () => {
|
const paste = () => {
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue