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

Add functional relationship cell

This commit is contained in:
Andrew Kingston 2023-03-15 09:53:32 +00:00
parent 06a0f75077
commit 616e25ac27
3 changed files with 224 additions and 28 deletions

View file

@ -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?.()
} }

View file

@ -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);

View file

@ -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>