Move filterbuilder to frontend-core

This commit is contained in:
Adria Navarro 2024-04-11 12:29:45 +02:00
parent a4a095b6a1
commit 709457477e
4 changed files with 325 additions and 283 deletions

View File

@ -1,25 +1,12 @@
<script>
import {
Body,
Button,
Combobox,
DatePicker,
DrawerContent,
Icon,
Input,
Label,
Layout,
Multiselect,
Select,
} from "@budibase/bbui"
import { DrawerContent, Select } from "@budibase/bbui"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
import { generate } from "shortid"
import { Constants, LuceneUtils } from "@budibase/frontend-core"
import { getFields } from "helpers/searchFields"
import { FieldType } from "@budibase/types"
import { FilterBuilder } from "@budibase/frontend-core"
import { createEventDispatcher, onMount } from "svelte"
import FilterUsers from "./FilterUsers.svelte"
// import FilterUsers from "./FilterUsers.svelte"
export let schemaFields
export let filters = []
@ -29,7 +16,7 @@
export let datasource
const dispatch = createEventDispatcher()
const { OperatorOptions } = Constants
const KeyedFieldRegex = /\d[0-9]*:/g
const behaviourOptions = [
{ value: "and", label: "Match all filters" },
@ -46,11 +33,6 @@
$: parseFilters(filters)
$: dispatch("change", enrichFilters(rawFilters, matchAny, onEmptyFilter))
$: enrichedSchemaFields = getFields(schemaFields || [], {
allowLinks: true,
}).filter(f => !!getValidOperatorsForType({ field: f.name, ...f }).length)
$: fieldOptions = enrichedSchemaFields.map(field => field.name) || []
$: valueTypeOptions = allowBindings ? ["Value", "Binding"] : ["Value"]
// Remove field key prefixes and determine which behaviours to use
const parseFilters = filters => {
@ -94,275 +76,52 @@
.concat(matchAny ? [{ operator: "allOr" }] : [])
.concat([{ onEmptyFilter }])
}
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 sanitizeTypes = filter => {
// Update type based on field
const fieldSchema = enrichedSchemaFields.find(x => x.name === filter.field)
filter.type = fieldSchema?.type
filter.subtype = fieldSchema?.subtype
// Update external type based on field
filter.externalType = getSchema(filter)?.externalType
}
const sanitizeOperator = filter => {
// Ensure a valid operator is selected
const operators = getValidOperatorsForType(filter).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 sanitizeValue = (filter, previousType) => {
// 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 = []
} else if (
previousType !== filter.type &&
(previousType === FieldType.BB_REFERENCE ||
filter.type === FieldType.BB_REFERENCE)
) {
filter.value = filter.type === "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 []
}
return LuceneUtils.getValidOperatorsForType(
{ type: filter.type, subtype: filter.subtype },
filter.field,
datasource
)
}
</script>
<DrawerContent>
<div class="container">
<Layout noPadding>
{#if fieldOptions?.length}
{#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}
/>
{/if}
</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)}
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={filter.field}
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} />
{: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 if filter.type === FieldType.BB_REFERENCE}
<FilterUsers
bind:value={filter.value}
multiselect={[
OperatorOptions.In.value,
OperatorOptions.ContainsAny.value,
].includes(filter.operator)}
disabled={filter.noValue}
/>
{: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>
{:else}
<Body size="S">
None of the table column can be used for filtering.
</Body>
<DrawerContent padding={false}>
<FilterBuilder bind:filters {schemaFields} {datasource} {allowBindings}>
<div slot="filteringHeroContent" class="filteringHeroContent">
<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}
/>
{/if}
</Layout>
</div>
</div>
<div slot="binding" let:filter>
<DrawerBindableInput
disabled={filter.noValue}
title={filter.field}
value={filter.value}
placeholder="Value"
{panel}
{bindings}
on:change={event => (filter.value = event.detail)}
/>
</div>
</FilterBuilder>
</DrawerContent>
<style>
.container {
width: 100%;
max-width: 1000px;
margin: 0 auto;
}
.fields {
.filteringHeroContent {
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>

View File

@ -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"
}
}

View File

@ -0,0 +1,281 @@
<script>
import {
Body,
Button,
Combobox,
DatePicker,
Icon,
Input,
Layout,
Select,
Label,
Multiselect,
} from "@budibase/bbui"
import { FieldType, SearchQueryOperators } from "@budibase/types"
import { generate } from "shortid"
import { LuceneUtils, Constants } from "@budibase/frontend-core"
import { getContext } from "svelte"
const { OperatorOptions } = Constants
export let schemaFields
export let filters = []
export let datasource
export let allowBindings = false
const context = getContext("context")
$: fieldOptions = (schemaFields ?? [])
.filter(field => getValidOperatorsForType(field).length)
.map(field => ({
label: field.displayName || field.name,
value: field.name,
}))
const addFilter = () => {
filters = [
...(filters || []),
{
id: generate(),
field: null,
operator: 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 = 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 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 = schemaFields.find(x => x.name === filter.field)
filter.type = fieldSchema?.type
filter.subtype = fieldSchema?.subtype
// Update external type based on field
filter.externalType = getSchema(filter)?.externalType
}
const sanitizeOperator = filter => {
// Ensure a valid operator is selected
const operators = getValidOperatorsForType(filter).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 sanitizeValue = (filter, previousType) => {
// 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 !== FieldType.ARRAY) {
filter.value = null
}
} 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 === FieldType.ARRAY ? [] : null
}
}
</script>
<div class="container" class:mobile={$context?.device?.mobile}>
<Layout noPadding>
<Body size="S">
{#if !filters?.length}
Add your first filter expression.
{:else}
<slot name="filteringHeroContent" />
{/if}
</Body>
{#if filters?.length}
<div class="filter-label">
<Label>Filters</Label>
</div>
<div class="fields">
{#each filters as filter}
<Select
bind:value={filter.field}
options={fieldOptions}
on:change={() => onFieldChange(filter)}
placeholder="Column"
/>
<Select
disabled={!filter.field}
options={getValidOperatorsForType(filter)}
bind:value={filter.operator}
on:change={() => onOperatorChange(filter)}
placeholder={null}
/>
{#if allowBindings}
<Select
disabled={filter.noValue || !filter.field}
options={valueTypeOptions}
bind:value={filter.valueType}
on:change={() => onValueTypeChange(filter)}
placeholder={null}
/>
{/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 === 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 === FieldType.OPTIONS}
<Combobox
disabled={filter.noValue}
options={getFieldOptions(filter.field)}
bind:value={filter.value}
/>
{:else if filter.type === FieldType.BOOLEAN}
<Combobox
disabled={filter.noValue}
options={[
{ label: "True", value: "true" },
{ label: "False", value: "false" },
]}
bind:value={filter.value}
/>
{:else if filter.type === FieldType.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
class
name="Duplicate"
hoverable
size="S"
on:click={() => duplicateFilter(filter.id)}
/>
</div>
<div class="controls">
<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: content;
}
.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);
}
</style>

View File

@ -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"