Merge pull request #15553 from Budibase/fix/relationship-select
Relationship field - make sure selected value correctly displays
This commit is contained in:
commit
1e979e4050
|
@ -14,7 +14,7 @@
|
|||
export let sort = false
|
||||
export let autoWidth = false
|
||||
export let searchTerm = null
|
||||
export let customPopoverHeight
|
||||
export let customPopoverHeight = undefined
|
||||
export let open = false
|
||||
export let loading
|
||||
export let onOptionMouseenter = () => {}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
const component = getContext("component")
|
||||
const block = getContext("block")
|
||||
|
||||
export let text
|
||||
export let text = undefined
|
||||
</script>
|
||||
|
||||
{#if $builderStore.inBuilder}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import { getContext, onDestroy } from "svelte"
|
||||
import { writable } from "svelte/store"
|
||||
import { Icon } from "@budibase/bbui"
|
||||
|
@ -6,33 +6,33 @@
|
|||
import Placeholder from "../Placeholder.svelte"
|
||||
import InnerForm from "./InnerForm.svelte"
|
||||
|
||||
export let label
|
||||
export let field
|
||||
export let fieldState
|
||||
export let fieldApi
|
||||
export let fieldSchema
|
||||
export let defaultValue
|
||||
export let type
|
||||
export let label: string | undefined = undefined
|
||||
export let field: string | undefined = undefined
|
||||
export let fieldState: any
|
||||
export let fieldApi: any
|
||||
export let fieldSchema: any
|
||||
export let defaultValue: string | undefined = undefined
|
||||
export let type: any
|
||||
export let disabled = false
|
||||
export let readonly = false
|
||||
export let validation
|
||||
export let validation: any
|
||||
export let span = 6
|
||||
export let helpText = null
|
||||
export let helpText: string | undefined = undefined
|
||||
|
||||
// Get contexts
|
||||
const formContext = getContext("form")
|
||||
const formStepContext = getContext("form-step")
|
||||
const fieldGroupContext = getContext("field-group")
|
||||
const formContext: any = getContext("form")
|
||||
const formStepContext: any = getContext("form-step")
|
||||
const fieldGroupContext: any = getContext("field-group")
|
||||
const { styleable, builderStore, Provider } = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
const component: any = getContext("component")
|
||||
|
||||
// Register field with form
|
||||
const formApi = formContext?.formApi
|
||||
const labelPos = fieldGroupContext?.labelPosition || "above"
|
||||
|
||||
let formField
|
||||
let formField: any
|
||||
let touched = false
|
||||
let labelNode
|
||||
let labelNode: any
|
||||
|
||||
// Memoize values required to register the field to avoid loops
|
||||
const formStep = formStepContext || writable(1)
|
||||
|
@ -65,7 +65,7 @@
|
|||
$: $component.editing && labelNode?.focus()
|
||||
|
||||
// Update form properties in parent component on every store change
|
||||
$: unsubscribe = formField?.subscribe(value => {
|
||||
$: unsubscribe = formField?.subscribe((value: any) => {
|
||||
fieldState = value?.fieldState
|
||||
fieldApi = value?.fieldApi
|
||||
fieldSchema = value?.fieldSchema
|
||||
|
@ -74,7 +74,7 @@
|
|||
// Determine label class from position
|
||||
$: labelClass = labelPos === "above" ? "" : `spectrum-FieldLabel--${labelPos}`
|
||||
|
||||
const registerField = info => {
|
||||
const registerField = (info: any) => {
|
||||
formField = formApi?.registerField(
|
||||
info.field,
|
||||
info.type,
|
||||
|
@ -86,8 +86,9 @@
|
|||
)
|
||||
}
|
||||
|
||||
const updateLabel = e => {
|
||||
const updateLabel = (e: any) => {
|
||||
if (touched) {
|
||||
// @ts-expect-error and TODO updateProp isn't recognised - need builder TS conversion
|
||||
builderStore.actions.updateProp("label", e.target.textContent)
|
||||
}
|
||||
touched = false
|
||||
|
|
|
@ -4,13 +4,13 @@
|
|||
import { createValidatorFromConstraints } from "./validation"
|
||||
import { Helpers } from "@budibase/bbui"
|
||||
|
||||
export let dataSource
|
||||
export let dataSource = undefined
|
||||
export let disabled = false
|
||||
export let readonly = false
|
||||
export let initialValues
|
||||
export let size
|
||||
export let schema
|
||||
export let definition
|
||||
export let initialValues = undefined
|
||||
export let size = undefined
|
||||
export let schema = undefined
|
||||
export let definition = undefined
|
||||
export let disableSchemaValidation = false
|
||||
export let editAutoColumns = false
|
||||
|
||||
|
|
|
@ -1,43 +1,61 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import { CoreSelect, CoreMultiselect } from "@budibase/bbui"
|
||||
import { FieldType } from "@budibase/types"
|
||||
import { fetchData, Utils } from "@budibase/frontend-core"
|
||||
import { getContext } from "svelte"
|
||||
import Field from "./Field.svelte"
|
||||
import type {
|
||||
SearchFilter,
|
||||
RelationshipFieldMetadata,
|
||||
Table,
|
||||
Row,
|
||||
} from "@budibase/types"
|
||||
|
||||
const { API } = getContext("sdk")
|
||||
|
||||
export let field
|
||||
export let label
|
||||
export let placeholder
|
||||
export let disabled = false
|
||||
export let readonly = false
|
||||
export let validation
|
||||
export let autocomplete = true
|
||||
export let defaultValue
|
||||
export let onChange
|
||||
export let filter
|
||||
export let datasourceType = "table"
|
||||
export let primaryDisplay
|
||||
export let span
|
||||
export let helpText = null
|
||||
export let type = FieldType.LINK
|
||||
export let field: string | undefined = undefined
|
||||
export let label: string | undefined = undefined
|
||||
export let placeholder: any = undefined
|
||||
export let disabled: boolean = false
|
||||
export let readonly: boolean = false
|
||||
export let validation: any
|
||||
export let autocomplete: boolean = true
|
||||
export let defaultValue: string | undefined = undefined
|
||||
export let onChange: any
|
||||
export let filter: SearchFilter[]
|
||||
export let datasourceType: "table" | "user" | "groupUser" = "table"
|
||||
export let primaryDisplay: string | undefined = undefined
|
||||
export let span: number | undefined = undefined
|
||||
export let helpText: string | undefined = undefined
|
||||
export let type:
|
||||
| FieldType.LINK
|
||||
| FieldType.BB_REFERENCE
|
||||
| FieldType.BB_REFERENCE_SINGLE = FieldType.LINK
|
||||
|
||||
let fieldState
|
||||
let fieldApi
|
||||
let fieldSchema
|
||||
let tableDefinition
|
||||
let searchTerm
|
||||
let open
|
||||
type RelationshipValue = { _id: string; [key: string]: any }
|
||||
type OptionObj = Record<string, RelationshipValue>
|
||||
type OptionsObjType = Record<string, OptionObj>
|
||||
|
||||
let fieldState: any
|
||||
let fieldApi: any
|
||||
let fieldSchema: RelationshipFieldMetadata | undefined
|
||||
let tableDefinition: Table | null | undefined
|
||||
let searchTerm: any
|
||||
let open: boolean
|
||||
let selectedValue: string[] | string
|
||||
|
||||
// need a cast version of this for reactivity, components below aren't typed
|
||||
$: castSelectedValue = selectedValue as any
|
||||
$: multiselect =
|
||||
[FieldType.LINK, FieldType.BB_REFERENCE].includes(type) &&
|
||||
fieldSchema?.relationshipType !== "one-to-many"
|
||||
$: linkedTableId = fieldSchema?.tableId
|
||||
$: linkedTableId = fieldSchema?.tableId!
|
||||
$: fetch = fetchData({
|
||||
API,
|
||||
datasource: {
|
||||
type: datasourceType,
|
||||
// typing here doesn't seem correct - we have the correct datasourceType options
|
||||
// but when we configure the fetchData, it seems to think only "table" is valid
|
||||
type: datasourceType as any,
|
||||
tableId: linkedTableId,
|
||||
},
|
||||
options: {
|
||||
|
@ -53,7 +71,8 @@
|
|||
$: component = multiselect ? CoreMultiselect : CoreSelect
|
||||
$: primaryDisplay = primaryDisplay || tableDefinition?.primaryDisplay
|
||||
|
||||
let optionsObj
|
||||
let optionsObj: OptionsObjType = {}
|
||||
const debouncedFetchRows = Utils.debounce(fetchRows, 250)
|
||||
|
||||
$: {
|
||||
if (primaryDisplay && fieldState && !optionsObj) {
|
||||
|
@ -63,27 +82,33 @@
|
|||
if (!Array.isArray(valueAsSafeArray)) {
|
||||
valueAsSafeArray = [fieldState.value]
|
||||
}
|
||||
optionsObj = valueAsSafeArray.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) {
|
||||
optionsObj = valueAsSafeArray.reduce(
|
||||
(
|
||||
accumulator: OptionObj,
|
||||
value: { _id: string; primaryDisplay: any }
|
||||
) => {
|
||||
// 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
|
||||
}
|
||||
accumulator[value._id] = {
|
||||
_id: value._id,
|
||||
[primaryDisplay]: value.primaryDisplay,
|
||||
}
|
||||
return accumulator
|
||||
}
|
||||
accumulator[value._id] = {
|
||||
_id: value._id,
|
||||
[primaryDisplay]: value.primaryDisplay,
|
||||
}
|
||||
return accumulator
|
||||
}, {})
|
||||
},
|
||||
{}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
$: enrichedOptions = enrichOptions(optionsObj, $fetch.rows)
|
||||
const enrichOptions = (optionsObj, fetchResults) => {
|
||||
const enrichOptions = (optionsObj: OptionsObjType, fetchResults: Row[]) => {
|
||||
const result = (fetchResults || [])?.reduce((accumulator, row) => {
|
||||
if (!accumulator[row._id]) {
|
||||
accumulator[row._id] = row
|
||||
if (!accumulator[row._id!]) {
|
||||
accumulator[row._id!] = row
|
||||
}
|
||||
return accumulator
|
||||
}, optionsObj || {})
|
||||
|
@ -92,24 +117,32 @@
|
|||
}
|
||||
$: {
|
||||
// We don't want to reorder while the dropdown is open, to avoid UX jumps
|
||||
if (!open) {
|
||||
enrichedOptions = enrichedOptions.sort((a, b) => {
|
||||
if (!open && primaryDisplay) {
|
||||
enrichedOptions = enrichedOptions.sort((a: OptionObj, b: OptionObj) => {
|
||||
const selectedValues = flatten(fieldState?.value) || []
|
||||
|
||||
const aIsSelected = selectedValues.find(v => v === a._id)
|
||||
const bIsSelected = selectedValues.find(v => v === b._id)
|
||||
const aIsSelected = selectedValues.find(
|
||||
(v: RelationshipValue) => v === a._id
|
||||
)
|
||||
const bIsSelected = selectedValues.find(
|
||||
(v: RelationshipValue) => v === b._id
|
||||
)
|
||||
if (aIsSelected && !bIsSelected) {
|
||||
return -1
|
||||
} else if (!aIsSelected && bIsSelected) {
|
||||
return 1
|
||||
}
|
||||
|
||||
return a[primaryDisplay] > b[primaryDisplay]
|
||||
return (a[primaryDisplay] > b[primaryDisplay]) as unknown as number
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
$: forceFetchRows(filter)
|
||||
$: {
|
||||
if (filter || defaultValue) {
|
||||
forceFetchRows()
|
||||
}
|
||||
}
|
||||
$: debouncedFetchRows(searchTerm, primaryDisplay, defaultValue)
|
||||
|
||||
const forceFetchRows = async () => {
|
||||
|
@ -119,7 +152,11 @@
|
|||
selectedValue = []
|
||||
debouncedFetchRows(searchTerm, primaryDisplay, defaultValue)
|
||||
}
|
||||
const fetchRows = async (searchTerm, primaryDisplay, defaultVal) => {
|
||||
async function fetchRows(
|
||||
searchTerm: any,
|
||||
primaryDisplay: string,
|
||||
defaultVal: string | string[]
|
||||
) {
|
||||
const allRowsFetched =
|
||||
$fetch.loaded &&
|
||||
!Object.keys($fetch.query?.string || {}).length &&
|
||||
|
@ -129,17 +166,39 @@
|
|||
return
|
||||
}
|
||||
// must be an array
|
||||
if (defaultVal && !Array.isArray(defaultVal)) {
|
||||
defaultVal = defaultVal.split(",")
|
||||
}
|
||||
if (defaultVal && optionsObj && defaultVal.some(val => !optionsObj[val])) {
|
||||
const defaultValArray: string[] = !defaultVal
|
||||
? []
|
||||
: !Array.isArray(defaultVal)
|
||||
? defaultVal.split(",")
|
||||
: defaultVal
|
||||
|
||||
if (
|
||||
defaultVal &&
|
||||
optionsObj &&
|
||||
defaultValArray.some(val => !optionsObj[val])
|
||||
) {
|
||||
await fetch.update({
|
||||
query: { oneOf: { _id: defaultVal } },
|
||||
query: { oneOf: { _id: defaultValArray } },
|
||||
})
|
||||
}
|
||||
|
||||
if (
|
||||
(Array.isArray(selectedValue) &&
|
||||
selectedValue.some(val => !optionsObj[val])) ||
|
||||
(selectedValue && !optionsObj[selectedValue as string])
|
||||
) {
|
||||
await fetch.update({
|
||||
query: {
|
||||
oneOf: {
|
||||
_id: Array.isArray(selectedValue) ? selectedValue : [selectedValue],
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Ensure we match all filters, rather than any
|
||||
const baseFilter = (filter || []).filter(x => x.operator !== "allOr")
|
||||
// @ts-expect-error this doesn't fit types, but don't want to change it yet
|
||||
const baseFilter: any = (filter || []).filter(x => x.operator !== "allOr")
|
||||
await fetch.update({
|
||||
filter: [
|
||||
...baseFilter,
|
||||
|
@ -152,9 +211,8 @@
|
|||
],
|
||||
})
|
||||
}
|
||||
const debouncedFetchRows = Utils.debounce(fetchRows, 250)
|
||||
|
||||
const flatten = values => {
|
||||
const flatten = (values: any | any[]) => {
|
||||
if (!values) {
|
||||
return []
|
||||
}
|
||||
|
@ -162,17 +220,17 @@
|
|||
if (!Array.isArray(values)) {
|
||||
values = [values]
|
||||
}
|
||||
values = values.map(value =>
|
||||
values = values.map((value: any) =>
|
||||
typeof value === "object" ? value._id : value
|
||||
)
|
||||
return values
|
||||
}
|
||||
|
||||
const getDisplayName = row => {
|
||||
return row?.[primaryDisplay] || "-"
|
||||
const getDisplayName = (row: Row) => {
|
||||
return row?.[primaryDisplay!] || "-"
|
||||
}
|
||||
|
||||
const handleChange = e => {
|
||||
const handleChange = (e: any) => {
|
||||
let value = e.detail
|
||||
if (!multiselect) {
|
||||
value = value == null ? [] : [value]
|
||||
|
@ -220,13 +278,12 @@
|
|||
this={component}
|
||||
options={enrichedOptions}
|
||||
{autocomplete}
|
||||
value={selectedValue}
|
||||
value={castSelectedValue}
|
||||
on:change={handleChange}
|
||||
on:loadMore={loadMore}
|
||||
id={fieldState.fieldId}
|
||||
disabled={fieldState.disabled}
|
||||
readonly={fieldState.readonly}
|
||||
error={fieldState.error}
|
||||
getOptionLabel={getDisplayName}
|
||||
getOptionValue={option => option._id}
|
||||
{placeholder}
|
||||
|
|
Loading…
Reference in New Issue