Merge branch 'develop' into cheeks-lab-day-portal-poc
This commit is contained in:
commit
a880960f98
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.9.39-alpha.9",
|
"version": "2.9.39-alpha.10",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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={{
|
||||||
|
|
|
@ -3647,9 +3647,9 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"label": "Autocomplete",
|
"label": "Search",
|
||||||
"key": "autocomplete",
|
"key": "autocomplete",
|
||||||
"defaultValue": false
|
"defaultValue": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue