Merge branch 'develop' into cheeks-lab-day-portal-poc

This commit is contained in:
Andrew Kingston 2023-09-06 17:00:26 +01:00 committed by GitHub
commit a880960f98
9 changed files with 201 additions and 24 deletions

View File

@ -1,5 +1,5 @@
{ {
"version": "2.9.39-alpha.9", "version": "2.9.39-alpha.10",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -18,6 +18,7 @@ export default function positionDropdown(element, opts) {
useAnchorWidth, useAnchorWidth,
offset = 5, offset = 5,
customUpdate, customUpdate,
offsetBelow,
} = opts } = opts
if (!anchor) { if (!anchor) {
return return
@ -47,7 +48,7 @@ export default function positionDropdown(element, opts) {
styles.top = anchorBounds.top - elementBounds.height - offset styles.top = anchorBounds.top - elementBounds.height - offset
styles.maxHeight = maxHeight || 240 styles.maxHeight = maxHeight || 240
} else { } else {
styles.top = anchorBounds.bottom + offset styles.top = anchorBounds.bottom + (offsetBelow || offset)
styles.maxHeight = styles.maxHeight =
maxHeight || window.innerHeight - anchorBounds.bottom - 20 maxHeight || window.innerHeight - anchorBounds.bottom - 20
} }

View File

@ -17,6 +17,9 @@
export let fetchTerm = null export let fetchTerm = null
export let useFetch = false export let useFetch = false
export let customPopoverHeight export let customPopoverHeight
export let customPopoverOffsetBelow
export let customPopoverMaxHeight
export let open = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -88,6 +91,7 @@
isPlaceholder={!arrayValue.length} isPlaceholder={!arrayValue.length}
{autocomplete} {autocomplete}
bind:fetchTerm bind:fetchTerm
bind:open
{useFetch} {useFetch}
{isOptionSelected} {isOptionSelected}
{getOptionLabel} {getOptionLabel}
@ -96,4 +100,6 @@
{sort} {sort}
{autoWidth} {autoWidth}
{customPopoverHeight} {customPopoverHeight}
{customPopoverOffsetBelow}
{customPopoverMaxHeight}
/> />

View File

@ -38,6 +38,8 @@
export let fetchTerm = null export let fetchTerm = null
export let useFetch = false export let useFetch = false
export let customPopoverHeight export let customPopoverHeight
export let customPopoverOffsetBelow
export let customPopoverMaxHeight
export let align = "left" export let align = "left"
export let footer = null export let footer = null
export let customAnchor = null export let customAnchor = null
@ -150,7 +152,9 @@
on:close={() => (open = false)} on:close={() => (open = false)}
useAnchorWidth={!autoWidth} useAnchorWidth={!autoWidth}
maxWidth={autoWidth ? 400 : null} maxWidth={autoWidth ? 400 : null}
maxHeight={customPopoverMaxHeight}
customHeight={customPopoverHeight} customHeight={customPopoverHeight}
offsetBelow={customPopoverOffsetBelow}
> >
<div <div
class="popover-content" class="popover-content"

View File

@ -21,10 +21,12 @@
export let sort = false export let sort = false
export let align export let align
export let footer = null export let footer = null
export let open = false
export let tag = null export let tag = null
const dispatch = createEventDispatcher() export let customPopoverOffsetBelow
export let customPopoverMaxHeight
let open = false const dispatch = createEventDispatcher()
$: fieldText = getFieldText(value, options, placeholder) $: fieldText = getFieldText(value, options, placeholder)
$: fieldIcon = getFieldAttribute(getOptionIcon, value, options) $: fieldIcon = getFieldAttribute(getOptionIcon, value, options)
@ -84,6 +86,8 @@
{autocomplete} {autocomplete}
{sort} {sort}
{tag} {tag}
{customPopoverOffsetBelow}
{customPopoverMaxHeight}
isPlaceholder={value == null || value === ""} isPlaceholder={value == null || value === ""}
placeholderOption={placeholder === false ? null : placeholder} placeholderOption={placeholder === false ? null : placeholder}
isOptionSelected={option => option === value} isOptionSelected={option => option === value}

View File

@ -19,6 +19,7 @@
export let useAnchorWidth = false export let useAnchorWidth = false
export let dismissible = true export let dismissible = true
export let offset = 5 export let offset = 5
export let offsetBelow
export let customHeight export let customHeight
export let animate = true export let animate = true
export let customZindex export let customZindex
@ -89,6 +90,7 @@
maxWidth, maxWidth,
useAnchorWidth, useAnchorWidth,
offset, offset,
offsetBelow,
customUpdate: handlePostionUpdate, customUpdate: handlePostionUpdate,
}} }}
use:clickOutside={{ use:clickOutside={{

View File

@ -3647,9 +3647,9 @@
}, },
{ {
"type": "boolean", "type": "boolean",
"label": "Autocomplete", "label": "Search",
"key": "autocomplete", "key": "autocomplete",
"defaultValue": false "defaultValue": true
}, },
{ {
"type": "boolean", "type": "boolean",

View File

@ -1,6 +1,11 @@
<script> <script>
import { CoreSelect, CoreMultiselect } from "@budibase/bbui" import {
import { fetchData } from "@budibase/frontend-core" CoreSelect,
CoreMultiselect,
Input,
ProgressCircle,
} from "@budibase/bbui"
import { fetchData, Utils } from "@budibase/frontend-core"
import { getContext } from "svelte" import { getContext } from "svelte"
import Field from "./Field.svelte" import Field from "./Field.svelte"
import { FieldTypes } from "../../../constants" import { FieldTypes } from "../../../constants"
@ -12,7 +17,7 @@
export let placeholder export let placeholder
export let disabled = false export let disabled = false
export let validation export let validation
export let autocomplete = false export let autocomplete = true
export let defaultValue export let defaultValue
export let onChange export let onChange
export let filter export let filter
@ -21,6 +26,16 @@
let fieldApi let fieldApi
let fieldSchema let fieldSchema
let tableDefinition let tableDefinition
let primaryDisplay
let options
let selectedOptions = []
let isOpen = false
let hasFilter
let searchResults
let searchString
let searching = false
let lastSearchId
$: multiselect = fieldSchema?.relationshipType !== "one-to-many" $: multiselect = fieldSchema?.relationshipType !== "one-to-many"
$: linkedTableId = fieldSchema?.tableId $: linkedTableId = fieldSchema?.tableId
@ -35,13 +50,57 @@
limit: 100, limit: 100,
}, },
}) })
$: hasFilter = !!filter?.filter(f => !!f.field)?.length
$: fetch.update({ filter }) $: fetch.update({ filter })
$: options = $fetch.rows $: {
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]
}
}
$: tableDefinition = $fetch.definition $: tableDefinition = $fetch.definition
$: primaryDisplay = tableDefinition?.primaryDisplay || "_id"
$: singleValue = flatten(fieldState?.value)?.[0] $: singleValue = flatten(fieldState?.value)?.[0]
$: multiValue = flatten(fieldState?.value) ?? [] $: multiValue = flatten(fieldState?.value) ?? []
$: component = multiselect ? CoreMultiselect : CoreSelect $: component = multiselect ? CoreMultiselect : CoreSelect
$: expandedDefaultValue = expand(defaultValue) $: expandedDefaultValue = expand(defaultValue)
$: 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))
})
}
}
const flatten = values => { const flatten = values => {
if (!values) { if (!values) {
@ -77,10 +136,66 @@
const handleChange = value => { const handleChange = value => {
const changed = fieldApi.setValue(value) const changed = fieldApi.setValue(value)
selectedOptions = value.map(val => ({
_id: val,
[primaryDisplay]: options.find(option => option._id === val)[
primaryDisplay
],
}))
if (onChange && changed) { if (onChange && changed) {
onChange({ value }) onChange({ value })
} }
} }
// 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)
</script> </script>
<Field <Field
@ -95,19 +210,63 @@
bind:fieldSchema bind:fieldSchema
> >
{#if fieldState} {#if fieldState}
<svelte:component <div class={autocomplete ? "field-with-search" : ""}>
this={component} <svelte:component
{options} this={component}
{autocomplete} bind:open={isOpen}
value={multiselect ? multiValue : singleValue} {options}
on:change={multiselect ? multiHandler : singleHandler} autocomplete={false}
id={fieldState.fieldId} value={multiselect ? multiValue : singleValue}
disabled={fieldState.disabled} on:change={multiselect ? multiHandler : singleHandler}
error={fieldState.error} id={fieldState.fieldId}
getOptionLabel={getDisplayName} disabled={fieldState.disabled}
getOptionValue={option => option._id} error={fieldState.error}
{placeholder} getOptionLabel={getDisplayName}
sort={true} 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>
{/if} {/if}
</Field> </Field>
<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>

View File

@ -1,5 +1,6 @@
export { createAPIClient } from "./api" export { createAPIClient } from "./api"
export { fetchData } from "./fetch/fetchData" export { fetchData } from "./fetch/fetchData"
export { Utils } from "./utils"
export * as Constants from "./constants" export * as Constants from "./constants"
export * from "./stores" export * from "./stores"
export * from "./utils" export * from "./utils"