1
0
Fork 0
mirror of synced 2024-09-15 00:38:01 +12:00

Fix race conditions and edge cases in relationship cell

This commit is contained in:
Andrew Kingston 2023-03-15 13:01:32 +00:00
parent 66c84b9f82
commit 6a4420cde4

View file

@ -16,6 +16,7 @@
let isOpen = false let isOpen = false
let searchResults let searchResults
let searchString let searchString
let lastSearchString
let definition let definition
let primaryDisplay let primaryDisplay
let candidateIndex let candidateIndex
@ -24,12 +25,14 @@
$: editable = selected && !readonly $: editable = selected && !readonly
$: results = getResults(searchResults, value) $: results = getResults(searchResults, value)
$: lookupMap = buildLookupMap(value, isOpen) $: lookupMap = buildLookupMap(value, isOpen)
$: search(searchString)
$: { $: {
if (!selected) { if (!selected) {
close() close()
} }
} }
// Builds a lookup map to quickly check which rows are selected
const buildLookupMap = (value, isOpen) => { const buildLookupMap = (value, isOpen) => {
let map = {} let map = {}
if (!isOpen || !value?.length) { if (!isOpen || !value?.length) {
@ -41,6 +44,7 @@
return map return map
} }
// Checks if a certain row is currently selected
const isRowSelected = row => { const isRowSelected = row => {
if (!row?._id) { if (!row?._id) {
return false return false
@ -48,14 +52,24 @@
return lookupMap?.[row._id] === true return lookupMap?.[row._id] === true
} }
const search = debounce(async value => { // Debounced function to search for rows based on the search string
if (!value || !schema?.tableId || !isOpen) { const search = debounce(async searchString => {
searchString = value // Avoid update state at all if we've already handled the update and this is
// a wasted search due to svelte reactivity
if (!searchString && !lastSearchString) {
return
}
// Reset state if this search is invalid
if (!searchString || !schema?.tableId || !isOpen) {
lastSearchString = null
candidateIndex = null candidateIndex = null
searchResults = [] searchResults = []
return return
} }
// Search for results, using IDs to track invocations and ensure we're
// handling the latest update
lastSearchId = Math.random() lastSearchId = Math.random()
const thisSearchId = lastSearchId const thisSearchId = lastSearchId
const results = await API.searchTable({ const results = await API.searchTable({
@ -64,7 +78,7 @@
limit: 20, limit: 20,
query: { query: {
string: { string: {
[`1:${primaryDisplay}`]: value, [`1:${primaryDisplay}`]: searchString,
}, },
}, },
}) })
@ -81,9 +95,10 @@
primaryDisplay: row[primaryDisplay], primaryDisplay: row[primaryDisplay],
})) }))
candidateIndex = searchResults?.length ? 0 : null candidateIndex = searchResults?.length ? 0 : null
searchString = value lastSearchString = searchString
}, 250) }, 250)
// Alphabetically sorts rows by their primary display column
const sortRows = rows => { const sortRows = rows => {
if (!rows?.length) { if (!rows?.length) {
return [] return []
@ -93,6 +108,7 @@
}) })
} }
// Generates the list of results to show inside the dropdown
const getResults = (searchResults, value) => { const getResults = (searchResults, value) => {
return searchString ? sortRows(searchResults) : sortRows(value) return searchString ? sortRows(searchResults) : sortRows(value)
} }
@ -110,8 +126,9 @@
const close = () => { const close = () => {
isOpen = false isOpen = false
searchString = null
searchResults = [] searchResults = []
searchString = null
lastSearchString = null
candidateIndex = null candidateIndex = null
} }
@ -120,6 +137,7 @@
return false return false
} }
if (e.key === "ArrowDown") { if (e.key === "ArrowDown") {
// Select next result on down arrow
e.preventDefault() e.preventDefault()
if (candidateIndex == null) { if (candidateIndex == null) {
candidateIndex = 0 candidateIndex = 0
@ -127,6 +145,7 @@
candidateIndex = Math.min(results.length - 1, candidateIndex + 1) candidateIndex = Math.min(results.length - 1, candidateIndex + 1)
} }
} else if (e.key === "ArrowUp") { } else if (e.key === "ArrowUp") {
// Select previous result on up array
e.preventDefault() e.preventDefault()
if (candidateIndex === 0) { if (candidateIndex === 0) {
candidateIndex = null candidateIndex = null
@ -134,6 +153,7 @@
candidateIndex = Math.max(0, candidateIndex - 1) candidateIndex = Math.max(0, candidateIndex - 1)
} }
} else if (e.key === "Enter") { } else if (e.key === "Enter") {
// Toggle the highlighted result on enter press
if (candidateIndex != null && results[candidateIndex] != null) { if (candidateIndex != null && results[candidateIndex] != null) {
toggleRow(results[candidateIndex]) toggleRow(results[candidateIndex])
} }
@ -141,22 +161,28 @@
return true return true
} }
const toggleRow = row => { // Toggles whether a row is included in the relationship or not
const toggleRow = async row => {
if (value?.some(x => x._id === row._id)) { if (value?.some(x => x._id === row._id)) {
// If the row is already included, remove it and update the candidate
// row to be the the same position if possible
const newValue = value.filter(x => x._id !== row._id) const newValue = value.filter(x => x._id !== row._id)
if (!newValue.length) { if (!newValue.length) {
candidateIndex = null candidateIndex = null
} else { } else {
candidateIndex = Math.min(candidateIndex, newValue.length - 1) candidateIndex = Math.min(candidateIndex, newValue.length - 1)
} }
onChange(newValue) await onChange(newValue)
} else { } else {
lookupMap[row._id] = true // If we don't have this row, include it
onChange(sortRows([...(value || []), row])) await onChange(sortRows([...(value || []), row]))
candidateIndex = null candidateIndex = null
} }
// Clear search state to allow finding a new row again
searchString = null searchString = null
searchResults = [] searchResults = []
lastSearchString = null
} }
onMount(() => { onMount(() => {
@ -181,15 +207,9 @@
{#if isOpen} {#if isOpen}
<div class="dropdown" on:wheel|stopPropagation> <div class="dropdown" on:wheel|stopPropagation>
<div class="search"> <div class="search">
<Input <Input autofocus quiet type="text" bind:value={searchString} />
autofocus
quiet
type="text"
value={searchString}
on:change={e => search(e.detail)}
/>
</div> </div>
{#if !searchString} {#if !lastSearchString}
{#if primaryDisplay} {#if primaryDisplay}
<div class="info"> <div class="info">
Search for {definition.name} rows by {primaryDisplay} Search for {definition.name} rows by {primaryDisplay}
@ -241,6 +261,7 @@
.container.editable:hover { .container.editable:hover {
cursor: pointer; cursor: pointer;
} }
.badge { .badge {
flex: 0 0 auto; flex: 0 0 auto;
padding: 2px var(--cell-padding); padding: 2px var(--cell-padding);
@ -251,9 +272,7 @@
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.result .badge {
max-width: 340px;
}
.dropdown { .dropdown {
position: absolute; position: absolute;
top: -1px; top: -1px;
@ -269,6 +288,7 @@
align-items: stretch; align-items: stretch;
background-color: var(--cell-background-hover); background-color: var(--cell-background-hover);
} }
.results { .results {
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
@ -288,6 +308,9 @@
background-color: var(--spectrum-global-color-gray-200); background-color: var(--spectrum-global-color-gray-200);
cursor: pointer; cursor: pointer;
} }
.result .badge {
max-width: 340px;
}
.search { .search {
flex: 0 0 calc(var(--cell-height) - 1px); flex: 0 0 calc(var(--cell-height) - 1px);