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",
|
||||
"packages": [
|
||||
"packages/*",
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit bd0e01d639ec3b2547e7c859a1c43b622dce8344
|
||||
Subproject commit 328c84234d11d97d840f0eb2c72665b04ba9e4f8
|
|
@ -8,19 +8,9 @@ import {
|
|||
SearchParams,
|
||||
WithRequired,
|
||||
} from "@budibase/types"
|
||||
import { dataFilters } from "@budibase/shared-core"
|
||||
|
||||
const QUERY_START_REGEX = /\d[0-9]*:/g
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
export const removeKeyNumbering = dataFilters.removeKeyNumbering
|
||||
|
||||
/**
|
||||
* Class to build lucene query URLs.
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
notifications,
|
||||
Checkbox,
|
||||
DatePicker,
|
||||
DrawerContent,
|
||||
} from "@budibase/bbui"
|
||||
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
||||
import { automationStore, selectedAutomation, tables } from "stores/builder"
|
||||
|
@ -37,7 +38,7 @@
|
|||
hbAutocomplete,
|
||||
EditorModes,
|
||||
} 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 {
|
||||
getSchemaForDatasourcePlus,
|
||||
|
@ -442,15 +443,16 @@
|
|||
<Button cta slot="buttons" on:click={() => saveFilters(key)}>
|
||||
Save
|
||||
</Button>
|
||||
<FilterDrawer
|
||||
slot="body"
|
||||
{filters}
|
||||
{bindings}
|
||||
{schemaFields}
|
||||
datasource={{ type: "table", tableId }}
|
||||
panel={AutomationBindingPanel}
|
||||
on:change={e => (tempFilters = e.detail)}
|
||||
/>
|
||||
<DrawerContent slot="body">
|
||||
<FilterBuilder
|
||||
{filters}
|
||||
{bindings}
|
||||
{schemaFields}
|
||||
datasource={{ type: "table", tableId }}
|
||||
panel={AutomationBindingPanel}
|
||||
on:change={e => (tempFilters = e.detail)}
|
||||
/>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
{:else if value.customType === "password"}
|
||||
<Input
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { createEventDispatcher } from "svelte"
|
||||
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 filters
|
||||
|
@ -40,7 +40,7 @@
|
|||
onConfirm={() => dispatch("change", tempValue)}
|
||||
>
|
||||
<div class="wrapper">
|
||||
<FilterDrawer
|
||||
<FilterBuilder
|
||||
allowBindings={false}
|
||||
{filters}
|
||||
{schemaFields}
|
||||
|
|
|
@ -27,14 +27,6 @@
|
|||
return []
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteAttachments(fileList) {
|
||||
try {
|
||||
return await API.deleteBuilderAttachments(fileList)
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dropzone
|
||||
|
@ -42,6 +34,5 @@
|
|||
{label}
|
||||
{...$$restProps}
|
||||
{processFiles}
|
||||
{deleteAttachments}
|
||||
{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>
|
||||
import { notifications, ActionButton, Button, Drawer } from "@budibase/bbui"
|
||||
import {
|
||||
notifications,
|
||||
ActionButton,
|
||||
Button,
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
} from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { getDatasourceForProvider, getSchemaForDatasource } from "dataBinding"
|
||||
import FilterDrawer from "./FilterDrawer.svelte"
|
||||
import FilterBuilder from "./FilterBuilder.svelte"
|
||||
import { selectedScreen } from "stores/builder"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
@ -40,14 +46,15 @@
|
|||
</div>
|
||||
<Drawer bind:this={drawer} title="Filtering" on:drawerHide on:drawerShow>
|
||||
<Button cta slot="buttons" on:click={saveFilter}>Save</Button>
|
||||
<FilterDrawer
|
||||
slot="body"
|
||||
filters={value}
|
||||
{bindings}
|
||||
{schemaFields}
|
||||
{datasource}
|
||||
on:change={e => (tempValue = e.detail)}
|
||||
/>
|
||||
<DrawerContent slot="body">
|
||||
<FilterBuilder
|
||||
filters={value}
|
||||
{bindings}
|
||||
{schemaFields}
|
||||
{datasource}
|
||||
on:change={e => (tempValue = e.detail)}
|
||||
/>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -1,216 +1,14 @@
|
|||
<script>
|
||||
import {
|
||||
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"
|
||||
import { FilterBuilder } from "@budibase/frontend-core"
|
||||
|
||||
export let schemaFields
|
||||
export let filters = []
|
||||
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>
|
||||
|
||||
<div class="container" class:mobile={$context.device.mobile}>
|
||||
<Layout noPadding>
|
||||
<Body size="S">
|
||||
{#if !filters?.length}
|
||||
Add your first filter expression.
|
||||
{:else}
|
||||
Results are filtered to only those which match all of the following
|
||||
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>
|
||||
{/each}
|
||||
</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>
|
||||
<FilterBuilder bind:filters {schemaFields} {datasource} filtersLabel={null}>
|
||||
<div slot="filtering-hero-content">
|
||||
Results are filtered to only those which match all of the following
|
||||
constraints.
|
||||
</div>
|
||||
</FilterBuilder>
|
||||
|
|
|
@ -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 value = fieldApiMapper.set(e.detail)
|
||||
const changed = fieldApi.setValue(value)
|
||||
|
@ -98,7 +87,6 @@
|
|||
error={fieldState.error}
|
||||
on:change={handleChange}
|
||||
{processFiles}
|
||||
{deleteAttachments}
|
||||
{handleFileTooLarge}
|
||||
{handleTooManyFiles}
|
||||
{maximum}
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
"@budibase/types": "0.0.0",
|
||||
"dayjs": "^1.10.8",
|
||||
"lodash": "4.17.21",
|
||||
"shortid": "2.2.15",
|
||||
"socket.io-client": "^4.6.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,34 +61,6 @@ export const buildAttachmentEndpoints = API => {
|
|||
})
|
||||
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.
|
||||
* @param datasourceId the ID of the datasource to download from
|
||||
|
|
|
@ -4,33 +4,36 @@
|
|||
Button,
|
||||
Combobox,
|
||||
DatePicker,
|
||||
DrawerContent,
|
||||
Icon,
|
||||
Input,
|
||||
Label,
|
||||
Layout,
|
||||
Multiselect,
|
||||
Select,
|
||||
Label,
|
||||
Multiselect,
|
||||
} from "@budibase/bbui"
|
||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
||||
import { FieldType, SearchQueryOperators } from "@budibase/types"
|
||||
import { generate } from "shortid"
|
||||
import { Constants, LuceneUtils } from "@budibase/frontend-core"
|
||||
import { getFields } from "helpers/searchFields"
|
||||
import { FieldType } from "@budibase/types"
|
||||
import { createEventDispatcher, onMount } from "svelte"
|
||||
import { LuceneUtils, Constants } from "@budibase/frontend-core"
|
||||
import { getContext } from "svelte"
|
||||
import FilterUsers from "./FilterUsers.svelte"
|
||||
|
||||
const { OperatorOptions } = Constants
|
||||
|
||||
export let schemaFields
|
||||
export let filters = []
|
||||
export let bindings = []
|
||||
export let panel = ClientBindingPanel
|
||||
export let allowBindings = true
|
||||
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 = [
|
||||
{ value: "and", label: "Match all filters" },
|
||||
{ value: "or", label: "Match any filter" },
|
||||
|
@ -40,62 +43,18 @@
|
|||
{ value: "none", label: "Return no rows" },
|
||||
]
|
||||
|
||||
let rawFilters
|
||||
let matchAny = false
|
||||
let onEmptyFilter = "all"
|
||||
const context = getContext("context")
|
||||
|
||||
$: parseFilters(filters)
|
||||
$: dispatch("change", enrichFilters(rawFilters, matchAny, onEmptyFilter))
|
||||
$: enrichedSchemaFields = getFields(schemaFields || [], { allowLinks: true })
|
||||
$: fieldOptions = enrichedSchemaFields.map(field => field.name) || []
|
||||
$: valueTypeOptions = allowBindings ? ["Value", "Binding"] : ["Value"]
|
||||
|
||||
// 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 }])
|
||||
}
|
||||
$: fieldOptions = (schemaFields ?? [])
|
||||
.filter(field => getValidOperatorsForType(field).length)
|
||||
.map(field => ({
|
||||
label: field.displayName || field.name,
|
||||
value: field.name,
|
||||
}))
|
||||
|
||||
const addFilter = () => {
|
||||
rawFilters = [
|
||||
...rawFilters,
|
||||
filters = [
|
||||
...(filters || []),
|
||||
{
|
||||
id: generate(),
|
||||
field: null,
|
||||
|
@ -107,22 +66,57 @@
|
|||
}
|
||||
|
||||
const removeFilter = id => {
|
||||
rawFilters = rawFilters.filter(field => field.id !== id)
|
||||
filters = filters.filter(field => field.id !== 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() }
|
||||
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 => {
|
||||
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 => {
|
||||
// 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.subtype = fieldSchema?.subtype
|
||||
|
||||
|
@ -154,88 +148,79 @@
|
|||
|
||||
// Ensure array values are properly set and cleared
|
||||
if (Array.isArray(filter.value)) {
|
||||
if (filter.valueType !== "Value" || filter.type !== "array") {
|
||||
if (filter.valueType !== "Value" || filter.type !== FieldType.ARRAY) {
|
||||
filter.value = null
|
||||
}
|
||||
} else if (filter.type === "array" && filter.valueType === "Value") {
|
||||
} else if (
|
||||
filter.type === FieldType.ARRAY &&
|
||||
filter.valueType === "Value"
|
||||
) {
|
||||
filter.value = []
|
||||
} else if (
|
||||
previousType !== filter.type &&
|
||||
(previousType === FieldType.BB_REFERENCE ||
|
||||
filter.type === FieldType.BB_REFERENCE)
|
||||
) {
|
||||
filter.value = filter.type === "array" ? [] : null
|
||||
filter.value = filter.type === FieldType.ARRAY ? [] : null
|
||||
}
|
||||
}
|
||||
|
||||
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 = enrichedSchemaFields.find(x => x.name === field)
|
||||
return schema?.constraints?.inclusion || []
|
||||
}
|
||||
|
||||
const getValidOperatorsForType = filter => {
|
||||
if (!filter?.field) {
|
||||
return []
|
||||
function handleAllOr(option) {
|
||||
filters = filters.filter(f => f.operator !== "allOr")
|
||||
if (option === "or") {
|
||||
filters.push({ operator: "allOr" })
|
||||
}
|
||||
}
|
||||
|
||||
return LuceneUtils.getValidOperatorsForType(
|
||||
{ type: filter.type, subtype: filter.subtype },
|
||||
filter.field,
|
||||
datasource
|
||||
)
|
||||
function handleOnEmptyFilter(value) {
|
||||
filters = filters?.filter(filter => !filter.onEmptyFilter)
|
||||
filters.push({ onEmptyFilter: value })
|
||||
}
|
||||
</script>
|
||||
|
||||
<DrawerContent>
|
||||
<div class="container">
|
||||
<Layout noPadding>
|
||||
{#if !rawFilters?.length}
|
||||
<Body size="S">Add your first filter expression.</Body>
|
||||
{:else}
|
||||
<div class="fields">
|
||||
<Select
|
||||
label="Behaviour"
|
||||
value={matchAny ? "or" : "and"}
|
||||
options={behaviourOptions}
|
||||
getOptionLabel={opt => opt.label}
|
||||
getOptionValue={opt => opt.value}
|
||||
on:change={e => (matchAny = e.detail === "or")}
|
||||
placeholder={null}
|
||||
/>
|
||||
{#if datasource?.type === "table"}
|
||||
<Select
|
||||
label="When filter empty"
|
||||
value={onEmptyFilter}
|
||||
options={onEmptyOptions}
|
||||
getOptionLabel={opt => opt.label}
|
||||
getOptionValue={opt => opt.value}
|
||||
on:change={e => (onEmptyFilter = e.detail)}
|
||||
placeholder={null}
|
||||
/>
|
||||
<div class="container" class:mobile={$context?.device?.mobile}>
|
||||
<Layout noPadding>
|
||||
{#if fieldOptions?.length}
|
||||
<Body size="S">
|
||||
{#if !fieldFilters?.length}
|
||||
Add your first filter expression.
|
||||
{:else}
|
||||
<slot name="filtering-hero-content" />
|
||||
{#if behaviourFilters}
|
||||
<div class="behaviour-filters">
|
||||
<Select
|
||||
label="Behaviour"
|
||||
value={matchAny ? "or" : "and"}
|
||||
options={behaviourOptions}
|
||||
getOptionLabel={opt => opt.label}
|
||||
getOptionValue={opt => opt.value}
|
||||
on:change={e => handleAllOr(e.detail)}
|
||||
placeholder={null}
|
||||
/>
|
||||
{#if datasource?.type === "table"}
|
||||
<Select
|
||||
label="When filter empty"
|
||||
value={onEmptyFilter}
|
||||
options={onEmptyOptions}
|
||||
getOptionLabel={opt => opt.label}
|
||||
getOptionValue={opt => opt.value}
|
||||
on:change={e => handleOnEmptyFilter(e.detail)}
|
||||
placeholder={null}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</Body>
|
||||
{#if fieldFilters?.length}
|
||||
<div>
|
||||
<div class="filter-label">
|
||||
<Label>Filters</Label>
|
||||
</div>
|
||||
<div class="fields">
|
||||
{#each rawFilters as filter}
|
||||
{#if filtersLabel}
|
||||
<div class="filter-label">
|
||||
<Label>{filtersLabel}</Label>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="fields" class:with-bindings={allowBindings}>
|
||||
{#each fieldFilters as filter}
|
||||
<Select
|
||||
bind:value={filter.field}
|
||||
options={fieldOptions}
|
||||
|
@ -249,38 +234,32 @@
|
|||
on:change={() => onOperatorChange(filter)}
|
||||
placeholder={null}
|
||||
/>
|
||||
<Select
|
||||
disabled={filter.noValue || !filter.field}
|
||||
options={valueTypeOptions}
|
||||
bind:value={filter.valueType}
|
||||
on:change={() => onValueTypeChange(filter)}
|
||||
placeholder={null}
|
||||
/>
|
||||
{#if filter.field && filter.valueType === "Binding"}
|
||||
<DrawerBindableInput
|
||||
disabled={filter.noValue}
|
||||
title={filter.field}
|
||||
value={filter.value}
|
||||
placeholder="Value"
|
||||
{panel}
|
||||
{bindings}
|
||||
on:change={event => (filter.value = event.detail)}
|
||||
{#if allowBindings}
|
||||
<Select
|
||||
disabled={filter.noValue || !filter.field}
|
||||
options={valueTypeOptions}
|
||||
bind:value={filter.valueType}
|
||||
on:change={() => onValueTypeChange(filter)}
|
||||
placeholder={null}
|
||||
/>
|
||||
{:else if ["string", "longform", "number", "bigint", "formula"].includes(filter.type)}
|
||||
{/if}
|
||||
{#if allowBindings && filter.field && filter.valueType === "Binding"}
|
||||
<slot name="binding" {filter} />
|
||||
{:else if [FieldType.STRING, FieldType.LONGFORM, FieldType.NUMBER, FieldType.BIGINT, FieldType.FORMULA].includes(filter.type)}
|
||||
<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
|
||||
disabled={filter.noValue}
|
||||
options={getFieldOptions(filter.field)}
|
||||
bind:value={filter.value}
|
||||
/>
|
||||
{:else if filter.type === "options"}
|
||||
{:else if filter.type === FieldType.OPTIONS}
|
||||
<Combobox
|
||||
disabled={filter.noValue}
|
||||
options={getFieldOptions(filter.field)}
|
||||
bind:value={filter.value}
|
||||
/>
|
||||
{:else if filter.type === "boolean"}
|
||||
{:else if filter.type === FieldType.BOOLEAN}
|
||||
<Combobox
|
||||
disabled={filter.noValue}
|
||||
options={[
|
||||
|
@ -289,7 +268,7 @@
|
|||
]}
|
||||
bind:value={filter.value}
|
||||
/>
|
||||
{:else if filter.type === "datetime"}
|
||||
{:else if filter.type === FieldType.DATETIME}
|
||||
<DatePicker
|
||||
disabled={filter.noValue}
|
||||
enableTime={!getSchema(filter)?.dateOnly}
|
||||
|
@ -306,32 +285,36 @@
|
|||
disabled={filter.noValue}
|
||||
/>
|
||||
{:else}
|
||||
<DrawerBindableInput disabled />
|
||||
<Input disabled />
|
||||
{/if}
|
||||
<Icon
|
||||
name="Duplicate"
|
||||
hoverable
|
||||
size="S"
|
||||
on:click={() => duplicateFilter(filter.id)}
|
||||
/>
|
||||
<Icon
|
||||
name="Close"
|
||||
hoverable
|
||||
size="S"
|
||||
on:click={() => removeFilter(filter.id)}
|
||||
/>
|
||||
<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>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="bottom">
|
||||
<div>
|
||||
<Button icon="AddCircle" size="M" secondary on:click={addFilter}>
|
||||
Add filter
|
||||
</Button>
|
||||
</div>
|
||||
</Layout>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
{:else}
|
||||
<Body size="S">None of the table column can be used for filtering.</Body>
|
||||
{/if}
|
||||
</Layout>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
|
@ -339,22 +322,42 @@
|
|||
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;
|
||||
}
|
||||
.fields.with-bindings {
|
||||
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 {
|
||||
margin-bottom: var(--spacing-s);
|
||||
}
|
||||
|
||||
.bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.behaviour-filters {
|
||||
display: grid;
|
||||
column-gap: var(--spacing-l);
|
||||
row-gap: var(--spacing-s);
|
||||
align-items: center;
|
||||
grid-template-columns: minmax(150px, 1fr) 170px 120px minmax(150px, 1fr) 16px 16px;
|
||||
}
|
||||
</style>
|
|
@ -1,9 +1,9 @@
|
|||
<script>
|
||||
import { Select, Multiselect } from "@budibase/bbui"
|
||||
import { fetchData } from "@budibase/frontend-core"
|
||||
import { createAPIClient } from "../api"
|
||||
|
||||
import { API } from "api"
|
||||
|
||||
export let API = createAPIClient()
|
||||
export let value = null
|
||||
export let disabled
|
||||
export let multiselect = false
|
|
@ -61,14 +61,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
const deleteAttachments = async fileList => {
|
||||
try {
|
||||
return await API.deleteBuilderAttachments(fileList)
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
api = {
|
||||
focus: () => open(),
|
||||
|
@ -101,7 +93,6 @@
|
|||
on:change={e => onChange(e.detail)}
|
||||
maximum={maximum || schema.constraints?.length?.maximum}
|
||||
{processFiles}
|
||||
{deleteAttachments}
|
||||
{handleFileTooLarge}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -6,3 +6,4 @@ export { default as UserAvatars } from "./UserAvatars.svelte"
|
|||
export { default as Updating } from "./Updating.svelte"
|
||||
export { Grid } from "./grid"
|
||||
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 appId = context.getAppId()
|
||||
if (!appId) {
|
||||
|
|
|
@ -32,11 +32,6 @@ router
|
|||
.get("/builder/:file*", controller.serveBuilder)
|
||||
.get("/api/assets/client", controller.serveClientLibrary)
|
||||
.post("/api/attachments/process", authorized(BUILDER), controller.uploadFile)
|
||||
.post(
|
||||
"/api/attachments/delete",
|
||||
authorized(BUILDER),
|
||||
controller.deleteObjects
|
||||
)
|
||||
.post("/api/beta/:feature", controller.toggleBetaUiFeature)
|
||||
.post(
|
||||
"/api/attachments/:tableId/upload",
|
||||
|
@ -44,12 +39,6 @@ router
|
|||
authorized(PermissionType.TABLE, PermissionLevel.WRITE),
|
||||
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/:appUrl/:path*", controller.serveApp)
|
||||
.get("/:appId/:path*", controller.serveApp)
|
||||
|
|
|
@ -2,6 +2,7 @@ import {
|
|||
Datasource,
|
||||
FieldSubtype,
|
||||
FieldType,
|
||||
FormulaType,
|
||||
SearchFilter,
|
||||
SearchQuery,
|
||||
SearchQueryFields,
|
||||
|
@ -19,9 +20,13 @@ const HBS_REGEX = /{{([^{].*?)}}/g
|
|||
* Returns the valid operator options for a certain data type
|
||||
*/
|
||||
export const getValidOperatorsForType = (
|
||||
fieldType: { type: FieldType; subtype?: FieldSubtype },
|
||||
fieldType: {
|
||||
type: FieldType
|
||||
subtype?: FieldSubtype
|
||||
formulaType?: FormulaType
|
||||
},
|
||||
field: string,
|
||||
datasource: Datasource & { tableId: any } // TODO: is this table id ever populated?
|
||||
datasource: Datasource & { tableId: any }
|
||||
) => {
|
||||
const Op = OperatorOptions
|
||||
const stringOps = [
|
||||
|
@ -46,7 +51,7 @@ export const getValidOperatorsForType = (
|
|||
value: string
|
||||
label: string
|
||||
}[] = []
|
||||
const { type, subtype } = fieldType
|
||||
const { type, subtype, formulaType } = fieldType
|
||||
if (type === FieldType.STRING) {
|
||||
ops = stringOps
|
||||
} else if (type === FieldType.NUMBER || type === FieldType.BIGINT) {
|
||||
|
@ -61,7 +66,7 @@ export const getValidOperatorsForType = (
|
|||
ops = stringOps
|
||||
} else if (type === FieldType.DATETIME) {
|
||||
ops = numOps
|
||||
} else if (type === FieldType.FORMULA) {
|
||||
} else if (type === FieldType.FORMULA && formulaType === FormulaType.STATIC) {
|
||||
ops = stringOps.concat([Op.MoreThan, Op.LessThan])
|
||||
} else if (type === FieldType.BB_REFERENCE && subtype == FieldSubtype.USER) {
|
||||
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
|
||||
*/
|
||||
const removeKeyNumbering = (key: string) => {
|
||||
export const removeKeyNumbering = (key: string): string => {
|
||||
if (typeof key === "string" && key.match(/\d[0-9]*:/g) != null) {
|
||||
const parts = key.split(":")
|
||||
// remove the number
|
||||
parts.shift()
|
||||
return parts.join(":")
|
||||
} else {
|
||||
|
|
Loading…
Reference in New Issue