Separate data fetching logic from main sheet and tidy up
This commit is contained in:
parent
30e1ecd67f
commit
b45ba0eba7
|
@ -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>
|
|
@ -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>
|
|
@ -1,16 +1,16 @@
|
|||
<script>
|
||||
import { setContext } from "svelte"
|
||||
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 { createViewportStores } from "./stores/viewport"
|
||||
import SpreadsheetHeader from "./SheetHeader.svelte"
|
||||
import SpreadsheetBody from "./SheetBody.svelte"
|
||||
import SpreadsheetCell from "./SheetCell.svelte"
|
||||
import SpreadsheetRow from "./SheetRow.svelte"
|
||||
import { createRowsStore } from "./stores/rows"
|
||||
import SheetHeader from "./SheetHeader.svelte"
|
||||
import SheetBody from "./SheetBody.svelte"
|
||||
import SheetRow from "./SheetRow.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 filter
|
||||
|
@ -20,17 +20,14 @@
|
|||
|
||||
// Sheet constants
|
||||
const cellHeight = 36
|
||||
const limit = 100
|
||||
const defaultWidth = 200
|
||||
const rand = Math.random()
|
||||
|
||||
// State stores
|
||||
const rows = writable([])
|
||||
const tableIdStore = writable(tableId)
|
||||
const columns = writable([])
|
||||
const selectedCellId = writable(null)
|
||||
const selectedRows = writable({})
|
||||
const changeCache = writable({})
|
||||
const newRows = writable([])
|
||||
const scroll = writable({
|
||||
left: 0,
|
||||
top: 0,
|
||||
|
@ -44,55 +41,28 @@
|
|||
|
||||
// Build up spreadsheet context and additional stores
|
||||
let context = {
|
||||
API,
|
||||
API: API || createAPIClient(),
|
||||
rand,
|
||||
rows,
|
||||
columns,
|
||||
selectedCellId,
|
||||
selectedRows,
|
||||
tableId,
|
||||
changeCache,
|
||||
newRows,
|
||||
cellHeight,
|
||||
bounds,
|
||||
scroll,
|
||||
tableId: tableIdStore,
|
||||
}
|
||||
const { rows, schema, primaryDisplay } = createRowsStore(context)
|
||||
context = { ...context, rows }
|
||||
const { visibleRows, visibleColumns } = createViewportStores(context)
|
||||
context = { ...context, visibleRows, visibleColumns }
|
||||
const { reorder } = createReorderStores(context)
|
||||
context = { ...context, reorder }
|
||||
|
||||
$: query = LuceneUtils.buildLuceneQuery(filter)
|
||||
$: fetch = createFetch(tableId)
|
||||
$: 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,
|
||||
},
|
||||
})
|
||||
}
|
||||
$: tableIdStore.set(tableId)
|
||||
$: generateColumns($schema, $primaryDisplay)
|
||||
|
||||
// Generates the column array the first time the schema loads
|
||||
const generateColumns = ({ schema, definition }) => {
|
||||
const generateColumns = (schema, primaryDisplay) => {
|
||||
if (!schema) {
|
||||
$columns = []
|
||||
return
|
||||
|
@ -101,7 +71,6 @@
|
|||
|
||||
// Get fields in new schema
|
||||
let fields = Object.keys(schema || {})
|
||||
const primaryDisplay = definition?.primaryDisplay
|
||||
if (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
|
||||
setContext("spreadsheet", {
|
||||
...context,
|
||||
reorder,
|
||||
visibleRows,
|
||||
visibleColumns,
|
||||
spreadsheetAPI,
|
||||
})
|
||||
setContext("spreadsheet", context)
|
||||
</script>
|
||||
|
||||
<div class="sheet" style="--cell-height:{cellHeight}px;" id="sheet-{rand}">
|
||||
<SpreadsheetHeader />
|
||||
<SpreadsheetBody>
|
||||
<div class="row">
|
||||
<!-- 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 -->
|
||||
<SheetHeader />
|
||||
<SheetBody>
|
||||
<HeaderRow />
|
||||
{#each $visibleRows as row (row._id)}
|
||||
<SpreadsheetRow {row} />
|
||||
<SheetRow {row} />
|
||||
{/each}
|
||||
|
||||
<!-- New row placeholder -->
|
||||
<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>
|
||||
<NewRow />
|
||||
</SheetBody>
|
||||
<ResizeOverlay />
|
||||
</div>
|
||||
|
||||
|
@ -289,22 +130,4 @@
|
|||
.sheet :global(*) {
|
||||
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>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { getContext, onMount } from "svelte"
|
||||
import { Utils } from "../../utils"
|
||||
|
||||
const { columns, selectedCellId, rand, cellHeight, rows, bounds, scroll } =
|
||||
const { columns, selectedCellId, cellHeight, rows, bounds, scroll } =
|
||||
getContext("spreadsheet")
|
||||
|
||||
const padding = 180
|
||||
|
@ -45,7 +45,6 @@
|
|||
class="sheet-body"
|
||||
class:horizontally-scrolled={$scroll.left > 0}
|
||||
on:click|self={() => ($selectedCellId = null)}
|
||||
id={`sheet-${rand}-body`}
|
||||
on:scroll={handleScroll}
|
||||
>
|
||||
<div
|
||||
|
|
|
@ -2,15 +2,7 @@
|
|||
import { getContext } from "svelte"
|
||||
import { ActionButton, Modal, ModalContent } from "@budibase/bbui"
|
||||
|
||||
const {
|
||||
selectedRows,
|
||||
rows,
|
||||
selectedCellId,
|
||||
hoveredRowId,
|
||||
tableId,
|
||||
spreadsheetAPI,
|
||||
API,
|
||||
} = getContext("spreadsheet")
|
||||
const { selectedRows, rows, selectedCellId } = getContext("spreadsheet")
|
||||
|
||||
let modal
|
||||
|
||||
|
@ -28,15 +20,10 @@
|
|||
|
||||
// Deletion callback when confirmed
|
||||
const performDeletion = async () => {
|
||||
await API.deleteRows({
|
||||
tableId,
|
||||
rows: rowsToDelete,
|
||||
})
|
||||
await spreadsheetAPI.refreshData()
|
||||
await rows.actions.deleteRows(rowsToDelete)
|
||||
|
||||
// Refresh state
|
||||
$selectedCellId = null
|
||||
$hoveredRowId = null
|
||||
$selectedRows = {}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -14,14 +14,12 @@
|
|||
selectedCellId,
|
||||
reorder,
|
||||
selectedRows,
|
||||
changeCache,
|
||||
spreadsheetAPI,
|
||||
rows,
|
||||
visibleColumns,
|
||||
cellHeight,
|
||||
} = getContext("spreadsheet")
|
||||
|
||||
$: rowSelected = !!$selectedRows[row._id]
|
||||
$: data = { ...row, ...$changeCache[row._id] }
|
||||
|
||||
const getCellForField = field => {
|
||||
const type = field.schema.type
|
||||
|
@ -71,10 +69,10 @@
|
|||
>
|
||||
<svelte:component
|
||||
this={getCellForField(column)}
|
||||
value={data[column.name]}
|
||||
value={row[column.name]}
|
||||
schema={column.schema}
|
||||
selected={$selectedCellId === cellIdx}
|
||||
onChange={val => spreadsheetAPI.updateValue(row._id, column, val)}
|
||||
onChange={val => rows.actions.updateRow(row._id, column, val)}
|
||||
readonly={column.schema.autocolumn}
|
||||
/>
|
||||
</SpreadsheetCell>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { get, writable } from "svelte/store"
|
||||
|
||||
export const createReorderStores = context => {
|
||||
const { columns, visibleColumns, rand, scroll, bounds } = context
|
||||
const { columns, rand, scroll, bounds } = context
|
||||
const reorderInitialState = {
|
||||
columnIdx: null,
|
||||
swapColumnIdx: null,
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
import { writable, derived } from "svelte/store"
|
||||
import { Utils } from "../../../utils"
|
||||
|
||||
export const createViewportStores = context => {
|
||||
const { cellHeight, columns, rows, scroll, bounds } = context
|
||||
|
|
Loading…
Reference in New Issue