Migrate sheet to data section, improve reordering and reszing

This commit is contained in:
Andrew Kingston 2023-02-26 21:29:46 +00:00
parent e9b801e205
commit fae24276f9
23 changed files with 466 additions and 477 deletions

View File

@ -21,7 +21,7 @@
Layout, Layout,
notifications, notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import { fetchData } from "@budibase/frontend-core" import { fetchData, Sheet } from "@budibase/frontend-core"
import { API } from "api" import { API } from "api"
let hideAutocolumns = true let hideAutocolumns = true
@ -153,112 +153,122 @@
</script> </script>
<div> <div>
<Table <Sheet tableId={$tables.selected?._id} {API} />
title={$tables.selected?.name}
schema={enrichedSchema}
{type}
tableId={id}
data={$fetch.rows}
bind:hideAutocolumns
loading={!$fetch.loaded}
on:sort={onSort}
allowEditing
disableSorting
on:updatecolumns={onUpdateColumns}
on:updaterows={onUpdateRows}
on:selectionUpdated={e => {
selectedRows = e.detail
}}
customPlaceholder
>
<div class="buttons">
<div class="left-buttons">
<CreateColumnButton
highlighted={$fetch.loaded && (!hasCols || !hasRows)}
on:updatecolumns={onUpdateColumns}
/>
{#if !isUsersTable}
<CreateRowButton
on:updaterows={onUpdateRows}
title={"Create row"}
modalContentComponent={CreateEditRow}
disabled={!hasCols}
highlighted={$fetch.loaded && hasCols && !hasRows}
/>
{/if}
{#if isInternal}
<CreateViewButton disabled={!hasCols || !hasRows} />
{/if}
</div>
<div class="right-buttons">
<ManageAccessButton resourceId={$tables.selected?._id} />
{#if isUsersTable}
<EditRolesButton />
{/if}
{#if !isInternal}
<ExistingRelationshipButton
table={$tables.selected}
on:updatecolumns={onUpdateColumns}
/>
{/if}
<HideAutocolumnButton bind:hideAutocolumns />
<ImportButton
disabled={$tables.selected?._id === "ta_users"}
tableId={$tables.selected?._id}
on:importrows={onImportData}
/>
<ExportButton
disabled={!hasRows || !hasCols}
view={$tables.selected?._id}
filters={appliedFilter}
sorting={appliedSort}
{selectedRows}
/>
{#key id}
<TableFilterButton
{schema}
{filters}
on:change={onFilter}
disabled={!hasCols}
tableId={id}
/>
{/key}
</div>
</div>
<div slot="placeholder">
<Layout gap="S">
{#if !hasCols}
<Heading>Let's create some columns</Heading>
<Body>
Start building out your table structure<br />
by adding some columns
</Body>
{:else}
<Heading>Now let's add a row</Heading>
<Body>
Add some data to your table<br />
by adding some rows
</Body>
{/if}
</Layout>
</div>
</Table>
{#key id}
<div in:fade={{ delay: 200, duration: 100 }}>
<div class="pagination">
<Pagination
page={$fetch.pageNumber + 1}
hasPrevPage={$fetch.hasPrevPage}
hasNextPage={$fetch.hasNextPage}
goToPrevPage={$fetch.loading ? null : fetch.prevPage}
goToNextPage={$fetch.loading ? null : fetch.nextPage}
/>
</div>
</div>
{/key}
</div> </div>
<!--<div>-->
<!-- <Table-->
<!-- title={$tables.selected?.name}-->
<!-- schema={enrichedSchema}-->
<!-- {type}-->
<!-- tableId={id}-->
<!-- data={$fetch.rows}-->
<!-- bind:hideAutocolumns-->
<!-- loading={!$fetch.loaded}-->
<!-- on:sort={onSort}-->
<!-- allowEditing-->
<!-- disableSorting-->
<!-- on:updatecolumns={onUpdateColumns}-->
<!-- on:updaterows={onUpdateRows}-->
<!-- on:selectionUpdated={e => {-->
<!-- selectedRows = e.detail-->
<!-- }}-->
<!-- customPlaceholder-->
<!-- >-->
<!-- <div class="buttons">-->
<!-- <div class="left-buttons">-->
<!-- <CreateColumnButton-->
<!-- highlighted={$fetch.loaded && (!hasCols || !hasRows)}-->
<!-- on:updatecolumns={onUpdateColumns}-->
<!-- />-->
<!-- {#if !isUsersTable}-->
<!-- <CreateRowButton-->
<!-- on:updaterows={onUpdateRows}-->
<!-- title={"Create row"}-->
<!-- modalContentComponent={CreateEditRow}-->
<!-- disabled={!hasCols}-->
<!-- highlighted={$fetch.loaded && hasCols && !hasRows}-->
<!-- />-->
<!-- {/if}-->
<!-- {#if isInternal}-->
<!-- <CreateViewButton disabled={!hasCols || !hasRows} />-->
<!-- {/if}-->
<!-- </div>-->
<!-- <div class="right-buttons">-->
<!-- <ManageAccessButton resourceId={$tables.selected?._id} />-->
<!-- {#if isUsersTable}-->
<!-- <EditRolesButton />-->
<!-- {/if}-->
<!-- {#if !isInternal}-->
<!-- <ExistingRelationshipButton-->
<!-- table={$tables.selected}-->
<!-- on:updatecolumns={onUpdateColumns}-->
<!-- />-->
<!-- {/if}-->
<!-- <HideAutocolumnButton bind:hideAutocolumns />-->
<!-- <ImportButton-->
<!-- disabled={$tables.selected?._id === "ta_users"}-->
<!-- tableId={$tables.selected?._id}-->
<!-- on:importrows={onImportData}-->
<!-- />-->
<!-- <ExportButton-->
<!-- disabled={!hasRows || !hasCols}-->
<!-- view={$tables.selected?._id}-->
<!-- filters={appliedFilter}-->
<!-- sorting={appliedSort}-->
<!-- {selectedRows}-->
<!-- />-->
<!-- {#key id}-->
<!-- <TableFilterButton-->
<!-- {schema}-->
<!-- {filters}-->
<!-- on:change={onFilter}-->
<!-- disabled={!hasCols}-->
<!-- tableId={id}-->
<!-- />-->
<!-- {/key}-->
<!-- </div>-->
<!-- </div>-->
<!-- <div slot="placeholder">-->
<!-- <Layout gap="S">-->
<!-- {#if !hasCols}-->
<!-- <Heading>Let's create some columns</Heading>-->
<!-- <Body>-->
<!-- Start building out your table structure<br />-->
<!-- by adding some columns-->
<!-- </Body>-->
<!-- {:else}-->
<!-- <Heading>Now let's add a row</Heading>-->
<!-- <Body>-->
<!-- Add some data to your table<br />-->
<!-- by adding some rows-->
<!-- </Body>-->
<!-- {/if}-->
<!-- </Layout>-->
<!-- </div>-->
<!-- </Table>-->
<!-- {#key id}-->
<!-- <div in:fade={{ delay: 200, duration: 100 }}>-->
<!-- <div class="pagination">-->
<!-- <Pagination-->
<!-- page={$fetch.pageNumber + 1}-->
<!-- hasPrevPage={$fetch.hasPrevPage}-->
<!-- hasNextPage={$fetch.hasNextPage}-->
<!-- goToPrevPage={$fetch.loading ? null : fetch.prevPage}-->
<!-- goToNextPage={$fetch.loading ? null : fetch.nextPage}-->
<!-- />-->
<!-- </div>-->
<!-- </div>-->
<!-- {/key}-->
<!--</div>-->
<style> <style>
div {
flex: 1 1 auto;
margin: -28px -40px -40px -40px;
display: flex;
flex-direction: column;
}
.pagination { .pagination {
display: flex; display: flex;
flex-direction: row; flex-direction: row;

View File

@ -1,32 +0,0 @@
<script>
import { getContext } from "svelte"
const { reorderPlaceholder } = getContext("spreadsheet")
$: style = getStyle($reorderPlaceholder)
const getStyle = state => {
return (
`--x:${state.x}px;` +
`--width:${state.width}px;` +
`--height:${state.height}px;`
)
}
</script>
{#if $reorderPlaceholder.x != null}
<div {style} />
{/if}
<style>
div {
height: min(calc(100% - 36px), var(--height));
width: var(--width);
left: var(--x);
position: absolute;
top: 36px;
background: var(--spectrum-global-color-blue-400);
opacity: 0.2;
z-index: 7;
}
</style>

View File

@ -1,36 +0,0 @@
<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

@ -1,13 +0,0 @@
<script>
import SpreadsheetCell from "./SpreadsheetCell.svelte"
import { getContext } from "svelte"
const { selectedCellId, hoveredRowId } = getContext("spreadsheet")
</script>
<SpreadsheetCell
{...$$props}
spacer
on:click={() => ($selectedCellId = null)}
on:mouseenter={() => ($hoveredRowId = null)}
/>

View File

@ -1,108 +0,0 @@
<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

@ -1,17 +0,0 @@
<script>
import { getContext } from "svelte"
const { selectedCellId, hoveredRowId } = getContext("spreadsheet")
</script>
<div
on:click={() => ($selectedCellId = null)}
on:mouseenter={() => ($hoveredRowId = null)}
/>
<style>
div {
grid-column: 1/-1;
height: 180px;
}
</style>

View File

@ -1 +0,0 @@
export { default as spreadsheet } from "./Spreadsheet.svelte"

View File

@ -1,2 +1,3 @@
export { default as SplitPage } from "./SplitPage.svelte" export { default as SplitPage } from "./SplitPage.svelte"
export { default as TestimonialPage } from "./TestimonialPage.svelte" export { default as TestimonialPage } from "./TestimonialPage.svelte"
export { default as Sheet } from "./sheet/Sheet.svelte"

View File

@ -1,26 +1,16 @@
import { writable, get } from "svelte/store" <script>
import { domDebounce } from "../../../../utils/domDebounce.js" import { get } from "svelte/store"
import { getContext } from "svelte"
const MinColumnWidth = 100 const { columns, rand, visibleColumns } = getContext("spreadsheet")
const MinColumnWidth = 100
export const createResizeStore = context => { let initialMouseX = null
const { columns, rand } = context let initialWidth = null
const initialState = { let columnIdx = null
initialMouseX: null,
initialWidth: null,
columnIdx: null,
}
let initialWidth = 0
let width = 0 let width = 0
let left = 0 let left = 0
let initialMouseX = null
let sheet
let columnIdx = 0
let columnCount = 0 let columnCount = 0
let styles
const resize = writable(initialState)
const startResizing = (idx, e) => { const startResizing = (idx, e) => {
const $columns = get(columns) const $columns = get(columns)
@ -37,6 +27,7 @@ export const createResizeStore = context => {
// Add mouse event listeners to handle resizing // Add mouse event listeners to handle resizing
document.addEventListener("mousemove", onResizeMouseMove) document.addEventListener("mousemove", onResizeMouseMove)
document.addEventListener("mouseup", stopResizing) document.addEventListener("mouseup", stopResizing)
document.getElementById(`sheet-${rand}`).classList.add("is-resizing")
} }
const onResizeMouseMove = e => { const onResizeMouseMove = e => {
@ -68,7 +59,6 @@ export const createResizeStore = context => {
// //
// sheet.style.cssText += newStyle // sheet.style.cssText += newStyle
// let cells = sheet.querySelectorAll(`[data-col="${columnIdx}"]`) // let cells = sheet.querySelectorAll(`[data-col="${columnIdx}"]`)
// let left // let left
// cells.forEach(cell => { // cells.forEach(cell => {
@ -93,8 +83,6 @@ export const createResizeStore = context => {
// offset += colWidth // offset += colWidth
// } // }
width = newWidth width = newWidth
// Update width of column // Update width of column
@ -113,15 +101,53 @@ export const createResizeStore = context => {
} }
const stopResizing = () => { const stopResizing = () => {
resize.set(initialState) columnIdx = null
document.removeEventListener("mousemove", onResizeMouseMove) document.removeEventListener("mousemove", onResizeMouseMove)
document.removeEventListener("mouseup", stopResizing) document.removeEventListener("mouseup", stopResizing)
document.getElementById(`sheet-${rand}`).classList.remove("is-resizing")
}
</script>
{#each $columns as col}
<div
class="resize-slider"
class:visible={columnIdx === col.idx}
on:mousedown={e => startResizing(col.idx, e)}
style="--left:{col.left + col.width}px;"
>
<div class="resize-indicator" />
</div>
{/each}
<style>
.resize-slider {
position: absolute;
top: var(--controls-height);
z-index: 6;
height: var(--cell-height);
left: var(--left);
opacity: 0;
padding: 0 16px;
transform: translateX(-50%);
user-select: none;
}
.resize-slider:hover,
.resize-slider.visible {
cursor: col-resize;
opacity: 1;
height: calc(100% - var(--controls-height));
}
.resize-indicator {
margin-left: -1px;
width: 2px;
height: 100%;
background: var(--spectrum-global-color-blue-400);
} }
return { :global(.sheet.is-resizing *) {
...resize, cursor: col-resize !important;
actions: {
startResizing,
},
} }
} :global(.sheet.is-reordering .resize-slider) {
display: none;
}
</style>

View File

@ -1,27 +1,24 @@
<script> <script>
import { getContext, setContext } from "svelte" import { setContext } from "svelte"
import { writable } from "svelte/store" import { writable } from "svelte/store"
import { fetchData, LuceneUtils } from "@budibase/frontend-core" import { fetchData } from "../../fetch/fetchData"
import { LuceneUtils } from "../../utils"
import { Icon } from "@budibase/bbui" import { Icon } from "@budibase/bbui"
import { createReorderStores } from "./stores/reorder" import { createReorderStores } from "./stores/reorder"
import { createResizeStore } from "./stores/resize" import SpreadsheetHeader from "./SheetHeader.svelte"
import ReorderPlaceholder from "./ReorderPlaceholder.svelte" import SpreadsheetBody from "./SheetBody.svelte"
import ResizeSlider from "./ResizeSlider.svelte" import SpreadsheetCell from "./SheetCell.svelte"
import SpreadsheetHeader from "./SpreadsheetHeader.svelte" import SpreadsheetRow from "./SheetRow.svelte"
import SpreadsheetBody from "./SpreadsheetBody.svelte" import ResizeOverlay from "./ResizeOverlay.svelte"
import SpreadsheetCell from "./SpreadsheetCell.svelte"
import SpreadsheetRow from "./SpreadsheetRow.svelte"
export let table export let tableId
export let filter export let filter
export let sortColumn export let sortColumn
export let sortOrder export let sortOrder
export let API
const { styleable, API } = getContext("sdk")
const component = getContext("component")
// Sheet constants // Sheet constants
const cellHeight = 32 const cellHeight = 36
const limit = 100 const limit = 100
const defaultWidth = 160 const defaultWidth = 160
const rand = Math.random() const rand = Math.random()
@ -32,7 +29,6 @@
const hoveredRowId = writable(null) const hoveredRowId = writable(null)
const selectedCellId = writable(null) const selectedCellId = writable(null)
const selectedRows = writable({}) const selectedRows = writable({})
const tableId = writable(table?.tableId)
const changeCache = writable({}) const changeCache = writable({})
const newRows = writable([]) const newRows = writable([])
const visibleRows = writable([0, 0]) const visibleRows = writable([0, 0])
@ -50,6 +46,7 @@
// Build up spreadsheet context and additional stores // Build up spreadsheet context and additional stores
const context = { const context = {
API,
rand, rand,
rows, rows,
columns, columns,
@ -66,11 +63,9 @@
scroll, scroll,
} }
const { reorder, reorderPlaceholder } = createReorderStores(context) const { reorder, reorderPlaceholder } = createReorderStores(context)
const resize = createResizeStore(context)
$: tableId.set(table?.tableId)
$: query = LuceneUtils.buildLuceneQuery(filter) $: query = LuceneUtils.buildLuceneQuery(filter)
$: fetch = createFetch(table) $: fetch = createFetch(tableId)
$: fetch.update({ $: fetch.update({
sortColumn, sortColumn,
sortOrder, sortOrder,
@ -83,10 +78,13 @@
$: updateSortedRows($fetch, $newRows) $: updateSortedRows($fetch, $newRows)
$: renderedRows = $rows.slice($visibleRows[0], $visibleRows[1]) $: renderedRows = $rows.slice($visibleRows[0], $visibleRows[1])
const createFetch = datasource => { const createFetch = tableId => {
return fetchData({ return fetchData({
API, API,
datasource, datasource: {
type: "table",
tableId,
},
options: { options: {
sortColumn, sortColumn,
sortOrder, sortOrder,
@ -99,23 +97,34 @@
// 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, definition }) => {
if (!$columns.length && schema) { if (!schema) {
let fields = Object.keys(schema || {}) $columns = []
const primaryDisplay = definition?.primaryDisplay return
if (primaryDisplay) {
fields = [primaryDisplay, ...fields.filter(x => x !== primaryDisplay)]
}
$columns = fields.map((field, idx) => {
return {
idx,
name: field,
width: defaultWidth,
left: 40 + idx * defaultWidth,
schema: schema[field],
primaryDisplay: field === primaryDisplay,
}
})
} }
const currentColumns = $columns
// Get fields in new schema
let fields = Object.keys(schema || {})
const primaryDisplay = definition?.primaryDisplay
if (primaryDisplay) {
fields = [primaryDisplay, ...fields.filter(x => x !== primaryDisplay)]
}
// Update columns, removing extraneous columns and adding missing ones
let offset = 40
$columns = fields.map((field, idx) => {
const existing = currentColumns.find(x => x.name === field)
const newCol = {
idx,
name: field,
width: existing?.width || defaultWidth,
left: offset,
schema: schema[field],
primaryDisplay: field === primaryDisplay,
}
offset += newCol.width
return newCol
})
} }
const getIconForField = field => { const getIconForField = field => {
@ -174,7 +183,7 @@
const updateSortedRows = (unsortedRows, newRows) => { const updateSortedRows = (unsortedRows, newRows) => {
let foo = unsortedRows.rows let foo = unsortedRows.rows
for (let i = 0; i < 3; i++) { for (let i = 0; i < 0; i++) {
foo = foo.concat(foo.map(x => ({ ...x, _id: x._id + "x" }))) foo = foo.concat(foo.map(x => ({ ...x, _id: x._id + "x" })))
} }
let sortedRows = foo.slice() let sortedRows = foo.slice()
@ -197,110 +206,98 @@
...context, ...context,
reorder, reorder,
reorderPlaceholder, reorderPlaceholder,
resize,
spreadsheetAPI, spreadsheetAPI,
}) })
</script> </script>
<div use:styleable={$component.styles}> <div class="sheet" style="--cell-height:{cellHeight}px;" id="sheet-{rand}">
<div <SpreadsheetHeader />
class="wrapper" <SpreadsheetBody>
class:resize={$resize.columnIdx != null} <div class="row">
style="--cell-height:{cellHeight}px;" <!-- Field headers -->
id="sheet-{rand}" <SpreadsheetCell header label on:click={selectAll} width="40" left="0">
> <input
<SpreadsheetHeader /> type="checkbox"
<SpreadsheetBody> checked={rowCount && selectedRowCount === rowCount}
<div class="row"> />
<!-- Field headers --> </SpreadsheetCell>
<SpreadsheetCell header label on:click={selectAll} width="40" left="0"> {#each $columns as field, fieldIdx}
<input
type="checkbox"
checked={rowCount && selectedRowCount === rowCount}
/>
</SpreadsheetCell>
{#each $columns as field, fieldIdx}
<SpreadsheetCell
header
sticky={fieldIdx === 0}
reorderSource={$reorder.columnIdx === fieldIdx}
reorderTarget={$reorder.swapColumnIdx === fieldIdx}
on:mousedown={e => reorder.actions.startReordering(fieldIdx, e)}
width={field.width}
left={field.left}
>
<Icon
size="S"
name={getIconForField(field)}
color="var(--spectrum-global-color-gray-600)"
/>
<span>
{field.name}
</span>
<ResizeSlider columnIdx={fieldIdx} />
</SpreadsheetCell>
{/each}
</div>
<!-- All real rows -->
{#each renderedRows as row, rowIdx (row._id)}
<SpreadsheetRow {row} rowIdx={rowIdx + $visibleRows[0]} />
{/each}
<!-- New row placeholder -->
<div class="row new" style="--top:{($rows.length + 1) * cellHeight}px;">
<SpreadsheetCell <SpreadsheetCell
label header
on:click={addRow} sticky={fieldIdx === 0}
on:mouseenter={() => ($hoveredRowId = "new")} reorderSource={$reorder.columnIdx === fieldIdx}
rowHovered={$hoveredRowId === "new"} reorderTarget={$reorder.swapColumnIdx === fieldIdx}
width="40" on:mousedown={e => reorder.actions.startReordering(fieldIdx, e)}
left="0" width={field.width}
left={field.left}
> >
<Icon hoverable name="Add" size="S" /> <Icon
</SpreadsheetCell> size="S"
{#each $columns as field, fieldIdx} name={getIconForField(field)}
<SpreadsheetCell color="var(--spectrum-global-color-gray-600)"
sticky={fieldIdx === 0}
rowHovered={$hoveredRowId === "new"}
reorderSource={$reorder.columnIdx === fieldIdx}
reorderTarget={$reorder.swapColumnIdx === fieldIdx}
on:click={() => addRow(field)}
on:mouseenter={() => ($hoveredRowId = "new")}
width={field.width}
left={field.left}
/> />
{/each} <span>
</div> {field.name}
</SpreadsheetBody> </span>
</SpreadsheetCell>
{/each}
</div>
<!-- Placeholder overlay for new column position --> <!-- All real rows -->
<ReorderPlaceholder /> {#each renderedRows as row, rowIdx (row._id)}
</div> <SpreadsheetRow {row} rowIdx={rowIdx + $visibleRows[0]} />
{/each}
<!-- New row placeholder -->
<div class="row new" style="--top:{($rows.length + 1) * cellHeight}px;">
<SpreadsheetCell
label
on:click={addRow}
on:mouseenter={() => ($hoveredRowId = "new")}
rowHovered={$hoveredRowId === "new"}
width="40"
left="0"
>
<Icon hoverable name="Add" size="S" />
</SpreadsheetCell>
{#each $columns as field, fieldIdx}
<SpreadsheetCell
sticky={fieldIdx === 0}
rowHovered={$hoveredRowId === "new"}
reorderSource={$reorder.columnIdx === fieldIdx}
reorderTarget={$reorder.swapColumnIdx === fieldIdx}
on:click={() => addRow(field)}
on:mouseenter={() => ($hoveredRowId = "new")}
width={field.width}
left={field.left}
/>
{/each}
</div>
</SpreadsheetBody>
<ResizeOverlay />
</div> </div>
<style> <style>
.wrapper { .sheet {
flex: 1 1 auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
align-items: stretch; align-items: stretch;
border: 1px solid var(--spectrum-global-color-gray-400);
border-radius: 4px;
position: relative; position: relative;
overflow: hidden;
/* Variables */ /* Variables */
--cell-background: var(--spectrum-global-color-gray-50); --cell-background: var(--spectrum-global-color-gray-50);
--cell-background-hover: var(--spectrum-global-color-gray-100); --cell-background-hover: var(--spectrum-global-color-gray-75);
--cell-padding: 8px; --cell-padding: 10px;
--cell-spacing: 4px; --cell-spacing: 4px;
--cell-font-size: 14px; --cell-font-size: 14px;
--controls-height: 50px;
} }
.wrapper.resize *:hover { .sheet,
cursor: col-resize; .sheet :global(*) {
} box-sizing: border-box;
.wrapper::-webkit-scrollbar-track {
background: var(--cell-background);
} }
.row { .row {
@ -314,4 +311,7 @@
position: absolute; position: absolute;
transform: translateY(var(--top)); transform: translateY(var(--top));
} }
.row :global(> :last-child) {
border-right-width: 1px;
}
</style> </style>

View File

@ -1,6 +1,6 @@
<script> <script>
import { getContext, onMount } from "svelte" import { getContext, onMount } from "svelte"
import { domDebounce } from "../../../utils/domDebounce" import { Utils } from "../../utils"
const { const {
columns, columns,
@ -55,7 +55,7 @@
// } // }
// } // }
const handleScroll = domDebounce( const handleScroll = Utils.domDebounce(
({ left, top }) => { ({ left, top }) => {
// Only update local state when big changes occur // Only update local state when big changes occur
if (Math.abs(top - scrollTop) > 100) { if (Math.abs(top - scrollTop) > 100) {
@ -116,7 +116,7 @@
<div <div
bind:this={ref} bind:this={ref}
class="spreadsheet" class="sheet-body"
class:horizontally-scrolled={horizontallyScrolled} class:horizontally-scrolled={horizontallyScrolled}
on:click|self={() => ($selectedCellId = null)} on:click|self={() => ($selectedCellId = null)}
id={`sheet-${rand}-body`} id={`sheet-${rand}-body`}
@ -131,12 +131,16 @@
</div> </div>
<style> <style>
.spreadsheet { .sheet-body {
display: block; display: block;
height: 800px;
position: relative; position: relative;
cursor: default; cursor: default;
overflow: auto; overflow: auto;
flex: 1 1 auto;
height: 0;
}
.sheet-body::-webkit-scrollbar-track {
background: var(--cell-background);
} }
.content { .content {
min-width: 100%; min-width: 100%;

View File

@ -10,10 +10,11 @@
export let reorderTarget = false export let reorderTarget = false
export let left export let left
export let width export let width
export let column
</script> </script>
<div <div
class="cell" class="cell col-{column}"
class:header class:header
class:label class:label
class:spacer class:spacer
@ -37,7 +38,7 @@
.cell { .cell {
height: var(--cell-height); height: var(--cell-height);
border-style: solid; border-style: solid;
border-color: var(--spectrum-global-color-gray-300); border-color: var(--spectrum-global-color-gray-200);
border-width: 0; border-width: 0;
border-bottom-width: 1px; border-bottom-width: 1px;
border-left-width: 1px; border-left-width: 1px;
@ -67,16 +68,26 @@
.cell:hover { .cell:hover {
cursor: default; cursor: default;
} }
.cell.row-selected { .cell.row-selected:after {
background-color: rgb(224, 242, 255); pointer-events: none;
content: " ";
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 100%;
opacity: 0.2;
background-color: var(--spectrum-global-color-blue-600);
} }
/* Header cells */ /* Header cells */
.cell.header { .cell.header {
background: var(--spectrum-global-color-gray-200); background: var(--background);
padding: 0 var(--cell-padding); padding: 0 var(--cell-padding);
z-index: 3; z-index: 3;
border-color: var(--spectrum-global-color-gray-400); border-color: var(--spectrum-global-color-gray-200);
font-weight: 600;
gap: calc(2 * var(--cell-spacing));
} }
.cell.header :global(span) { .cell.header :global(span) {
flex: 1 1 auto; flex: 1 1 auto;
@ -90,7 +101,7 @@
.cell.sticky { .cell.sticky {
position: sticky; position: sticky;
z-index: 2; z-index: 2;
border-left-color: transparent; border-left-width: 0;
transform: none; transform: none;
left: 40px; left: 40px;
} }
@ -109,7 +120,16 @@
background: var(--spectrum-global-color-gray-200); background: var(--spectrum-global-color-gray-200);
} }
.cell.reorder-target { .cell.reorder-target {
border-left-color: var(--spectrum-global-color-blue-400); z-index: 100;
}
.cell.reorder-target:before {
content: " ";
position: absolute;
left: -2px;
background: var(--spectrum-global-color-blue-400);
width: 2px;
z-index: 100;
height: calc(var(--cell-height) + 1px);
} }
/* Label cells */ /* Label cells */

View File

@ -0,0 +1,112 @@
<script>
import { getContext } from "svelte"
import { ActionButton, Modal, ModalContent } from "@budibase/bbui"
const {
selectedRows,
rows,
selectedCellId,
hoveredRowId,
tableId,
spreadsheetAPI,
API,
} = getContext("spreadsheet")
let modal
$: selectedRowCount = Object.values($selectedRows).filter(x => !!x).length
$: rowCount = $rows.length
$: 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,
rows: rowsToDelete,
})
await spreadsheetAPI.refreshData()
// Refresh state
$selectedCellId = null
$hoveredRowId = null
$selectedRows = {}
}
</script>
<div class="controls">
<div class="buttons">
<ActionButton icon="Filter" quiet size="M">Filter</ActionButton>
<ActionButton icon="Group" quiet size="M">Group</ActionButton>
<ActionButton icon="SortOrderDown" quiet size="M">Sort</ActionButton>
<ActionButton icon="VisibilityOff" quiet size="M">Hide fields</ActionButton>
</div>
<div class="title" />
<div class="delete">
{#if selectedRowCount}
<ActionButton icon="Delete" size="S" on:click={modal.show}>
Delete {selectedRowCount} row{selectedRowCount === 1 ? "" : "s"}
</ActionButton>
{:else}
{rowCount} row{rowCount === 1 ? "" : "s"}
{/if}
</div>
</div>
<Modal bind:this={modal}>
<ModalContent
title="Add screens"
confirmText="Continue"
cancelText="Cancel"
onConfirm={performDeletion}
size="M"
>
Are you sure you want to delete {selectedRowCount}
row{selectedRowCount === 1 ? "" : "s"}?
</ModalContent>
</Modal>
<style>
.controls {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
height: var(--controls-height);
padding: 0 12px;
background: var(--background);
gap: 8px;
border-bottom: 2px solid var(--spectrum-global-color-gray-200);
}
.title {
font-weight: 600;
}
.buttons {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: calc(1 * var(--cell-spacing));
margin-left: -8px;
}
.delete {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
color: var(--spectrum-global-color-gray-900);
font-size: 14px;
}
.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

@ -1,6 +1,6 @@
<script> <script>
import { getContext } from "svelte" import { getContext } from "svelte"
import SpreadsheetCell from "./SpreadsheetCell.svelte" import SpreadsheetCell from "./SheetCell.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"

View File

@ -165,10 +165,10 @@
left: 0; left: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
box-shadow: 0 0 8px 4px rgba(0, 0, 0, 0.15);
justify-content: flex-start; justify-content: flex-start;
align-items: stretch; align-items: stretch;
box-shadow: 0 0 8px 4px rgba(0, 0, 0, 0.15); max-height: calc(6 * var(--cell-height) - 1px);
max-height: 191px;
overflow-y: auto; overflow-y: auto;
z-index: 1; z-index: 1;
} }
@ -180,12 +180,12 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
gap: var(--cell-spacing); gap: var(--cell-spacing);
background-color: var(--cell-background); background-color: var(--cell-background-hover);
} }
.option:first-child { .option:first-child {
flex: 0 0 calc(var(--cell-height) - 1px); flex: 0 0 calc(var(--cell-height) - 1px);
} }
.option:hover { .option:hover {
background-color: var(--cell-background-hover); background-color: var(--spectrum-global-color-gray-200);
} }
</style> </style>

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, bounds, rows, scroll } = context const { columns, bounds, rows, scroll, rand } = context
const reorderInitialState = { const reorderInitialState = {
columnIdx: null, columnIdx: null,
swapColumnIdx: null, swapColumnIdx: null,
@ -61,6 +61,7 @@ export const createReorderStores = context => {
// Trigger a move event immediately so ensure a candidate column is chosen // Trigger a move event immediately so ensure a candidate column is chosen
onReorderMouseMove(e) onReorderMouseMove(e)
document.getElementById(`sheet-${rand}`).classList.add("is-reordering")
} }
// Callback when moving the mouse when reordering columns // Callback when moving the mouse when reordering columns
@ -135,6 +136,7 @@ export const createReorderStores = context => {
// Remove event handlers // Remove event handlers
document.removeEventListener("mousemove", onReorderMouseMove) document.removeEventListener("mousemove", onReorderMouseMove)
document.removeEventListener("mouseup", stopReordering) document.removeEventListener("mouseup", stopReordering)
document.getElementById(`sheet-${rand}`).classList.remove("is-reordering")
} }
return { return {

View File

@ -86,3 +86,24 @@ export const throttle = (callback, minDelay = 1000) => {
} }
return invoke return invoke
} }
/**
* Utility to debounce DOM activities using requestAnimationFrame
* @param callback the function to run
* @param extractParams
* @returns {Function}
*/
export const domDebounce = (callback, extractParams = x => x) => {
let active = false
let lastParams
return (...params) => {
lastParams = extractParams(...params)
if (!active) {
active = true
requestAnimationFrame(() => {
callback(lastParams)
active = false
})
}
}
}