Break out other components from spreadsheet for cleaner code

This commit is contained in:
Andrew Kingston 2023-02-23 07:44:59 +00:00
parent fc06811b2c
commit c834a236b7
4 changed files with 228 additions and 150 deletions

View File

@ -0,0 +1,108 @@
<script>
import { getContext } from "svelte"
import { ActionButton } from "@budibase/bbui"
const {
selectedRows,
rows,
selectedCellId,
hoveredRowId,
tableId,
spreadsheetAPI,
} = getContext("spreadsheet")
const { API, confirmationStore } = getContext("sdk")
$: selectedRowCount = Object.values($selectedRows).filter(x => !!x).length
$: rowCount = $rows.length
const deleteRows = () => {
// Fetch full row objects to be deleted
const rowsToDelete = Object.entries($selectedRows)
.map(entry => {
if (entry[1] === true) {
return $rows.find(x => x._id === entry[0])
} else {
return null
}
})
.filter(x => x != null)
// Deletion callback when confirmed
const performDeletion = async () => {
await API.deleteRows({
tableId: $tableId,
rows: rowsToDelete,
})
await spreadsheetAPI.refreshData()
// Refresh state
$selectedCellId = null
$hoveredRowId = null
$selectedRows = {}
}
// Show confirmation
confirmationStore.actions.showConfirmation(
"Delete rows",
`Are you sure you want to delete ${selectedRowCount} row${
selectedRowCount === 1 ? "" : "s"
}?`,
performDeletion
)
}
</script>
<div class="controls">
<div class="buttons">
<ActionButton icon="Filter" size="S">Filter</ActionButton>
<ActionButton icon="Group" size="S">Group</ActionButton>
<ActionButton icon="SortOrderDown" size="S">Sort</ActionButton>
<ActionButton icon="VisibilityOff" size="S">Hide fields</ActionButton>
</div>
<div class="title">Sales Records</div>
<div class="delete">
{#if selectedRowCount}
<ActionButton icon="Delete" size="S" on:click={deleteRows}>
Delete {selectedRowCount} row{selectedRowCount === 1 ? "" : "s"}
</ActionButton>
{:else}
{rowCount} row{rowCount === 1 ? "" : "s"}
{/if}
</div>
</div>
<style>
.controls {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
height: 36px;
padding: 0 12px;
background: var(--spectrum-global-color-gray-200);
gap: 8px;
border-bottom: 1px solid var(--spectrum-global-color-gray-400);
}
.title {
font-weight: 600;
}
.buttons {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--cell-spacing);
}
.delete {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
color: var(--spectrum-global-color-gray-700);
}
.delete :global(.spectrum-ActionButton) {
color: var(--spectrum-global-color-red-600);
}
.delete :global(.spectrum-Icon) {
fill: var(--spectrum-global-color-red-600);
}
</style>

View File

@ -0,0 +1,36 @@
<script>
import { getContext } from "svelte"
export let columnIdx
const { resize } = getContext("spreadsheet")
</script>
<div on:mousedown={e => resize.actions.startResizing(columnIdx, e)} />
<style>
div {
position: absolute;
top: 0;
right: 0;
width: 16px;
height: 100%;
}
div:after {
opacity: 0;
content: " ";
position: absolute;
width: 4px;
right: 0;
top: 0;
height: 100%;
background: var(--spectrum-global-color-gray-600);
transition: opacity 130ms ease-out;
}
div:hover {
cursor: col-resize;
}
div:hover:after {
opacity: 1;
}
</style>

View File

@ -2,16 +2,18 @@
import { getContext, setContext } from "svelte"
import { writable } from "svelte/store"
import { fetchData, LuceneUtils } from "@budibase/frontend-core"
import { Icon, ActionButton } from "@budibase/bbui"
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 { getColor } from "./utils.js"
import { createReorderStores } from "./stores/reorder"
import { createResizeStore } from "./stores/resize"
import ReorderPlaceholder from "./ReorderPlaceholder.svelte"
import ResizeSlider from "./ResizeSlider.svelte"
import Header from "./Header.svelte"
export let table
export let filter
@ -24,15 +26,15 @@
// Sheet constants
const limit = 100
const defaultWidth = 160
const minWidth = 100
const rand = Math.random()
// State stores
const rows = writable([])
const columns = writable([])
const hoveredRowId = writable(null)
const selectedCellId = writable(null)
const selectedRows = writable({})
const rows = writable([])
const tableId = writable(table?.tableId)
// Build up spreadsheet context and additional stores
const context = {
@ -42,23 +44,30 @@
hoveredRowId,
selectedCellId,
selectedRows,
tableId,
}
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 horizontallyScrolled = false
let changeCache = {}
let newRows = []
// State for resizing columns
let resizeInitialX
let resizeInitialWidth
let resizeFieldIndex
$: tableId.set(table?.tableId)
$: query = LuceneUtils.buildLuceneQuery(filter)
$: fetch = createFetch(table)
$: fetch.update({
@ -183,42 +192,6 @@
delete changeCache[rowId]
}
const deleteRows = () => {
// Fetch full row objects to be deleted
const rowsToDelete = Object.entries($selectedRows)
.map(entry => {
if (entry[1] === true) {
return $rows.find(x => x._id === entry[0])
} else {
return null
}
})
.filter(x => x != null)
// Deletion callback when confirmed
const performDeletion = async () => {
await API.deleteRows({
tableId: table.tableId,
rows: rowsToDelete,
})
await fetch.refresh()
// Refresh state
$selectedCellId = null
$hoveredRowId = null
$selectedRows = {}
}
// Show confirmation
confirmationStore.actions.showConfirmation(
"Delete rows",
`Are you sure you want to delete ${selectedRowCount} row${
selectedRowCount === 1 ? "" : "s"
}?`,
performDeletion
)
}
const addRow = async field => {
const res = await API.saveRow({ tableId: table.tableId })
$selectedCellId = `${res._id}-${field.name}`
@ -235,53 +208,11 @@
})
$rows = sortedRows
}
const startResizing = (fieldIdx, e) => {
e.stopPropagation()
resizeInitialX = e.clientX
resizeInitialWidth = $columns[fieldIdx].width
resizeFieldIndex = fieldIdx
document.addEventListener("mousemove", onResizeMove)
document.addEventListener("mouseup", stopResizing)
}
const onResizeMove = e => {
const dx = e.clientX - resizeInitialX
columns.update(state => {
state[resizeFieldIndex].width = Math.max(
minWidth,
resizeInitialWidth + dx
)
return state
})
}
const stopResizing = () => {
document.removeEventListener("mousemove", onResizeMove)
document.removeEventListener("mouseup", stopResizing)
}
</script>
<div use:styleable={$component.styles}>
<div class="wrapper" style="--highlight-color:{getColor(0)}">
<div class="controls">
<div class="buttons">
<ActionButton icon="Filter" size="S">Filter</ActionButton>
<ActionButton icon="Group" size="S">Group</ActionButton>
<ActionButton icon="SortOrderDown" size="S">Sort</ActionButton>
<ActionButton icon="VisibilityOff" size="S">Hide fields</ActionButton>
</div>
<div class="title">Sales Records</div>
<div class="delete">
{#if selectedRowCount}
<ActionButton icon="Delete" size="S" on:click={deleteRows}>
Delete {selectedRowCount} row{selectedRowCount === 1 ? "" : "s"}
</ActionButton>
{:else}
{rowCount} row{rowCount === 1 ? "" : "s"}
{/if}
</div>
</div>
<div class="wrapper" class:resize={$resize.columnIdx != null}>
<Header />
<div
class="spreadsheet"
on:scroll={handleScroll}
@ -314,7 +245,7 @@
<span>
{field.name}
</span>
<div class="slider" on:mousedown={e => startResizing(fieldIdx, e)} />
<ResizeSlider columnIdx={fieldIdx} />
</div>
{/each}
<!-- Horizontal spacer -->
@ -434,6 +365,9 @@
--cell-height: 32px;
--cell-font-size: 14px;
}
.wrapper.resize *:hover {
cursor: col-resize;
}
.spreadsheet {
display: grid;
grid-template-columns: var(--grid);
@ -453,40 +387,6 @@
background: var(--cell-background);
}
.controls {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
height: 36px;
padding: 0 12px;
background: var(--spectrum-global-color-gray-200);
gap: 8px;
border-bottom: 1px solid var(--spectrum-global-color-gray-400);
}
.title {
font-weight: 600;
}
.buttons {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--cell-spacing);
}
.delete {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
color: var(--spectrum-global-color-gray-700);
}
.delete :global(.spectrum-ActionButton) {
color: var(--spectrum-global-color-red-600);
}
.delete :global(.spectrum-Icon) {
fill: var(--spectrum-global-color-red-600);
}
/* Cells */
.cell {
height: var(--cell-height);
@ -580,32 +480,6 @@
border-left-color: var(--spectrum-global-color-blue-400);
}
/* Column resizing */
.slider {
position: absolute;
top: 0;
right: 0;
width: 16px;
height: 100%;
}
.slider:after {
opacity: 0;
content: " ";
position: absolute;
width: 4px;
right: 0;
top: 0;
height: 100%;
background: var(--spectrum-global-color-gray-600);
transition: opacity 130ms ease-out;
}
.slider:hover {
cursor: col-resize;
}
.slider:hover:after {
opacity: 1;
}
.label {
padding: 0 12px;
border-right: none;

View File

@ -0,0 +1,60 @@
import { writable, get } from "svelte/store"
const MinColumnWidth = 100
export const createResizeStore = context => {
const { columns } = context
const initialState = {
initialMouseX: null,
initialWidth: null,
columnIdx: null,
}
const resize = writable(initialState)
const startResizing = (columnIdx, e) => {
// Prevent propagation to stop reordering triggering
e.stopPropagation()
// Update state
resize.set({
columnIdx,
initialWidth: get(columns)[columnIdx].width,
initialMouseX: e.clientX,
})
// Add mouse event listeners to handle resizing
document.addEventListener("mousemove", onResizeMouseMove)
document.addEventListener("mouseup", stopResizing)
}
const onResizeMouseMove = e => {
const $resize = get(resize)
const dx = e.clientX - $resize.initialMouseX
const width = get(columns)[$resize.columnIdx].width
const newWidth = Math.max(MinColumnWidth, $resize.initialWidth + dx)
// Skip small updates
if (Math.abs(width - newWidth) < 10) {
return
}
// Update width of column
columns.update(state => {
state[$resize.columnIdx].width = newWidth
return state
})
}
const stopResizing = () => {
resize.set(initialState)
document.removeEventListener("mousemove", onResizeMouseMove)
document.removeEventListener("mouseup", stopResizing)
}
return {
...resize,
actions: {
startResizing,
},
}
}