Refactor stores to fix dependency issues, use modals for adding rows, simplify sheet

This commit is contained in:
Andrew Kingston 2023-04-11 12:17:08 +01:00
parent 81a28eb4da
commit fe70793e75
26 changed files with 533 additions and 490 deletions

View File

@ -4,7 +4,7 @@
import { getCellRenderer } from "../lib/renderers"
import { derived, writable } from "svelte/store"
const { rows, focusedCellId, menu, sheetAPI, config, validation } =
const { rows, focusedCellId, focusedCellAPI, menu, config, validation } =
getContext("sheet")
export let rowSelected
@ -25,13 +25,6 @@
let api
$: {
// 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)
@ -40,8 +33,8 @@
// Register this cell API if the row is focused
$: {
if (rowFocused) {
sheetAPI.actions.registerCellAPI(cellId, cellAPI)
if (focused) {
focusedCellAPI.set(cellAPI)
}
}
@ -57,30 +50,9 @@
blur: () => api?.blur(),
onKeyDown: (...params) => api?.onKeyDown(...params),
isReadonly: () => readonly,
isRequired: () => !!column.schema.constraints?.presence,
validate: value => {
// Validate the current value if no new value is provided
if (value === undefined) {
value = row[column.name]
}
let newError = null
if (cellAPI.isReadonly() && !(value == null || value === "")) {
// Ensure cell isn't readonly
newError = "Auto columns can't be edited"
} else if (cellAPI.isRequired() && (value == null || value === "")) {
// Sanity check required fields
newError = "Required field"
} else {
newError = null
}
validation.actions.setError(cellId, newError)
return newError
},
updateValue: value => {
cellAPI.validate(value)
if (!$error) {
updateRow(row._id, column.name, value)
}
validation.actions.setError(cellId, null)
updateRow(row._id, column.name, value)
},
}
</script>

View File

@ -0,0 +1,16 @@
<script>
import { ActionButton } from "@budibase/bbui"
import { getContext } from "svelte"
const { config, dispatch } = getContext("sheet")
</script>
<ActionButton
icon="TableColumnAddRight"
quiet
size="M"
on:click={() => dispatch("add-column")}
disabled={!$config.allowAddColumns}
>
Create column
</ActionButton>

View File

@ -2,15 +2,15 @@
import { ActionButton } from "@budibase/bbui"
import { getContext } from "svelte"
const { dispatch, columns, stickyColumn } = getContext("sheet")
const { dispatch, columns, stickyColumn, config } = getContext("sheet")
</script>
<ActionButton
icon="Add"
icon="TableRowAddBottom"
quiet
size="M"
on:click={() => dispatch("add-row-inline")}
disabled={!$columns.length && !$stickyColumn}
on:click={() => dispatch("add-row")}
disabled={!$config.allowAddRows || (!$columns.length && !$stickyColumn)}
>
Create row
</ActionButton>

View File

@ -2,14 +2,8 @@
import { getContext } from "svelte"
import SheetScrollWrapper from "./SheetScrollWrapper.svelte"
import HeaderCell from "../cells/HeaderCell.svelte"
import { Icon } from "@budibase/bbui"
const { renderedColumns, dispatch, config, ui } = getContext("sheet")
const addColumn = () => {
ui.actions.blur()
dispatch("add-column")
}
const { renderedColumns } = getContext("sheet")
</script>
<div class="header">
@ -20,11 +14,6 @@
{/each}
</div>
</SheetScrollWrapper>
{#if $config.allowAddColumns}
<div class="new-column" on:click={addColumn}>
<Icon size="S" name="Add" />
</div>
{/if}
</div>
<style>
@ -38,20 +27,4 @@
.row {
display: flex;
}
.new-column {
position: absolute;
right: 0;
top: 0;
height: var(--row-height);
background: var(--spectrum-global-color-gray-100);
display: grid;
place-items: center;
width: 46px;
border-left: var(--cell-border);
border-bottom: var(--cell-border);
}
.new-column:hover {
cursor: pointer;
background: var(--spectrum-global-color-gray-200);
}
</style>

View File

@ -1,7 +1,7 @@
<script>
import SheetCell from "../cells/SheetCell.svelte"
import { getContext, onMount, tick } from "svelte"
import { Icon, Button } from "@budibase/bbui"
import { getContext, onMount } from "svelte"
import { Icon, Button, clickOutside } from "@budibase/bbui"
import SheetScrollWrapper from "./SheetScrollWrapper.svelte"
import DataCell from "../cells/DataCell.svelte"
import { fly } from "svelte/transition"
@ -19,7 +19,7 @@
showHScrollbar,
tableId,
subscribe,
sheetAPI,
scrollLeft,
} = getContext("sheet")
let isAdding = false
@ -30,22 +30,24 @@
$: rowHovered = $hoveredRowId === "new"
$: rowFocused = $focusedCellId?.startsWith("new-")
$: width = gutterWidth + ($stickyColumn?.width || 0)
$: scrollLeft = $scroll.left
$: $tableId, (isAdding = false)
const addRow = async () => {
// Create row
const savedRow = await rows.actions.addRow(newRow, 0)
if (savedRow && firstColumn) {
$focusedCellId = `${savedRow._id}-${firstColumn.name}`
isAdding = false
}
if (savedRow) {
// Select the first cell if possible
if (firstColumn) {
$focusedCellId = `${savedRow._id}-${firstColumn.name}`
}
// Reset scroll
scroll.set({
left: 0,
top: 0,
})
// Reset state
isAdding = false
scroll.set({
left: 0,
top: 0,
})
}
}
const cancel = () => {
@ -151,17 +153,28 @@
position: absolute;
top: var(--row-height);
left: 0;
background: linear-gradient(
to bottom,
var(--cell-background) 20%,
transparent 100%
);
/*background: linear-gradient(*/
/* to bottom,*/
/* var(--cell-background) 20%,*/
/* transparent 100%*/
/*);*/
width: 100%;
padding-bottom: 100px;
padding-bottom: 800px;
display: flex;
flex-direction: column;
align-items: stretch;
}
.container:before {
position: absolute;
content: "";
left: 0;
top: 0;
height: 100%;
width: 100%;
background: var(--cell-background);
opacity: 0.8;
z-index: -1;
}
.content {
pointer-events: all;
background: var(--background);
@ -227,7 +240,7 @@
display: flex;
flex-direction: row;
gap: 8px;
margin: 24px 0 0 16px;
margin: 24px 0 0 var(--gutter-width);
pointer-events: all;
align-self: flex-start;
}

View File

@ -3,20 +3,7 @@
import { writable } from "svelte/store"
import { createEventManagers } from "../lib/events"
import { createAPIClient } from "../../../api"
import { createReorderStores } from "../stores/reorder"
import { createViewportStores } from "../stores/viewport"
import { createRowsStore } from "../stores/rows"
import { createColumnsStores } from "../stores/columns"
import { createScrollStores } from "../stores/scroll"
import { createBoundsStores } from "../stores/bounds"
import { createUIStores } from "../stores/ui"
import { createUserStores } from "../stores/users"
import { createResizeStores } from "../stores/resize"
import { createMenuStores } from "../stores/menu"
import { createMaxScrollStores } from "../stores/max-scroll"
import { createPaginationStores } from "../stores/pagination"
import { createSheetAPIStores } from "../stores/sheet-api"
import { createValidationStores } from "../stores/validation"
import { createStores } from "../stores"
import DeleteButton from "../controls/DeleteButton.svelte"
import SheetBody from "./SheetBody.svelte"
import ResizeOverlay from "../overlays/ResizeOverlay.svelte"
@ -28,8 +15,11 @@
import KeyboardManager from "../overlays/KeyboardManager.svelte"
import { clickOutside } from "@budibase/bbui"
import SheetControls from "./SheetControls.svelte"
import NewRowTop from "./NewRowTop.svelte"
import { MaxCellRenderHeight } from "../lib/constants"
import SortButton from "../controls/SortButton.svelte"
import AddColumnButton from "../controls/AddColumnButton.svelte"
import HideColumnsButton from "../controls/HideColumnsButton.svelte"
import AddRowButton from "../controls/AddRowButton.svelte"
export let API
export let tableId
@ -56,7 +46,6 @@
})
// Build up spreadsheet context
// Stores are listed in order of dependency on each other
let context = {
API: API || createAPIClient(),
rand,
@ -65,20 +54,7 @@
tableId: tableIdStore,
}
context = { ...context, ...createEventManagers() }
context = { ...context, ...createValidationStores(context) }
context = { ...context, ...createBoundsStores(context) }
context = { ...context, ...createScrollStores(context) }
context = { ...context, ...createRowsStore(context) }
context = { ...context, ...createColumnsStores(context) }
context = { ...context, ...createUIStores(context) }
context = { ...context, ...createSheetAPIStores(context) }
context = { ...context, ...createResizeStores(context) }
context = { ...context, ...createViewportStores(context) }
context = { ...context, ...createMaxScrollStores(context) }
context = { ...context, ...createReorderStores(context) }
context = { ...context, ...createUserStores(context) }
context = { ...context, ...createMenuStores(context) }
context = { ...context, ...createPaginationStores(context) }
context = { ...context, ...createStores(context) }
// Reference some stores for local use
const { isResizing, isReordering, ui, loaded, rowHeight } = context
@ -113,8 +89,12 @@
>
<div class="controls">
<div class="controls-left">
<AddRowButton />
<AddColumnButton />
<SheetControls />
<slot name="controls" />
<HideColumnsButton />
<SortButton />
</div>
<div class="controls-right">
<DeleteButton />
@ -130,9 +110,6 @@
<SheetBody />
</div>
<div class="overlays">
{#if $config.allowAddRows}
<NewRowTop />
{/if}
<ResizeOverlay />
<ScrollOverlay />
<MenuOverlay />

View File

@ -2,10 +2,5 @@
import SortButton from "../controls/SortButton.svelte"
import HideColumnsButton from "../controls/HideColumnsButton.svelte"
import AddRowButton from "../controls/AddRowButton.svelte"
import RowHeightButton from "../controls/RowHeightButton.svelte"
import AddColumnButton from "../controls/AddColumnButton.svelte"
</script>
<AddRowButton />
<HideColumnsButton />
<SortButton />
<RowHeightButton />

View File

@ -20,9 +20,9 @@
focusedRow,
gutterWidth,
dispatch,
scrollLeft,
} = getContext("sheet")
$: scrollLeft = $scroll.left
$: rowCount = $rows.length
$: selectedRowCount = Object.values($selectedRows).filter(x => !!x).length
$: width = gutterWidth + ($stickyColumn?.width || 0)
@ -91,6 +91,7 @@
{@const rowSelected = !!$selectedRows[row._id]}
{@const rowHovered = $hoveredRowId === row._id}
{@const rowFocused = $focusedRow?._id === row._id}
{@const cellId = `${row._id}-${$stickyColumn.name}`}
<div
class="row"
on:mouseenter={() => ($hoveredRowId = row._id)}
@ -132,9 +133,7 @@
{/if}
</div>
</SheetCell>
{#if $stickyColumn}
{@const cellId = `${row._id}-${$stickyColumn.name}`}
<DataCell
{rowSelected}
{rowHovered}
@ -176,7 +175,7 @@
/*z-index: 1;*/
}
.sticky-column.scrolled {
/*box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.1);*/
box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.1);
}
/* Don't show borders between cells in the sticky column */

View File

@ -1,7 +1,6 @@
<script>
import { getContext, onMount } from "svelte"
import { debounce } from "../../../utils/utils"
import { notifications } from "@budibase/bbui"
const {
rows,
@ -9,7 +8,7 @@
visibleColumns,
focusedRow,
stickyColumn,
selectedCellAPI,
focusedCellAPI,
} = getContext("sheet")
const handleKeyDown = e => {
@ -22,7 +21,7 @@
}
// Always intercept certain key presses
const api = $selectedCellAPI
const api = $focusedCellAPI
if (e.key === "Escape") {
api?.blur?.()
} else if (e.key === "Tab") {
@ -112,17 +111,17 @@
// Debounce to avoid holding down delete and spamming requests
const deleteSelectedCell = debounce(() => {
if (!$focusedCellId) {
if ($focusedCellAPI?.isReadonly()) {
return
}
$selectedCellAPI.updateValue(null)
$focusedCellAPI.updateValue(null)
}, 100)
const focusSelectedCell = () => {
if ($selectedCellAPI?.isReadonly()) {
if ($focusedCellAPI?.isReadonly()) {
return
}
$selectedCellAPI?.focus?.()
$focusedCellAPI?.focus?.()
}
onMount(() => {

View File

@ -9,9 +9,9 @@
stickyColumn,
isReordering,
gutterWidth,
scrollLeft,
} = getContext("sheet")
$: scrollLeft = $scroll.left
$: cutoff = scrollLeft + gutterWidth + ($columns[0]?.width || 0)
$: offset = gutterWidth + ($stickyColumn?.width || 0)
$: column = $resize.column

View File

@ -4,7 +4,6 @@
const {
scroll,
bounds,
rowHeight,
contentHeight,
maxScrollTop,
@ -13,6 +12,9 @@
screenWidth,
showHScrollbar,
showVScrollbar,
scrollLeft,
scrollTop,
height,
} = getContext("sheet")
// Bar config
@ -22,32 +24,27 @@
let initialMouse
let initialScroll
// Memoize store primitives to reduce reactive statement invalidations
$: scrollTop = $scroll.top
$: scrollLeft = $scroll.left
$: height = $bounds.height
$: width = $bounds.width
// Calculate V scrollbar size and offset
// Terminology is the same for both axes:
// renderX - the space available to render the bar in, edge to edge
// availX - the space available to render the bar in, until the edge
$: renderHeight = height - 2 * barOffset
$: barHeight = Math.max(50, (height / $contentHeight) * renderHeight)
$: renderHeight = $height - 2 * barOffset
$: barHeight = Math.max(50, ($height / $contentHeight) * renderHeight)
$: availHeight = renderHeight - barHeight
$: barTop = barOffset + $rowHeight + availHeight * (scrollTop / $maxScrollTop)
$: barTop =
barOffset + $rowHeight + availHeight * ($scrollTop / $maxScrollTop)
// Calculate H scrollbar size and offset
$: renderWidth = $screenWidth - 2 * barOffset
$: barWidth = Math.max(50, ($screenWidth / $contentWidth) * renderWidth)
$: availWidth = renderWidth - barWidth
$: barLeft = barOffset + availWidth * (scrollLeft / $maxScrollLeft)
$: barLeft = barOffset + availWidth * ($scrollLeft / $maxScrollLeft)
// V scrollbar drag handlers
const startVDragging = e => {
e.preventDefault()
initialMouse = e.clientY
initialScroll = scrollTop
initialScroll = $scrollTop
document.addEventListener("mousemove", moveVDragging)
document.addEventListener("mouseup", stopVDragging)
}
@ -69,7 +66,7 @@
const startHDragging = e => {
e.preventDefault()
initialMouse = e.clientX
initialScroll = scrollLeft
initialScroll = $scrollLeft
document.addEventListener("mousemove", moveHDragging)
document.addEventListener("mouseup", stopHDragging)
}

View File

@ -1,11 +1,16 @@
import { writable } from "svelte/store"
import { derived, writable } from "svelte/store"
export const createBoundsStores = () => {
export const createStores = () => {
const bounds = writable({
left: 0,
top: 0,
width: 0,
height: 0,
})
return { bounds }
// Derive height and width as primitives to avoid wasted computation
const width = derived(bounds, $bounds => $bounds.width, 0)
const height = derived(bounds, $bounds => $bounds.height, 0)
return { bounds, height, width }
}

View File

@ -2,8 +2,7 @@ import { derived, get, writable } from "svelte/store"
export const DefaultColumnWidth = 200
export const createColumnsStores = context => {
const { table, gutterWidth } = context
export const createStores = () => {
const columns = writable([])
const stickyColumn = writable(null)
@ -36,6 +35,19 @@ export const createColumnsStores = context => {
[]
)
return {
columns: {
...columns,
subscribe: enrichedColumns.subscribe,
},
stickyColumn,
visibleColumns,
}
}
export const deriveStores = context => {
const { table, gutterWidth, columns, stickyColumn } = context
// Merge new schema fields with existing schema in order to preserve widths
table.subscribe($table => {
const schema = $table?.schema
@ -109,12 +121,5 @@ export const createColumnsStores = context => {
})
})
return {
columns: {
...columns,
subscribe: enrichedColumns.subscribe,
},
stickyColumn,
visibleColumns,
}
return null
}

View File

@ -0,0 +1,43 @@
import * as Bounds from "./bounds"
import * as Columns from "./columns"
import * as Menu from "./menu"
import * as Pagination from "./pagination"
import * as Reorder from "./reorder"
import * as Resize from "./resize"
import * as Rows from "./rows"
import * as Scroll from "./scroll"
import * as UI from "./ui"
import * as Users from "./users"
import * as Validation from "./validation"
import * as Viewport from "./viewport"
const DependencyOrderedStores = [
Bounds,
Scroll,
Rows,
Columns,
UI,
Validation,
Resize,
Viewport,
Reorder,
Users,
Menu,
Pagination,
]
export const createStores = context => {
let stores = {}
// Atomic store creation
for (let store of DependencyOrderedStores) {
stores = { ...stores, ...store.createStores?.({ ...context, ...stores }) }
}
// Derived store creation
for (let store of DependencyOrderedStores) {
stores = { ...stores, ...store.deriveStores?.({ ...context, ...stores }) }
}
return stores
}

View File

@ -1,182 +0,0 @@
import { derived, get } from "svelte/store"
import { tick } from "svelte"
export const createMaxScrollStores = context => {
const {
rows,
visibleColumns,
stickyColumn,
bounds,
rowHeight,
scroll,
focusedRow,
focusedCellId,
gutterWidth,
} = context
const padding = 264
// Memoize store primitives
const scrollTop = derived(scroll, $scroll => $scroll.top, 0)
const scrollLeft = derived(scroll, $scroll => $scroll.left, 0)
const stickyColumnWidth = derived(stickyColumn, $col => $col?.width || 0, 0)
// Derive vertical limits
const height = derived(bounds, $bounds => $bounds.height, 0)
const width = derived(bounds, $bounds => $bounds.width, 0)
const contentHeight = derived(
[rows, rowHeight],
([$rows, $rowHeight]) => $rows.length * $rowHeight + padding,
0
)
const maxScrollTop = derived(
[height, contentHeight],
([$height, $contentHeight]) => Math.max($contentHeight - $height, 0),
0
)
// Derive horizontal limits
const contentWidth = derived(
[visibleColumns, stickyColumnWidth],
([$visibleColumns, $stickyColumnWidth]) => {
let width = gutterWidth + padding + $stickyColumnWidth
$visibleColumns.forEach(col => {
width += col.width
})
return width
},
0
)
const screenWidth = derived(
[width, stickyColumnWidth],
([$width, $stickyColumnWidth]) => $width + gutterWidth + $stickyColumnWidth,
0
)
const maxScrollLeft = derived(
[contentWidth, screenWidth],
([$contentWidth, $screenWidth]) => {
return Math.max($contentWidth - $screenWidth, 0)
},
0
)
// Ensure scroll state never goes invalid, which can happen when changing
// rows or tables
const overscrollTop = derived(
[scrollTop, maxScrollTop],
([$scrollTop, $maxScrollTop]) => $scrollTop > $maxScrollTop,
false
)
const overscrollLeft = derived(
[scrollLeft, maxScrollLeft],
([$scrollLeft, $maxScrollLeft]) => $scrollLeft > $maxScrollLeft,
false
)
overscrollTop.subscribe(overscroll => {
if (overscroll) {
scroll.update(state => ({
...state,
top: get(maxScrollTop),
}))
}
})
overscrollLeft.subscribe(overscroll => {
if (overscroll) {
scroll.update(state => ({
...state,
left: get(maxScrollLeft),
}))
}
})
// Ensure the selected cell is visible
focusedCellId.subscribe(async $focusedCellId => {
await tick()
const $focusedRow = get(focusedRow)
const $scroll = get(scroll)
const $bounds = get(bounds)
const $rowHeight = get(rowHeight)
const verticalOffset = $rowHeight * 1.5
// Ensure vertical position is viewable
if ($focusedRow) {
// Ensure row is not below bottom of screen
const rowYPos = $focusedRow.__idx * $rowHeight
const bottomCutoff =
$scroll.top + $bounds.height - $rowHeight - verticalOffset
let delta = rowYPos - bottomCutoff
if (delta > 0) {
scroll.update(state => ({
...state,
top: state.top + delta,
}))
}
// Ensure row is not above top of screen
else {
const delta = $scroll.top - rowYPos + verticalOffset
if (delta > 0) {
scroll.update(state => ({
...state,
top: Math.max(0, state.top - delta),
}))
}
}
}
// Ensure horizontal position is viewable
// Check horizontal position of columns next
const $visibleColumns = get(visibleColumns)
const columnName = $focusedCellId?.split("-")[1]
const column = $visibleColumns.find(col => col.name === columnName)
const horizontalOffset = 24
if (!column) {
return
}
// Ensure column is not cutoff on left edge
let delta = $scroll.left - column.left + horizontalOffset
if (delta > 0) {
scroll.update(state => ({
...state,
left: Math.max(0, state.left - delta),
}))
}
// Ensure column is not cutoff on right edge
else {
const rightEdge = column.left + column.width
const rightBound = $bounds.width + $scroll.left - horizontalOffset
delta = rightEdge - rightBound
if (delta > 0) {
scroll.update(state => ({
...state,
left: state.left + delta,
}))
}
}
})
// Derive whether to show scrollbars or not
const showVScrollbar = derived(
[contentHeight, height],
([$contentHeight, $height]) => {
return $contentHeight > $height
}
)
const showHScrollbar = derived(
[contentWidth, screenWidth],
([$contentWidth, $screenWidth]) => {
return $contentWidth > $screenWidth
}
)
return {
contentHeight,
contentWidth,
screenWidth,
maxScrollTop,
maxScrollLeft,
showHScrollbar,
showVScrollbar,
}
}

View File

@ -1,13 +1,19 @@
import { writable, get } from "svelte/store"
export const createMenuStores = context => {
const { bounds, focusedCellId, stickyColumn, rowHeight } = context
export const createStores = () => {
const menu = writable({
x: 0,
y: 0,
visible: false,
selectedRow: null,
})
return {
menu,
}
}
export const deriveStores = context => {
const { menu, bounds, focusedCellId, stickyColumn, rowHeight } = context
const open = (cellId, e) => {
const $bounds = get(bounds)

View File

@ -1,6 +1,6 @@
import { derived } from "svelte/store"
export const createPaginationStores = context => {
export const deriveStores = context => {
const { scrolledRowCount, rows, visualRowCapacity } = context
// Derive how many rows we have in total

View File

@ -1,21 +1,29 @@
import { get, writable, derived } from "svelte/store"
export const createReorderStores = context => {
const { columns, scroll, bounds, stickyColumn, ui } = context
const reorderInitialState = {
sourceColumn: null,
targetColumn: null,
breakpoints: [],
initialMouseX: null,
scrollLeft: 0,
sheetLeft: 0,
}
const reorderInitialState = {
sourceColumn: null,
targetColumn: null,
breakpoints: [],
initialMouseX: null,
scrollLeft: 0,
sheetLeft: 0,
}
export const createStores = () => {
const reorder = writable(reorderInitialState)
const isReordering = derived(
reorder,
$reorder => !!$reorder.sourceColumn,
false
)
return {
reorder,
isReordering,
}
}
export const deriveStores = context => {
const { reorder, columns, scroll, bounds, stickyColumn, ui } = context
// Callback when dragging on a colum header and starting reordering
const startReordering = (column, e) => {
@ -142,6 +150,5 @@ export const createReorderStores = context => {
moveColumnRight,
},
},
isReordering,
}
}

View File

@ -3,18 +3,26 @@ import { DefaultColumnWidth } from "./columns"
export const MinColumnWidth = 100
export const createResizeStores = context => {
const { columns, stickyColumn, ui } = context
const initialState = {
initialMouseX: null,
initialWidth: null,
column: null,
columnIdx: null,
width: 0,
left: 0,
}
const initialState = {
initialMouseX: null,
initialWidth: null,
column: null,
columnIdx: null,
width: 0,
left: 0,
}
export const createStores = () => {
const resize = writable(initialState)
const isResizing = derived(resize, $resize => $resize.column != null, false)
return {
resize,
isResizing,
}
}
export const deriveStores = context => {
const { resize, columns, stickyColumn, ui } = context
// Starts resizing a certain column
const startResizing = (column, e) => {
@ -105,6 +113,5 @@ export const createResizeStores = context => {
resetSize,
},
},
isResizing,
}
}

View File

@ -2,18 +2,16 @@ import { writable, derived, get } from "svelte/store"
import { fetchData } from "../../../fetch/fetchData"
import { notifications } from "@budibase/bbui"
export const createRowsStore = context => {
const { tableId, API, scroll, validation } = context
const initialSortState = {
column: null,
order: "ascending",
}
export const createStores = () => {
const rows = writable([])
const table = writable(null)
const filter = writable([])
const loaded = writable(false)
const instanceLoaded = writable(false)
const fetch = writable(null)
const initialSortState = {
column: null,
order: "ascending",
}
const sort = writable(initialSortState)
// Enrich rows with an index property
@ -41,6 +39,37 @@ export const createRowsStore = context => {
{}
)
return {
rows: {
...rows,
subscribe: enrichedRows.subscribe,
},
rowLookupMap,
table,
filter,
loaded,
sort,
}
}
export const deriveStores = context => {
const {
rows,
rowLookupMap,
table,
filter,
loaded,
sort,
tableId,
API,
scroll,
validation,
focusedCellId,
columns,
} = context
const instanceLoaded = writable(false)
const fetch = writable(null)
// Local cache of row IDs to speed up checking if a row exists
let rowCacheMap = {}
@ -117,19 +146,32 @@ export const createRowsStore = context => {
// Gets a row by ID
const getRow = id => {
const index = get(rowLookupMap)[id]
return index >= 0 ? get(enrichedRows)[index] : null
return index >= 0 ? get(rows)[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)) {
const keys = Object.keys(error.json.validationErrors)
const $columns = get(columns)
for (let column of keys) {
validation.actions.setError(
`${rowId}-${column}`,
`${column} ${error.json.validationErrors[column]}`
)
// Ensure the column is visible
const index = $columns.findIndex(x => x.name === column)
if (index !== -1 && !$columns[index].visible) {
columns.update(state => {
state[index].visible = true
return state.slice()
})
}
}
// Focus the first cell with an error
focusedCellId.set(`${rowId}-${keys[0]}`)
} else {
notifications.error(`Error saving row: ${error?.message}`)
}
@ -299,7 +341,6 @@ export const createRowsStore = context => {
return {
rows: {
...rows,
subscribe: enrichedRows.subscribe,
actions: {
addRow,
getRow,
@ -312,10 +353,5 @@ export const createRowsStore = context => {
refreshTableDefinition,
},
},
rowLookupMap,
table,
sort,
filter,
loaded,
}
}

View File

@ -1,11 +1,199 @@
import { writable } from "svelte/store"
import { writable, derived, get } from "svelte/store"
import { tick } from "svelte"
export const createScrollStores = () => {
export const createStores = () => {
const scroll = writable({
left: 0,
top: 0,
})
// Derive height and width as primitives to avoid wasted computation
const scrollTop = derived(scroll, $scroll => $scroll.top, 0)
const scrollLeft = derived(scroll, $scroll => $scroll.left, 0)
return {
scroll,
scrollTop,
scrollLeft,
}
}
export const deriveStores = context => {
const {
scroll,
rows,
visibleColumns,
stickyColumn,
bounds,
rowHeight,
focusedRow,
focusedCellId,
gutterWidth,
} = context
const padding = 264
// Memoize store primitives
const scrollTop = derived(scroll, $scroll => $scroll.top, 0)
const scrollLeft = derived(scroll, $scroll => $scroll.left, 0)
const stickyColumnWidth = derived(stickyColumn, $col => $col?.width || 0, 0)
// Derive vertical limits
const height = derived(bounds, $bounds => $bounds.height, 0)
const width = derived(bounds, $bounds => $bounds.width, 0)
const contentHeight = derived(
[rows, rowHeight],
([$rows, $rowHeight]) => $rows.length * $rowHeight + padding,
0
)
const maxScrollTop = derived(
[height, contentHeight],
([$height, $contentHeight]) => Math.max($contentHeight - $height, 0),
0
)
// Derive horizontal limits
const contentWidth = derived(
[visibleColumns, stickyColumnWidth],
([$visibleColumns, $stickyColumnWidth]) => {
let width = gutterWidth + padding + $stickyColumnWidth
$visibleColumns.forEach(col => {
width += col.width
})
return width
},
0
)
const screenWidth = derived(
[width, stickyColumnWidth],
([$width, $stickyColumnWidth]) => $width + gutterWidth + $stickyColumnWidth,
0
)
const maxScrollLeft = derived(
[contentWidth, screenWidth],
([$contentWidth, $screenWidth]) => {
return Math.max($contentWidth - $screenWidth, 0)
},
0
)
// Ensure scroll state never goes invalid, which can happen when changing
// rows or tables
const overscrollTop = derived(
[scrollTop, maxScrollTop],
([$scrollTop, $maxScrollTop]) => $scrollTop > $maxScrollTop,
false
)
const overscrollLeft = derived(
[scrollLeft, maxScrollLeft],
([$scrollLeft, $maxScrollLeft]) => $scrollLeft > $maxScrollLeft,
false
)
overscrollTop.subscribe(overscroll => {
if (overscroll) {
scroll.update(state => ({
...state,
top: get(maxScrollTop),
}))
}
})
overscrollLeft.subscribe(overscroll => {
if (overscroll) {
scroll.update(state => ({
...state,
left: get(maxScrollLeft),
}))
}
})
// Ensure the selected cell is visible
focusedCellId.subscribe(async $focusedCellId => {
await tick()
const $focusedRow = get(focusedRow)
const $scroll = get(scroll)
const $bounds = get(bounds)
const $rowHeight = get(rowHeight)
const verticalOffset = $rowHeight * 1.5
// Ensure vertical position is viewable
if ($focusedRow) {
// Ensure row is not below bottom of screen
const rowYPos = $focusedRow.__idx * $rowHeight
const bottomCutoff =
$scroll.top + $bounds.height - $rowHeight - verticalOffset
let delta = rowYPos - bottomCutoff
if (delta > 0) {
scroll.update(state => ({
...state,
top: state.top + delta,
}))
}
// Ensure row is not above top of screen
else {
const delta = $scroll.top - rowYPos + verticalOffset
if (delta > 0) {
scroll.update(state => ({
...state,
top: Math.max(0, state.top - delta),
}))
}
}
}
// Ensure horizontal position is viewable
// Check horizontal position of columns next
const $visibleColumns = get(visibleColumns)
const columnName = $focusedCellId?.split("-")[1]
const column = $visibleColumns.find(col => col.name === columnName)
const horizontalOffset = 24
if (!column) {
return
}
// Ensure column is not cutoff on left edge
let delta = $scroll.left - column.left + horizontalOffset
if (delta > 0) {
scroll.update(state => ({
...state,
left: Math.max(0, state.left - delta),
}))
}
// Ensure column is not cutoff on right edge
else {
const rightEdge = column.left + column.width
const rightBound = $bounds.width + $scroll.left - horizontalOffset
delta = rightEdge - rightBound
if (delta > 0) {
scroll.update(state => ({
...state,
left: state.left + delta,
}))
}
}
})
// Derive whether to show scrollbars or not
const showVScrollbar = derived(
[contentHeight, height],
([$contentHeight, $height]) => {
return $contentHeight > $height
}
)
const showHScrollbar = derived(
[contentWidth, screenWidth],
([$contentWidth, $screenWidth]) => {
return $contentWidth > $screenWidth
}
)
return {
contentHeight,
contentWidth,
screenWidth,
maxScrollTop,
maxScrollLeft,
showHScrollbar,
showVScrollbar,
}
}

View File

@ -1,66 +0,0 @@
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

@ -1,19 +1,38 @@
import { writable, get, derived } from "svelte/store"
export const createUIStores = context => {
const { rows, rowLookupMap } = context
export const createStores = () => {
const focusedCellId = writable(null)
const focusedCellAPI = writable(null)
const selectedRows = writable({})
const hoveredRowId = writable(null)
const rowHeight = writable(36)
return {
focusedCellId,
focusedCellAPI,
selectedRows,
hoveredRowId,
rowHeight,
}
}
export const deriveStores = context => {
const { focusedCellId, selectedRows, hoveredRowId, rows, rowLookupMap } =
context
// Derive the row that contains the selected cell
const focusedRow = derived(
[focusedCellId, rowLookupMap, rows],
([$focusedCellId, $rowLookupMap, $rows]) => {
const rowId = $focusedCellId?.split("-")[0]
const index = $rowLookupMap[rowId]
return $rows[index]
if (rowId === "new") {
// Edge case for new row
return { _id: rowId }
} else {
// All normal rows
const index = $rowLookupMap[rowId]
return $rows[index]
}
},
null
)
@ -80,11 +99,7 @@ export const createUIStores = context => {
})
return {
focusedCellId,
selectedRows,
hoveredRowId,
focusedRow,
rowHeight,
ui: {
actions: {
blur,

View File

@ -1,6 +1,6 @@
import { writable, get, derived } from "svelte/store"
export const createUserStores = () => {
export const createStores = () => {
const users = writable([])
const userId = writable(null)
@ -55,10 +55,22 @@ export const createUserStores = () => {
[]
)
return {
users: {
...users,
subscribe: enrichedUsers.subscribe,
},
userId,
}
}
export const deriveStores = context => {
const { users, userId } = context
// Generate a lookup map of cell ID to the user that has it selected, to make
// lookups inside sheet cells extremely fast
const selectedCellMap = derived(
[enrichedUsers, userId],
[users, userId],
([$enrichedUsers, $userId]) => {
let map = {}
$enrichedUsers.forEach(user => {
@ -92,15 +104,12 @@ export const createUserStores = () => {
return {
users: {
...enrichedUsers,
set: users.set,
update: users.update,
...users,
actions: {
updateUser,
removeUser,
},
},
selectedCellMap,
userId,
}
}

View File

@ -1,25 +1,52 @@
import { writable, get } from "svelte/store"
import { writable, get, derived } from "svelte/store"
export const createValidationStores = () => {
export const createStores = () => {
const validation = writable({})
const setError = (cellId, error) => {
if (!cellId) {
return
}
validation.update(state => ({
...state,
[cellId]: error,
}))
}
return {
validation: {
subscribe: validation.subscribe,
...validation,
actions: {
setError: (cellId, error) => {
if (!cellId) {
return
}
validation.update(state => ({
...state,
[cellId]: error,
}))
},
getError: cellId => {
return get(validation)[cellId]
},
setError,
},
},
}
}
export const deriveStores = context => {
const { validation, focusedRow, columns, stickyColumn } = context
const focusedRowId = derived(focusedRow, $focusedRow => $focusedRow?._id)
// Store the row ID that was previously focused, so we can remove errors from
// it when we focus a new row
let previousFocusedRowId = null
focusedRowId.subscribe(id => {
// Remove validation errors from previous focused row
if (previousFocusedRowId) {
const $columns = get(columns)
const $stickyColumn = get(stickyColumn)
validation.update(state => {
$columns.forEach(column => {
state[`${previousFocusedRowId}-${column.name}`] = null
})
if ($stickyColumn) {
state[`${previousFocusedRowId}-${$stickyColumn.name}`] = null
}
return state
})
}
// Store row ID
previousFocusedRowId = id
})
}

View File

@ -1,13 +1,15 @@
import { derived, get } from "svelte/store"
export const createViewportStores = context => {
const { rowHeight, visibleColumns, rows, scroll, bounds } = context
const scrollTop = derived(scroll, $scroll => $scroll.top, 0)
const scrollLeft = derived(scroll, $scroll => $scroll.left, 0)
// Derive height and width as primitives to avoid wasted computation
const width = derived(bounds, $bounds => $bounds.width, 0)
const height = derived(bounds, $bounds => $bounds.height, 0)
export const deriveStores = context => {
const {
rowHeight,
visibleColumns,
rows,
scrollTop,
scrollLeft,
width,
height,
} = context
// Derive visible rows
// Split into multiple stores containing primitives to optimise invalidation