Add functional relationship cell
This commit is contained in:
parent
06a0f75077
commit
616e25ac27
|
@ -31,9 +31,11 @@
|
||||||
|
|
||||||
// Pass the key event to the selected cell and let it decide whether to
|
// Pass the key event to the selected cell and let it decide whether to
|
||||||
// capture it or not
|
// capture it or not
|
||||||
const handled = api?.onKeyDown?.(e)
|
if (!api?.isReadonly()) {
|
||||||
if (handled) {
|
const handled = api?.onKeyDown?.(e)
|
||||||
return
|
if (handled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
|
@ -120,6 +122,9 @@
|
||||||
}, 100)
|
}, 100)
|
||||||
|
|
||||||
const focusSelectedCell = () => {
|
const focusSelectedCell = () => {
|
||||||
|
if ($selectedCellAPI?.isReadonly()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
$selectedCellAPI?.focus?.()
|
$selectedCellAPI?.focus?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -128,6 +128,7 @@
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
img {
|
img {
|
||||||
height: calc(var(--cell-height) - 12px);
|
height: calc(var(--cell-height) - 12px);
|
||||||
|
|
|
@ -1,48 +1,150 @@
|
||||||
<script>
|
<script>
|
||||||
import { getColor } from "../utils"
|
import { getColor } from "../utils"
|
||||||
import { onMount } from "svelte"
|
import { onMount, getContext } from "svelte"
|
||||||
|
import { Icon, Input } from "@budibase/bbui"
|
||||||
|
import { debounce } from "../../../utils/utils"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
export let api
|
export let api
|
||||||
export let readonly
|
export let readonly
|
||||||
export let selected
|
export let selected
|
||||||
|
export let schema
|
||||||
|
export let onChange
|
||||||
|
|
||||||
|
const { API } = getContext("sheet")
|
||||||
|
|
||||||
let isOpen = false
|
let isOpen = false
|
||||||
let searchResults
|
let searchResults
|
||||||
|
let searchString
|
||||||
|
let definition
|
||||||
|
let primaryDisplay
|
||||||
|
let candidateIndex
|
||||||
|
|
||||||
$: editable = selected && !readonly
|
$: editable = selected && !readonly
|
||||||
|
$: search(searchString)
|
||||||
|
$: results = getResults(searchResults, value)
|
||||||
|
$: lookupMap = buildLookupMap(value, isOpen)
|
||||||
$: {
|
$: {
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$: orderedResults = orderResults(searchResults, value)
|
|
||||||
|
|
||||||
const orderResults = () => {
|
const buildLookupMap = (value, isOpen) => {
|
||||||
let results = []
|
let map = {}
|
||||||
if (value?.length) {
|
if (!isOpen || !value?.length) {
|
||||||
results = results.concat(value)
|
return map
|
||||||
}
|
}
|
||||||
if (searchResults?.length) {
|
for (let i = 0; i < value.length; i++) {
|
||||||
results = results.concat(
|
map[value[i]._id] = true
|
||||||
searchResults.filter(result => {
|
|
||||||
return !value.some(x => x._id === result._id)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return results
|
return map
|
||||||
}
|
}
|
||||||
|
|
||||||
const open = () => {
|
const isRowSelected = row => {
|
||||||
|
if (!row?._id) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return lookupMap?.[row._id] === true
|
||||||
|
}
|
||||||
|
|
||||||
|
const search = debounce(async searchString => {
|
||||||
|
if (!searchString || !schema?.tableId || !isOpen) {
|
||||||
|
candidateIndex = null
|
||||||
|
searchResults = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await API.searchTable({
|
||||||
|
paginate: false,
|
||||||
|
tableId: schema.tableId,
|
||||||
|
limit: 20,
|
||||||
|
query: {
|
||||||
|
string: {
|
||||||
|
[`1:${primaryDisplay}`]: searchString,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
searchResults = results.rows?.map(row => ({
|
||||||
|
...row,
|
||||||
|
primaryDisplay: row[primaryDisplay],
|
||||||
|
}))
|
||||||
|
candidateIndex = searchResults?.length ? 0 : null
|
||||||
|
}, 500)
|
||||||
|
|
||||||
|
const sortRows = rows => {
|
||||||
|
if (!rows?.length) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return rows.slice().sort((a, b) => {
|
||||||
|
return a.primaryDisplay < b.primaryDisplay ? -1 : 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getResults = (searchResults, value) => {
|
||||||
|
return searchString ? sortRows(searchResults) : sortRows(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const open = async () => {
|
||||||
isOpen = true
|
isOpen = true
|
||||||
|
|
||||||
|
// Fetch definition if required
|
||||||
|
if (!definition) {
|
||||||
|
definition = await API.fetchTableDefinition(schema.tableId)
|
||||||
|
primaryDisplay =
|
||||||
|
definition?.primaryDisplay || definition?.schema?.[0]?.name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
isOpen = false
|
isOpen = false
|
||||||
|
searchString = null
|
||||||
|
searchResults = []
|
||||||
|
candidateIndex = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const onKeyDown = () => {
|
const onKeyDown = e => {
|
||||||
return isOpen
|
if (!isOpen) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (e.key === "ArrowDown") {
|
||||||
|
e.preventDefault()
|
||||||
|
if (candidateIndex == null) {
|
||||||
|
candidateIndex = 0
|
||||||
|
} else {
|
||||||
|
candidateIndex = Math.min(results.length - 1, candidateIndex + 1)
|
||||||
|
}
|
||||||
|
} else if (e.key === "ArrowUp") {
|
||||||
|
e.preventDefault()
|
||||||
|
if (candidateIndex === 0) {
|
||||||
|
candidateIndex = null
|
||||||
|
} else if (candidateIndex != null) {
|
||||||
|
candidateIndex = Math.max(0, candidateIndex - 1)
|
||||||
|
}
|
||||||
|
} else if (e.key === "Enter") {
|
||||||
|
if (candidateIndex != null && results[candidateIndex] != null) {
|
||||||
|
toggleRow(results[candidateIndex])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleRow = row => {
|
||||||
|
if (value?.some(x => x._id === row._id)) {
|
||||||
|
const newValue = value.filter(x => x._id !== row._id)
|
||||||
|
if (!newValue.length) {
|
||||||
|
candidateIndex = null
|
||||||
|
} else {
|
||||||
|
candidateIndex = Math.min(candidateIndex, newValue.length - 1)
|
||||||
|
}
|
||||||
|
onChange(newValue)
|
||||||
|
} else {
|
||||||
|
lookupMap[row._id] = true
|
||||||
|
onChange(sortRows([...(value || []), row]))
|
||||||
|
candidateIndex = null
|
||||||
|
}
|
||||||
|
searchString = null
|
||||||
|
searchResults = []
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
@ -59,18 +161,54 @@
|
||||||
{#if relationship.primaryDisplay}
|
{#if relationship.primaryDisplay}
|
||||||
<div class="badge" style="--color: {getColor(idx)}">
|
<div class="badge" style="--color: {getColor(idx)}">
|
||||||
{relationship.primaryDisplay}
|
{relationship.primaryDisplay}
|
||||||
|
{relationship.primaryDisplay}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if isOpen}
|
{#if isOpen}
|
||||||
<div class="dropdown">
|
<div class="dropdown" on:wheel|stopPropagation>
|
||||||
{#each orderedResults as result, idx}
|
<div class="search">
|
||||||
<div class="badge" style="--color: {getColor(idx)}">
|
<Input autofocus quiet type="text" bind:value={searchString} />
|
||||||
{result.primaryDisplay}
|
</div>
|
||||||
|
{#if !searchResults?.length}
|
||||||
|
{#if primaryDisplay}
|
||||||
|
<div class="info">
|
||||||
|
Search for {definition.name} rows by {primaryDisplay}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<div class="info">
|
||||||
|
{searchResults.length} row{searchResults.length === 1 ? "" : "s"} found
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/if}
|
||||||
|
{#if results?.length}
|
||||||
|
<div class="results">
|
||||||
|
{#each results as row, idx}
|
||||||
|
<div
|
||||||
|
class="result"
|
||||||
|
on:click={() => toggleRow(row)}
|
||||||
|
class:candidate={idx === candidateIndex}
|
||||||
|
on:mouseenter={() => (candidateIndex = idx)}
|
||||||
|
>
|
||||||
|
<div class="badge" style="--color: {getColor(idx)}">
|
||||||
|
{row.primaryDisplay}
|
||||||
|
{row.primaryDisplay}
|
||||||
|
{row.primaryDisplay}
|
||||||
|
{row.primaryDisplay}
|
||||||
|
</div>
|
||||||
|
{#if isRowSelected(row)}
|
||||||
|
<Icon
|
||||||
|
size="S"
|
||||||
|
name="Checkmark"
|
||||||
|
color="var(--spectrum-global-color-blue-400)"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
@ -95,20 +233,72 @@
|
||||||
background: var(--color);
|
background: var(--color);
|
||||||
border-radius: var(--cell-padding);
|
border-radius: var(--cell-padding);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.result .badge {
|
||||||
|
max-width: calc(100% - 24px);
|
||||||
}
|
}
|
||||||
.dropdown {
|
.dropdown {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -1px;
|
top: -1px;
|
||||||
left: -1px;
|
left: -1px;
|
||||||
width: calc(100% + 2px);
|
min-width: calc(100% + 2px);
|
||||||
height: 300px;
|
max-width: calc(100% + 240px);
|
||||||
|
max-height: calc(var(--cell-height) + 240px);
|
||||||
background: var(--cell-background);
|
background: var(--cell-background);
|
||||||
border: var(--cell-border);
|
border: var(--cell-border);
|
||||||
box-shadow: 0 0 8px 4px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 0 8px 4px rgba(0, 0, 0, 0.15);
|
||||||
padding: var(--cell-padding);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
background-color: var(--cell-background-hover);
|
||||||
|
}
|
||||||
|
.results {
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
.result {
|
||||||
|
padding: 0 var(--cell-padding);
|
||||||
|
flex: 0 0 var(--cell-height);
|
||||||
|
display: flex;
|
||||||
gap: var(--cell-spacing);
|
gap: var(--cell-spacing);
|
||||||
align-items: flex-start;
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.result.candidate {
|
||||||
|
background-color: var(--spectrum-global-color-gray-200);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search {
|
||||||
|
flex: 0 0 calc(var(--cell-height) - 1px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0 var(--cell-padding);
|
||||||
|
width: calc(100% - 2 * var(--cell-padding));
|
||||||
|
}
|
||||||
|
.search :global(.spectrum-Textfield) {
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.search :global(.spectrum-Form-item) {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
font-size: 12px;
|
||||||
|
padding: var(--cell-padding);
|
||||||
|
flex: 0 0 var(--cell-height);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
Loading…
Reference in New Issue