2021-02-01 14:15:35 +01:00
|
|
|
<script>
|
2023-09-06 17:38:11 +02:00
|
|
|
import {
|
|
|
|
CoreSelect,
|
|
|
|
CoreMultiselect,
|
|
|
|
Input,
|
|
|
|
ProgressCircle,
|
|
|
|
} from "@budibase/bbui"
|
|
|
|
import { fetchData, Utils } from "@budibase/frontend-core"
|
2021-02-01 14:15:35 +01:00
|
|
|
import { getContext } from "svelte"
|
2021-02-05 11:53:25 +01:00
|
|
|
import Field from "./Field.svelte"
|
2021-11-08 18:25:05 +01:00
|
|
|
import { FieldTypes } from "../../../constants"
|
2021-02-01 14:15:35 +01:00
|
|
|
|
|
|
|
const { API } = getContext("sdk")
|
|
|
|
|
|
|
|
export let field
|
|
|
|
export let label
|
2021-02-12 16:47:20 +01:00
|
|
|
export let placeholder
|
2021-02-17 16:16:44 +01:00
|
|
|
export let disabled = false
|
2021-08-10 15:37:14 +02:00
|
|
|
export let validation
|
2023-09-06 17:38:11 +02:00
|
|
|
export let autocomplete = true
|
2022-02-04 09:50:56 +01:00
|
|
|
export let defaultValue
|
2022-04-14 11:01:14 +02:00
|
|
|
export let onChange
|
2023-07-18 10:36:20 +02:00
|
|
|
export let filter
|
2021-02-01 14:15:35 +01:00
|
|
|
|
|
|
|
let fieldState
|
|
|
|
let fieldApi
|
|
|
|
let fieldSchema
|
|
|
|
let tableDefinition
|
2023-09-06 17:38:11 +02:00
|
|
|
let primaryDisplay
|
|
|
|
let options
|
|
|
|
let selectedOptions = []
|
|
|
|
let isOpen = false
|
|
|
|
let hasFilter
|
|
|
|
|
|
|
|
let searchResults
|
|
|
|
let searchString
|
|
|
|
let searching = false
|
|
|
|
let lastSearchId
|
2021-02-22 12:40:24 +01:00
|
|
|
|
2021-04-15 20:43:18 +02:00
|
|
|
$: multiselect = fieldSchema?.relationshipType !== "one-to-many"
|
2021-02-01 14:15:35 +01:00
|
|
|
$: linkedTableId = fieldSchema?.tableId
|
2023-07-18 10:36:20 +02:00
|
|
|
$: fetch = fetchData({
|
|
|
|
API,
|
|
|
|
datasource: {
|
|
|
|
type: "table",
|
|
|
|
tableId: linkedTableId,
|
|
|
|
},
|
|
|
|
options: {
|
|
|
|
filter,
|
|
|
|
limit: 100,
|
|
|
|
},
|
|
|
|
})
|
2023-09-06 17:38:11 +02:00
|
|
|
$: hasFilter = !!filter?.filter(f => !!f.field)?.length
|
2023-07-18 10:36:20 +02:00
|
|
|
$: fetch.update({ filter })
|
2023-09-06 17:38:11 +02:00
|
|
|
$: {
|
|
|
|
options = searchResults ? searchResults : $fetch.rows
|
|
|
|
const nonMatchingOptions = selectedOptions.filter(
|
|
|
|
option => !options.map(opt => opt._id).includes(option._id)
|
|
|
|
)
|
|
|
|
// Append initially selected options if there is no filter
|
|
|
|
// and hasn't already been appended
|
|
|
|
if (!hasFilter) {
|
|
|
|
options = [...options, ...nonMatchingOptions]
|
|
|
|
}
|
|
|
|
}
|
2023-07-18 10:36:20 +02:00
|
|
|
$: tableDefinition = $fetch.definition
|
2023-09-06 17:38:11 +02:00
|
|
|
$: primaryDisplay = tableDefinition?.primaryDisplay || "_id"
|
2021-08-19 13:53:31 +02:00
|
|
|
$: singleValue = flatten(fieldState?.value)?.[0]
|
|
|
|
$: multiValue = flatten(fieldState?.value) ?? []
|
2021-08-17 15:13:57 +02:00
|
|
|
$: component = multiselect ? CoreMultiselect : CoreSelect
|
2022-02-04 09:50:56 +01:00
|
|
|
$: expandedDefaultValue = expand(defaultValue)
|
2023-09-06 17:38:11 +02:00
|
|
|
$: debouncedSearch(searchString)
|
|
|
|
$: {
|
|
|
|
if (searching) {
|
|
|
|
isOpen = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Fetch the initially selected values
|
|
|
|
// as they may not be within the first 100 records
|
|
|
|
$: {
|
|
|
|
if (
|
|
|
|
primaryDisplay !== "_id" &&
|
|
|
|
fieldState?.value?.length &&
|
|
|
|
!selectedOptions?.length
|
|
|
|
) {
|
|
|
|
API.searchTable({
|
|
|
|
paginate: false,
|
|
|
|
tableId: linkedTableId,
|
|
|
|
limit: 100,
|
|
|
|
query: {
|
|
|
|
oneOf: {
|
|
|
|
[`1:${primaryDisplay}`]: fieldState?.value?.map(
|
|
|
|
value => value.primaryDisplay
|
|
|
|
),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}).then(response => {
|
|
|
|
const value = multiselect ? multiValue : singleValue
|
|
|
|
selectedOptions = response.rows.filter(row => value.includes(row._id))
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2021-02-01 14:15:35 +01:00
|
|
|
|
2021-05-04 12:32:22 +02:00
|
|
|
const flatten = values => {
|
2021-04-16 12:32:41 +02:00
|
|
|
if (!values) {
|
|
|
|
return []
|
|
|
|
}
|
2022-08-05 15:53:41 +02:00
|
|
|
if (!Array.isArray(values)) {
|
|
|
|
values = [values]
|
|
|
|
}
|
2021-05-04 12:32:22 +02:00
|
|
|
return values.map(value => (typeof value === "object" ? value._id : value))
|
2021-04-16 12:32:41 +02:00
|
|
|
}
|
|
|
|
|
2021-05-04 12:32:22 +02:00
|
|
|
const getDisplayName = row => {
|
2021-02-25 12:06:16 +01:00
|
|
|
return row?.[tableDefinition?.primaryDisplay || "_id"] || "-"
|
2021-02-01 14:15:35 +01:00
|
|
|
}
|
|
|
|
|
2021-05-04 12:32:22 +02:00
|
|
|
const singleHandler = e => {
|
2022-04-14 11:01:14 +02:00
|
|
|
handleChange(e.detail == null ? [] : [e.detail])
|
2021-02-01 14:15:35 +01:00
|
|
|
}
|
|
|
|
|
2021-05-04 12:32:22 +02:00
|
|
|
const multiHandler = e => {
|
2022-04-14 11:01:14 +02:00
|
|
|
handleChange(e.detail)
|
2021-02-01 14:15:35 +01:00
|
|
|
}
|
2022-02-04 09:50:56 +01:00
|
|
|
|
|
|
|
const expand = values => {
|
|
|
|
if (!values) {
|
|
|
|
return []
|
|
|
|
}
|
|
|
|
if (Array.isArray(values)) {
|
|
|
|
return values
|
|
|
|
}
|
|
|
|
return values.split(",").map(value => value.trim())
|
|
|
|
}
|
2022-04-14 11:01:14 +02:00
|
|
|
|
|
|
|
const handleChange = value => {
|
2022-08-31 12:39:04 +02:00
|
|
|
const changed = fieldApi.setValue(value)
|
2023-09-06 17:38:11 +02:00
|
|
|
selectedOptions = value.map(val => ({
|
|
|
|
_id: val,
|
|
|
|
[primaryDisplay]: options.find(option => option._id === val)[
|
|
|
|
primaryDisplay
|
|
|
|
],
|
|
|
|
}))
|
2022-08-31 12:39:04 +02:00
|
|
|
if (onChange && changed) {
|
2022-04-14 11:01:14 +02:00
|
|
|
onChange({ value })
|
|
|
|
}
|
|
|
|
}
|
2023-09-06 17:38:11 +02:00
|
|
|
|
|
|
|
// Search for rows based on the search string
|
|
|
|
const search = async searchString => {
|
|
|
|
// Reset state if this search is invalid
|
|
|
|
if (!linkedTableId || !searchString) {
|
|
|
|
searchResults = null
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// If a filter exists, then do a client side search
|
|
|
|
if (hasFilter) {
|
|
|
|
searchResults = $fetch.rows.filter(option =>
|
|
|
|
option[primaryDisplay].startsWith(searchString)
|
|
|
|
)
|
|
|
|
isOpen = true
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Search for results, using IDs to track invocations and ensure we're
|
|
|
|
// handling the latest update
|
|
|
|
lastSearchId = Math.random()
|
|
|
|
searching = true
|
|
|
|
const thisSearchId = lastSearchId
|
|
|
|
const results = await API.searchTable({
|
|
|
|
paginate: false,
|
|
|
|
tableId: linkedTableId,
|
|
|
|
limit: 100,
|
|
|
|
query: {
|
|
|
|
string: {
|
|
|
|
[`1:${primaryDisplay}`]: searchString || "",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
searching = false
|
|
|
|
|
|
|
|
// In case searching takes longer than our debounced update, abandon these
|
|
|
|
// results
|
|
|
|
if (thisSearchId !== lastSearchId) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Process results
|
|
|
|
searchResults = results.rows?.map(row => ({
|
|
|
|
...row,
|
|
|
|
primaryDisplay: row[primaryDisplay],
|
|
|
|
}))
|
|
|
|
}
|
|
|
|
|
|
|
|
// Debounced version of searching
|
|
|
|
const debouncedSearch = Utils.debounce(search, 250)
|
2021-02-01 14:15:35 +01:00
|
|
|
</script>
|
|
|
|
|
2021-02-05 11:53:25 +01:00
|
|
|
<Field
|
2021-02-01 14:15:35 +01:00
|
|
|
{label}
|
|
|
|
{field}
|
2021-02-17 16:16:44 +01:00
|
|
|
{disabled}
|
2021-08-10 15:37:14 +02:00
|
|
|
{validation}
|
2022-02-04 09:50:56 +01:00
|
|
|
defaultValue={expandedDefaultValue}
|
2021-11-08 18:25:05 +01:00
|
|
|
type={FieldTypes.LINK}
|
2021-02-01 14:15:35 +01:00
|
|
|
bind:fieldState
|
|
|
|
bind:fieldApi
|
|
|
|
bind:fieldSchema
|
2021-05-04 12:04:42 +02:00
|
|
|
>
|
2021-04-15 20:43:18 +02:00
|
|
|
{#if fieldState}
|
2023-09-06 17:38:11 +02:00
|
|
|
<div class={autocomplete ? "field-with-search" : ""}>
|
|
|
|
<svelte:component
|
|
|
|
this={component}
|
|
|
|
bind:open={isOpen}
|
|
|
|
{options}
|
|
|
|
autocomplete={false}
|
|
|
|
value={multiselect ? multiValue : singleValue}
|
|
|
|
on:change={multiselect ? multiHandler : singleHandler}
|
|
|
|
id={fieldState.fieldId}
|
|
|
|
disabled={fieldState.disabled}
|
|
|
|
error={fieldState.error}
|
|
|
|
getOptionLabel={getDisplayName}
|
|
|
|
getOptionValue={option => option._id}
|
|
|
|
{placeholder}
|
|
|
|
customPopoverOffsetBelow={autocomplete ? 32 : null}
|
|
|
|
customPopoverMaxHeight={autocomplete ? 240 : null}
|
|
|
|
sort={true}
|
|
|
|
/>
|
|
|
|
{#if autocomplete}
|
|
|
|
<div class="search">
|
|
|
|
<Input
|
|
|
|
autofocus
|
|
|
|
quiet
|
|
|
|
type="text"
|
|
|
|
bind:value={searchString}
|
|
|
|
placeholder={primaryDisplay ? `Search by ${primaryDisplay}` : null}
|
|
|
|
/>
|
|
|
|
{#if searching}
|
|
|
|
<div>
|
|
|
|
<ProgressCircle size="S" />
|
|
|
|
</div>
|
|
|
|
{/if}
|
|
|
|
</div>
|
|
|
|
{/if}
|
|
|
|
</div>
|
2021-04-15 20:43:18 +02:00
|
|
|
{/if}
|
2021-02-05 11:53:25 +01:00
|
|
|
</Field>
|
2023-09-06 17:38:11 +02:00
|
|
|
|
|
|
|
<style>
|
|
|
|
.search {
|
|
|
|
flex: 0 0 calc(var(--default-row-height) - 1px);
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
margin: 4px var(--cell-padding);
|
|
|
|
width: calc(100% - 2 * var(--cell-padding));
|
|
|
|
}
|
|
|
|
.search :global(.spectrum-Textfield) {
|
|
|
|
min-width: 0;
|
|
|
|
width: 100%;
|
|
|
|
}
|
|
|
|
.search :global(.spectrum-Textfield-input) {
|
|
|
|
font-size: 13px;
|
|
|
|
}
|
|
|
|
.search :global(.spectrum-Form-item) {
|
|
|
|
flex: 1 1 auto;
|
|
|
|
}
|
|
|
|
.field-with-search {
|
|
|
|
min-height: 80px;
|
|
|
|
}
|
|
|
|
</style>
|