budibase/packages/client/src/components/app/forms/RelationshipField.svelte

261 lines
6.8 KiB
Svelte
Raw Normal View History

<script>
2023-09-21 11:39:02 +02:00
import { CoreSelect, CoreMultiselect } from "@budibase/bbui"
import { fetchData, Utils } from "@budibase/frontend-core"
import { getContext, onMount } from "svelte"
import Field from "./Field.svelte"
import { FieldTypes } from "../../../constants"
const { API } = getContext("sdk")
export let field
export let label
export let placeholder
export let disabled = false
2023-11-01 17:01:45 +01:00
export let readonly = false
export let validation
export let autocomplete = true
export let defaultValue
export let onChange
2023-07-18 10:36:20 +02:00
export let filter
2023-09-20 16:44:26 +02:00
export let datasourceType = "table"
2023-09-20 17:22:07 +02:00
export let primaryDisplay
export let span
export let helpText = null
let fieldState
let fieldApi
let fieldSchema
let tableDefinition
2023-09-22 13:04:44 +02:00
let searchTerm
2023-09-25 12:23:17 +02:00
let open
let initialValue
2021-02-22 12:40:24 +01:00
2023-09-20 17:22:07 +02:00
$: type =
datasourceType === "table" ? FieldTypes.LINK : FieldTypes.BB_REFERENCE
$: multiselect = fieldSchema?.relationshipType !== "one-to-many"
$: linkedTableId = fieldSchema?.tableId
2023-07-18 10:36:20 +02:00
$: fetch = fetchData({
API,
datasource: {
2023-09-20 16:44:26 +02:00
type: datasourceType,
2023-07-18 10:36:20 +02:00
tableId: linkedTableId,
},
options: {
filter,
limit: 100,
},
})
2023-09-21 12:29:39 +02:00
2023-07-18 10:36:20 +02:00
$: tableDefinition = $fetch.definition
2023-09-22 10:14:17 +02:00
$: selectedValue = multiselect
? flatten(fieldState?.value) ?? []
: flatten(fieldState?.value)?.[0]
2021-08-17 15:13:57 +02:00
$: component = multiselect ? CoreMultiselect : CoreSelect
$: expandedDefaultValue = expand(defaultValue)
2023-09-20 17:22:07 +02:00
$: primaryDisplay = primaryDisplay || tableDefinition?.primaryDisplay
2023-09-22 12:16:54 +02:00
2023-09-25 10:16:01 +02:00
let optionsObj = {}
let initialValuesProcessed
2023-09-25 12:33:21 +02:00
2023-09-25 10:16:01 +02:00
$: {
if (!initialValuesProcessed && primaryDisplay) {
2023-09-25 12:33:21 +02:00
// Persist the initial values as options, allowing them to be present in the dropdown,
// even if they are not in the inital fetch results
2023-09-25 10:16:01 +02:00
initialValuesProcessed = true
2023-09-26 10:26:12 +02:00
optionsObj = (fieldState?.value || []).reduce((accumulator, value) => {
// fieldState has to be an array of strings to be valid for an update
// therefore we cannot guarantee value will be an object
// https://linear.app/budibase/issue/BUDI-7577/refactor-the-relationshipfield-component-to-have-better-support-for
if (!value._id) {
return accumulator
}
2023-09-25 18:21:02 +02:00
accumulator[value._id] = {
_id: value._id,
[primaryDisplay]: value.primaryDisplay,
}
return accumulator
}, optionsObj)
2023-09-25 10:16:01 +02:00
}
}
2023-09-22 12:39:49 +02:00
2023-10-13 11:22:29 +02:00
$: enrichedOptions = enrichOptions(optionsObj, $fetch.rows)
const enrichOptions = (optionsObj, fetchResults) => {
2023-09-25 18:21:02 +02:00
const result = (fetchResults || [])?.reduce((accumulator, row) => {
if (!accumulator[row._id]) {
accumulator[row._id] = row
}
return accumulator
}, optionsObj)
2023-09-25 12:40:12 +02:00
2023-10-13 11:22:29 +02:00
return Object.values(result)
2023-09-25 12:33:21 +02:00
}
2023-09-25 12:23:17 +02:00
$: {
// We don't want to reorder while the dropdown is open, to avoid UX jumps
if (!open) {
2023-09-25 12:33:21 +02:00
enrichedOptions = enrichedOptions.sort((a, b) => {
2023-09-25 12:23:17 +02:00
const selectedValues = flatten(fieldState?.value) || []
const aIsSelected = selectedValues.find(v => v === a._id)
const bIsSelected = selectedValues.find(v => v === b._id)
if (aIsSelected && !bIsSelected) {
return -1
} else if (!aIsSelected && bIsSelected) {
return 1
}
return a[primaryDisplay] > b[primaryDisplay]
})
}
}
$: forceFetchRows(filter)
$: debouncedFetchRows(
searchTerm,
primaryDisplay,
initialValue || defaultValue
)
2023-09-22 12:16:54 +02:00
const forceFetchRows = async () => {
// if the filter has changed, then we need to reset the options, clear the selection, and re-fetch
optionsObj = {}
fieldApi?.setValue([])
selectedValue = []
debouncedFetchRows(searchTerm, primaryDisplay, defaultValue)
}
2023-10-12 20:07:04 +02:00
const fetchRows = async (searchTerm, primaryDisplay, defaultVal) => {
2023-09-22 13:04:44 +02:00
const allRowsFetched =
$fetch.loaded &&
2023-09-25 10:16:01 +02:00
!Object.keys($fetch.query?.string || {}).length &&
2023-09-22 13:04:44 +02:00
!$fetch.hasNextPage
// Don't request until we have the primary display or default value has been fetched
2023-10-12 20:07:04 +02:00
if (allRowsFetched || !primaryDisplay) {
return
}
// must be an array
if (defaultVal && !Array.isArray(defaultVal)) {
defaultVal = defaultVal.split(",")
}
if (defaultVal && defaultVal.some(val => !optionsObj[val])) {
await fetch.update({
query: { oneOf: { _id: defaultVal } },
2023-09-25 10:17:26 +02:00
})
2023-09-21 12:29:39 +02:00
}
// Ensure we match all filters, rather than any
const baseFilter = (filter || []).filter(x => x.operator !== "allOr")
2023-10-12 20:07:04 +02:00
await fetch.update({
filter: [
...baseFilter,
{
2023-11-28 10:41:29 +01:00
// Use a big numeric prefix to avoid clashing with an existing filter
field: `999:${primaryDisplay}`,
operator: "string",
value: searchTerm,
},
],
2023-10-12 20:07:04 +02:00
})
2023-09-21 12:29:39 +02:00
}
const debouncedFetchRows = Utils.debounce(fetchRows, 250)
2021-05-04 12:32:22 +02:00
const flatten = values => {
if (!values) {
return []
}
2022-08-05 15:53:41 +02:00
if (!Array.isArray(values)) {
values = [values]
}
values = values.map(value =>
typeof value === "object" ? value._id : value
)
// Make sure field state is valid
if (values?.length > 0) {
fieldApi.setValue(values)
}
return values
}
2021-05-04 12:32:22 +02:00
const getDisplayName = row => {
2023-09-21 12:29:39 +02:00
return row?.[primaryDisplay] || "-"
}
2021-05-04 12:32:22 +02:00
const singleHandler = e => {
handleChange(e.detail == null ? [] : [e.detail])
}
2021-05-04 12:32:22 +02:00
const multiHandler = e => {
handleChange(e.detail)
}
const expand = values => {
if (!values) {
return []
}
if (Array.isArray(values)) {
return values
}
return values.split(",").map(value => value.trim())
}
const handleChange = value => {
const changed = fieldApi.setValue(value)
if (onChange && changed) {
onChange({
2023-10-10 12:22:59 +02:00
value,
})
}
}
2023-09-22 13:51:08 +02:00
const loadMore = () => {
if (!$fetch.loading) {
fetch.nextPage()
}
}
onMount(() => {
// if the form is in 'Update' mode, then we need to fetch the matching row so that the value is correctly set
if (fieldState?.value) {
initialValue =
fieldSchema?.relationshipType !== "one-to-many"
? flatten(fieldState?.value) ?? []
: flatten(fieldState?.value)?.[0]
}
})
</script>
<Field
{label}
{field}
{disabled}
2023-11-01 17:27:52 +01:00
{readonly}
{validation}
2023-10-10 12:22:59 +02:00
defaultValue={expandedDefaultValue}
2023-09-20 17:22:07 +02:00
{type}
{span}
{helpText}
bind:fieldState
bind:fieldApi
bind:fieldSchema
>
{#if fieldState}
2023-09-21 11:39:02 +02:00
<svelte:component
this={component}
2023-09-25 12:33:21 +02:00
options={enrichedOptions}
2023-09-21 11:39:02 +02:00
{autocomplete}
2023-09-22 11:00:56 +02:00
value={selectedValue}
2023-09-21 11:39:02 +02:00
on:change={multiselect ? multiHandler : singleHandler}
2023-09-22 13:51:08 +02:00
on:loadMore={loadMore}
2023-09-21 11:39:02 +02:00
id={fieldState.fieldId}
disabled={fieldState.disabled}
2023-11-01 17:27:52 +01:00
readonly={fieldState.readonly}
2023-09-21 11:39:02 +02:00
error={fieldState.error}
getOptionLabel={getDisplayName}
getOptionValue={option => option._id}
{placeholder}
2023-09-22 13:04:44 +02:00
bind:searchTerm
2023-09-22 14:03:23 +02:00
loading={$fetch.loading}
2023-09-25 12:23:17 +02:00
bind:open
2023-09-21 11:39:02 +02:00
/>
{/if}
</Field>