From ddb70ba3f68bf74f6c6afd1faeb7974912b0d911 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 23 Feb 2023 16:40:48 +0000 Subject: [PATCH] Split into more modular components and try virtual rendering --- .../app/spreadsheet/SpacerCell.svelte | 13 + .../app/spreadsheet/Spreadsheet.svelte | 319 ++++-------------- .../app/spreadsheet/SpreadsheetBody.svelte | 33 +- .../app/spreadsheet/SpreadsheetCell.svelte | 138 ++++++++ .../app/spreadsheet/SpreadsheetRow.svelte | 107 ++++++ .../app/spreadsheet/VerticalSpacer.svelte | 17 + .../app/spreadsheet/stores/resize.js | 2 +- 7 files changed, 379 insertions(+), 250 deletions(-) create mode 100644 packages/client/src/components/app/spreadsheet/SpacerCell.svelte create mode 100644 packages/client/src/components/app/spreadsheet/SpreadsheetCell.svelte create mode 100644 packages/client/src/components/app/spreadsheet/SpreadsheetRow.svelte create mode 100644 packages/client/src/components/app/spreadsheet/VerticalSpacer.svelte diff --git a/packages/client/src/components/app/spreadsheet/SpacerCell.svelte b/packages/client/src/components/app/spreadsheet/SpacerCell.svelte new file mode 100644 index 0000000000..d4a9a87470 --- /dev/null +++ b/packages/client/src/components/app/spreadsheet/SpacerCell.svelte @@ -0,0 +1,13 @@ + + + ($selectedCellId = null)} + on:mouseenter={() => ($hoveredRowId = null)} +/> diff --git a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte index 53b6b6fe06..dde5566dd7 100644 --- a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte +++ b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte @@ -3,18 +3,16 @@ import { writable } from "svelte/store" import { fetchData, LuceneUtils } from "@budibase/frontend-core" import { Icon } from "@budibase/bbui" - import TextCell from "./cells/TextCell.svelte" - import OptionsCell from "./cells/OptionsCell.svelte" - import DateCell from "./cells/DateCell.svelte" - import MultiSelectCell from "./cells/MultiSelectCell.svelte" - import NumberCell from "./cells/NumberCell.svelte" - import RelationshipCell from "./cells/RelationshipCell.svelte" import { createReorderStores } from "./stores/reorder" import { createResizeStore } from "./stores/resize" import ReorderPlaceholder from "./ReorderPlaceholder.svelte" import ResizeSlider from "./ResizeSlider.svelte" import SpreadsheetHeader from "./SpreadsheetHeader.svelte" import SpreadsheetBody from "./SpreadsheetBody.svelte" + import SpreadsheetCell from "./SpreadsheetCell.svelte" + import SpacerCell from "./SpacerCell.svelte" + import VerticalSpacer from "./VerticalSpacer.svelte" + import SpreadsheetRow from "./SpreadsheetRow.svelte" export let table export let filter @@ -25,6 +23,7 @@ const component = getContext("component") // Sheet constants + const cellHeight = 32 const limit = 100 const defaultWidth = 160 const rand = Math.random() @@ -36,6 +35,9 @@ const selectedCellId = writable(null) const selectedRows = writable({}) const tableId = writable(table?.tableId) + const changeCache = writable({}) + const newRows = writable([]) + const visibleRows = writable([0, 0]) // Build up spreadsheet context and additional stores const context = { @@ -46,27 +48,14 @@ selectedCellId, selectedRows, tableId, + changeCache, + newRows, + cellHeight, + visibleRows, } const { reorder, reorderPlaceholder } = createReorderStores(context) const resize = createResizeStore(context) - // API for children to consume - const spreadsheetAPI = { - refreshData: () => fetch?.refresh(), - } - - // Set context for children to consume - setContext("spreadsheet", { - ...context, - reorder, - reorderPlaceholder, - resize, - spreadsheetAPI, - }) - - let changeCache = {} - let newRows = [] - $: tableId.set(table?.tableId) $: query = LuceneUtils.buildLuceneQuery(filter) $: fetch = createFetch(table) @@ -79,7 +68,7 @@ $: generateColumns($fetch) $: rowCount = $rows.length $: selectedRowCount = Object.values($selectedRows).filter(x => !!x).length - $: updateSortedRows($fetch.rows, newRows) + $: updateSortedRows($fetch.rows, $newRows) const createFetch = datasource => { return fetchData({ @@ -112,22 +101,6 @@ } } - const getCellForField = field => { - const type = field.schema.type - if (type === "options") { - return OptionsCell - } else if (type === "datetime") { - return DateCell - } else if (type === "array") { - return MultiSelectCell - } else if (type === "number") { - return NumberCell - } else if (type === "link") { - return RelationshipCell - } - return TextCell - } - const getIconForField = field => { const type = field.schema.type if (type === "options") { @@ -138,13 +111,6 @@ return "Text" } - const selectRow = id => { - selectedRows.update(state => { - state[id] = !state[id] - return state - }) - } - const selectAll = () => { const allSelected = selectedRowCount === rowCount if (allSelected) { @@ -159,7 +125,7 @@ } } - const handleChange = async (rowId, field, value) => { + const updateValue = async (rowId, field, value) => { let row = $rows.find(x => x._id === rowId) if (!row) { return @@ -167,19 +133,25 @@ if (row[field.name] === value) { return } - changeCache[rowId] = { [field.name]: value } + changeCache.update(state => { + state[rowId] = { [field.name]: value } + return state + }) await API.saveRow({ ...row, - ...changeCache[rowId], + ...$changeCache[rowId], }) await fetch.refresh() - delete changeCache[rowId] + changeCache.update(state => { + delete state[rowId] + return state + }) } const addRow = async field => { const res = await API.saveRow({ tableId: table.tableId }) $selectedCellId = `${res._id}-${field.name}` - newRows.push(res._id) + newRows.update(state => [...state, res._id]) await fetch.refresh() } @@ -192,25 +164,44 @@ }) $rows = sortedRows } + + // API for children to consume + const spreadsheetAPI = { + refreshData: () => fetch?.refresh(), + updateValue, + } + + // Set context for children to consume + setContext("spreadsheet", { + ...context, + reorder, + reorderPlaceholder, + resize, + spreadsheetAPI, + })
-
+
-
+ -
+ {#each $columns as field, fieldIdx} -
reorder.actions.startReordering(fieldIdx, e)} id={`sheet-${rand}-header-${fieldIdx}`} > @@ -223,101 +214,44 @@ {field.name} -
+ {/each} - -
{#each $rows as row, rowIdx (row._id)} - {@const rowSelected = !!$selectedRows[row._id]} - {@const rowHovered = $hoveredRowId === row._id} - {@const data = { ...row, ...changeCache[row._id] }} -
($hoveredRowId = row._id)} - on:click={() => selectRow(row._id)} - > - {#if rowSelected || rowHovered} - - {:else} - - {rowIdx + 1} - - {/if} -
- {#each $columns as field, fieldIdx} - {@const cellIdx = `${row._id}-${field.name}`} - {#key cellIdx} -
($hoveredRowId = row._id)} - on:click={() => ($selectedCellId = cellIdx)} - > - handleChange(row._id, field, val)} - readonly={field.schema.autocolumn} - /> -
- {/key} - {/each} - -
+ {/each} -
($hoveredRowId = "new")} - class:hovered={$hoveredRowId === "new"} + on:mouseenter={() => ($hoveredRowId = "new")} + rowHovered={$hoveredRowId === "new"} > -
+ {#each $columns as field, fieldIdx} -
addRow(field)} - on:focus - on:mouseover={() => ($hoveredRowId = "new")} + on:mouseenter={() => ($hoveredRowId = "new")} /> {/each} - -
+ - -
+ + - +
@@ -337,121 +271,12 @@ --cell-background-hover: var(--spectrum-global-color-gray-100); --cell-padding: 8px; --cell-spacing: 4px; - --cell-height: 32px; --cell-font-size: 14px; } .wrapper.resize *:hover { cursor: col-resize; } - .wrapper ::-webkit-scrollbar-track { + .wrapper::-webkit-scrollbar-track { background: var(--cell-background); } - - /* Cells */ - .cell { - height: var(--cell-height); - border-style: solid; - border-color: var(--spectrum-global-color-gray-300); - border-width: 0; - border-bottom-width: 1px; - border-left-width: 1px; - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - color: var(--spectrum-global-color-gray-900); - font-size: var(--cell-font-size); - gap: var(--cell-spacing); - background: var(--cell-background); - position: relative; - transition: border-color 130ms ease-out; - } - .cell.hovered { - background: var(--cell-background-hover); - } - .cell.selected { - box-shadow: inset 0 0 0 2px var(--spectrum-global-color-blue-400); - z-index: 1; - } - .cell:not(.selected) { - user-select: none; - } - .cell:hover { - cursor: default; - } - .cell.row-selected { - background-color: rgb(224, 242, 255); - } - .cell.new:hover { - cursor: pointer; - } - - /* Header cells */ - .header { - background: var(--spectrum-global-color-gray-200); - position: sticky; - top: 0; - padding: 0 var(--cell-padding); - z-index: 3; - border-color: var(--spectrum-global-color-gray-400); - } - .header span { - flex: 1 1 auto; - width: 0; - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - } - .header.sticky { - z-index: 4; - } - - /* Sticky styles */ - .sticky { - position: sticky; - left: 40px; - z-index: 2; - border-left-color: transparent; - } - .sticky.selected { - z-index: 3; - } - - /* Spacer cells */ - .spacer { - background: none; - border-bottom: none; - } - .vertical-spacer { - grid-column: 1/-1; - height: 180px; - } - - /* Reorder styles */ - .cell.reorder-source { - background: var(--spectrum-global-color-gray-200); - } - .cell.reorder-target { - border-left-color: var(--spectrum-global-color-blue-400); - } - - .label { - padding: 0 12px; - border-right: none; - position: sticky; - left: 0; - z-index: 2; - } - .label.header { - z-index: 4; - } - .label span { - min-width: 14px; - text-align: center; - color: var(--spectrum-global-color-gray-500); - } - - input[type="checkbox"] { - margin: 0; - } diff --git a/packages/client/src/components/app/spreadsheet/SpreadsheetBody.svelte b/packages/client/src/components/app/spreadsheet/SpreadsheetBody.svelte index 839e45b8e8..40084f0066 100644 --- a/packages/client/src/components/app/spreadsheet/SpreadsheetBody.svelte +++ b/packages/client/src/components/app/spreadsheet/SpreadsheetBody.svelte @@ -1,11 +1,17 @@
+ export let header = false + export let label = false + export let spacer = false + export let rowHovered = false + export let rowSelected = false + export let sticky = false + export let selected = false + export let reorderSource = false + export let reorderTarget = false + export let id = null + + +
+ +
+ + diff --git a/packages/client/src/components/app/spreadsheet/SpreadsheetRow.svelte b/packages/client/src/components/app/spreadsheet/SpreadsheetRow.svelte new file mode 100644 index 0000000000..2311ffab60 --- /dev/null +++ b/packages/client/src/components/app/spreadsheet/SpreadsheetRow.svelte @@ -0,0 +1,107 @@ + + +{#if !visible} +
+{:else} + ($hoveredRowId = row._id)} + on:click={() => selectRow(row._id)} + > + {#if rowSelected || rowHovered} + + {:else} + + {rowIdx + 1} + + {/if} + + {#each $columns as field, fieldIdx} + {@const cellIdx = `${row._id}-${field.name}`} + {#key cellIdx} + ($hoveredRowId = row._id)} + on:click={() => ($selectedCellId = cellIdx)} + > + spreadsheetAPI.updateValue(row._id, field, val)} + readonly={field.schema.autocolumn} + /> + + {/key} + {/each} + +{/if} + + diff --git a/packages/client/src/components/app/spreadsheet/VerticalSpacer.svelte b/packages/client/src/components/app/spreadsheet/VerticalSpacer.svelte new file mode 100644 index 0000000000..fcde0fcce9 --- /dev/null +++ b/packages/client/src/components/app/spreadsheet/VerticalSpacer.svelte @@ -0,0 +1,17 @@ + + +
($selectedCellId = null)} + on:mouseenter={() => ($hoveredRowId = null)} +/> + + diff --git a/packages/client/src/components/app/spreadsheet/stores/resize.js b/packages/client/src/components/app/spreadsheet/stores/resize.js index 9b4f8619a0..58c6151640 100644 --- a/packages/client/src/components/app/spreadsheet/stores/resize.js +++ b/packages/client/src/components/app/spreadsheet/stores/resize.js @@ -34,7 +34,7 @@ export const createResizeStore = context => { const newWidth = Math.max(MinColumnWidth, $resize.initialWidth + dx) // Skip small updates - if (Math.abs(width - newWidth) < 10) { + if (Math.abs(width - newWidth) < 20) { return }