Fix race conditions and edge cases in relationship cell
This commit is contained in:
parent
66c84b9f82
commit
6a4420cde4
|
@ -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);
|
||||||
|
|
Loading…
Reference in New Issue