Separate data fetching logic from main sheet and tidy up

This commit is contained in:
Andrew Kingston 2023-02-27 19:01:23 +00:00
parent 30e1ecd67f
commit b45ba0eba7
9 changed files with 285 additions and 224 deletions

View File

@ -0,0 +1,87 @@
<script>
import SheetCell from "./SheetCell.svelte"
import { getContext } from "svelte"
import { Icon } from "@budibase/bbui"
const { visibleColumns, reorder, selectedRows, rows } =
getContext("spreadsheet")
$: rowCount = $rows.length
$: selectedRowCount = Object.values($selectedRows).filter(x => !!x).length
const getIconForField = field => {
const type = field.schema.type
if (type === "options") {
return "ChevronDown"
} else if (type === "datetime") {
return "Date"
}
return "Text"
}
const selectAll = () => {
const allSelected = selectedRowCount === rowCount
if (allSelected) {
$selectedRows = {}
} else {
selectedRows.update(state => {
$rows.forEach(row => {
state[row._id] = true
})
return state
})
}
}
</script>
<div class="row">
<!-- Field headers -->
<SheetCell header label on:click={selectAll} width="40" left="0">
<input
type="checkbox"
checked={rowCount && selectedRowCount === rowCount}
/>
</SheetCell>
{#each $visibleColumns as column}
<SheetCell
header
sticky={column.idx === 0}
reorderSource={$reorder.columnIdx === column.idx}
reorderTarget={$reorder.swapColumnIdx === column.idx}
on:mousedown={column.idx === 0
? null
: e => reorder.actions.startReordering(column.idx, e)}
width={column.width}
left={column.left}
>
<Icon
size="S"
name={getIconForField(column)}
color="var(--spectrum-global-color-gray-600)"
/>
<span>
{column.name}
</span>
</SheetCell>
{/each}
</div>
<style>
.row {
display: flex;
position: sticky;
top: 0;
width: inherit;
z-index: 10;
}
.row.new {
position: absolute;
transform: translateY(var(--top));
}
.row :global(> :last-child) {
border-right-width: 1px;
}
input[type="checkbox"] {
margin: 0;
}
</style>

View File

@ -0,0 +1,43 @@
<script>
import SheetCell from "./SheetCell.svelte"
import { Icon } from "@budibase/bbui"
import { getContext } from "svelte"
const { visibleColumns, cellHeight, rows, selectedCellId } =
getContext("spreadsheet")
const addRow = async field => {
const newRow = await rows.actions.addRow()
$selectedCellId = `${newRow._id}-${field.name}`
}
</script>
<div class="row new" style="--top:{($rows.length + 1) * cellHeight}px;">
<SheetCell label on:click={addRow} width="40" left="0">
<Icon hoverable name="Add" size="S" />
</SheetCell>
{#each $visibleColumns as column}
<SheetCell
sticky={column.idx === 0}
on:click={() => addRow(column)}
width={column.width}
left={column.left}
/>
{/each}
</div>
<style>
.row {
display: flex;
top: var(--top);
width: inherit;
position: absolute;
}
.row:hover :global(.cell) {
background: var(--cell-background-hover);
cursor: pointer;
}
.row :global(> :last-child) {
border-right-width: 1px;
}
</style>

View File

@ -1,16 +1,16 @@
<script> <script>
import { setContext } from "svelte" import { setContext } from "svelte"
import { writable } from "svelte/store" import { writable } from "svelte/store"
import { fetchData } from "../../fetch/fetchData"
import { LuceneUtils } from "../../utils"
import { Icon } from "@budibase/bbui"
import { createReorderStores } from "./stores/reorder" import { createReorderStores } from "./stores/reorder"
import { createViewportStores } from "./stores/viewport" import { createViewportStores } from "./stores/viewport"
import SpreadsheetHeader from "./SheetHeader.svelte" import { createRowsStore } from "./stores/rows"
import SpreadsheetBody from "./SheetBody.svelte" import SheetHeader from "./SheetHeader.svelte"
import SpreadsheetCell from "./SheetCell.svelte" import SheetBody from "./SheetBody.svelte"
import SpreadsheetRow from "./SheetRow.svelte" import SheetRow from "./SheetRow.svelte"
import ResizeOverlay from "./ResizeOverlay.svelte" import ResizeOverlay from "./ResizeOverlay.svelte"
import HeaderRow from "./HeaderRow.svelte"
import NewRow from "./NewRow.svelte"
import { createAPIClient } from "../../api"
export let tableId export let tableId
export let filter export let filter
@ -20,17 +20,14 @@
// Sheet constants // Sheet constants
const cellHeight = 36 const cellHeight = 36
const limit = 100
const defaultWidth = 200 const defaultWidth = 200
const rand = Math.random() const rand = Math.random()
// State stores // State stores
const rows = writable([]) const tableIdStore = writable(tableId)
const columns = writable([]) const columns = writable([])
const selectedCellId = writable(null) const selectedCellId = writable(null)
const selectedRows = writable({}) const selectedRows = writable({})
const changeCache = writable({})
const newRows = writable([])
const scroll = writable({ const scroll = writable({
left: 0, left: 0,
top: 0, top: 0,
@ -44,55 +41,28 @@
// Build up spreadsheet context and additional stores // Build up spreadsheet context and additional stores
let context = { let context = {
API, API: API || createAPIClient(),
rand, rand,
rows,
columns, columns,
selectedCellId, selectedCellId,
selectedRows, selectedRows,
tableId,
changeCache,
newRows,
cellHeight, cellHeight,
bounds, bounds,
scroll, scroll,
tableId: tableIdStore,
} }
const { rows, schema, primaryDisplay } = createRowsStore(context)
context = { ...context, rows }
const { visibleRows, visibleColumns } = createViewportStores(context) const { visibleRows, visibleColumns } = createViewportStores(context)
context = { ...context, visibleRows, visibleColumns } context = { ...context, visibleRows, visibleColumns }
const { reorder } = createReorderStores(context) const { reorder } = createReorderStores(context)
context = { ...context, reorder }
$: query = LuceneUtils.buildLuceneQuery(filter) $: tableIdStore.set(tableId)
$: fetch = createFetch(tableId) $: generateColumns($schema, $primaryDisplay)
$: fetch.update({
sortColumn,
sortOrder,
query,
limit,
})
$: generateColumns($fetch)
$: rowCount = $rows.length
$: selectedRowCount = Object.values($selectedRows).filter(x => !!x).length
$: updateSortedRows($fetch, $newRows)
const createFetch = tableId => {
return fetchData({
API,
datasource: {
type: "table",
tableId,
},
options: {
sortColumn,
sortOrder,
query,
limit,
paginate: true,
},
})
}
// Generates the column array the first time the schema loads // Generates the column array the first time the schema loads
const generateColumns = ({ schema, definition }) => { const generateColumns = (schema, primaryDisplay) => {
if (!schema) { if (!schema) {
$columns = [] $columns = []
return return
@ -101,7 +71,6 @@
// Get fields in new schema // Get fields in new schema
let fields = Object.keys(schema || {}) let fields = Object.keys(schema || {})
const primaryDisplay = definition?.primaryDisplay
if (primaryDisplay) { if (primaryDisplay) {
fields = [primaryDisplay, ...fields.filter(x => x !== primaryDisplay)] fields = [primaryDisplay, ...fields.filter(x => x !== primaryDisplay)]
} }
@ -123,147 +92,19 @@
}) })
} }
const getIconForField = field => {
const type = field.schema.type
if (type === "options") {
return "ChevronDown"
} else if (type === "datetime") {
return "Date"
}
return "Text"
}
const selectAll = () => {
const allSelected = selectedRowCount === rowCount
if (allSelected) {
$selectedRows = {}
} else {
selectedRows.update(state => {
$rows.forEach(row => {
state[row._id] = true
})
return state
})
}
}
const updateValue = async (rowId, field, value) => {
let row = $rows.find(x => x._id === rowId)
if (!row) {
return
}
if (row[field.name] === value) {
return
}
changeCache.update(state => {
state[rowId] = { [field.name]: value }
return state
})
await API.saveRow({
...row,
...$changeCache[rowId],
})
await fetch.refresh()
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.update(state => [...state, res._id])
await fetch.refresh()
}
const updateSortedRows = (unsortedRows, newRows) => {
let foo = unsortedRows.rows
for (let i = 0; i < 0; i++) {
foo = foo.concat(foo.map(x => ({ ...x, _id: x._id + "x" })))
}
let sortedRows = foo.slice()
sortedRows.sort((a, b) => {
const aIndex = newRows.indexOf(a._id)
const bIndex = newRows.indexOf(b._id)
return aIndex < bIndex ? -1 : 1
})
$rows = sortedRows.map((x, idx) => ({ ...x, __idx: idx }))
}
// API for children to consume
const spreadsheetAPI = {
refreshData: () => fetch?.refresh(),
updateValue,
}
// Set context for children to consume // Set context for children to consume
setContext("spreadsheet", { setContext("spreadsheet", context)
...context,
reorder,
visibleRows,
visibleColumns,
spreadsheetAPI,
})
</script> </script>
<div class="sheet" style="--cell-height:{cellHeight}px;" id="sheet-{rand}"> <div class="sheet" style="--cell-height:{cellHeight}px;" id="sheet-{rand}">
<SpreadsheetHeader /> <SheetHeader />
<SpreadsheetBody> <SheetBody>
<div class="row"> <HeaderRow />
<!-- Field headers -->
<SpreadsheetCell header label on:click={selectAll} width="40" left="0">
<input
type="checkbox"
checked={rowCount && selectedRowCount === rowCount}
/>
</SpreadsheetCell>
{#each $visibleColumns as column}
<SpreadsheetCell
header
sticky={column.idx === 0}
reorderSource={$reorder.columnIdx === column.idx}
reorderTarget={$reorder.swapColumnIdx === column.idx}
on:mousedown={column.idx === 0
? null
: e => reorder.actions.startReordering(column.idx, e)}
width={column.width}
left={column.left}
>
<Icon
size="S"
name={getIconForField(column)}
color="var(--spectrum-global-color-gray-600)"
/>
<span>
{column.name}
</span>
</SpreadsheetCell>
{/each}
</div>
<!-- All real rows -->
{#each $visibleRows as row (row._id)} {#each $visibleRows as row (row._id)}
<SpreadsheetRow {row} /> <SheetRow {row} />
{/each} {/each}
<NewRow />
<!-- New row placeholder --> </SheetBody>
<div class="row new" style="--top:{($rows.length + 1) * cellHeight}px;">
<SpreadsheetCell label on:click={addRow} width="40" left="0">
<Icon hoverable name="Add" size="S" />
</SpreadsheetCell>
{#each $visibleColumns as column}
<SpreadsheetCell
sticky={column.idx === 0}
reorderSource={$reorder.columnIdx === column.idx}
reorderTarget={$reorder.swapColumnIdx === column.idx}
on:click={() => addRow(column)}
width={column.width}
left={column.left}
/>
{/each}
</div>
</SpreadsheetBody>
<ResizeOverlay /> <ResizeOverlay />
</div> </div>
@ -289,22 +130,4 @@
.sheet :global(*) { .sheet :global(*) {
box-sizing: border-box; box-sizing: border-box;
} }
.row {
display: flex;
position: sticky;
top: 0;
width: inherit;
z-index: 10;
}
.row.new {
position: absolute;
transform: translateY(var(--top));
}
.row :global(> :last-child) {
border-right-width: 1px;
}
input[type="checkbox"] {
margin: 0;
}
</style> </style>

View File

@ -2,7 +2,7 @@
import { getContext, onMount } from "svelte" import { getContext, onMount } from "svelte"
import { Utils } from "../../utils" import { Utils } from "../../utils"
const { columns, selectedCellId, rand, cellHeight, rows, bounds, scroll } = const { columns, selectedCellId, cellHeight, rows, bounds, scroll } =
getContext("spreadsheet") getContext("spreadsheet")
const padding = 180 const padding = 180
@ -45,7 +45,6 @@
class="sheet-body" class="sheet-body"
class:horizontally-scrolled={$scroll.left > 0} class:horizontally-scrolled={$scroll.left > 0}
on:click|self={() => ($selectedCellId = null)} on:click|self={() => ($selectedCellId = null)}
id={`sheet-${rand}-body`}
on:scroll={handleScroll} on:scroll={handleScroll}
> >
<div <div

View File

@ -2,15 +2,7 @@
import { getContext } from "svelte" import { getContext } from "svelte"
import { ActionButton, Modal, ModalContent } from "@budibase/bbui" import { ActionButton, Modal, ModalContent } from "@budibase/bbui"
const { const { selectedRows, rows, selectedCellId } = getContext("spreadsheet")
selectedRows,
rows,
selectedCellId,
hoveredRowId,
tableId,
spreadsheetAPI,
API,
} = getContext("spreadsheet")
let modal let modal
@ -28,15 +20,10 @@
// Deletion callback when confirmed // Deletion callback when confirmed
const performDeletion = async () => { const performDeletion = async () => {
await API.deleteRows({ await rows.actions.deleteRows(rowsToDelete)
tableId,
rows: rowsToDelete,
})
await spreadsheetAPI.refreshData()
// Refresh state // Refresh state
$selectedCellId = null $selectedCellId = null
$hoveredRowId = null
$selectedRows = {} $selectedRows = {}
} }
</script> </script>

View File

@ -14,14 +14,12 @@
selectedCellId, selectedCellId,
reorder, reorder,
selectedRows, selectedRows,
changeCache, rows,
spreadsheetAPI,
visibleColumns, visibleColumns,
cellHeight, cellHeight,
} = getContext("spreadsheet") } = getContext("spreadsheet")
$: rowSelected = !!$selectedRows[row._id] $: rowSelected = !!$selectedRows[row._id]
$: data = { ...row, ...$changeCache[row._id] }
const getCellForField = field => { const getCellForField = field => {
const type = field.schema.type const type = field.schema.type
@ -71,10 +69,10 @@
> >
<svelte:component <svelte:component
this={getCellForField(column)} this={getCellForField(column)}
value={data[column.name]} value={row[column.name]}
schema={column.schema} schema={column.schema}
selected={$selectedCellId === cellIdx} selected={$selectedCellId === cellIdx}
onChange={val => spreadsheetAPI.updateValue(row._id, column, val)} onChange={val => rows.actions.updateRow(row._id, column, val)}
readonly={column.schema.autocolumn} readonly={column.schema.autocolumn}
/> />
</SpreadsheetCell> </SpreadsheetCell>

View File

@ -1,7 +1,7 @@
import { get, writable } from "svelte/store" import { get, writable } from "svelte/store"
export const createReorderStores = context => { export const createReorderStores = context => {
const { columns, visibleColumns, rand, scroll, bounds } = context const { columns, rand, scroll, bounds } = context
const reorderInitialState = { const reorderInitialState = {
columnIdx: null, columnIdx: null,
swapColumnIdx: null, swapColumnIdx: null,

View File

@ -0,0 +1,125 @@
import { writable, derived, get } from "svelte/store"
import { buildLuceneQuery } from "../../../utils/lucene"
import { fetchData } from "../../../fetch/fetchData"
export const createRowsStore = context => {
const { tableId, filter, API } = context
const rows = writable([])
const schema = writable({})
const primaryDisplay = writable(null)
const query = derived(filter, $filter => buildLuceneQuery($filter))
const fetch = derived(tableId, $tableId => {
return fetchData({
API,
datasource: {
type: "table",
tableId: $tableId,
},
options: {
sortColumn: null,
sortOrder: null,
query: get(query),
limit: 100,
paginate: true,
},
})
})
// Update fetch when query changes
query.subscribe($query => {
get(fetch).update({
query: $query,
})
})
// Observe each data fetch and extract some data
fetch.subscribe($fetch => {
$fetch.subscribe($$fetch => {
console.log("new fetch")
rows.set($$fetch.rows.map((row, idx) => ({ ...row, __idx: idx })))
schema.set($$fetch.schema)
primaryDisplay.set($$fetch.definition?.primaryDisplay)
})
})
// Adds a new empty row
const addRow = async () => {
let newRow = await API.saveRow({ tableId: get(tableId) })
newRow.__idx = get(rows).length
rows.update(state => {
state.push(newRow)
return state
})
return newRow
}
// Updates a value of a row
const updateRow = async (rowId, column, value) => {
const $rows = get(rows)
const index = $rows.findIndex(x => x._id === rowId)
const row = $rows[index]
if (index === -1 || row?.[column.name] === value) {
return
}
// Immediately update state so that the change is reflected
let newRow = { ...row, [column.name]: value }
rows.update(state => {
state[index] = { ...newRow }
return state
})
// Save change
delete newRow.__idx
await API.saveRow(newRow)
// Fetch row from the server again
newRow = await API.fetchRow({
tableId: get(tableId),
rowId: row._id,
})
// Update state again with this row
newRow = { ...newRow, __idx: row.__idx }
rows.update(state => {
state[index] = newRow
return state
})
return newRow
}
// Deletes an array of rows
const deleteRows = async rowsToDelete => {
const deletedIds = rowsToDelete.map(row => row._id)
// Actually delete rows
rowsToDelete.forEach(row => {
delete row.__idx
})
await API.deleteRows({
tableId: get(tableId),
rows: rowsToDelete,
})
// Update state
rows.update(state => {
return state
.filter(row => !deletedIds.includes(row._id))
.map((row, idx) => ({ ...row, __idx: idx }))
})
}
return {
rows: {
...rows,
actions: {
addRow,
updateRow,
deleteRows,
},
},
schema,
primaryDisplay,
}
}

View File

@ -1,5 +1,4 @@
import { writable, derived } from "svelte/store" import { writable, derived } from "svelte/store"
import { Utils } from "../../../utils"
export const createViewportStores = context => { export const createViewportStores = context => {
const { cellHeight, columns, rows, scroll, bounds } = context const { cellHeight, columns, rows, scroll, bounds } = context