diff --git a/packages/frontend-core/src/components/sheet/cells/DataCell.svelte b/packages/frontend-core/src/components/sheet/cells/DataCell.svelte
index 35143ce732..0f846d2b2f 100644
--- a/packages/frontend-core/src/components/sheet/cells/DataCell.svelte
+++ b/packages/frontend-core/src/components/sheet/cells/DataCell.svelte
@@ -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)
},
}
diff --git a/packages/frontend-core/src/components/sheet/controls/AddColumnButton.svelte b/packages/frontend-core/src/components/sheet/controls/AddColumnButton.svelte
new file mode 100644
index 0000000000..56cec9126f
--- /dev/null
+++ b/packages/frontend-core/src/components/sheet/controls/AddColumnButton.svelte
@@ -0,0 +1,16 @@
+
+
+ dispatch("add-column")}
+ disabled={!$config.allowAddColumns}
+>
+ Create column
+
diff --git a/packages/frontend-core/src/components/sheet/controls/AddRowButton.svelte b/packages/frontend-core/src/components/sheet/controls/AddRowButton.svelte
index f961cabc19..41b1ff402d 100644
--- a/packages/frontend-core/src/components/sheet/controls/AddRowButton.svelte
+++ b/packages/frontend-core/src/components/sheet/controls/AddRowButton.svelte
@@ -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")
dispatch("add-row-inline")}
- disabled={!$columns.length && !$stickyColumn}
+ on:click={() => dispatch("add-row")}
+ disabled={!$config.allowAddRows || (!$columns.length && !$stickyColumn)}
>
Create row
diff --git a/packages/frontend-core/src/components/sheet/layout/HeaderRow.svelte b/packages/frontend-core/src/components/sheet/layout/HeaderRow.svelte
index efbf5cfdbe..e28c0eb545 100644
--- a/packages/frontend-core/src/components/sheet/layout/HeaderRow.svelte
+++ b/packages/frontend-core/src/components/sheet/layout/HeaderRow.svelte
@@ -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")
- {#if $config.allowAddColumns}
-
-
-
- {/if}
diff --git a/packages/frontend-core/src/components/sheet/layout/NewRowTop.svelte b/packages/frontend-core/src/components/sheet/layout/NewRowTop.svelte
index 4bd5c3f946..3b332f2b0b 100644
--- a/packages/frontend-core/src/components/sheet/layout/NewRowTop.svelte
+++ b/packages/frontend-core/src/components/sheet/layout/NewRowTop.svelte
@@ -1,7 +1,7 @@
-
-
-
-
-
diff --git a/packages/frontend-core/src/components/sheet/layout/StickyColumn.svelte b/packages/frontend-core/src/components/sheet/layout/StickyColumn.svelte
index 83a2552806..5ab946ad3f 100644
--- a/packages/frontend-core/src/components/sheet/layout/StickyColumn.svelte
+++ b/packages/frontend-core/src/components/sheet/layout/StickyColumn.svelte
@@ -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}`}
($hoveredRowId = row._id)}
@@ -132,9 +133,7 @@
{/if}
-
{#if $stickyColumn}
- {@const cellId = `${row._id}-${$stickyColumn.name}`}
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(() => {
diff --git a/packages/frontend-core/src/components/sheet/overlays/ResizeOverlay.svelte b/packages/frontend-core/src/components/sheet/overlays/ResizeOverlay.svelte
index 20d6ecd177..fb7397d09e 100644
--- a/packages/frontend-core/src/components/sheet/overlays/ResizeOverlay.svelte
+++ b/packages/frontend-core/src/components/sheet/overlays/ResizeOverlay.svelte
@@ -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
diff --git a/packages/frontend-core/src/components/sheet/overlays/ScrollOverlay.svelte b/packages/frontend-core/src/components/sheet/overlays/ScrollOverlay.svelte
index 5478ec7c63..30726292c4 100644
--- a/packages/frontend-core/src/components/sheet/overlays/ScrollOverlay.svelte
+++ b/packages/frontend-core/src/components/sheet/overlays/ScrollOverlay.svelte
@@ -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)
}
diff --git a/packages/frontend-core/src/components/sheet/stores/bounds.js b/packages/frontend-core/src/components/sheet/stores/bounds.js
index 37a16e7362..c0939f7389 100644
--- a/packages/frontend-core/src/components/sheet/stores/bounds.js
+++ b/packages/frontend-core/src/components/sheet/stores/bounds.js
@@ -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 }
}
diff --git a/packages/frontend-core/src/components/sheet/stores/columns.js b/packages/frontend-core/src/components/sheet/stores/columns.js
index 47cc56e012..7a3131012a 100644
--- a/packages/frontend-core/src/components/sheet/stores/columns.js
+++ b/packages/frontend-core/src/components/sheet/stores/columns.js
@@ -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
}
diff --git a/packages/frontend-core/src/components/sheet/stores/index.js b/packages/frontend-core/src/components/sheet/stores/index.js
new file mode 100644
index 0000000000..2c88d685e3
--- /dev/null
+++ b/packages/frontend-core/src/components/sheet/stores/index.js
@@ -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
+}
diff --git a/packages/frontend-core/src/components/sheet/stores/max-scroll.js b/packages/frontend-core/src/components/sheet/stores/max-scroll.js
deleted file mode 100644
index 28759d0a77..0000000000
--- a/packages/frontend-core/src/components/sheet/stores/max-scroll.js
+++ /dev/null
@@ -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,
- }
-}
diff --git a/packages/frontend-core/src/components/sheet/stores/menu.js b/packages/frontend-core/src/components/sheet/stores/menu.js
index 0b5dfc0641..d350137287 100644
--- a/packages/frontend-core/src/components/sheet/stores/menu.js
+++ b/packages/frontend-core/src/components/sheet/stores/menu.js
@@ -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)
diff --git a/packages/frontend-core/src/components/sheet/stores/pagination.js b/packages/frontend-core/src/components/sheet/stores/pagination.js
index 24bd10f19e..ad50f1fe8d 100644
--- a/packages/frontend-core/src/components/sheet/stores/pagination.js
+++ b/packages/frontend-core/src/components/sheet/stores/pagination.js
@@ -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
diff --git a/packages/frontend-core/src/components/sheet/stores/reorder.js b/packages/frontend-core/src/components/sheet/stores/reorder.js
index 2200707682..525a895063 100644
--- a/packages/frontend-core/src/components/sheet/stores/reorder.js
+++ b/packages/frontend-core/src/components/sheet/stores/reorder.js
@@ -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,
}
}
diff --git a/packages/frontend-core/src/components/sheet/stores/resize.js b/packages/frontend-core/src/components/sheet/stores/resize.js
index 50d544a1ee..5e3ec4a0e8 100644
--- a/packages/frontend-core/src/components/sheet/stores/resize.js
+++ b/packages/frontend-core/src/components/sheet/stores/resize.js
@@ -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,
}
}
diff --git a/packages/frontend-core/src/components/sheet/stores/rows.js b/packages/frontend-core/src/components/sheet/stores/rows.js
index df301c45c4..ff83b5ce8a 100644
--- a/packages/frontend-core/src/components/sheet/stores/rows.js
+++ b/packages/frontend-core/src/components/sheet/stores/rows.js
@@ -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,
}
}
diff --git a/packages/frontend-core/src/components/sheet/stores/scroll.js b/packages/frontend-core/src/components/sheet/stores/scroll.js
index e0f33c7d87..ea80760420 100644
--- a/packages/frontend-core/src/components/sheet/stores/scroll.js
+++ b/packages/frontend-core/src/components/sheet/stores/scroll.js
@@ -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,
}
}
diff --git a/packages/frontend-core/src/components/sheet/stores/sheet-api.js b/packages/frontend-core/src/components/sheet/stores/sheet-api.js
deleted file mode 100644
index 45d30f9452..0000000000
--- a/packages/frontend-core/src/components/sheet/stores/sheet-api.js
+++ /dev/null
@@ -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,
- },
- },
- }
-}
diff --git a/packages/frontend-core/src/components/sheet/stores/ui.js b/packages/frontend-core/src/components/sheet/stores/ui.js
index e600a83aa3..75ebeb8735 100644
--- a/packages/frontend-core/src/components/sheet/stores/ui.js
+++ b/packages/frontend-core/src/components/sheet/stores/ui.js
@@ -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,
diff --git a/packages/frontend-core/src/components/sheet/stores/users.js b/packages/frontend-core/src/components/sheet/stores/users.js
index 5a2f74e67e..4f89bb9a5b 100644
--- a/packages/frontend-core/src/components/sheet/stores/users.js
+++ b/packages/frontend-core/src/components/sheet/stores/users.js
@@ -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,
}
}
diff --git a/packages/frontend-core/src/components/sheet/stores/validation.js b/packages/frontend-core/src/components/sheet/stores/validation.js
index d3217038a1..afa2ae3033 100644
--- a/packages/frontend-core/src/components/sheet/stores/validation.js
+++ b/packages/frontend-core/src/components/sheet/stores/validation.js
@@ -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
+ })
+}
diff --git a/packages/frontend-core/src/components/sheet/stores/viewport.js b/packages/frontend-core/src/components/sheet/stores/viewport.js
index d2f29dd558..679e32ad29 100644
--- a/packages/frontend-core/src/components/sheet/stores/viewport.js
+++ b/packages/frontend-core/src/components/sheet/stores/viewport.js
@@ -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