Add dynamic filter component
This commit is contained in:
parent
a1ff7e7262
commit
f77f7c1e5f
|
@ -67,6 +67,7 @@
|
||||||
"icon",
|
"icon",
|
||||||
"embed"
|
"embed"
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"dynamicfilter"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -2623,6 +2623,17 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"dynamicfilter": {
|
||||||
|
"name": "Dynamic Filter",
|
||||||
|
"icon": "FilterEdit",
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "dataProvider",
|
||||||
|
"label": "Provider",
|
||||||
|
"key": "dataProvider"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"tableblock": {
|
"tableblock": {
|
||||||
"block": true,
|
"block": true,
|
||||||
"name": "Table block",
|
"name": "Table block",
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
<script>
|
||||||
|
import { Button, ModalContent, Modal } from "@budibase/bbui"
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
import FilterModal from "./FilterModal.svelte"
|
||||||
|
|
||||||
|
export let dataProvider
|
||||||
|
|
||||||
|
const component = getContext("component")
|
||||||
|
const { styleable } = getContext("sdk")
|
||||||
|
|
||||||
|
$: schema = dataProvider?.schema
|
||||||
|
$: schemaFields = Object.values(schema || {})
|
||||||
|
|
||||||
|
let modal
|
||||||
|
let tmpFilters = []
|
||||||
|
let filters = []
|
||||||
|
|
||||||
|
const openEditor = () => {
|
||||||
|
tmpFilters = [...filters]
|
||||||
|
modal.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateQuery = () => {
|
||||||
|
filters = [...tmpFilters]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div use:styleable={$component.styles}>
|
||||||
|
<Button on:click={openEditor} secondary icon="FilterEdit">
|
||||||
|
Edit filters
|
||||||
|
</Button>
|
||||||
|
<Modal bind:this={modal}>
|
||||||
|
<ModalContent title="Edit filters" size="XL" onConfirm={updateQuery}>
|
||||||
|
<FilterModal bind:filters={tmpFilters} {schemaFields} />
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
|
@ -0,0 +1,176 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Button,
|
||||||
|
Combobox,
|
||||||
|
DatePicker,
|
||||||
|
DrawerContent,
|
||||||
|
Icon,
|
||||||
|
Input,
|
||||||
|
Layout,
|
||||||
|
Select,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { generate } from "shortid"
|
||||||
|
import {
|
||||||
|
getValidOperatorsForType,
|
||||||
|
OperatorOptions,
|
||||||
|
} from "builder/src/constants/lucene"
|
||||||
|
|
||||||
|
export let schemaFields
|
||||||
|
export let filters = []
|
||||||
|
|
||||||
|
const BannedTypes = ["link", "attachment", "formula"]
|
||||||
|
|
||||||
|
$: fieldOptions = (schemaFields ?? [])
|
||||||
|
.filter(field => !BannedTypes.includes(field.type))
|
||||||
|
.map(field => 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 = (expression, field) => {
|
||||||
|
// Update the field type
|
||||||
|
expression.type = schemaFields.find(x => x.name === field)?.type
|
||||||
|
|
||||||
|
// Ensure a valid operator is set
|
||||||
|
const validOperators = getValidOperatorsForType(expression.type).map(
|
||||||
|
x => x.value
|
||||||
|
)
|
||||||
|
if (!validOperators.includes(expression.operator)) {
|
||||||
|
expression.operator = validOperators[0] ?? 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 = [
|
||||||
|
OperatorOptions.Empty.value,
|
||||||
|
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 || []
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DrawerContent>
|
||||||
|
<div class="container">
|
||||||
|
<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, idx}
|
||||||
|
<Select
|
||||||
|
bind:value={filter.field}
|
||||||
|
options={fieldOptions}
|
||||||
|
on:change={e => onFieldChange(filter, e.detail)}
|
||||||
|
placeholder="Column"
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
disabled={!filter.field}
|
||||||
|
options={getValidOperatorsForType(filter.type)}
|
||||||
|
bind:value={filter.operator}
|
||||||
|
on:change={e => onOperatorChange(filter, e.detail)}
|
||||||
|
placeholder={null}
|
||||||
|
/>
|
||||||
|
{#if ["string", "longform", "number"].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} bind:value={filter.value} />
|
||||||
|
{:else}
|
||||||
|
<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)}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div>
|
||||||
|
<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: 1fr 120px 1fr auto auto;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as dynamicfilter } from "./DynamicFilter.svelte"
|
|
@ -33,6 +33,7 @@ export * from "./charts"
|
||||||
export * from "./forms"
|
export * from "./forms"
|
||||||
export * from "./table"
|
export * from "./table"
|
||||||
export * from "./blocks"
|
export * from "./blocks"
|
||||||
|
export * from "./dynamic-filter"
|
||||||
|
|
||||||
// Deprecated component left for compatibility in old apps
|
// Deprecated component left for compatibility in old apps
|
||||||
export { default as navigation } from "./deprecated/Navigation.svelte"
|
export { default as navigation } from "./deprecated/Navigation.svelte"
|
||||||
|
|
Loading…
Reference in New Issue