Add sheet button to control column visibilty, improve sorting, improve disabled states

This commit is contained in:
Andrew Kingston 2023-03-11 14:10:45 +00:00
parent c573955998
commit 57c82c4a5d
17 changed files with 206 additions and 115 deletions

View File

@ -1467,11 +1467,6 @@
dependencies:
"@sinonjs/commons" "^1.7.0"
"@socket.io/component-emitter@~3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553"
integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==
"@spectrum-css/accordion@^3.0.24":
version "3.0.30"
resolved "https://registry.yarnpkg.com/@spectrum-css/accordion/-/accordion-3.0.30.tgz#0893a6db28bab984bf5adaf7e1ba194e741db615"
@ -2586,7 +2581,7 @@ dayjs@^1.10.4, dayjs@^1.11.2:
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.7.tgz#4b296922642f70999544d1144a2c25730fce63e2"
integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==
debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2:
debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
@ -2779,22 +2774,6 @@ end-of-stream@^1.1.0:
dependencies:
once "^1.4.0"
engine.io-client@~6.4.0:
version "6.4.0"
resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.4.0.tgz#88cd3082609ca86d7d3c12f0e746d12db4f47c91"
integrity sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g==
dependencies:
"@socket.io/component-emitter" "~3.1.0"
debug "~4.3.1"
engine.io-parser "~5.0.3"
ws "~8.11.0"
xmlhttprequest-ssl "~2.0.0"
engine.io-parser@~5.0.3:
version "5.0.6"
resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.6.tgz#7811244af173e157295dec9b2718dfe42a64ef45"
integrity sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==
enquirer@^2.3.6:
version "2.3.6"
resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d"
@ -5917,24 +5896,6 @@ snapdragon@^0.8.1:
source-map-resolve "^0.5.0"
use "^3.1.0"
socket.io-client@^4.6.1:
version "4.6.1"
resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.6.1.tgz#80d97d5eb0feca448a0fb6d69a7b222d3d547eab"
integrity sha512-5UswCV6hpaRsNg5kkEHVcbBIXEYoVbMQaHJBXJCyEQ+CiFPV1NIOY0XOFWG4XR4GZcB8Kn6AsRs/9cy9TbqVMQ==
dependencies:
"@socket.io/component-emitter" "~3.1.0"
debug "~4.3.2"
engine.io-client "~6.4.0"
socket.io-parser "~4.2.1"
socket.io-parser@~4.2.1:
version "4.2.2"
resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.2.tgz#1dd384019e25b7a3d374877f492ab34f2ad0d206"
integrity sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw==
dependencies:
"@socket.io/component-emitter" "~3.1.0"
debug "~4.3.1"
source-map-js@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
@ -6752,11 +6713,6 @@ ws@^7.4.6:
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591"
integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==
ws@~8.11.0:
version "8.11.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143"
integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==
xml-name-validator@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
@ -6767,11 +6723,6 @@ xmlchars@^2.2.0:
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
xmlhttprequest-ssl@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67"
integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==
y18n@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"

View File

@ -2,12 +2,14 @@
import { Icon } from "@budibase/bbui"
import { getContext } from "svelte"
const { dispatch } = getContext("sheet")
const { dispatch, columns } = getContext("sheet")
</script>
<div class="add-component" on:click={() => dispatch("add-row")}>
<Icon size="XL" name="Add" />
</div>
{#if $columns.length}
<div class="add-component" on:click={() => dispatch("add-row")}>
<Icon size="XL" name="Add" />
</div>
{/if}
<style>
.add-component {

View File

@ -4,14 +4,14 @@
import HeaderCell from "./cells/HeaderCell.svelte"
import { Icon } from "@budibase/bbui"
const { visibleColumns, dispatch, config } = getContext("sheet")
const { renderedColumns, dispatch, config } = getContext("sheet")
</script>
<div class="header">
<SheetScrollWrapper scrollVertically={false} wheelInteractive={false}>
<div class="row">
{#each $visibleColumns as column}
<HeaderCell {column} />
{#each $renderedColumns as column, idx}
<HeaderCell {column} {idx} />
{/each}
</div>
</SheetScrollWrapper>
@ -28,6 +28,7 @@
border-bottom: var(--cell-border);
position: relative;
z-index: 1;
height: var(--cell-height);
}
.row {
display: flex;
@ -42,6 +43,7 @@
place-items: center;
width: 46px;
border-left: var(--cell-border);
border-bottom: var(--cell-border);
}
.new-column:hover {
cursor: pointer;

View File

@ -6,7 +6,7 @@
import { getCellRenderer } from "./renderers"
const {
visibleColumns,
renderedColumns,
hoveredRowId,
rows,
selectedCellId,
@ -71,7 +71,7 @@
</div>
<SheetScrollWrapper scrollVertically={false}>
<div class="row">
{#each $visibleColumns as column}
{#each $renderedColumns as column}
{@const cellId = `new-${column.name}`}
<SheetCell
width={column.width}

View File

@ -5,7 +5,7 @@
columns,
resize,
scroll,
visibleColumns,
renderedColumns,
stickyColumn,
isReordering,
} = getContext("sheet")
@ -13,7 +13,7 @@
$: scrollLeft = $scroll.left
$: cutoff = scrollLeft + 40 + ($columns[0]?.width || 0)
$: offset = 40 + ($stickyColumn?.width || 0)
$: columnIdx = $resize.columnIdx
$: column = $resize.column
const getStyle = (column, offset, scrollLeft) => {
const left = offset + column.left + column.width - scrollLeft
@ -25,17 +25,17 @@
{#if $stickyColumn}
<div
class="resize-slider sticky"
class:visible={columnIdx === "sticky"}
class:visible={column === $stickyColumn.name}
on:mousedown={e => resize.actions.startResizing($stickyColumn, e)}
style="left:{40 + $stickyColumn.width}px;"
>
<div class="resize-indicator" />
</div>
{/if}
{#each $visibleColumns as column}
{#each $renderedColumns as column}
<div
class="resize-slider"
class:visible={columnIdx === column.idx}
class:visible={column === column.name}
on:mousedown={e => resize.actions.startResizing(column, e)}
style={getStyle(column, offset, scrollLeft)}
>

View File

@ -3,7 +3,7 @@
import SheetScrollWrapper from "./SheetScrollWrapper.svelte"
import SheetRow from "./SheetRow.svelte"
const { bounds, visibleRows } = getContext("sheet")
const { bounds, renderedRows } = getContext("sheet")
let body
@ -21,7 +21,7 @@
<div bind:this={body} class="sheet-body">
<SheetScrollWrapper>
{#each $visibleRows as row, idx}
{#each $renderedRows as row, idx}
<SheetRow {row} {idx} />
{/each}
</SheetScrollWrapper>

View File

@ -1,7 +1,7 @@
<script>
import { ActionButton } from "@budibase/bbui"
import SortButton from "./controls/SortButton.svelte"
import HideColumnsButton from "./controls/HideColumnsButton.svelte"
</script>
<ActionButton icon="VisibilityOff" quiet size="M">Hide fields</ActionButton>
<HideColumnsButton />
<SortButton />

View File

@ -11,7 +11,7 @@
reorder,
selectedRows,
rows,
visibleColumns,
renderedColumns,
hoveredRowId,
selectedCellMap,
selectedCellRow,
@ -29,7 +29,7 @@
on:mouseover={() => ($hoveredRowId = row._id)}
on:mouseleave={() => ($hoveredRowId = null)}
>
{#each $visibleColumns as column (column.name)}
{#each $renderedColumns as column (column.name)}
{@const cellId = `${row._id}-${column.name}`}
<SheetCell
rowSelected={rowSelected || containsSelectedCell}

View File

@ -6,9 +6,9 @@
cellHeight,
scroll,
bounds,
columns,
visibleRows,
visibleColumns,
renderedRows,
renderedColumns,
hoveredRowId,
maxScrollTop,
maxScrollLeft,
@ -19,7 +19,7 @@
export let scrollHorizontally = true
export let wheelInteractive = true
$: hiddenWidths = calculateHiddenWidths($visibleColumns)
$: hiddenWidths = calculateHiddenWidths($renderedColumns)
$: scrollLeft = $scroll.left
$: scrollTop = $scroll.top
$: style = generateStyle($scroll, hiddenWidths)
@ -31,12 +31,14 @@
}
// Calculates with total width of all columns currently not rendered
const calculateHiddenWidths = visibleColumns => {
const idx = visibleColumns[0]?.idx
const calculateHiddenWidths = renderedColumns => {
const idx = $visibleColumns.findIndex(
col => col.name === renderedColumns[0]?.name
)
let width = 0
if (idx > 0) {
for (let i = 0; i < idx; i++) {
width += $columns[i].width
width += $visibleColumns[i].width
}
}
return width
@ -67,7 +69,7 @@
// Hover row under cursor
const y = clientY - $bounds.top + (newScrollTop % cellHeight)
const hoveredRow = $visibleRows[Math.floor(y / cellHeight)]
const hoveredRow = $renderedRows[Math.floor(y / cellHeight)]
$hoveredRowId = hoveredRow?._id
})
</script>

View File

@ -10,7 +10,7 @@
rows,
selectedRows,
stickyColumn,
visibleRows,
renderedRows,
selectedCellId,
hoveredRowId,
scroll,
@ -77,7 +77,7 @@
<div class="content" on:mouseleave={() => ($hoveredRowId = null)}>
<SheetScrollWrapper scrollHorizontally={false}>
{#each $visibleRows as row, idx}
{#each $renderedRows as row, idx}
{@const rowSelected = !!$selectedRows[row._id]}
{@const rowHovered = $hoveredRowId === row._id}
{@const containsSelectedRow = $selectedCellRow?._id === row._id}
@ -161,13 +161,11 @@
}
.header {
border-bottom: var(--cell-border);
position: relative;
z-index: 2;
}
.header :global(.cell) {
background: var(--spectrum-global-color-gray-100);
border-bottom: none;
}
.row {
display: flex;

View File

@ -5,6 +5,7 @@
import { getIconForField } from "../utils"
export let column
export let idx
export let orderable = true
const {
@ -13,7 +14,7 @@
isResizing,
rand,
sort,
columns,
renderedColumns,
dispatch,
config,
} = getContext("sheet")
@ -23,8 +24,8 @@
let timeout
$: sortedBy = column.name === $sort.column
$: canMoveLeft = orderable && column.idx > 0
$: canMoveRight = orderable && column.idx < $columns.length - 1
$: canMoveLeft = orderable && idx > 0
$: canMoveRight = orderable && idx < $renderedColumns.length - 1
const editColumn = () => {
dispatch("edit-column", column.schema)
@ -147,7 +148,6 @@
background: var(--background);
padding: 0 var(--cell-padding);
gap: calc(2 * var(--cell-spacing));
border-bottom: none;
}
.name {

View File

@ -0,0 +1,88 @@
<script>
import { getContext } from "svelte"
import { ActionButton, Popover, Toggle } from "@budibase/bbui"
const { columns } = getContext("sheet")
let open = false
let anchor
$: anyHidden = $columns.some(col => !col.visible)
const toggleVisibility = (column, visible) => {
columns.update(state => {
const index = state.findIndex(col => col.name === column.name)
state[index].visible = visible
return state.slice()
})
}
const showAll = () => {
columns.update(state => {
return state.map(col => ({
...col,
visible: true,
}))
})
}
const hideAll = () => {
columns.update(state => {
return state.map(col => ({
...col,
visible: false,
}))
})
}
</script>
<div bind:this={anchor}>
<ActionButton
icon="VisibilityOff"
quiet
size="M"
on:click={() => (open = !open)}
selected={open || anyHidden}
disabled={!$columns.length}
>
Hide columns
</ActionButton>
</div>
<Popover bind:open {anchor} align="left">
<div class="content">
<div class="columns">
{#each $columns as column}
<Toggle
size="S"
value={column.visible}
on:change={e => toggleVisibility(column, e.detail)}
/>
<span>{column.name}</span>
{/each}
</div>
<div class="buttons">
<ActionButton on:click={showAll}>Show all</ActionButton>
<ActionButton on:click={hideAll}>Hide all</ActionButton>
</div>
</div>
</Popover>
<style>
.content {
padding: 12px 12px;
display: flex;
flex-direction: column;
gap: 12px;
}
.buttons {
display: flex;
flex-direction: row;
gap: 8px;
}
.columns {
display: grid;
align-items: center;
grid-template-columns: auto 1fr;
}
</style>

View File

@ -2,7 +2,7 @@
import { getContext } from "svelte"
import { ActionButton, Popover, Select } from "@budibase/bbui"
const { sort, columns, stickyColumn } = getContext("sheet")
const { sort, visibleColumns, stickyColumn } = getContext("sheet")
const orderOptions = [
{ label: "A-Z", value: "ascending" },
{ label: "Z-A", value: "descending" },
@ -11,7 +11,8 @@
let open = false
let anchor
$: columnOptions = getColumnOptions($stickyColumn, $columns)
$: columnOptions = getColumnOptions($stickyColumn, $visibleColumns)
$: checkValidSortColumn($sort.column, $stickyColumn, $visibleColumns)
const getColumnOptions = (stickyColumn, columns) => {
let options = []
@ -20,6 +21,29 @@
}
return [...options, ...columns.map(col => col.name)]
}
// Ensure we never have a sort column selected that is not visible
const checkValidSortColumn = (sortColumn, stickyColumn, visibleColumns) => {
if (!sortColumn) {
return
}
if (
sortColumn !== stickyColumn?.name &&
!visibleColumns.some(col => col.name === sortColumn)
) {
if (stickyColumn) {
sort.update(state => ({
...state,
column: stickyColumn.name,
}))
} else {
sort.update(state => ({
...state,
column: visibleColumns[0]?.name,
}))
}
}
}
</script>
<div bind:this={anchor}>
@ -28,7 +52,8 @@
quiet
size="M"
on:click={() => (open = !open)}
selected={!!$sort.order}
selected={!!$sort.column}
disabled={!$visibleColumns.length}
>
Sort
</ActionButton>

View File

@ -10,17 +10,23 @@ export const createColumnsStores = context => {
// automatically calculated
const enrichedColumns = derived(columns, $columns => {
let offset = 0
return $columns.map((column, idx) => {
return $columns.map(column => {
const enriched = {
...column,
idx,
left: offset,
}
offset += column.width
if (column.visible) {
offset += column.width
}
return enriched
})
})
// Derived list of columns which have not been explicitly hidden
const visibleColumns = derived(enrichedColumns, $columns => {
return $columns.filter(col => col.visible)
})
// Merge new schema fields with existing schema in order to preserve widths
schema.subscribe($schema => {
const currentColumns = get(columns)
@ -45,6 +51,7 @@ export const createColumnsStores = context => {
name: field,
width: existing?.width || defaultWidth,
schema: $schema[field],
visible: existing?.visible ?? true,
}
})
)
@ -75,5 +82,6 @@ export const createColumnsStores = context => {
subscribe: enrichedColumns.subscribe,
},
stickyColumn,
visibleColumns,
}
}

View File

@ -5,12 +5,13 @@ export const createResizeStores = context => {
const initialState = {
initialMouseX: null,
initialWidth: null,
column: null,
columnIdx: null,
width: 0,
left: 0,
}
const resize = writable(initialState)
const isResizing = derived(resize, $resize => $resize.columnIdx != null)
const isResizing = derived(resize, $resize => $resize.column != null)
const MinColumnWidth = 100
// Starts resizing a certain column
@ -18,12 +19,20 @@ export const createResizeStores = context => {
// Prevent propagation to stop reordering triggering
e.stopPropagation()
// Find and cache index
let columnIdx = get(columns).findIndex(col => col.name === column.name)
if (columnIdx === -1) {
columnIdx = "sticky"
}
// Set initial store state
resize.set({
width: column.width,
left: column.left,
initialWidth: column.width,
initialMouseX: e.clientX,
columnIdx: column.idx,
column: column.name,
columnIdx,
})
// Add mouse event listeners to handle resizing

View File

@ -17,7 +17,7 @@ export const createScrollStores = context => {
const width = derived(bounds, $bounds => $bounds.width, 0)
const contentHeight = derived(
rows,
$rows => ($rows.length + 1) * cellHeight + padding,
$rows => $rows.length * cellHeight + padding,
0
)
const maxScrollTop = derived(

View File

@ -1,7 +1,7 @@
import { derived, get } from "svelte/store"
export const createViewportStores = context => {
const { cellHeight, columns, rows, scroll, bounds } = context
const { cellHeight, visibleColumns, rows, scroll, bounds } = context
const scrollTop = derived(scroll, $scroll => $scroll.top, 0)
const scrollLeft = derived(scroll, $scroll => $scroll.left, 0)
@ -15,53 +15,59 @@ export const createViewportStores = context => {
const firstRowIdx = derived(scrollTop, $scrollTop => {
return Math.floor($scrollTop / cellHeight)
})
const visibleRowCount = derived(height, $height => {
const renderedRowCount = derived(height, $height => {
return Math.ceil($height / cellHeight)
})
const visibleRows = derived(
[rows, firstRowIdx, visibleRowCount],
const renderedRows = derived(
[rows, firstRowIdx, renderedRowCount],
([$rows, $firstRowIdx, $visibleRowCount]) => {
return $rows.slice($firstRowIdx, $firstRowIdx + $visibleRowCount)
}
)
// Derive visible columns
const visibleColumns = derived(
[columns, scrollLeft, width],
([$columns, $scrollLeft, $width]) => {
if (!$columns.length) {
const renderedColumns = derived(
[visibleColumns, scrollLeft, width],
([$visibleColumns, $scrollLeft, $width]) => {
if (!$visibleColumns.length) {
return []
}
let startColIdx = 0
let rightEdge = $columns[0].width
while (rightEdge < $scrollLeft && startColIdx < $columns.length - 1) {
let rightEdge = $visibleColumns[0].width
while (
rightEdge < $scrollLeft &&
startColIdx < $visibleColumns.length - 1
) {
startColIdx++
rightEdge += $columns[startColIdx].width
rightEdge += $visibleColumns[startColIdx].width
}
let endColIdx = startColIdx + 1
let leftEdge = rightEdge
while (leftEdge < $width + $scrollLeft && endColIdx < $columns.length) {
leftEdge += $columns[endColIdx].width
while (
leftEdge < $width + $scrollLeft &&
endColIdx < $visibleColumns.length
) {
leftEdge += $visibleColumns[endColIdx].width
endColIdx++
}
const nextVisibleColumns = $columns.slice(startColIdx, endColIdx)
const nextRenderedColumns = $visibleColumns.slice(startColIdx, endColIdx)
// Cautiously shrink the number of rendered columns.
// This is to avoid rapidly shrinking and growing the visible column count
// which results in remounting cells
const currentCount = get(visibleColumns).length
if (currentCount === nextVisibleColumns.length + 1) {
return $columns.slice(startColIdx, endColIdx + 1)
const currentCount = get(renderedColumns).length
if (currentCount === nextRenderedColumns.length + 1) {
return $visibleColumns.slice(startColIdx, endColIdx + 1)
} else {
return nextVisibleColumns
return nextRenderedColumns
}
},
[]
)
// Fetch next page when approaching end of data
visibleRows.subscribe($visibleRows => {
const lastVisible = $visibleRows[$visibleRows.length - 1]
renderedRows.subscribe($renderedRows => {
const lastVisible = $renderedRows[$renderedRows.length - 1]
const $rows = get(rows)
const lastRow = $rows[$rows.length - 1]
if (lastVisible && lastRow && lastVisible._id === lastRow._id) {
@ -69,5 +75,5 @@ export const createViewportStores = context => {
}
})
return { visibleRows, visibleColumns }
return { renderedRows, renderedColumns }
}