Large refactors to row creation, naming and sheet APIs

This commit is contained in:
Andrew Kingston 2023-04-10 18:46:34 +01:00
parent da2023974e
commit 81a28eb4da
31 changed files with 376 additions and 508 deletions

View File

@ -84,7 +84,7 @@
"@spectrum-css/vars": "3.0.1", "@spectrum-css/vars": "3.0.1",
"dayjs": "^1.10.4", "dayjs": "^1.10.4",
"easymde": "^2.16.1", "easymde": "^2.16.1",
"svelte-flatpickr": "^3.2.3", "svelte-flatpickr": "^3.3.2",
"svelte-portal": "^1.0.0" "svelte-portal": "^1.0.0"
}, },
"resolutions": { "resolutions": {

View File

@ -2576,10 +2576,10 @@ supports-color@^7.0.0, supports-color@^7.1.0:
dependencies: dependencies:
has-flag "^4.0.0" has-flag "^4.0.0"
svelte-flatpickr@^3.2.3: svelte-flatpickr@^3.3.2:
version "3.2.3" version "3.3.2"
resolved "https://registry.yarnpkg.com/svelte-flatpickr/-/svelte-flatpickr-3.2.3.tgz#db5dd7ad832ef83262b45e09737955ad3d591fc8" resolved "https://registry.yarnpkg.com/svelte-flatpickr/-/svelte-flatpickr-3.3.2.tgz#f08bcde83d439cb30df6fd07b974d87371f130c1"
integrity sha512-PNkqK4Napx8nTvCwkaUXdnKo8dISThaxEOK+szTUXcY6H0dQM0TSyuoMaVWY2yX7pM+PN5cpCQCcVe8YvTRFSw== integrity sha512-VNJLYyLRDplI63oWX5hJylzAJc2VhTh3z9SNecfjtuPZmP6FZPpg9Fw7rXpkEV2DPovIWj2PtaVxB6Kp9r423w==
dependencies: dependencies:
flatpickr "^4.5.2" flatpickr "^4.5.2"

View File

@ -4,7 +4,7 @@
import { Dropzone, notifications } from "@budibase/bbui" import { Dropzone, notifications } from "@budibase/bbui"
export let value export let value
export let selected = false export let focused = false
export let onChange export let onChange
export let readonly = false export let readonly = false
export let api export let api
@ -15,9 +15,9 @@
let isOpen = false let isOpen = false
$: editable = selected && !readonly $: editable = focused && !readonly
$: { $: {
if (!selected) { if (!focused) {
close() close()
} }
} }

View File

@ -3,12 +3,12 @@
import { Checkbox } from "@budibase/bbui" import { Checkbox } from "@budibase/bbui"
export let value export let value
export let selected = false export let focused = false
export let onChange export let onChange
export let readonly = false export let readonly = false
export let api export let api
$: editable = selected && !readonly $: editable = focused && !readonly
const handleChange = e => { const handleChange = e => {
onChange(e.detail) onChange(e.detail)

View File

@ -1,15 +1,17 @@
<script> <script>
import { getContext, onMount } from "svelte" import { getContext } from "svelte"
import SheetCell from "./SheetCell.svelte" import SheetCell from "./SheetCell.svelte"
import { getCellRenderer } from "../lib/renderers" import { getCellRenderer } from "../lib/renderers"
import { derived, writable } from "svelte/store"
const { rows, selectedCellId, menu, selectedCellAPI, config } = const { rows, focusedCellId, menu, sheetAPI, config, validation } =
getContext("sheet") getContext("sheet")
export let rowSelected export let rowSelected
export let rowHovered export let rowHovered
export let rowFocused
export let rowIdx export let rowIdx
export let selected export let focused
export let selectedUser export let selectedUser
export let reorderSource export let reorderSource
export let reorderTarget export let reorderTarget
@ -19,8 +21,36 @@
export let updateRow = rows.actions.updateRow export let updateRow = rows.actions.updateRow
export let invert = false export let invert = false
const emptyError = writable(null)
let api let api
let error
$: {
// Wipe error if row is unfocused
if (!rowFocused && $error) {
validation.actions.setError(cellId, null)
}
}
// Get the error for this cell if the row is focused
$: error = getErrorStore(rowFocused, cellId)
// Determine if the cell is editable
$: readonly = column.schema.autocolumn || (!$config.allowEditRows && row._id)
// Register this cell API if the row is focused
$: {
if (rowFocused) {
sheetAPI.actions.registerCellAPI(cellId, cellAPI)
}
}
const getErrorStore = (selected, cellId) => {
if (!selected) {
return emptyError
}
return derived(validation, $validation => $validation[cellId])
}
const cellAPI = { const cellAPI = {
focus: () => api?.focus(), focus: () => api?.focus(),
@ -29,76 +59,54 @@
isReadonly: () => readonly, isReadonly: () => readonly,
isRequired: () => !!column.schema.constraints?.presence, isRequired: () => !!column.schema.constraints?.presence,
validate: value => { validate: value => {
// Validate the current value if no new value is provided
if (value === undefined) { if (value === undefined) {
value = row[column.name] value = row[column.name]
} }
let newError = null
if (cellAPI.isReadonly() && !(value == null || value === "")) { if (cellAPI.isReadonly() && !(value == null || value === "")) {
// Ensure cell isn't readonly // Ensure cell isn't readonly
error = "Auto columns can't be edited" newError = "Auto columns can't be edited"
} else if (cellAPI.isRequired() && (value == null || value === "")) { } else if (cellAPI.isRequired() && (value == null || value === "")) {
// Sanity check required fields // Sanity check required fields
error = "Required field" newError = "Required field"
} else { } else {
error = null newError = null
} }
return error validation.actions.setError(cellId, newError)
return newError
}, },
updateValue: value => { updateValue: value => {
try { cellAPI.validate(value)
cellAPI.validate(value) if (!$error) {
if (!error) { updateRow(row._id, column.name, value)
updateRow(row._id, column.name, value)
}
} catch (err) {
error = err
} }
}, },
} }
// Determine if the cell is editable
$: readonly = column.schema.autocolumn || (!$config.allowEditRows && row._id)
// Update selected cell API if selected
$: {
if (selected) {
selectedCellAPI.set(cellAPI)
} else if (error) {
// error = null
}
}
</script> </script>
{#key error} <SheetCell
<SheetCell {rowSelected}
{rowSelected} {rowHovered}
{rowHovered} {rowFocused}
{rowIdx} {rowIdx}
{selected} {focused}
{selectedUser} {selectedUser}
{reorderSource} {reorderSource}
{reorderTarget} {reorderTarget}
{error} error={$error}
on:click={() => selectedCellId.set(cellId)} on:click={() => focusedCellId.set(cellId)}
on:contextmenu={e => menu.actions.open(cellId, e)} on:contextmenu={e => menu.actions.open(cellId, e)}
width={column.width} width={column.width}
> >
<svelte:component <svelte:component
this={getCellRenderer(column)} this={getCellRenderer(column)}
bind:api bind:api
value={row[column.name]} value={row[column.name]}
schema={column.schema} schema={column.schema}
{selected} onChange={cellAPI.updateValue}
onChange={cellAPI.updateValue} {focused}
{readonly} {readonly}
{invert} {invert}
placeholder="error" />
/> </SheetCell>
</SheetCell>
{/key}
<style>
.placeholder {
font-style: italic;
padding: var(--cell-padding);
}
</style>

View File

@ -5,7 +5,7 @@
export let value export let value
export let schema export let schema
export let onChange export let onChange
export let selected = false export let focused = false
export let readonly = false export let readonly = false
// adding the 0- will turn a string like 00:00:00 into a valid ISO // adding the 0- will turn a string like 00:00:00 into a valid ISO
@ -18,7 +18,7 @@
: dateOnly : dateOnly
? "MMM D YYYY" ? "MMM D YYYY"
: "MMM D YYYY, HH:mm" : "MMM D YYYY, HH:mm"
$: editable = selected && !readonly $: editable = focused && !readonly
</script> </script>
<div class="container"> <div class="container">

View File

@ -181,6 +181,7 @@
.header-cell :global(.cell) { .header-cell :global(.cell) {
padding: 0 var(--cell-padding); padding: 0 var(--cell-padding);
gap: calc(2 * var(--cell-spacing)); gap: calc(2 * var(--cell-spacing));
background: var(--spectrum-global-color-gray-100);
} }
.header-cell.sorted :global(.cell) { .header-cell.sorted :global(.cell) {
background: var(--spectrum-global-color-gray-200); background: var(--spectrum-global-color-gray-200);

View File

@ -2,7 +2,7 @@
import { onMount, tick } from "svelte" import { onMount, tick } from "svelte"
export let value export let value
export let selected = false export let focused = false
export let onChange export let onChange
export let readonly = false export let readonly = false
export let api export let api
@ -11,9 +11,9 @@
let textarea let textarea
let isOpen = false let isOpen = false
$: editable = selected && !readonly $: editable = focused && !readonly
$: { $: {
if (!selected) { if (!focused) {
isOpen = false isOpen = false
} }
} }

View File

@ -6,7 +6,7 @@
export let value export let value
export let schema export let schema
export let onChange export let onChange
export let selected = false export let focused = false
export let multi = false export let multi = false
export let readonly = false export let readonly = false
export let api export let api
@ -16,11 +16,11 @@
let focusedOptionIdx = null let focusedOptionIdx = null
$: options = schema?.constraints?.inclusion || [] $: options = schema?.constraints?.inclusion || []
$: editable = selected && !readonly $: editable = focused && !readonly
$: values = Array.isArray(value) ? value : [value].filter(x => x != null) $: values = Array.isArray(value) ? value : [value].filter(x => x != null)
$: { $: {
// Close when deselected // Close when deselected
if (!selected) { if (!focused) {
close() close()
} }
} }
@ -180,7 +180,7 @@
left: 0; left: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
box-shadow: 0 0 8px 4px rgba(0, 0, 0, 0.15); box-shadow: 4px 4px 10px 2px rgba(0, 0, 0, 0.1);
justify-content: flex-start; justify-content: flex-start;
align-items: stretch; align-items: stretch;
max-height: var(--max-cell-render-height); max-height: var(--max-cell-render-height);

View File

@ -7,7 +7,7 @@
export let value export let value
export let api export let api
export let readonly export let readonly
export let selected export let focused
export let schema export let schema
export let onChange export let onChange
export let invert = false export let invert = false
@ -25,12 +25,12 @@
let results let results
$: oneRowOnly = schema?.relationshipType === "one-to-many" $: oneRowOnly = schema?.relationshipType === "one-to-many"
$: editable = selected && !readonly $: editable = focused && !readonly
$: results = getResults(searchResults, value) $: results = getResults(searchResults, value)
$: lookupMap = buildLookupMap(value, isOpen) $: lookupMap = buildLookupMap(value, isOpen)
$: search(searchString) $: search(searchString)
$: { $: {
if (!selected) { if (!focused) {
close() close()
} }
} }

View File

@ -1,7 +1,8 @@
<script> <script>
export let rowSelected = false export let rowSelected = false
export let rowHovered = false export let rowHovered = false
export let selected = false export let rowFocused = false
export let focused = false
export let reorderSource = false export let reorderSource = false
export let reorderTarget = false export let reorderTarget = false
export let width = "" export let width = ""
@ -25,12 +26,13 @@
class="cell" class="cell"
class:row-selected={rowSelected} class:row-selected={rowSelected}
class:row-hovered={rowHovered} class:row-hovered={rowHovered}
class:selected class:row-focused={rowFocused}
class:focused
class:selected-other={selectedUser != null} class:selected-other={selectedUser != null}
class:reorder-source={reorderSource} class:reorder-source={reorderSource}
class:reorder-target={reorderTarget} class:reorder-target={reorderTarget}
class:center class:center
class:error={error && selected} class:error
on:focus on:focus
on:mousedown on:mousedown
on:mouseup on:mouseup
@ -39,8 +41,14 @@
{style} {style}
data-row={rowIdx} data-row={rowIdx}
> >
{#if error}
<div class="label">
{error}
</div>
{/if}
<slot /> <slot />
{#if selectedUser && !selected}
{#if selectedUser && !focused}
<div class="label"> <div class="label">
{selectedUser.label} {selectedUser.label}
</div> </div>
@ -64,9 +72,9 @@
position: relative; position: relative;
width: 0; width: 0;
} }
.cell.selected:after, .cell.focused:after,
.cell.error:after, .cell.error:after,
.cell.selected-other:not(.selected):after { .cell.selected-other:not(.focused):after {
content: " "; content: " ";
position: absolute; position: absolute;
top: 0; top: 0;
@ -78,28 +86,32 @@
border-radius: 2px; border-radius: 2px;
box-sizing: border-box; box-sizing: border-box;
} }
.cell.selected { .cell:hover {
z-index: 1;
}
.cell.focused {
z-index: 2; z-index: 2;
} }
.cell.selected:after { .cell.focused:after {
border-color: var(--spectrum-global-color-blue-400); border-color: var(--spectrum-global-color-blue-400);
} }
.cell.error:after { .cell.error:after {
border-color: var(--spectrum-global-color-red-400); border-color: var(--spectrum-global-color-red-400);
} }
.cell.selected-other:not(.selected) { .cell.selected-other:not(.focused) {
z-index: 1; z-index: 1;
} }
.cell.selected-other:not(.selected):after { .cell.selected-other:not(.focused):after {
border-color: var(--spectrum-global-color-red-400); border-color: var(--spectrum-global-color-red-400);
} }
.cell:not(.selected) { .cell:not(.focused) {
user-select: none; user-select: none;
} }
.cell:hover { .cell:hover {
cursor: default; cursor: default;
} }
.cell.row-selected { .cell.row-selected,
.cell.row-focused {
--cell-background: var(--spectrum-global-color-gray-75); --cell-background: var(--spectrum-global-color-gray-75);
} }
.cell.row-hovered { .cell.row-hovered {
@ -126,10 +138,11 @@
.label { .label {
position: absolute; position: absolute;
bottom: 100%; bottom: 100%;
left: 0;
margin: 0 0 -2px 0; margin: 0 0 -2px 0;
padding: 1px 4px 3px 4px; padding: 1px 4px 3px 4px;
background: var(--user-color); background: var(--user-color);
border-radius: 2px 2px 0 0; border-radius: 2px;
display: none; display: none;
color: white; color: white;
font-size: 12px; font-size: 12px;
@ -143,7 +156,7 @@
.cell[data-row="0"] .label { .cell[data-row="0"] .label {
bottom: auto; bottom: auto;
top: 100%; top: 100%;
border-radius: 0 0 2px 2px; border-radius: 0 2px 2px 2px;
padding: 2px 4px 2px 4px; padding: 2px 4px 2px 4px;
margin: -2px 0 0 0; margin: -2px 0 0 0;
} }
@ -152,6 +165,8 @@
} }
.error .label { .error .label {
background: var(--spectrum-global-color-red-400); background: var(--spectrum-global-color-red-400);
}
.error.focused .label {
display: block; display: block;
} }
</style> </style>

View File

@ -2,23 +2,23 @@
import { onMount } from "svelte" import { onMount } from "svelte"
export let value export let value
export let selected = false export let focused = false
export let onChange export let onChange
export let type = "text" export let type = "text"
export let readonly = false export let readonly = false
export let api export let api
let input let input
let focused = false let active = false
$: editable = selected && !readonly $: editable = focused && !readonly
const handleChange = e => { const handleChange = e => {
onChange(e.target.value) onChange(e.target.value)
} }
const onKeyDown = e => { const onKeyDown = e => {
if (!focused) { if (!active) {
return false return false
} }
if (e.key === "Enter") { if (e.key === "Enter") {
@ -41,8 +41,8 @@
{#if editable} {#if editable}
<input <input
bind:this={input} bind:this={input}
on:focus={() => (focused = true)} on:focus={() => (active = true)}
on:blur={() => (focused = false)} on:blur={() => (active = false)}
{type} {type}
value={value || ""} value={value || ""}
on:change={handleChange} on:change={handleChange}

View File

@ -32,8 +32,8 @@
background: var(--background); background: var(--background);
border-bottom: var(--cell-border); border-bottom: var(--cell-border);
position: relative; position: relative;
z-index: 2;
height: var(--row-height); height: var(--row-height);
z-index: 1;
} }
.row { .row {
display: flex; display: flex;

View File

@ -1,246 +0,0 @@
<script>
import SheetCell from "../cells/SheetCell.svelte"
import { getContext } from "svelte"
import { Icon, Button } from "@budibase/bbui"
import SheetScrollWrapper from "./SheetScrollWrapper.svelte"
import DataCell from "../cells/DataCell.svelte"
const {
renderedColumns,
hoveredRowId,
selectedCellId,
stickyColumn,
gutterWidth,
scroll,
config,
dispatch,
visibleColumns,
rows,
wheel,
showHScrollbar,
tableId,
} = getContext("sheet")
let isAdding = false
let newRow = {}
let touched = false
$: firstColumn = $stickyColumn || $visibleColumns[0]
$: rowHovered = $hoveredRowId === "new"
$: containsSelectedCell = $selectedCellId?.startsWith("new-")
$: width = gutterWidth + ($stickyColumn?.width || 0)
$: scrollLeft = $scroll.left
$: $tableId, (isAdding = false)
const addRow = async () => {
const savedRow = await rows.actions.addRow(newRow)
if (savedRow && firstColumn) {
$selectedCellId = `${savedRow._id}-${firstColumn.name}`
isAdding = false
}
}
const cancel = () => {
isAdding = false
}
const startAdding = () => {
newRow = {}
isAdding = true
}
const updateRow = (rowId, columnName, val) => {
touched = true
newRow[columnName] = val
}
const addViaModal = () => {
isAdding = false
dispatch("add-row")
}
</script>
<!-- Only show new row functionality if we have any columns -->
{#if firstColumn}
<div
class="add-button"
class:visible={!isAdding}
class:above-scrollbar={$showHScrollbar}
>
<Button size="M" cta icon="Add" on:click={startAdding}>Add row</Button>
</div>
<div class="container" class:visible={isAdding}>
<div class="buttons">
<Button size="M" cta on:click={addRow}>Save</Button>
<Button size="M" secondary newStyles on:click={cancel}>Cancel</Button>
</div>
<div class="content" class:above-scrollbar={$showHScrollbar}>
<div
class="new-row"
on:mouseenter={() => ($hoveredRowId = "new")}
on:mouseleave={() => ($hoveredRowId = null)}
>
<div
class="sticky-column"
style="flex: 0 0 {width}px"
class:scrolled={scrollLeft > 0}
>
<SheetCell
width={gutterWidth}
{rowHovered}
rowSelected={containsSelectedCell}
>
<div class="gutter">
{#if $config.allowExpandRows}
<Icon
name="Maximize"
size="S"
hoverable
on:click={addViaModal}
/>
{/if}
</div>
</SheetCell>
{#if $stickyColumn}
{@const cellId = `new-${$stickyColumn.name}`}
<DataCell
{cellId}
column={$stickyColumn}
row={newRow}
{rowHovered}
selected={$selectedCellId === cellId}
rowSelected={containsSelectedCell}
width={$stickyColumn.width}
{updateRow}
invert
/>
{/if}
</div>
<SheetScrollWrapper scrollVertically={false}>
<div class="row">
{#each $renderedColumns as column}
{@const cellId = `new-${column.name}`}
<DataCell
{cellId}
{column}
row={newRow}
{rowHovered}
selected={$selectedCellId === cellId}
rowSelected={containsSelectedCell}
width={column.width}
{updateRow}
invert
/>
{/each}
</div>
</SheetScrollWrapper>
</div>
</div>
</div>
{/if}
<style>
.add-button {
position: absolute;
left: 16px;
bottom: 16px;
z-index: 1;
transform: translateY(calc(16px + 100%));
transition: transform 130ms ease-out;
}
.add-button.above-scrollbar {
bottom: 32px;
}
.add-button.visible {
transform: translateY(0);
}
.container {
pointer-events: none;
position: absolute;
bottom: 0;
transform: translateY(100%);
z-index: 1;
transition: transform 130ms ease-out;
background: linear-gradient(
to bottom,
transparent,
var(--cell-background) 80%
);
width: 100%;
padding-top: 64px;
display: flex;
flex-direction: column;
align-items: stretch;
}
.container.visible {
transform: translateY(0);
}
.content {
pointer-events: all;
background: var(--background);
border-top: var(--cell-border);
}
.content.above-scrollbar {
padding: 0 0 24px 0;
}
.new-row {
display: flex;
bottom: 0;
left: 0;
width: 100%;
transition: margin-bottom 130ms ease-out;
}
.new-row.visible {
margin-bottom: 0;
}
.new-row :global(.cell) {
--cell-background: var(--background) !important;
}
.sticky-column {
display: flex;
z-index: 1;
}
/* Don't show borders between cells in the sticky column */
.sticky-column :global(.cell:not(:last-child)) {
border-right: none;
}
.row {
width: 0;
display: flex;
}
/* Add shadow when scrolled */
.sticky.scrolled :global(.cell:last-child:after) {
content: " ";
position: absolute;
width: 10px;
height: 100%;
left: 100%;
background: linear-gradient(to right, rgba(0, 0, 0, 0.08), transparent);
}
/* Styles for gutter */
.gutter {
flex: 1 1 auto;
display: grid;
align-items: center;
padding: var(--cell-padding);
grid-template-columns: 1fr 16px;
gap: var(--cell-spacing);
}
/* Floating buttons */
.buttons {
display: flex;
flex-direction: row;
gap: 8px;
margin: 0 0 16px 16px;
pointer-events: all;
align-self: flex-start;
}
</style>

View File

@ -4,11 +4,11 @@
import { Icon, Button } from "@budibase/bbui" import { Icon, Button } from "@budibase/bbui"
import SheetScrollWrapper from "./SheetScrollWrapper.svelte" import SheetScrollWrapper from "./SheetScrollWrapper.svelte"
import DataCell from "../cells/DataCell.svelte" import DataCell from "../cells/DataCell.svelte"
import { fly } from "svelte/transition"
const { const {
renderedColumns,
hoveredRowId, hoveredRowId,
selectedCellId, focusedCellId,
stickyColumn, stickyColumn,
gutterWidth, gutterWidth,
scroll, scroll,
@ -19,7 +19,7 @@
showHScrollbar, showHScrollbar,
tableId, tableId,
subscribe, subscribe,
selectedCellAPI, sheetAPI,
} = getContext("sheet") } = getContext("sheet")
let isAdding = false let isAdding = false
@ -28,33 +28,16 @@
$: firstColumn = $stickyColumn || $visibleColumns[0] $: firstColumn = $stickyColumn || $visibleColumns[0]
$: rowHovered = $hoveredRowId === "new" $: rowHovered = $hoveredRowId === "new"
$: containsSelectedCell = $selectedCellId?.startsWith("new-") $: rowFocused = $focusedCellId?.startsWith("new-")
$: width = gutterWidth + ($stickyColumn?.width || 0) $: width = gutterWidth + ($stickyColumn?.width || 0)
$: scrollLeft = $scroll.left $: scrollLeft = $scroll.left
$: $tableId, (isAdding = false) $: $tableId, (isAdding = false)
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
const addRow = async () => { const addRow = async () => {
// Validate new row
let allColumns = []
if ($stickyColumn) {
allColumns.push($stickyColumn)
}
allColumns = allColumns.concat($visibleColumns)
for (let col of allColumns) {
$selectedCellId = `new-${col.name}`
await tick()
const error = $selectedCellAPI.validate()
if (error) {
return
}
}
// Create row // Create row
const savedRow = await rows.actions.addRow(newRow, 0) const savedRow = await rows.actions.addRow(newRow, 0)
if (savedRow && firstColumn) { if (savedRow && firstColumn) {
$selectedCellId = `${savedRow._id}-${firstColumn.name}` $focusedCellId = `${savedRow._id}-${firstColumn.name}`
isAdding = false isAdding = false
} }
@ -70,14 +53,10 @@
} }
const startAdding = () => { const startAdding = () => {
scroll.set({
left: 0,
top: 0,
})
newRow = {} newRow = {}
isAdding = true isAdding = true
if (firstColumn) { if (firstColumn) {
$selectedCellId = `new-${firstColumn.name}` $focusedCellId = `new-${firstColumn.name}`
} }
} }
@ -95,8 +74,8 @@
</script> </script>
<!-- Only show new row functionality if we have any columns --> <!-- Only show new row functionality if we have any columns -->
{#if firstColumn} {#if isAdding}
<div class="container" class:visible={isAdding}> <div class="container" transition:fly={{ y: 20, duration: 130 }}>
<div class="content" class:above-scrollbar={$showHScrollbar}> <div class="content" class:above-scrollbar={$showHScrollbar}>
<div <div
class="new-row" class="new-row"
@ -108,11 +87,7 @@
style="flex: 0 0 {width}px" style="flex: 0 0 {width}px"
class:scrolled={scrollLeft > 0} class:scrolled={scrollLeft > 0}
> >
<SheetCell <SheetCell width={gutterWidth} {rowHovered} {rowFocused}>
width={gutterWidth}
{rowHovered}
rowSelected={containsSelectedCell}
>
<div class="gutter"> <div class="gutter">
<div class="number">1</div> <div class="number">1</div>
{#if $config.allowExpandRows} {#if $config.allowExpandRows}
@ -132,8 +107,8 @@
column={$stickyColumn} column={$stickyColumn}
row={newRow} row={newRow}
{rowHovered} {rowHovered}
selected={$selectedCellId === cellId} focused={$focusedCellId === cellId}
rowSelected={containsSelectedCell} {rowFocused}
width={$stickyColumn.width} width={$stickyColumn.width}
{updateRow} {updateRow}
rowIdx={0} rowIdx={0}
@ -151,8 +126,8 @@
{column} {column}
row={newRow} row={newRow}
{rowHovered} {rowHovered}
selected={$selectedCellId === cellId} focused={$focusedCellId === cellId}
rowSelected={containsSelectedCell} {rowFocused}
width={column.width} width={column.width}
{updateRow} {updateRow}
rowIdx={0} rowIdx={0}
@ -176,22 +151,17 @@
position: absolute; position: absolute;
top: var(--row-height); top: var(--row-height);
left: 0; left: 0;
transform: translateY(-100%);
transition: transform 130ms ease-out;
background: linear-gradient( background: linear-gradient(
to bottom, to bottom,
var(--cell-background) 20%, var(--cell-background) 20%,
transparent 100% transparent 100%
); );
width: 100%; width: 100%;
padding-bottom: 64px; padding-bottom: 100px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
} }
.container.visible {
transform: translateY(0);
}
.content { .content {
pointer-events: all; pointer-events: all;
background: var(--background); background: var(--background);
@ -216,6 +186,7 @@
.sticky-column { .sticky-column {
display: flex; display: flex;
z-index: 1; z-index: 1;
position: relative;
} }
/* Don't show borders between cells in the sticky column */ /* Don't show borders between cells in the sticky column */
.sticky-column :global(.cell:not(:last-child)) { .sticky-column :global(.cell:not(:last-child)) {
@ -228,13 +199,17 @@
} }
/* Add shadow when scrolled */ /* Add shadow when scrolled */
.sticky.scrolled :global(.cell:last-child:after) { .sticky-column.scrolled {
content: " "; /*box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.1);*/
position: absolute; }
.sticky-column.scrolled:after {
content: "";
width: 10px; width: 10px;
height: 100%; height: 100%;
background: linear-gradient(to right, rgba(0, 0, 0, 0.05), transparent);
left: 100%; left: 100%;
background: linear-gradient(to right, rgba(0, 0, 0, 0.08), transparent); top: 0;
position: absolute;
} }
/* Styles for gutter */ /* Styles for gutter */
@ -252,7 +227,7 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 8px; gap: 8px;
margin: 16px 0 0 16px; margin: 24px 0 0 16px;
pointer-events: all; pointer-events: all;
align-self: flex-start; align-self: flex-start;
} }

View File

@ -15,6 +15,8 @@
import { createMenuStores } from "../stores/menu" import { createMenuStores } from "../stores/menu"
import { createMaxScrollStores } from "../stores/max-scroll" import { createMaxScrollStores } from "../stores/max-scroll"
import { createPaginationStores } from "../stores/pagination" import { createPaginationStores } from "../stores/pagination"
import { createSheetAPIStores } from "../stores/sheet-api"
import { createValidationStores } from "../stores/validation"
import DeleteButton from "../controls/DeleteButton.svelte" import DeleteButton from "../controls/DeleteButton.svelte"
import SheetBody from "./SheetBody.svelte" import SheetBody from "./SheetBody.svelte"
import ResizeOverlay from "../overlays/ResizeOverlay.svelte" import ResizeOverlay from "../overlays/ResizeOverlay.svelte"
@ -63,11 +65,13 @@
tableId: tableIdStore, tableId: tableIdStore,
} }
context = { ...context, ...createEventManagers() } context = { ...context, ...createEventManagers() }
context = { ...context, ...createValidationStores(context) }
context = { ...context, ...createBoundsStores(context) } context = { ...context, ...createBoundsStores(context) }
context = { ...context, ...createScrollStores(context) } context = { ...context, ...createScrollStores(context) }
context = { ...context, ...createRowsStore(context) } context = { ...context, ...createRowsStore(context) }
context = { ...context, ...createColumnsStores(context) } context = { ...context, ...createColumnsStores(context) }
context = { ...context, ...createUIStores(context) } context = { ...context, ...createUIStores(context) }
context = { ...context, ...createSheetAPIStores(context) }
context = { ...context, ...createResizeStores(context) } context = { ...context, ...createResizeStores(context) }
context = { ...context, ...createViewportStores(context) } context = { ...context, ...createViewportStores(context) }
context = { ...context, ...createMaxScrollStores(context) } context = { ...context, ...createMaxScrollStores(context) }
@ -75,7 +79,6 @@
context = { ...context, ...createUserStores(context) } context = { ...context, ...createUserStores(context) }
context = { ...context, ...createMenuStores(context) } context = { ...context, ...createMenuStores(context) }
context = { ...context, ...createPaginationStores(context) } context = { ...context, ...createPaginationStores(context) }
context = { ...context, ...context }
// Reference some stores for local use // Reference some stores for local use
const { isResizing, isReordering, ui, loaded, rowHeight } = context const { isResizing, isReordering, ui, loaded, rowHeight } = context

View File

@ -7,41 +7,47 @@
export let invert = false export let invert = false
const { const {
selectedCellId, focusedCellId,
reorder, reorder,
selectedRows, selectedRows,
visibleColumns,
renderedColumns, renderedColumns,
hoveredRowId, hoveredRowId,
selectedCellMap, selectedCellMap,
selectedCellRow, focusedRow,
hiddenColumnsWidth,
} = getContext("sheet") } = getContext("sheet")
$: rowSelected = !!$selectedRows[row._id] $: rowSelected = !!$selectedRows[row._id]
$: rowHovered = $hoveredRowId === row._id $: rowHovered = $hoveredRowId === row._id
$: containsSelectedCell = $selectedCellRow?._id === row._id $: rowFocused = $focusedRow?._id === row._id
$: cols = rowFocused ? $visibleColumns : $renderedColumns
$: foo = `margin-left: ${-1 * $hiddenColumnsWidth}px;`
</script> </script>
<div <div
class="row" class="row"
style={rowFocused ? foo : null}
on:focus on:focus
on:mouseenter={() => ($hoveredRowId = row._id)} on:mouseenter={() => ($hoveredRowId = row._id)}
on:mouseleave={() => ($hoveredRowId = null)} on:mouseleave={() => ($hoveredRowId = null)}
> >
{#each $renderedColumns as column (column.name)} {#each cols as column (column.name)}
{@const cellId = `${row._id}-${column.name}`} {@const cellId = `${row._id}-${column.name}`}
<DataCell <DataCell
rowSelected={rowSelected || containsSelectedCell} {rowSelected}
{rowHovered} {rowHovered}
rowIdx={idx} {rowFocused}
selected={$selectedCellId === cellId}
selectedUser={$selectedCellMap[cellId]}
reorderSource={$reorder.sourceColumn === column.name}
reorderTarget={$reorder.targetColumn === column.name}
width={column.width}
{cellId} {cellId}
{column} {column}
{row} {row}
{invert} {invert}
rowIdx={idx}
focused={$focusedCellId === cellId}
selectedUser={$selectedCellMap[cellId]}
reorderSource={$reorder.sourceColumn === column.name}
reorderTarget={$reorder.targetColumn === column.name}
width={column.width}
/> />
{/each} {/each}
</div> </div>
@ -50,5 +56,6 @@
.row { .row {
width: 0; width: 0;
display: flex; display: flex;
height: var(--row-height);
} }
</style> </style>

View File

@ -5,14 +5,13 @@
const { const {
rowHeight, rowHeight,
scroll, scroll,
visibleColumns, focusedCellId,
renderedColumns,
selectedCellId,
renderedRows, renderedRows,
maxScrollTop, maxScrollTop,
maxScrollLeft, maxScrollLeft,
bounds, bounds,
hoveredRowId, hoveredRowId,
hiddenColumnsWidth,
} = getContext("sheet") } = getContext("sheet")
export let scrollVertically = true export let scrollVertically = true
@ -20,8 +19,7 @@
export let wheelInteractive = true export let wheelInteractive = true
export let foo = false export let foo = false
$: hiddenWidths = calculateHiddenWidths($renderedColumns) $: style = generateStyle($scroll, $rowHeight, $hiddenColumnsWidth, foo)
$: style = generateStyle($scroll, $rowHeight, hiddenWidths, foo)
const generateStyle = (scroll, rowHeight, hiddenWidths, foo) => { const generateStyle = (scroll, rowHeight, hiddenWidths, foo) => {
let offsetX, offsetY let offsetX, offsetY
@ -35,20 +33,6 @@
return `transform: translate3d(${offsetX}px, ${offsetY}px, 0);` return `transform: translate3d(${offsetX}px, ${offsetY}px, 0);`
} }
// Calculates with total width of all columns currently not rendered
const calculateHiddenWidths = renderedColumns => {
const idx = $visibleColumns.findIndex(
col => col.name === renderedColumns[0]?.name
)
let width = 0
if (idx > 0) {
for (let i = 0; i < idx; i++) {
width += $visibleColumns[i].width
}
}
return width
}
// Handles a wheel even and updates the scroll offsets // Handles a wheel even and updates the scroll offsets
const handleWheel = e => { const handleWheel = e => {
e.preventDefault() e.preventDefault()
@ -84,7 +68,7 @@
<div <div
class="outer" class="outer"
on:wheel={wheelInteractive ? handleWheel : null} on:wheel={wheelInteractive ? handleWheel : null}
on:click|self={() => ($selectedCellId = null)} on:click|self={() => ($focusedCellId = null)}
> >
<div {style}> <div {style}>
<slot /> <slot />

View File

@ -11,13 +11,13 @@
selectedRows, selectedRows,
stickyColumn, stickyColumn,
renderedRows, renderedRows,
selectedCellId, focusedCellId,
hoveredRowId, hoveredRowId,
scroll, scroll,
reorder, reorder,
config, config,
selectedCellMap, selectedCellMap,
selectedCellRow, focusedRow,
gutterWidth, gutterWidth,
dispatch, dispatch,
} = getContext("sheet") } = getContext("sheet")
@ -90,7 +90,7 @@
{#each $renderedRows as row, idx} {#each $renderedRows as row, idx}
{@const rowSelected = !!$selectedRows[row._id]} {@const rowSelected = !!$selectedRows[row._id]}
{@const rowHovered = $hoveredRowId === row._id} {@const rowHovered = $hoveredRowId === row._id}
{@const containsSelectedRow = $selectedCellRow?._id === row._id} {@const rowFocused = $focusedRow?._id === row._id}
<div <div
class="row" class="row"
on:mouseenter={() => ($hoveredRowId = row._id)} on:mouseenter={() => ($hoveredRowId = row._id)}
@ -98,7 +98,8 @@
> >
<SheetCell <SheetCell
width={gutterWidth} width={gutterWidth}
rowSelected={rowSelected || containsSelectedRow} {rowSelected}
{rowFocused}
{rowHovered} {rowHovered}
> >
<div class="gutter"> <div class="gutter">
@ -106,22 +107,19 @@
on:click={() => selectRow(row._id)} on:click={() => selectRow(row._id)}
class="checkbox" class="checkbox"
class:visible={$config.allowSelectRows && class:visible={$config.allowSelectRows &&
(rowSelected || rowHovered || containsSelectedRow)} (rowSelected || rowHovered || rowFocused)}
> >
<Checkbox value={rowSelected} /> <Checkbox value={rowSelected} />
</div> </div>
<div <div
class="number" class="number"
class:visible={!$config.allowSelectRows || class:visible={!$config.allowSelectRows ||
!(rowSelected || rowHovered || containsSelectedRow)} !(rowSelected || rowHovered || rowFocused)}
> >
{row.__idx + 1} {row.__idx + 1}
</div> </div>
{#if $config.allowExpandRows} {#if $config.allowExpandRows}
<div <div class="expand" class:visible={rowFocused || rowHovered}>
class="expand"
class:visible={containsSelectedRow || rowHovered}
>
<Icon <Icon
name="Maximize" name="Maximize"
hoverable hoverable
@ -138,10 +136,11 @@
{#if $stickyColumn} {#if $stickyColumn}
{@const cellId = `${row._id}-${$stickyColumn.name}`} {@const cellId = `${row._id}-${$stickyColumn.name}`}
<DataCell <DataCell
rowSelected={rowSelected || containsSelectedRow} {rowSelected}
{rowHovered} {rowHovered}
{rowFocused}
rowIdx={idx} rowIdx={idx}
selected={$selectedCellId === cellId} focused={$focusedCellId === cellId}
selectedUser={$selectedCellMap[cellId]} selectedUser={$selectedCellMap[cellId]}
width={$stickyColumn.width} width={$stickyColumn.width}
reorderTarget={$reorder.targetColumn === $stickyColumn.name} reorderTarget={$reorder.targetColumn === $stickyColumn.name}
@ -162,12 +161,22 @@
flex-direction: column; flex-direction: column;
position: relative; position: relative;
border-right: var(--cell-border); border-right: var(--cell-border);
z-index: 3; z-index: 2;
} }
/* Add shadow when scrolled */ /*Add shadow when scrolled */
.sticky-column.scrolled:after {
content: "";
width: 10px;
height: 100%;
background: linear-gradient(to right, rgba(0, 0, 0, 0.05), transparent);
left: 100%;
top: 0;
position: absolute;
/*z-index: 1;*/
}
.sticky-column.scrolled { .sticky-column.scrolled {
box-shadow: 0 0 8px -2px rgba(0, 0, 0, 0.2); /*box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.1);*/
} }
/* Don't show borders between cells in the sticky column */ /* Don't show borders between cells in the sticky column */

View File

@ -2,7 +2,7 @@ import { get } from "svelte/store"
import { io } from "socket.io-client" import { io } from "socket.io-client"
export const createWebsocket = context => { export const createWebsocket = context => {
const { rows, tableId, users, userId, selectedCellId } = context const { rows, tableId, users, userId, focusedCellId } = context
// Determine connection info // Determine connection info
const tls = location.protocol === "https:" const tls = location.protocol === "https:"
@ -55,8 +55,8 @@ export const createWebsocket = context => {
tableId.subscribe(connectToDataspace) tableId.subscribe(connectToDataspace)
// Notify selected cell changes // Notify selected cell changes
selectedCellId.subscribe($selectedCellId => { focusedCellId.subscribe($focusedCellId => {
socket.emit("select-cell", $selectedCellId) socket.emit("select-cell", $focusedCellId)
}) })
return () => socket?.disconnect() return () => socket?.disconnect()

View File

@ -5,16 +5,16 @@
const { const {
rows, rows,
selectedCellId, focusedCellId,
visibleColumns, visibleColumns,
selectedCellRow, focusedRow,
stickyColumn, stickyColumn,
selectedCellAPI, selectedCellAPI,
} = getContext("sheet") } = getContext("sheet")
const handleKeyDown = e => { const handleKeyDown = e => {
// If nothing selected avoid processing further key presses // If nothing selected avoid processing further key presses
if (!$selectedCellId) { if (!$focusedCellId) {
if (e.key === "Tab") { if (e.key === "Tab") {
selectFirstCell() selectFirstCell()
} }
@ -72,15 +72,15 @@
if (!firstColumn) { if (!firstColumn) {
return return
} }
selectedCellId.set(`${firstRow._id}-${firstColumn.name}`) focusedCellId.set(`${firstRow._id}-${firstColumn.name}`)
} }
const changeSelectedColumn = delta => { const changeSelectedColumn = delta => {
if (!$selectedCellId) { if (!$focusedCellId) {
return return
} }
const cols = $visibleColumns const cols = $visibleColumns
const split = $selectedCellId.split("-") const split = $focusedCellId.split("-")
const columnName = split[1] const columnName = split[1]
let newColumnName let newColumnName
if (columnName === $stickyColumn?.name) { if (columnName === $stickyColumn?.name) {
@ -95,24 +95,24 @@
} }
} }
if (newColumnName) { if (newColumnName) {
$selectedCellId = `${split[0]}-${newColumnName}` $focusedCellId = `${split[0]}-${newColumnName}`
} }
} }
const changeSelectedRow = delta => { const changeSelectedRow = delta => {
if (!$selectedCellRow) { if (!$focusedRow) {
return return
} }
const newRow = $rows[$selectedCellRow.__idx + delta] const newRow = $rows[$focusedRow.__idx + delta]
if (newRow) { if (newRow) {
const split = $selectedCellId.split("-") const split = $focusedCellId.split("-")
$selectedCellId = `${newRow._id}-${split[1]}` $focusedCellId = `${newRow._id}-${split[1]}`
} }
} }
// Debounce to avoid holding down delete and spamming requests // Debounce to avoid holding down delete and spamming requests
const deleteSelectedCell = debounce(() => { const deleteSelectedCell = debounce(() => {
if (!$selectedCellId) { if (!$focusedCellId) {
return return
} }
$selectedCellAPI.updateValue(null) $selectedCellAPI.updateValue(null)

View File

@ -3,11 +3,11 @@
import { getContext } from "svelte" import { getContext } from "svelte"
const { const {
selectedCellRow, focusedRow,
menu, menu,
rows, rows,
columns, columns,
selectedCellId, focusedCellId,
stickyColumn, stickyColumn,
config, config,
} = getContext("sheet") } = getContext("sheet")
@ -19,20 +19,20 @@
} }
const deleteRow = () => { const deleteRow = () => {
rows.actions.deleteRows([$selectedCellRow]) rows.actions.deleteRows([$focusedRow])
menu.actions.close() menu.actions.close()
notifications.success("Deleted 1 row") notifications.success("Deleted 1 row")
} }
const duplicate = async () => { const duplicate = async () => {
let clone = { ...$selectedCellRow } let clone = { ...$focusedRow }
delete clone._id delete clone._id
delete clone._rev delete clone._rev
delete clone.__idx delete clone.__idx
const newRow = await rows.actions.addRow(clone, $selectedCellRow.__idx + 1) const newRow = await rows.actions.addRow(clone, $focusedRow.__idx + 1)
if (newRow) { if (newRow) {
const column = $stickyColumn?.name || $columns[0].name const column = $stickyColumn?.name || $columns[0].name
$selectedCellId = `${newRow._id}-${column}` $focusedCellId = `${newRow._id}-${column}`
menu.actions.close() menu.actions.close()
} }
} }

View File

@ -25,7 +25,7 @@
{#if !$isReordering} {#if !$isReordering}
{#if $stickyColumn} {#if $stickyColumn}
<div <div
class="resize-slider sticky" class="resize-slider"
class:visible={column === $stickyColumn.name} class:visible={column === $stickyColumn.name}
on:mousedown={e => resize.actions.startResizing($stickyColumn, e)} on:mousedown={e => resize.actions.startResizing($stickyColumn, e)}
on:dblclick={() => resize.actions.resetSize($stickyColumn)} on:dblclick={() => resize.actions.resetSize($stickyColumn)}
@ -62,9 +62,6 @@
cursor: col-resize; cursor: col-resize;
opacity: 1; opacity: 1;
} }
.resize-slider.sticky {
z-index: 3;
}
.resize-indicator { .resize-indicator {
margin-left: -1px; margin-left: -1px;
width: 2px; width: 2px;

View File

@ -9,8 +9,8 @@ export const createMaxScrollStores = context => {
bounds, bounds,
rowHeight, rowHeight,
scroll, scroll,
selectedCellRow, focusedRow,
selectedCellId, focusedCellId,
gutterWidth, gutterWidth,
} = context } = context
const padding = 264 const padding = 264
@ -89,18 +89,18 @@ export const createMaxScrollStores = context => {
}) })
// Ensure the selected cell is visible // Ensure the selected cell is visible
selectedCellId.subscribe(async $selectedCellId => { focusedCellId.subscribe(async $focusedCellId => {
await tick() await tick()
const $selectedCellRow = get(selectedCellRow) const $focusedRow = get(focusedRow)
const $scroll = get(scroll) const $scroll = get(scroll)
const $bounds = get(bounds) const $bounds = get(bounds)
const $rowHeight = get(rowHeight) const $rowHeight = get(rowHeight)
const verticalOffset = $rowHeight * 1.5 const verticalOffset = $rowHeight * 1.5
// Ensure vertical position is viewable // Ensure vertical position is viewable
if ($selectedCellRow) { if ($focusedRow) {
// Ensure row is not below bottom of screen // Ensure row is not below bottom of screen
const rowYPos = $selectedCellRow.__idx * $rowHeight const rowYPos = $focusedRow.__idx * $rowHeight
const bottomCutoff = const bottomCutoff =
$scroll.top + $bounds.height - $rowHeight - verticalOffset $scroll.top + $bounds.height - $rowHeight - verticalOffset
let delta = rowYPos - bottomCutoff let delta = rowYPos - bottomCutoff
@ -126,7 +126,7 @@ export const createMaxScrollStores = context => {
// Ensure horizontal position is viewable // Ensure horizontal position is viewable
// Check horizontal position of columns next // Check horizontal position of columns next
const $visibleColumns = get(visibleColumns) const $visibleColumns = get(visibleColumns)
const columnName = $selectedCellId?.split("-")[1] const columnName = $focusedCellId?.split("-")[1]
const column = $visibleColumns.find(col => col.name === columnName) const column = $visibleColumns.find(col => col.name === columnName)
const horizontalOffset = 24 const horizontalOffset = 24
if (!column) { if (!column) {

View File

@ -1,7 +1,7 @@
import { writable, get } from "svelte/store" import { writable, get } from "svelte/store"
export const createMenuStores = context => { export const createMenuStores = context => {
const { bounds, selectedCellId, stickyColumn, rowHeight } = context const { bounds, focusedCellId, stickyColumn, rowHeight } = context
const menu = writable({ const menu = writable({
x: 0, x: 0,
y: 0, y: 0,
@ -14,7 +14,7 @@ export const createMenuStores = context => {
const $stickyColumn = get(stickyColumn) const $stickyColumn = get(stickyColumn)
const $rowHeight = get(rowHeight) const $rowHeight = get(rowHeight)
e.preventDefault() e.preventDefault()
selectedCellId.set(cellId) focusedCellId.set(cellId)
menu.set({ menu.set({
left: e.clientX - $bounds.left + 44 + ($stickyColumn?.width || 0), left: e.clientX - $bounds.left + 44 + ($stickyColumn?.width || 0),
top: e.clientY - $bounds.top + $rowHeight + 4, top: e.clientY - $bounds.top + $rowHeight + 4,

View File

@ -3,7 +3,7 @@ import { fetchData } from "../../../fetch/fetchData"
import { notifications } from "@budibase/bbui" import { notifications } from "@budibase/bbui"
export const createRowsStore = context => { export const createRowsStore = context => {
const { tableId, API, scroll } = context const { tableId, API, scroll, validation } = context
const rows = writable([]) const rows = writable([])
const table = writable(null) const table = writable(null)
const filter = writable([]) const filter = writable([])
@ -120,6 +120,21 @@ export const createRowsStore = context => {
return index >= 0 ? get(enrichedRows)[index] : null return index >= 0 ? get(enrichedRows)[index] : null
} }
// Handles validation errors from the rows API and updates local validation
// state, storing error messages against relevant cells
const handleValidationError = (rowId, error) => {
if (error?.json?.validationErrors) {
for (let column of Object.keys(error.json.validationErrors)) {
validation.actions.setError(
`${rowId}-${column}`,
`${column} ${error.json.validationErrors[column]}`
)
}
} else {
notifications.error(`Error saving row: ${error?.message}`)
}
}
// Adds a new row // Adds a new row
const addRow = async (row, idx) => { const addRow = async (row, idx) => {
try { try {
@ -139,7 +154,7 @@ export const createRowsStore = context => {
notifications.success("Row created successfully") notifications.success("Row created successfully")
return newRow return newRow
} catch (error) { } catch (error) {
notifications.error(`Error adding row: ${error?.message}`) handleValidationError("new", error)
} }
} }
@ -208,7 +223,7 @@ export const createRowsStore = context => {
try { try {
await API.saveRow(newRow) await API.saveRow(newRow)
} catch (error) { } catch (error) {
notifications.error(`Error saving row: ${error?.message}`) handleValidationError(newRow._id, error)
// Revert change // Revert change
rows.update(state => { rows.update(state => {

View File

@ -0,0 +1,66 @@
import { derived, get, writable } from "svelte/store"
export const createSheetAPIStores = context => {
const { focusedCellId } = context
const cellAPIs = writable({})
const registerCellAPI = (cellId, api) => {
// Ignore registration if cell is not selected
const [rowId, column] = cellId.split("-")
if (rowId !== "new" && !get(focusedCellId)?.startsWith(rowId)) {
return
}
// Store API
cellAPIs.update(state => ({
...state,
[column]: api,
}))
}
const getCellAPI = column => {
return get(cellAPIs)[column]
}
// Derive the selected cell API
const selectedCellAPI = derived(
[cellAPIs, focusedCellId],
([$apis, $focusedCellId]) => {
if (!$focusedCellId) {
return null
}
const [, column] = $focusedCellId.split("-")
return $apis[column]
},
null
)
const focusedRowAPI = derived(cellAPIs, $apis => {
return {
validate: () => {
let errors = null
for (let [column, api] of Object.entries($apis || {})) {
const error = api.validate()
if (error) {
errors = {
...errors,
[column]: error,
}
}
}
return errors
},
}
})
return {
selectedCellAPI,
focusedRowAPI,
sheetAPI: {
actions: {
registerCellAPI,
getCellAPI,
},
},
}
}

View File

@ -2,17 +2,16 @@ import { writable, get, derived } from "svelte/store"
export const createUIStores = context => { export const createUIStores = context => {
const { rows, rowLookupMap } = context const { rows, rowLookupMap } = context
const selectedCellId = writable(null) const focusedCellId = writable(null)
const selectedRows = writable({}) const selectedRows = writable({})
const hoveredRowId = writable(null) const hoveredRowId = writable(null)
const selectedCellAPI = writable(null)
const rowHeight = writable(36) const rowHeight = writable(36)
// Derive the row that contains the selected cell // Derive the row that contains the selected cell
const selectedCellRow = derived( const focusedRow = derived(
[selectedCellId, rowLookupMap, rows], [focusedCellId, rowLookupMap, rows],
([$selectedCellId, $rowLookupMap, $rows]) => { ([$focusedCellId, $rowLookupMap, $rows]) => {
const rowId = $selectedCellId?.split("-")[0] const rowId = $focusedCellId?.split("-")[0]
const index = $rowLookupMap[rowId] const index = $rowLookupMap[rowId]
return $rows[index] return $rows[index]
}, },
@ -21,15 +20,15 @@ export const createUIStores = context => {
// Ensure we clear invalid rows from state if they disappear // Ensure we clear invalid rows from state if they disappear
rows.subscribe(() => { rows.subscribe(() => {
const $selectedCellId = get(selectedCellId) const $focusedCellId = get(focusedCellId)
const $selectedRows = get(selectedRows) const $selectedRows = get(selectedRows)
const $hoveredRowId = get(hoveredRowId) const $hoveredRowId = get(hoveredRowId)
const hasRow = rows.actions.hasRow const hasRow = rows.actions.hasRow
// Check selected cell // Check selected cell
const selectedRowId = $selectedCellId?.split("-")[0] const selectedRowId = $focusedCellId?.split("-")[0]
if (selectedRowId && !hasRow(selectedRowId)) { if (selectedRowId && !hasRow(selectedRowId)) {
selectedCellId.set(null) focusedCellId.set(null)
} }
// Check hovered row // Check hovered row
@ -53,7 +52,7 @@ export const createUIStores = context => {
}) })
// Reset selected rows when selected cell changes // Reset selected rows when selected cell changes
selectedCellId.subscribe(id => { focusedCellId.subscribe(id => {
if (id) { if (id) {
selectedRows.set({}) selectedRows.set({})
} }
@ -62,37 +61,29 @@ export const createUIStores = context => {
// Unset selected cell when rows are selected // Unset selected cell when rows are selected
selectedRows.subscribe(rows => { selectedRows.subscribe(rows => {
if (Object.keys(rows || {}).length) { if (Object.keys(rows || {}).length) {
selectedCellId.set(null) focusedCellId.set(null)
} }
}) })
// Callback when leaving the sheet, deselecting all focussed or selected items // Callback when leaving the sheet, deselecting all focussed or selected items
const blur = () => { const blur = () => {
selectedCellId.set(null) focusedCellId.set(null)
selectedRows.set({}) selectedRows.set({})
hoveredRowId.set(null) hoveredRowId.set(null)
} }
// Remove selected cell API when no selected cell is present
selectedCellId.subscribe(cell => {
if (!cell && get(selectedCellAPI)) {
selectedCellAPI.set(null)
}
})
// Remove hovered row when a cell is selected // Remove hovered row when a cell is selected
selectedCellId.subscribe(cell => { focusedCellId.subscribe(cell => {
if (cell && get(hoveredRowId)) { if (cell && get(hoveredRowId)) {
hoveredRowId.set(null) hoveredRowId.set(null)
} }
}) })
return { return {
selectedCellId, focusedCellId,
selectedRows, selectedRows,
hoveredRowId, hoveredRowId,
selectedCellRow, focusedRow,
selectedCellAPI,
rowHeight, rowHeight,
ui: { ui: {
actions: { actions: {

View File

@ -62,8 +62,8 @@ export const createUserStores = () => {
([$enrichedUsers, $userId]) => { ([$enrichedUsers, $userId]) => {
let map = {} let map = {}
$enrichedUsers.forEach(user => { $enrichedUsers.forEach(user => {
if (user.selectedCellId && user.id !== $userId) { if (user.focusedCellId && user.id !== $userId) {
map[user.selectedCellId] = user map[user.focusedCellId] = user
} }
}) })
return map return map

View File

@ -0,0 +1,25 @@
import { writable, get } from "svelte/store"
export const createValidationStores = () => {
const validation = writable({})
return {
validation: {
subscribe: validation.subscribe,
actions: {
setError: (cellId, error) => {
if (!cellId) {
return
}
validation.update(state => ({
...state,
[cellId]: error,
}))
},
getError: cellId => {
return get(validation)[cellId]
},
},
},
}
}

View File

@ -77,10 +77,28 @@ export const createViewportStores = context => {
[] []
) )
const hiddenColumnsWidth = derived(
[renderedColumns, visibleColumns],
([$renderedColumns, $visibleColumns]) => {
const idx = $visibleColumns.findIndex(
col => col.name === $renderedColumns[0]?.name
)
let width = 0
if (idx > 0) {
for (let i = 0; i < idx; i++) {
width += $visibleColumns[i].width
}
}
return width
},
0
)
return { return {
scrolledRowCount, scrolledRowCount,
visualRowCapacity, visualRowCapacity,
renderedRows, renderedRows,
renderedColumns, renderedColumns,
hiddenColumnsWidth,
} }
} }