Merge branch 'master' of github.com:Budibase/budibase into more-sqs-tests-4
This commit is contained in:
commit
2d3b850da8
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.23.5",
|
"version": "2.23.6",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit bd0e01d639ec3b2547e7c859a1c43b622dce8344
|
Subproject commit 328c84234d11d97d840f0eb2c72665b04ba9e4f8
|
|
@ -8,19 +8,9 @@ import {
|
||||||
SearchParams,
|
SearchParams,
|
||||||
WithRequired,
|
WithRequired,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
import { dataFilters } from "@budibase/shared-core"
|
||||||
|
|
||||||
const QUERY_START_REGEX = /\d[0-9]*:/g
|
export const removeKeyNumbering = dataFilters.removeKeyNumbering
|
||||||
|
|
||||||
export function removeKeyNumbering(key: any): string {
|
|
||||||
if (typeof key === "string" && key.match(QUERY_START_REGEX) != null) {
|
|
||||||
const parts = key.split(":")
|
|
||||||
// remove the number
|
|
||||||
parts.shift()
|
|
||||||
return parts.join(":")
|
|
||||||
} else {
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class to build lucene query URLs.
|
* Class to build lucene query URLs.
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
notifications,
|
notifications,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
DatePicker,
|
DatePicker,
|
||||||
|
DrawerContent,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
||||||
import { automationStore, selectedAutomation, tables } from "stores/builder"
|
import { automationStore, selectedAutomation, tables } from "stores/builder"
|
||||||
|
@ -37,7 +38,7 @@
|
||||||
hbAutocomplete,
|
hbAutocomplete,
|
||||||
EditorModes,
|
EditorModes,
|
||||||
} from "components/common/CodeEditor"
|
} from "components/common/CodeEditor"
|
||||||
import FilterDrawer from "components/design/settings/controls/FilterEditor/FilterDrawer.svelte"
|
import FilterBuilder from "components/design/settings/controls/FilterEditor/FilterBuilder.svelte"
|
||||||
import { LuceneUtils, Utils } from "@budibase/frontend-core"
|
import { LuceneUtils, Utils } from "@budibase/frontend-core"
|
||||||
import {
|
import {
|
||||||
getSchemaForDatasourcePlus,
|
getSchemaForDatasourcePlus,
|
||||||
|
@ -442,8 +443,8 @@
|
||||||
<Button cta slot="buttons" on:click={() => saveFilters(key)}>
|
<Button cta slot="buttons" on:click={() => saveFilters(key)}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
<FilterDrawer
|
<DrawerContent slot="body">
|
||||||
slot="body"
|
<FilterBuilder
|
||||||
{filters}
|
{filters}
|
||||||
{bindings}
|
{bindings}
|
||||||
{schemaFields}
|
{schemaFields}
|
||||||
|
@ -451,6 +452,7 @@
|
||||||
panel={AutomationBindingPanel}
|
panel={AutomationBindingPanel}
|
||||||
on:change={e => (tempFilters = e.detail)}
|
on:change={e => (tempFilters = e.detail)}
|
||||||
/>
|
/>
|
||||||
|
</DrawerContent>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
{:else if value.customType === "password"}
|
{:else if value.customType === "password"}
|
||||||
<Input
|
<Input
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import { ActionButton, Modal, ModalContent } from "@budibase/bbui"
|
import { ActionButton, Modal, ModalContent } from "@budibase/bbui"
|
||||||
import FilterDrawer from "components/design/settings/controls/FilterEditor/FilterDrawer.svelte"
|
import FilterBuilder from "components/design/settings/controls/FilterEditor/FilterBuilder.svelte"
|
||||||
|
|
||||||
export let schema
|
export let schema
|
||||||
export let filters
|
export let filters
|
||||||
|
@ -40,7 +40,7 @@
|
||||||
onConfirm={() => dispatch("change", tempValue)}
|
onConfirm={() => dispatch("change", tempValue)}
|
||||||
>
|
>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<FilterDrawer
|
<FilterBuilder
|
||||||
allowBindings={false}
|
allowBindings={false}
|
||||||
{filters}
|
{filters}
|
||||||
{schemaFields}
|
{schemaFields}
|
||||||
|
|
|
@ -27,14 +27,6 @@
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteAttachments(fileList) {
|
|
||||||
try {
|
|
||||||
return await API.deleteBuilderAttachments(fileList)
|
|
||||||
} catch (error) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Dropzone
|
<Dropzone
|
||||||
|
@ -42,6 +34,5 @@
|
||||||
{label}
|
{label}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
{processFiles}
|
{processFiles}
|
||||||
{deleteAttachments}
|
|
||||||
{handleFileTooLarge}
|
{handleFileTooLarge}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
<script>
|
||||||
|
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||||
|
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
||||||
|
|
||||||
|
import { dataFilters } from "@budibase/shared-core"
|
||||||
|
import { FilterBuilder } from "@budibase/frontend-core"
|
||||||
|
|
||||||
|
import { createEventDispatcher, onMount } from "svelte"
|
||||||
|
|
||||||
|
export let schemaFields
|
||||||
|
export let filters = []
|
||||||
|
export let bindings = []
|
||||||
|
export let panel = ClientBindingPanel
|
||||||
|
export let allowBindings = true
|
||||||
|
export let datasource
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
let rawFilters
|
||||||
|
|
||||||
|
$: parseFilters(rawFilters)
|
||||||
|
$: dispatch("change", enrichFilters(rawFilters))
|
||||||
|
|
||||||
|
// Remove field key prefixes and determine which behaviours to use
|
||||||
|
const parseFilters = filters => {
|
||||||
|
rawFilters = (filters || []).map(filter => {
|
||||||
|
const { field } = filter
|
||||||
|
let newFilter = { ...filter }
|
||||||
|
delete newFilter.allOr
|
||||||
|
newFilter.field = dataFilters.removeKeyNumbering(field)
|
||||||
|
return newFilter
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
parseFilters(filters)
|
||||||
|
rawFilters.forEach(filter => {
|
||||||
|
filter.type =
|
||||||
|
schemaFields.find(field => field.name === filter.field)?.type ||
|
||||||
|
filter.type
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add field key prefixes and a special metadata filter object to indicate
|
||||||
|
// how to handle filter behaviour
|
||||||
|
const enrichFilters = rawFilters => {
|
||||||
|
let count = 1
|
||||||
|
return rawFilters
|
||||||
|
.filter(filter => filter.field)
|
||||||
|
.map(filter => ({
|
||||||
|
...filter,
|
||||||
|
field: `${count++}:${filter.field}`,
|
||||||
|
}))
|
||||||
|
.concat(...rawFilters.filter(filter => !filter.field))
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FilterBuilder
|
||||||
|
bind:filters={rawFilters}
|
||||||
|
behaviourFilters={true}
|
||||||
|
{schemaFields}
|
||||||
|
{datasource}
|
||||||
|
{allowBindings}
|
||||||
|
>
|
||||||
|
<div slot="filtering-hero-content" />
|
||||||
|
|
||||||
|
<DrawerBindableInput
|
||||||
|
let:filter
|
||||||
|
slot="binding"
|
||||||
|
disabled={filter.noValue}
|
||||||
|
title={filter.field}
|
||||||
|
value={filter.value}
|
||||||
|
placeholder="Value"
|
||||||
|
{panel}
|
||||||
|
{bindings}
|
||||||
|
on:change={event => {
|
||||||
|
const indexToUpdate = rawFilters.findIndex(f => f.id === filter.id)
|
||||||
|
rawFilters[indexToUpdate] = {
|
||||||
|
...rawFilters[indexToUpdate],
|
||||||
|
value: event.detail,
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FilterBuilder>
|
|
@ -1,8 +1,14 @@
|
||||||
<script>
|
<script>
|
||||||
import { notifications, ActionButton, Button, Drawer } from "@budibase/bbui"
|
import {
|
||||||
|
notifications,
|
||||||
|
ActionButton,
|
||||||
|
Button,
|
||||||
|
Drawer,
|
||||||
|
DrawerContent,
|
||||||
|
} from "@budibase/bbui"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import { getDatasourceForProvider, getSchemaForDatasource } from "dataBinding"
|
import { getDatasourceForProvider, getSchemaForDatasource } from "dataBinding"
|
||||||
import FilterDrawer from "./FilterDrawer.svelte"
|
import FilterBuilder from "./FilterBuilder.svelte"
|
||||||
import { selectedScreen } from "stores/builder"
|
import { selectedScreen } from "stores/builder"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
@ -40,14 +46,15 @@
|
||||||
</div>
|
</div>
|
||||||
<Drawer bind:this={drawer} title="Filtering" on:drawerHide on:drawerShow>
|
<Drawer bind:this={drawer} title="Filtering" on:drawerHide on:drawerShow>
|
||||||
<Button cta slot="buttons" on:click={saveFilter}>Save</Button>
|
<Button cta slot="buttons" on:click={saveFilter}>Save</Button>
|
||||||
<FilterDrawer
|
<DrawerContent slot="body">
|
||||||
slot="body"
|
<FilterBuilder
|
||||||
filters={value}
|
filters={value}
|
||||||
{bindings}
|
{bindings}
|
||||||
{schemaFields}
|
{schemaFields}
|
||||||
{datasource}
|
{datasource}
|
||||||
on:change={e => (tempValue = e.detail)}
|
on:change={e => (tempValue = e.detail)}
|
||||||
/>
|
/>
|
||||||
|
</DrawerContent>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -1,216 +1,14 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import { FilterBuilder } from "@budibase/frontend-core"
|
||||||
Body,
|
|
||||||
Button,
|
|
||||||
Combobox,
|
|
||||||
DatePicker,
|
|
||||||
Icon,
|
|
||||||
Input,
|
|
||||||
Layout,
|
|
||||||
Select,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { generate } from "shortid"
|
|
||||||
import { LuceneUtils, Constants } from "@budibase/frontend-core"
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
|
|
||||||
export let schemaFields
|
export let schemaFields
|
||||||
export let filters = []
|
export let filters = []
|
||||||
export let datasource
|
export let datasource
|
||||||
|
|
||||||
const context = getContext("context")
|
|
||||||
const BannedTypes = ["link", "attachment", "json"]
|
|
||||||
|
|
||||||
$: fieldOptions = (schemaFields ?? [])
|
|
||||||
.filter(
|
|
||||||
field =>
|
|
||||||
!BannedTypes.includes(field.type) ||
|
|
||||||
(field.type === "formula" && field.formulaType === "static")
|
|
||||||
)
|
|
||||||
.map(field => ({
|
|
||||||
label: field.displayName || field.name,
|
|
||||||
value: field.name,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const addFilter = () => {
|
|
||||||
filters = [
|
|
||||||
...filters,
|
|
||||||
{
|
|
||||||
id: generate(),
|
|
||||||
field: null,
|
|
||||||
operator: Constants.OperatorOptions.Equals.value,
|
|
||||||
value: null,
|
|
||||||
valueType: "Value",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeFilter = id => {
|
|
||||||
filters = filters.filter(field => field.id !== id)
|
|
||||||
}
|
|
||||||
|
|
||||||
const duplicateFilter = id => {
|
|
||||||
const existingFilter = filters.find(filter => filter.id === id)
|
|
||||||
const duplicate = { ...existingFilter, id: generate() }
|
|
||||||
filters = [...filters, duplicate]
|
|
||||||
}
|
|
||||||
|
|
||||||
const onFieldChange = (expression, field) => {
|
|
||||||
// Update the field type
|
|
||||||
expression.type = schemaFields.find(x => x.name === field)?.type
|
|
||||||
expression.externalType = schemaFields.find(
|
|
||||||
x => x.name === field
|
|
||||||
)?.externalType
|
|
||||||
|
|
||||||
// Ensure a valid operator is set
|
|
||||||
const validOperators = LuceneUtils.getValidOperatorsForType(
|
|
||||||
{ type: expression.type },
|
|
||||||
expression.field,
|
|
||||||
datasource
|
|
||||||
).map(x => x.value)
|
|
||||||
if (!validOperators.includes(expression.operator)) {
|
|
||||||
expression.operator =
|
|
||||||
validOperators[0] ?? Constants.OperatorOptions.Equals.value
|
|
||||||
onOperatorChange(expression, expression.operator)
|
|
||||||
}
|
|
||||||
|
|
||||||
// if changed to an array, change default value to empty array
|
|
||||||
const idx = filters.findIndex(x => x.field === field)
|
|
||||||
if (expression.type === "array") {
|
|
||||||
filters[idx].value = []
|
|
||||||
} else {
|
|
||||||
filters[idx].value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onOperatorChange = (expression, operator) => {
|
|
||||||
const noValueOptions = [
|
|
||||||
Constants.OperatorOptions.Empty.value,
|
|
||||||
Constants.OperatorOptions.NotEmpty.value,
|
|
||||||
]
|
|
||||||
expression.noValue = noValueOptions.includes(operator)
|
|
||||||
if (expression.noValue) {
|
|
||||||
expression.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getFieldOptions = field => {
|
|
||||||
const schema = schemaFields.find(x => x.name === field)
|
|
||||||
return schema?.constraints?.inclusion || []
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSchema = filter => {
|
|
||||||
return schemaFields.find(field => field.name === filter.field)
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container" class:mobile={$context.device.mobile}>
|
<FilterBuilder bind:filters {schemaFields} {datasource} filtersLabel={null}>
|
||||||
<Layout noPadding>
|
<div slot="filtering-hero-content">
|
||||||
<Body size="S">
|
|
||||||
{#if !filters?.length}
|
|
||||||
Add your first filter expression.
|
|
||||||
{:else}
|
|
||||||
Results are filtered to only those which match all of the following
|
Results are filtered to only those which match all of the following
|
||||||
constraints.
|
constraints.
|
||||||
{/if}
|
|
||||||
</Body>
|
|
||||||
{#if filters?.length}
|
|
||||||
<div class="fields">
|
|
||||||
{#each filters as filter}
|
|
||||||
<Select
|
|
||||||
bind:value={filter.field}
|
|
||||||
options={fieldOptions}
|
|
||||||
on:change={e => onFieldChange(filter, e.detail)}
|
|
||||||
placeholder="Column"
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
disabled={!filter.field}
|
|
||||||
options={LuceneUtils.getValidOperatorsForType(
|
|
||||||
{ type: filter.type, subtype: filter.subtype },
|
|
||||||
filter.field,
|
|
||||||
datasource
|
|
||||||
)}
|
|
||||||
bind:value={filter.operator}
|
|
||||||
on:change={e => onOperatorChange(filter, e.detail)}
|
|
||||||
placeholder={null}
|
|
||||||
/>
|
|
||||||
{#if ["string", "longform", "number", "bigint", "formula"].includes(filter.type)}
|
|
||||||
<Input disabled={filter.noValue} bind:value={filter.value} />
|
|
||||||
{:else if ["options", "array"].includes(filter.type)}
|
|
||||||
<Combobox
|
|
||||||
disabled={filter.noValue}
|
|
||||||
options={getFieldOptions(filter.field)}
|
|
||||||
bind:value={filter.value}
|
|
||||||
/>
|
|
||||||
{:else if filter.type === "boolean"}
|
|
||||||
<Combobox
|
|
||||||
disabled={filter.noValue}
|
|
||||||
options={[
|
|
||||||
{ label: "True", value: "true" },
|
|
||||||
{ label: "False", value: "false" },
|
|
||||||
]}
|
|
||||||
bind:value={filter.value}
|
|
||||||
/>
|
|
||||||
{:else if filter.type === "datetime"}
|
|
||||||
<DatePicker
|
|
||||||
disabled={filter.noValue}
|
|
||||||
enableTime={!getSchema(filter).dateOnly}
|
|
||||||
timeOnly={getSchema(filter).timeOnly}
|
|
||||||
bind:value={filter.value}
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<Input disabled />
|
|
||||||
{/if}
|
|
||||||
<div class="controls">
|
|
||||||
<Icon
|
|
||||||
name="Duplicate"
|
|
||||||
hoverable
|
|
||||||
size="S"
|
|
||||||
on:click={() => duplicateFilter(filter.id)}
|
|
||||||
/>
|
|
||||||
<Icon
|
|
||||||
name="Close"
|
|
||||||
hoverable
|
|
||||||
size="S"
|
|
||||||
on:click={() => removeFilter(filter.id)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
</FilterBuilder>
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<div>
|
|
||||||
<Button icon="AddCircle" size="M" secondary on:click={addFilter}>
|
|
||||||
Add filter
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.container {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 1000px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
.fields {
|
|
||||||
display: grid;
|
|
||||||
column-gap: var(--spacing-l);
|
|
||||||
row-gap: var(--spacing-s);
|
|
||||||
align-items: center;
|
|
||||||
grid-template-columns: 1fr 120px 1fr auto auto;
|
|
||||||
}
|
|
||||||
.controls {
|
|
||||||
display: contents;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container.mobile .fields {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
.container.mobile .controls {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
padding: var(--spacing-s) 0;
|
|
||||||
gap: var(--spacing-s);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -58,17 +58,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteAttachments = async fileList => {
|
|
||||||
try {
|
|
||||||
return await API.deleteAttachments({
|
|
||||||
keys: fileList,
|
|
||||||
tableId: formContext?.dataSource?.tableId,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleChange = e => {
|
const handleChange = e => {
|
||||||
const value = fieldApiMapper.set(e.detail)
|
const value = fieldApiMapper.set(e.detail)
|
||||||
const changed = fieldApi.setValue(value)
|
const changed = fieldApi.setValue(value)
|
||||||
|
@ -98,7 +87,6 @@
|
||||||
error={fieldState.error}
|
error={fieldState.error}
|
||||||
on:change={handleChange}
|
on:change={handleChange}
|
||||||
{processFiles}
|
{processFiles}
|
||||||
{deleteAttachments}
|
|
||||||
{handleFileTooLarge}
|
{handleFileTooLarge}
|
||||||
{handleTooManyFiles}
|
{handleTooManyFiles}
|
||||||
{maximum}
|
{maximum}
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
"@budibase/types": "0.0.0",
|
"@budibase/types": "0.0.0",
|
||||||
"dayjs": "^1.10.8",
|
"dayjs": "^1.10.8",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
|
"shortid": "2.2.15",
|
||||||
"socket.io-client": "^4.6.1"
|
"socket.io-client": "^4.6.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,34 +61,6 @@ export const buildAttachmentEndpoints = API => {
|
||||||
})
|
})
|
||||||
return { publicUrl }
|
return { publicUrl }
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes attachments from the bucket.
|
|
||||||
* @param keys the attachments to delete
|
|
||||||
* @param tableId the associated table ID
|
|
||||||
*/
|
|
||||||
deleteAttachments: async ({ keys, tableId }) => {
|
|
||||||
return await API.post({
|
|
||||||
url: `/api/attachments/${tableId}/delete`,
|
|
||||||
body: {
|
|
||||||
keys,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes attachments from the builder bucket.
|
|
||||||
* @param keys the attachments to delete
|
|
||||||
*/
|
|
||||||
deleteBuilderAttachments: async keys => {
|
|
||||||
return await API.post({
|
|
||||||
url: `/api/attachments/delete`,
|
|
||||||
body: {
|
|
||||||
keys,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download an attachment from a row given its column name.
|
* Download an attachment from a row given its column name.
|
||||||
* @param datasourceId the ID of the datasource to download from
|
* @param datasourceId the ID of the datasource to download from
|
||||||
|
|
|
@ -4,33 +4,36 @@
|
||||||
Button,
|
Button,
|
||||||
Combobox,
|
Combobox,
|
||||||
DatePicker,
|
DatePicker,
|
||||||
DrawerContent,
|
|
||||||
Icon,
|
Icon,
|
||||||
Input,
|
Input,
|
||||||
Label,
|
|
||||||
Layout,
|
Layout,
|
||||||
Multiselect,
|
|
||||||
Select,
|
Select,
|
||||||
|
Label,
|
||||||
|
Multiselect,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
import { FieldType, SearchQueryOperators } from "@budibase/types"
|
||||||
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
|
||||||
import { generate } from "shortid"
|
import { generate } from "shortid"
|
||||||
import { Constants, LuceneUtils } from "@budibase/frontend-core"
|
import { LuceneUtils, Constants } from "@budibase/frontend-core"
|
||||||
import { getFields } from "helpers/searchFields"
|
import { getContext } from "svelte"
|
||||||
import { FieldType } from "@budibase/types"
|
|
||||||
import { createEventDispatcher, onMount } from "svelte"
|
|
||||||
import FilterUsers from "./FilterUsers.svelte"
|
import FilterUsers from "./FilterUsers.svelte"
|
||||||
|
|
||||||
|
const { OperatorOptions } = Constants
|
||||||
|
|
||||||
export let schemaFields
|
export let schemaFields
|
||||||
export let filters = []
|
export let filters = []
|
||||||
export let bindings = []
|
|
||||||
export let panel = ClientBindingPanel
|
|
||||||
export let allowBindings = true
|
|
||||||
export let datasource
|
export let datasource
|
||||||
|
export let behaviourFilters = false
|
||||||
|
export let allowBindings = false
|
||||||
|
export let filtersLabel = "Filters"
|
||||||
|
|
||||||
|
$: matchAny = filters?.find(filter => filter.operator === "allOr") != null
|
||||||
|
$: onEmptyFilter =
|
||||||
|
filters?.find(filter => filter.onEmptyFilter)?.onEmptyFilter ?? "all"
|
||||||
|
|
||||||
|
$: fieldFilters = filters.filter(
|
||||||
|
filter => filter.operator !== "allOr" && !filter.onEmptyFilter
|
||||||
|
)
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
|
||||||
const { OperatorOptions } = Constants
|
|
||||||
const KeyedFieldRegex = /\d[0-9]*:/g
|
|
||||||
const behaviourOptions = [
|
const behaviourOptions = [
|
||||||
{ value: "and", label: "Match all filters" },
|
{ value: "and", label: "Match all filters" },
|
||||||
{ value: "or", label: "Match any filter" },
|
{ value: "or", label: "Match any filter" },
|
||||||
|
@ -40,62 +43,18 @@
|
||||||
{ value: "none", label: "Return no rows" },
|
{ value: "none", label: "Return no rows" },
|
||||||
]
|
]
|
||||||
|
|
||||||
let rawFilters
|
const context = getContext("context")
|
||||||
let matchAny = false
|
|
||||||
let onEmptyFilter = "all"
|
|
||||||
|
|
||||||
$: parseFilters(filters)
|
$: fieldOptions = (schemaFields ?? [])
|
||||||
$: dispatch("change", enrichFilters(rawFilters, matchAny, onEmptyFilter))
|
.filter(field => getValidOperatorsForType(field).length)
|
||||||
$: enrichedSchemaFields = getFields(schemaFields || [], { allowLinks: true })
|
.map(field => ({
|
||||||
$: fieldOptions = enrichedSchemaFields.map(field => field.name) || []
|
label: field.displayName || field.name,
|
||||||
$: valueTypeOptions = allowBindings ? ["Value", "Binding"] : ["Value"]
|
value: field.name,
|
||||||
|
|
||||||
// Remove field key prefixes and determine which behaviours to use
|
|
||||||
const parseFilters = filters => {
|
|
||||||
matchAny = filters?.find(filter => filter.operator === "allOr") != null
|
|
||||||
onEmptyFilter =
|
|
||||||
filters?.find(filter => filter.onEmptyFilter)?.onEmptyFilter ?? "all"
|
|
||||||
rawFilters = (filters || [])
|
|
||||||
.filter(filter => filter.operator !== "allOr" && !filter.onEmptyFilter)
|
|
||||||
.map(filter => {
|
|
||||||
const { field } = filter
|
|
||||||
let newFilter = { ...filter }
|
|
||||||
delete newFilter.allOr
|
|
||||||
if (typeof field === "string" && field.match(KeyedFieldRegex) != null) {
|
|
||||||
const parts = field.split(":")
|
|
||||||
parts.shift()
|
|
||||||
newFilter.field = parts.join(":")
|
|
||||||
}
|
|
||||||
return newFilter
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
parseFilters(filters)
|
|
||||||
rawFilters.forEach(filter => {
|
|
||||||
filter.type =
|
|
||||||
schemaFields.find(field => field.name === filter.field)?.type ||
|
|
||||||
filter.type
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Add field key prefixes and a special metadata filter object to indicate
|
|
||||||
// how to handle filter behaviour
|
|
||||||
const enrichFilters = (rawFilters, matchAny, onEmptyFilter) => {
|
|
||||||
let count = 1
|
|
||||||
return rawFilters
|
|
||||||
.filter(filter => filter.field)
|
|
||||||
.map(filter => ({
|
|
||||||
...filter,
|
|
||||||
field: `${count++}:${filter.field}`,
|
|
||||||
}))
|
}))
|
||||||
.concat(matchAny ? [{ operator: "allOr" }] : [])
|
|
||||||
.concat([{ onEmptyFilter }])
|
|
||||||
}
|
|
||||||
|
|
||||||
const addFilter = () => {
|
const addFilter = () => {
|
||||||
rawFilters = [
|
filters = [
|
||||||
...rawFilters,
|
...(filters || []),
|
||||||
{
|
{
|
||||||
id: generate(),
|
id: generate(),
|
||||||
field: null,
|
field: null,
|
||||||
|
@ -107,22 +66,57 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeFilter = id => {
|
const removeFilter = id => {
|
||||||
rawFilters = rawFilters.filter(field => field.id !== id)
|
filters = filters.filter(field => field.id !== id)
|
||||||
}
|
}
|
||||||
|
|
||||||
const duplicateFilter = id => {
|
const duplicateFilter = id => {
|
||||||
const existingFilter = rawFilters.find(filter => filter.id === id)
|
const existingFilter = filters.find(filter => filter.id === id)
|
||||||
const duplicate = { ...existingFilter, id: generate() }
|
const duplicate = { ...existingFilter, id: generate() }
|
||||||
rawFilters = [...rawFilters, duplicate]
|
filters = [...filters, duplicate]
|
||||||
|
}
|
||||||
|
|
||||||
|
const onFieldChange = filter => {
|
||||||
|
const previousType = filter.type
|
||||||
|
sanitizeTypes(filter)
|
||||||
|
sanitizeOperator(filter)
|
||||||
|
sanitizeValue(filter, previousType)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onOperatorChange = filter => {
|
||||||
|
sanitizeOperator(filter)
|
||||||
|
sanitizeValue(filter, filter.type)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onValueTypeChange = filter => {
|
||||||
|
sanitizeValue(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFieldOptions = field => {
|
||||||
|
const schema = schemaFields.find(x => x.name === field)
|
||||||
|
return schema?.constraints?.inclusion || []
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSchema = filter => {
|
const getSchema = filter => {
|
||||||
return enrichedSchemaFields.find(field => field.name === filter.field)
|
return schemaFields.find(field => field.name === filter.field)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getValidOperatorsForType = filter => {
|
||||||
|
if (!filter?.field && !filter?.name) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return LuceneUtils.getValidOperatorsForType(
|
||||||
|
filter,
|
||||||
|
filter.field || filter.name,
|
||||||
|
datasource
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
$: valueTypeOptions = allowBindings ? ["Value", "Binding"] : ["Value"]
|
||||||
|
|
||||||
const sanitizeTypes = 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 = schemaFields.find(x => x.name === filter.field)
|
||||||
filter.type = fieldSchema?.type
|
filter.type = fieldSchema?.type
|
||||||
filter.subtype = fieldSchema?.subtype
|
filter.subtype = fieldSchema?.subtype
|
||||||
|
|
||||||
|
@ -154,68 +148,53 @@
|
||||||
|
|
||||||
// Ensure array values are properly set and cleared
|
// Ensure array values are properly set and cleared
|
||||||
if (Array.isArray(filter.value)) {
|
if (Array.isArray(filter.value)) {
|
||||||
if (filter.valueType !== "Value" || filter.type !== "array") {
|
if (filter.valueType !== "Value" || filter.type !== FieldType.ARRAY) {
|
||||||
filter.value = null
|
filter.value = null
|
||||||
}
|
}
|
||||||
} else if (filter.type === "array" && filter.valueType === "Value") {
|
} else if (
|
||||||
|
filter.type === FieldType.ARRAY &&
|
||||||
|
filter.valueType === "Value"
|
||||||
|
) {
|
||||||
filter.value = []
|
filter.value = []
|
||||||
} else if (
|
} else if (
|
||||||
previousType !== filter.type &&
|
previousType !== filter.type &&
|
||||||
(previousType === FieldType.BB_REFERENCE ||
|
(previousType === FieldType.BB_REFERENCE ||
|
||||||
filter.type === FieldType.BB_REFERENCE)
|
filter.type === FieldType.BB_REFERENCE)
|
||||||
) {
|
) {
|
||||||
filter.value = filter.type === "array" ? [] : null
|
filter.value = filter.type === FieldType.ARRAY ? [] : null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onFieldChange = filter => {
|
function handleAllOr(option) {
|
||||||
const previousType = filter.type
|
filters = filters.filter(f => f.operator !== "allOr")
|
||||||
sanitizeTypes(filter)
|
if (option === "or") {
|
||||||
sanitizeOperator(filter)
|
filters.push({ operator: "allOr" })
|
||||||
sanitizeValue(filter, previousType)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onOperatorChange = filter => {
|
function handleOnEmptyFilter(value) {
|
||||||
sanitizeOperator(filter)
|
filters = filters?.filter(filter => !filter.onEmptyFilter)
|
||||||
sanitizeValue(filter, filter.type)
|
filters.push({ onEmptyFilter: value })
|
||||||
}
|
|
||||||
|
|
||||||
const onValueTypeChange = filter => {
|
|
||||||
sanitizeValue(filter)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getFieldOptions = field => {
|
|
||||||
const schema = enrichedSchemaFields.find(x => x.name === field)
|
|
||||||
return schema?.constraints?.inclusion || []
|
|
||||||
}
|
|
||||||
|
|
||||||
const getValidOperatorsForType = filter => {
|
|
||||||
if (!filter?.field) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
return LuceneUtils.getValidOperatorsForType(
|
|
||||||
{ type: filter.type, subtype: filter.subtype },
|
|
||||||
filter.field,
|
|
||||||
datasource
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DrawerContent>
|
<div class="container" class:mobile={$context?.device?.mobile}>
|
||||||
<div class="container">
|
|
||||||
<Layout noPadding>
|
<Layout noPadding>
|
||||||
{#if !rawFilters?.length}
|
{#if fieldOptions?.length}
|
||||||
<Body size="S">Add your first filter expression.</Body>
|
<Body size="S">
|
||||||
|
{#if !fieldFilters?.length}
|
||||||
|
Add your first filter expression.
|
||||||
{:else}
|
{:else}
|
||||||
<div class="fields">
|
<slot name="filtering-hero-content" />
|
||||||
|
{#if behaviourFilters}
|
||||||
|
<div class="behaviour-filters">
|
||||||
<Select
|
<Select
|
||||||
label="Behaviour"
|
label="Behaviour"
|
||||||
value={matchAny ? "or" : "and"}
|
value={matchAny ? "or" : "and"}
|
||||||
options={behaviourOptions}
|
options={behaviourOptions}
|
||||||
getOptionLabel={opt => opt.label}
|
getOptionLabel={opt => opt.label}
|
||||||
getOptionValue={opt => opt.value}
|
getOptionValue={opt => opt.value}
|
||||||
on:change={e => (matchAny = e.detail === "or")}
|
on:change={e => handleAllOr(e.detail)}
|
||||||
placeholder={null}
|
placeholder={null}
|
||||||
/>
|
/>
|
||||||
{#if datasource?.type === "table"}
|
{#if datasource?.type === "table"}
|
||||||
|
@ -225,17 +204,23 @@
|
||||||
options={onEmptyOptions}
|
options={onEmptyOptions}
|
||||||
getOptionLabel={opt => opt.label}
|
getOptionLabel={opt => opt.label}
|
||||||
getOptionValue={opt => opt.value}
|
getOptionValue={opt => opt.value}
|
||||||
on:change={e => (onEmptyFilter = e.detail)}
|
on:change={e => handleOnEmptyFilter(e.detail)}
|
||||||
placeholder={null}
|
placeholder={null}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</Body>
|
||||||
|
{#if fieldFilters?.length}
|
||||||
<div>
|
<div>
|
||||||
|
{#if filtersLabel}
|
||||||
<div class="filter-label">
|
<div class="filter-label">
|
||||||
<Label>Filters</Label>
|
<Label>{filtersLabel}</Label>
|
||||||
</div>
|
</div>
|
||||||
<div class="fields">
|
{/if}
|
||||||
{#each rawFilters as filter}
|
<div class="fields" class:with-bindings={allowBindings}>
|
||||||
|
{#each fieldFilters as filter}
|
||||||
<Select
|
<Select
|
||||||
bind:value={filter.field}
|
bind:value={filter.field}
|
||||||
options={fieldOptions}
|
options={fieldOptions}
|
||||||
|
@ -249,6 +234,7 @@
|
||||||
on:change={() => onOperatorChange(filter)}
|
on:change={() => onOperatorChange(filter)}
|
||||||
placeholder={null}
|
placeholder={null}
|
||||||
/>
|
/>
|
||||||
|
{#if allowBindings}
|
||||||
<Select
|
<Select
|
||||||
disabled={filter.noValue || !filter.field}
|
disabled={filter.noValue || !filter.field}
|
||||||
options={valueTypeOptions}
|
options={valueTypeOptions}
|
||||||
|
@ -256,31 +242,24 @@
|
||||||
on:change={() => onValueTypeChange(filter)}
|
on:change={() => onValueTypeChange(filter)}
|
||||||
placeholder={null}
|
placeholder={null}
|
||||||
/>
|
/>
|
||||||
{#if filter.field && filter.valueType === "Binding"}
|
{/if}
|
||||||
<DrawerBindableInput
|
{#if allowBindings && filter.field && filter.valueType === "Binding"}
|
||||||
disabled={filter.noValue}
|
<slot name="binding" {filter} />
|
||||||
title={filter.field}
|
{:else if [FieldType.STRING, FieldType.LONGFORM, FieldType.NUMBER, FieldType.BIGINT, FieldType.FORMULA].includes(filter.type)}
|
||||||
value={filter.value}
|
|
||||||
placeholder="Value"
|
|
||||||
{panel}
|
|
||||||
{bindings}
|
|
||||||
on:change={event => (filter.value = event.detail)}
|
|
||||||
/>
|
|
||||||
{:else if ["string", "longform", "number", "bigint", "formula"].includes(filter.type)}
|
|
||||||
<Input disabled={filter.noValue} bind:value={filter.value} />
|
<Input disabled={filter.noValue} bind:value={filter.value} />
|
||||||
{:else if filter.type === "array" || (filter.type === "options" && filter.operator === "oneOf")}
|
{:else if filter.type === FieldType.ARRAY || (filter.type === FieldType.OPTIONS && filter.operator === SearchQueryOperators.ONE_OF)}
|
||||||
<Multiselect
|
<Multiselect
|
||||||
disabled={filter.noValue}
|
disabled={filter.noValue}
|
||||||
options={getFieldOptions(filter.field)}
|
options={getFieldOptions(filter.field)}
|
||||||
bind:value={filter.value}
|
bind:value={filter.value}
|
||||||
/>
|
/>
|
||||||
{:else if filter.type === "options"}
|
{:else if filter.type === FieldType.OPTIONS}
|
||||||
<Combobox
|
<Combobox
|
||||||
disabled={filter.noValue}
|
disabled={filter.noValue}
|
||||||
options={getFieldOptions(filter.field)}
|
options={getFieldOptions(filter.field)}
|
||||||
bind:value={filter.value}
|
bind:value={filter.value}
|
||||||
/>
|
/>
|
||||||
{:else if filter.type === "boolean"}
|
{:else if filter.type === FieldType.BOOLEAN}
|
||||||
<Combobox
|
<Combobox
|
||||||
disabled={filter.noValue}
|
disabled={filter.noValue}
|
||||||
options={[
|
options={[
|
||||||
|
@ -289,7 +268,7 @@
|
||||||
]}
|
]}
|
||||||
bind:value={filter.value}
|
bind:value={filter.value}
|
||||||
/>
|
/>
|
||||||
{:else if filter.type === "datetime"}
|
{:else if filter.type === FieldType.DATETIME}
|
||||||
<DatePicker
|
<DatePicker
|
||||||
disabled={filter.noValue}
|
disabled={filter.noValue}
|
||||||
enableTime={!getSchema(filter)?.dateOnly}
|
enableTime={!getSchema(filter)?.dateOnly}
|
||||||
|
@ -306,8 +285,9 @@
|
||||||
disabled={filter.noValue}
|
disabled={filter.noValue}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<DrawerBindableInput disabled />
|
<Input disabled />
|
||||||
{/if}
|
{/if}
|
||||||
|
<div class="controls">
|
||||||
<Icon
|
<Icon
|
||||||
name="Duplicate"
|
name="Duplicate"
|
||||||
hoverable
|
hoverable
|
||||||
|
@ -320,18 +300,21 @@
|
||||||
size="S"
|
size="S"
|
||||||
on:click={() => removeFilter(filter.id)}
|
on:click={() => removeFilter(filter.id)}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="bottom">
|
<div>
|
||||||
<Button icon="AddCircle" size="M" secondary on:click={addFilter}>
|
<Button icon="AddCircle" size="M" secondary on:click={addFilter}>
|
||||||
Add filter
|
Add filter
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
{:else}
|
||||||
|
<Body size="S">None of the table column can be used for filtering.</Body>
|
||||||
|
{/if}
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
</div>
|
||||||
</DrawerContent>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.container {
|
.container {
|
||||||
|
@ -339,22 +322,42 @@
|
||||||
max-width: 1000px;
|
max-width: 1000px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fields {
|
.fields {
|
||||||
display: grid;
|
display: grid;
|
||||||
column-gap: var(--spacing-l);
|
column-gap: var(--spacing-l);
|
||||||
row-gap: var(--spacing-s);
|
row-gap: var(--spacing-s);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
grid-template-columns: 1fr 120px 1fr auto auto;
|
||||||
|
}
|
||||||
|
.fields.with-bindings {
|
||||||
grid-template-columns: minmax(150px, 1fr) 170px 120px minmax(150px, 1fr) 16px 16px;
|
grid-template-columns: minmax(150px, 1fr) 170px 120px minmax(150px, 1fr) 16px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container.mobile .fields {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.container.mobile .controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--spacing-s) 0;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
.filter-label {
|
.filter-label {
|
||||||
margin-bottom: var(--spacing-s);
|
margin-bottom: var(--spacing-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom {
|
.behaviour-filters {
|
||||||
display: flex;
|
display: grid;
|
||||||
justify-content: space-between;
|
column-gap: var(--spacing-l);
|
||||||
|
row-gap: var(--spacing-s);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
grid-template-columns: minmax(150px, 1fr) 170px 120px minmax(150px, 1fr) 16px 16px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
|
@ -1,9 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
import { Select, Multiselect } from "@budibase/bbui"
|
import { Select, Multiselect } from "@budibase/bbui"
|
||||||
import { fetchData } from "@budibase/frontend-core"
|
import { fetchData } from "@budibase/frontend-core"
|
||||||
|
import { createAPIClient } from "../api"
|
||||||
|
|
||||||
import { API } from "api"
|
export let API = createAPIClient()
|
||||||
|
|
||||||
export let value = null
|
export let value = null
|
||||||
export let disabled
|
export let disabled
|
||||||
export let multiselect = false
|
export let multiselect = false
|
|
@ -61,14 +61,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteAttachments = async fileList => {
|
|
||||||
try {
|
|
||||||
return await API.deleteBuilderAttachments(fileList)
|
|
||||||
} catch (error) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
api = {
|
api = {
|
||||||
focus: () => open(),
|
focus: () => open(),
|
||||||
|
@ -101,7 +93,6 @@
|
||||||
on:change={e => onChange(e.detail)}
|
on:change={e => onChange(e.detail)}
|
||||||
maximum={maximum || schema.constraints?.length?.maximum}
|
maximum={maximum || schema.constraints?.length?.maximum}
|
||||||
{processFiles}
|
{processFiles}
|
||||||
{deleteAttachments}
|
|
||||||
{handleFileTooLarge}
|
{handleFileTooLarge}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,3 +6,4 @@ export { default as UserAvatars } from "./UserAvatars.svelte"
|
||||||
export { default as Updating } from "./Updating.svelte"
|
export { default as Updating } from "./Updating.svelte"
|
||||||
export { Grid } from "./grid"
|
export { Grid } from "./grid"
|
||||||
export { default as ClientAppSkeleton } from "./ClientAppSkeleton.svelte"
|
export { default as ClientAppSkeleton } from "./ClientAppSkeleton.svelte"
|
||||||
|
export { default as FilterBuilder } from "./FilterBuilder.svelte"
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit ef186d00241f96037f9fd34d7a3826041977ab3a
|
Subproject commit c68183402b8fb17248572006531d5293ffc8a9ac
|
|
@ -127,13 +127,6 @@ export const uploadFile = async function (
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const deleteObjects = async function (ctx: Ctx) {
|
|
||||||
ctx.body = await objectStore.deleteFiles(
|
|
||||||
ObjectStoreBuckets.APPS,
|
|
||||||
ctx.request.body.keys
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const requiresMigration = async (ctx: Ctx) => {
|
const requiresMigration = async (ctx: Ctx) => {
|
||||||
const appId = context.getAppId()
|
const appId = context.getAppId()
|
||||||
if (!appId) {
|
if (!appId) {
|
||||||
|
|
|
@ -32,11 +32,6 @@ router
|
||||||
.get("/builder/:file*", controller.serveBuilder)
|
.get("/builder/:file*", controller.serveBuilder)
|
||||||
.get("/api/assets/client", controller.serveClientLibrary)
|
.get("/api/assets/client", controller.serveClientLibrary)
|
||||||
.post("/api/attachments/process", authorized(BUILDER), controller.uploadFile)
|
.post("/api/attachments/process", authorized(BUILDER), controller.uploadFile)
|
||||||
.post(
|
|
||||||
"/api/attachments/delete",
|
|
||||||
authorized(BUILDER),
|
|
||||||
controller.deleteObjects
|
|
||||||
)
|
|
||||||
.post("/api/beta/:feature", controller.toggleBetaUiFeature)
|
.post("/api/beta/:feature", controller.toggleBetaUiFeature)
|
||||||
.post(
|
.post(
|
||||||
"/api/attachments/:tableId/upload",
|
"/api/attachments/:tableId/upload",
|
||||||
|
@ -44,12 +39,6 @@ router
|
||||||
authorized(PermissionType.TABLE, PermissionLevel.WRITE),
|
authorized(PermissionType.TABLE, PermissionLevel.WRITE),
|
||||||
controller.uploadFile
|
controller.uploadFile
|
||||||
)
|
)
|
||||||
.post(
|
|
||||||
"/api/attachments/:tableId/delete",
|
|
||||||
paramResource("tableId"),
|
|
||||||
authorized(PermissionType.TABLE, PermissionLevel.WRITE),
|
|
||||||
controller.deleteObjects
|
|
||||||
)
|
|
||||||
.get("/app/preview", authorized(BUILDER), controller.serveBuilderPreview)
|
.get("/app/preview", authorized(BUILDER), controller.serveBuilderPreview)
|
||||||
.get("/app/:appUrl/:path*", controller.serveApp)
|
.get("/app/:appUrl/:path*", controller.serveApp)
|
||||||
.get("/:appId/:path*", controller.serveApp)
|
.get("/:appId/:path*", controller.serveApp)
|
||||||
|
|
|
@ -2,6 +2,7 @@ import {
|
||||||
Datasource,
|
Datasource,
|
||||||
FieldSubtype,
|
FieldSubtype,
|
||||||
FieldType,
|
FieldType,
|
||||||
|
FormulaType,
|
||||||
SearchFilter,
|
SearchFilter,
|
||||||
SearchQuery,
|
SearchQuery,
|
||||||
SearchQueryFields,
|
SearchQueryFields,
|
||||||
|
@ -19,9 +20,13 @@ const HBS_REGEX = /{{([^{].*?)}}/g
|
||||||
* Returns the valid operator options for a certain data type
|
* Returns the valid operator options for a certain data type
|
||||||
*/
|
*/
|
||||||
export const getValidOperatorsForType = (
|
export const getValidOperatorsForType = (
|
||||||
fieldType: { type: FieldType; subtype?: FieldSubtype },
|
fieldType: {
|
||||||
|
type: FieldType
|
||||||
|
subtype?: FieldSubtype
|
||||||
|
formulaType?: FormulaType
|
||||||
|
},
|
||||||
field: string,
|
field: string,
|
||||||
datasource: Datasource & { tableId: any } // TODO: is this table id ever populated?
|
datasource: Datasource & { tableId: any }
|
||||||
) => {
|
) => {
|
||||||
const Op = OperatorOptions
|
const Op = OperatorOptions
|
||||||
const stringOps = [
|
const stringOps = [
|
||||||
|
@ -46,7 +51,7 @@ export const getValidOperatorsForType = (
|
||||||
value: string
|
value: string
|
||||||
label: string
|
label: string
|
||||||
}[] = []
|
}[] = []
|
||||||
const { type, subtype } = fieldType
|
const { type, subtype, formulaType } = fieldType
|
||||||
if (type === FieldType.STRING) {
|
if (type === FieldType.STRING) {
|
||||||
ops = stringOps
|
ops = stringOps
|
||||||
} else if (type === FieldType.NUMBER || type === FieldType.BIGINT) {
|
} else if (type === FieldType.NUMBER || type === FieldType.BIGINT) {
|
||||||
|
@ -61,7 +66,7 @@ export const getValidOperatorsForType = (
|
||||||
ops = stringOps
|
ops = stringOps
|
||||||
} else if (type === FieldType.DATETIME) {
|
} else if (type === FieldType.DATETIME) {
|
||||||
ops = numOps
|
ops = numOps
|
||||||
} else if (type === FieldType.FORMULA) {
|
} else if (type === FieldType.FORMULA && formulaType === FormulaType.STATIC) {
|
||||||
ops = stringOps.concat([Op.MoreThan, Op.LessThan])
|
ops = stringOps.concat([Op.MoreThan, Op.LessThan])
|
||||||
} else if (type === FieldType.BB_REFERENCE && subtype == FieldSubtype.USER) {
|
} else if (type === FieldType.BB_REFERENCE && subtype == FieldSubtype.USER) {
|
||||||
ops = [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty, Op.In]
|
ops = [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty, Op.In]
|
||||||
|
@ -115,9 +120,10 @@ const cleanupQuery = (query: SearchQuery) => {
|
||||||
/**
|
/**
|
||||||
* Removes a numeric prefix on field names designed to give fields uniqueness
|
* Removes a numeric prefix on field names designed to give fields uniqueness
|
||||||
*/
|
*/
|
||||||
const removeKeyNumbering = (key: string) => {
|
export const removeKeyNumbering = (key: string): string => {
|
||||||
if (typeof key === "string" && key.match(/\d[0-9]*:/g) != null) {
|
if (typeof key === "string" && key.match(/\d[0-9]*:/g) != null) {
|
||||||
const parts = key.split(":")
|
const parts = key.split(":")
|
||||||
|
// remove the number
|
||||||
parts.shift()
|
parts.shift()
|
||||||
return parts.join(":")
|
return parts.join(":")
|
||||||
} else {
|
} else {
|
||||||
|
|
Loading…
Reference in New Issue