Break out other components from spreadsheet for cleaner code
This commit is contained in:
parent
fc06811b2c
commit
c834a236b7
|
@ -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>
|
|
@ -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>
|
|
@ -2,16 +2,18 @@
|
||||||
import { getContext, setContext } from "svelte"
|
import { getContext, setContext } from "svelte"
|
||||||
import { writable } from "svelte/store"
|
import { writable } from "svelte/store"
|
||||||
import { fetchData, LuceneUtils } from "@budibase/frontend-core"
|
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 TextCell from "./cells/TextCell.svelte"
|
||||||
import OptionsCell from "./cells/OptionsCell.svelte"
|
import OptionsCell from "./cells/OptionsCell.svelte"
|
||||||
import DateCell from "./cells/DateCell.svelte"
|
import DateCell from "./cells/DateCell.svelte"
|
||||||
import MultiSelectCell from "./cells/MultiSelectCell.svelte"
|
import MultiSelectCell from "./cells/MultiSelectCell.svelte"
|
||||||
import NumberCell from "./cells/NumberCell.svelte"
|
import NumberCell from "./cells/NumberCell.svelte"
|
||||||
import RelationshipCell from "./cells/RelationshipCell.svelte"
|
import RelationshipCell from "./cells/RelationshipCell.svelte"
|
||||||
import { getColor } from "./utils.js"
|
|
||||||
import { createReorderStores } from "./stores/reorder"
|
import { createReorderStores } from "./stores/reorder"
|
||||||
|
import { createResizeStore } from "./stores/resize"
|
||||||
import ReorderPlaceholder from "./ReorderPlaceholder.svelte"
|
import ReorderPlaceholder from "./ReorderPlaceholder.svelte"
|
||||||
|
import ResizeSlider from "./ResizeSlider.svelte"
|
||||||
|
import Header from "./Header.svelte"
|
||||||
|
|
||||||
export let table
|
export let table
|
||||||
export let filter
|
export let filter
|
||||||
|
@ -24,15 +26,15 @@
|
||||||
// Sheet constants
|
// Sheet constants
|
||||||
const limit = 100
|
const limit = 100
|
||||||
const defaultWidth = 160
|
const defaultWidth = 160
|
||||||
const minWidth = 100
|
|
||||||
const rand = Math.random()
|
const rand = Math.random()
|
||||||
|
|
||||||
// State stores
|
// State stores
|
||||||
|
const rows = writable([])
|
||||||
const columns = writable([])
|
const columns = writable([])
|
||||||
const hoveredRowId = writable(null)
|
const hoveredRowId = writable(null)
|
||||||
const selectedCellId = writable(null)
|
const selectedCellId = writable(null)
|
||||||
const selectedRows = writable({})
|
const selectedRows = writable({})
|
||||||
const rows = writable([])
|
const tableId = writable(table?.tableId)
|
||||||
|
|
||||||
// Build up spreadsheet context and additional stores
|
// Build up spreadsheet context and additional stores
|
||||||
const context = {
|
const context = {
|
||||||
|
@ -42,23 +44,30 @@
|
||||||
hoveredRowId,
|
hoveredRowId,
|
||||||
selectedCellId,
|
selectedCellId,
|
||||||
selectedRows,
|
selectedRows,
|
||||||
|
tableId,
|
||||||
}
|
}
|
||||||
const { reorder, reorderPlaceholder } = createReorderStores(context)
|
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", {
|
setContext("spreadsheet", {
|
||||||
...context,
|
...context,
|
||||||
reorder,
|
reorder,
|
||||||
reorderPlaceholder,
|
reorderPlaceholder,
|
||||||
|
resize,
|
||||||
|
spreadsheetAPI,
|
||||||
})
|
})
|
||||||
|
|
||||||
let horizontallyScrolled = false
|
let horizontallyScrolled = false
|
||||||
let changeCache = {}
|
let changeCache = {}
|
||||||
let newRows = []
|
let newRows = []
|
||||||
|
|
||||||
// State for resizing columns
|
$: tableId.set(table?.tableId)
|
||||||
let resizeInitialX
|
|
||||||
let resizeInitialWidth
|
|
||||||
let resizeFieldIndex
|
|
||||||
|
|
||||||
$: query = LuceneUtils.buildLuceneQuery(filter)
|
$: query = LuceneUtils.buildLuceneQuery(filter)
|
||||||
$: fetch = createFetch(table)
|
$: fetch = createFetch(table)
|
||||||
$: fetch.update({
|
$: fetch.update({
|
||||||
|
@ -183,42 +192,6 @@
|
||||||
delete changeCache[rowId]
|
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 addRow = async field => {
|
||||||
const res = await API.saveRow({ tableId: table.tableId })
|
const res = await API.saveRow({ tableId: table.tableId })
|
||||||
$selectedCellId = `${res._id}-${field.name}`
|
$selectedCellId = `${res._id}-${field.name}`
|
||||||
|
@ -235,53 +208,11 @@
|
||||||
})
|
})
|
||||||
$rows = sortedRows
|
$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>
|
</script>
|
||||||
|
|
||||||
<div use:styleable={$component.styles}>
|
<div use:styleable={$component.styles}>
|
||||||
<div class="wrapper" style="--highlight-color:{getColor(0)}">
|
<div class="wrapper" class:resize={$resize.columnIdx != null}>
|
||||||
<div class="controls">
|
<Header />
|
||||||
<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
|
<div
|
||||||
class="spreadsheet"
|
class="spreadsheet"
|
||||||
on:scroll={handleScroll}
|
on:scroll={handleScroll}
|
||||||
|
@ -314,7 +245,7 @@
|
||||||
<span>
|
<span>
|
||||||
{field.name}
|
{field.name}
|
||||||
</span>
|
</span>
|
||||||
<div class="slider" on:mousedown={e => startResizing(fieldIdx, e)} />
|
<ResizeSlider columnIdx={fieldIdx} />
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
<!-- Horizontal spacer -->
|
<!-- Horizontal spacer -->
|
||||||
|
@ -434,6 +365,9 @@
|
||||||
--cell-height: 32px;
|
--cell-height: 32px;
|
||||||
--cell-font-size: 14px;
|
--cell-font-size: 14px;
|
||||||
}
|
}
|
||||||
|
.wrapper.resize *:hover {
|
||||||
|
cursor: col-resize;
|
||||||
|
}
|
||||||
.spreadsheet {
|
.spreadsheet {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: var(--grid);
|
grid-template-columns: var(--grid);
|
||||||
|
@ -453,40 +387,6 @@
|
||||||
background: var(--cell-background);
|
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 */
|
/* Cells */
|
||||||
.cell {
|
.cell {
|
||||||
height: var(--cell-height);
|
height: var(--cell-height);
|
||||||
|
@ -580,32 +480,6 @@
|
||||||
border-left-color: var(--spectrum-global-color-blue-400);
|
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 {
|
.label {
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
border-right: none;
|
border-right: none;
|
||||||
|
|
|
@ -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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue