314 lines
9.3 KiB
Svelte
314 lines
9.3 KiB
Svelte
<script>
|
|
import {
|
|
Body,
|
|
Button,
|
|
Combobox,
|
|
Multiselect,
|
|
DatePicker,
|
|
DrawerContent,
|
|
Icon,
|
|
Input,
|
|
Layout,
|
|
Select,
|
|
Label,
|
|
} from "@budibase/bbui"
|
|
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
|
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
|
import { generate } from "shortid"
|
|
import { LuceneUtils, Constants } from "@budibase/frontend-core"
|
|
import { getFields } from "helpers/searchFields"
|
|
import { createEventDispatcher } from "svelte"
|
|
|
|
export let schemaFields
|
|
export let filters = []
|
|
export let bindings = []
|
|
export let panel = ClientBindingPanel
|
|
export let allowBindings = true
|
|
export let fillWidth = false
|
|
export let datasource
|
|
|
|
const dispatch = createEventDispatcher()
|
|
const { OperatorOptions } = Constants
|
|
const { getValidOperatorsForType } = LuceneUtils
|
|
const KeyedFieldRegex = /\d[0-9]*:/g
|
|
const behaviourOptions = [
|
|
{ value: "and", label: "Match all filters" },
|
|
{ value: "or", label: "Match any filter" },
|
|
]
|
|
|
|
let rawFilters
|
|
let matchAny = false
|
|
|
|
$: parseFilters(filters)
|
|
$: dispatch("change", enrichFilters(rawFilters, matchAny))
|
|
$: enrichedSchemaFields = getFields(schemaFields || [], { allowLinks: true })
|
|
$: fieldOptions = enrichedSchemaFields.map(field => field.name) || []
|
|
$: valueTypeOptions = allowBindings ? ["Value", "Binding"] : ["Value"]
|
|
|
|
// Remove field key prefixes and determine whether to use the "match all"
|
|
// or "match any" behaviour
|
|
const parseFilters = filters => {
|
|
matchAny = filters?.find(filter => filter.operator === "allOr") != null
|
|
rawFilters = (filters || [])
|
|
.filter(filter => filter.operator !== "allOr")
|
|
.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
|
|
})
|
|
}
|
|
|
|
// Add field key prefixes and a special metadata filter object to indicate
|
|
// whether to use the "match all" or "match any" behaviour
|
|
const enrichFilters = (rawFilters, matchAny) => {
|
|
let count = 1
|
|
return rawFilters
|
|
.filter(filter => filter.field)
|
|
.map(filter => ({
|
|
...filter,
|
|
field: `${count++}:${filter.field}`,
|
|
}))
|
|
.concat(matchAny ? [{ operator: "allOr" }] : [])
|
|
}
|
|
|
|
const addFilter = () => {
|
|
rawFilters = [
|
|
...rawFilters,
|
|
{
|
|
id: generate(),
|
|
field: null,
|
|
operator: OperatorOptions.Equals.value,
|
|
value: null,
|
|
valueType: "Value",
|
|
},
|
|
]
|
|
}
|
|
|
|
const removeFilter = id => {
|
|
rawFilters = rawFilters.filter(field => field.id !== id)
|
|
}
|
|
|
|
const duplicateFilter = id => {
|
|
const existingFilter = rawFilters.find(filter => filter.id === id)
|
|
const duplicate = { ...existingFilter, id: generate() }
|
|
rawFilters = [...rawFilters, duplicate]
|
|
}
|
|
|
|
const getSchema = filter => {
|
|
return enrichedSchemaFields.find(field => field.name === filter.field)
|
|
}
|
|
|
|
const santizeTypes = filter => {
|
|
// Update type based on field
|
|
const fieldSchema = enrichedSchemaFields.find(x => x.name === filter.field)
|
|
filter.type = fieldSchema?.type
|
|
|
|
// Update external type based on field
|
|
filter.externalType = getSchema(filter)?.externalType
|
|
}
|
|
|
|
const santizeOperator = filter => {
|
|
// Ensure a valid operator is selected
|
|
const operators = getValidOperatorsForType(
|
|
filter.type,
|
|
filter.field,
|
|
datasource
|
|
).map(x => x.value)
|
|
if (!operators.includes(filter.operator)) {
|
|
filter.operator = operators[0] ?? OperatorOptions.Equals.value
|
|
}
|
|
|
|
// Update the noValue flag if the operator does not take a value
|
|
const noValueOptions = [
|
|
OperatorOptions.Empty.value,
|
|
OperatorOptions.NotEmpty.value,
|
|
]
|
|
filter.noValue = noValueOptions.includes(filter.operator)
|
|
}
|
|
|
|
const santizeValue = filter => {
|
|
// Check if the operator allows a value at all
|
|
if (filter.noValue) {
|
|
filter.value = null
|
|
return
|
|
}
|
|
|
|
// Ensure array values are properly set and cleared
|
|
if (Array.isArray(filter.value)) {
|
|
if (filter.valueType !== "Value" || filter.type !== "array") {
|
|
filter.value = null
|
|
}
|
|
} else if (filter.type === "array" && filter.valueType === "Value") {
|
|
filter.value = []
|
|
}
|
|
}
|
|
|
|
const onFieldChange = filter => {
|
|
santizeTypes(filter)
|
|
santizeOperator(filter)
|
|
santizeValue(filter)
|
|
}
|
|
|
|
const onOperatorChange = filter => {
|
|
santizeOperator(filter)
|
|
santizeValue(filter)
|
|
}
|
|
|
|
const onValueTypeChange = filter => {
|
|
santizeValue(filter)
|
|
}
|
|
|
|
const getFieldOptions = field => {
|
|
const schema = enrichedSchemaFields.find(x => x.name === field)
|
|
return schema?.constraints?.inclusion || []
|
|
}
|
|
</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}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<div class="filter-label">
|
|
<Label>Filters</Label>
|
|
</div>
|
|
<div class="fields">
|
|
{#each rawFilters as filter}
|
|
<Select
|
|
bind:value={filter.field}
|
|
options={fieldOptions}
|
|
on:change={() => onFieldChange(filter)}
|
|
placeholder="Column"
|
|
/>
|
|
<Select
|
|
disabled={!filter.field}
|
|
options={getValidOperatorsForType(
|
|
filter.type,
|
|
filter.field,
|
|
datasource
|
|
)}
|
|
bind:value={filter.operator}
|
|
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={`Value for "${filter.field}"`}
|
|
value={filter.value}
|
|
placeholder="Value"
|
|
{panel}
|
|
{bindings}
|
|
on:change={event => (filter.value = event.detail)}
|
|
{fillWidth}
|
|
/>
|
|
{:else if ["string", "longform", "number", "formula"].includes(filter.type)}
|
|
<Input disabled={filter.noValue} bind:value={filter.value} />
|
|
{:else if filter.type === "array" || (filter.type === "options" && filter.operator === "oneOf")}
|
|
<Multiselect
|
|
disabled={filter.noValue}
|
|
options={getFieldOptions(filter.field)}
|
|
bind:value={filter.value}
|
|
/>
|
|
{:else if filter.type === "options"}
|
|
<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}
|
|
<DrawerBindableInput disabled />
|
|
{/if}
|
|
<Icon
|
|
name="Duplicate"
|
|
hoverable
|
|
size="S"
|
|
on:click={() => duplicateFilter(filter.id)}
|
|
/>
|
|
<Icon
|
|
name="Close"
|
|
hoverable
|
|
size="S"
|
|
on:click={() => removeFilter(filter.id)}
|
|
/>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
<div class="bottom">
|
|
<Button icon="AddCircle" size="M" secondary on:click={addFilter}>
|
|
Add filter
|
|
</Button>
|
|
</div>
|
|
</Layout>
|
|
</div>
|
|
</DrawerContent>
|
|
|
|
<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: minmax(150px, 1fr) 170px 120px minmax(150px, 1fr) 16px 16px;
|
|
}
|
|
|
|
.filter-label {
|
|
margin-bottom: var(--spacing-s);
|
|
}
|
|
|
|
.bottom {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
</style>
|