Improve sheet integration with data section and add horizontal cell inversion

This commit is contained in:
Andrew Kingston 2023-04-13 12:01:16 +01:00
parent 724bff60f2
commit 69f6834886
32 changed files with 296 additions and 332 deletions

View File

@ -143,7 +143,7 @@
} }
fields?.forEach(field => { fields?.forEach(field => {
const fieldSchema = schema[field] const fieldSchema = schema[field]
if (fieldSchema.width) { if (fieldSchema.width && typeof fieldSchema.width === "string") {
style += ` ${fieldSchema.width}` style += ` ${fieldSchema.width}`
} else { } else {
style += " minmax(auto, 1fr)" style += " minmax(auto, 1fr)"

View File

@ -21,7 +21,13 @@
</script> </script>
<div class="wrapper"> <div class="wrapper">
<Sheet {API} tableId={id} allowAddRows={!isUsersTable}> <Sheet
{API}
tableId={id}
allowAddRows={!isUsersTable}
allowDeleteRows={!isUsersTable}
on:updatetable={e => tables.updateTable(e.detail)}
>
<svelte:fragment slot="controls"> <svelte:fragment slot="controls">
{#if isInternal} {#if isInternal}
<SheetCreateViewButton /> <SheetCreateViewButton />

View File

@ -1,15 +1,10 @@
<script> <script>
import { fade } from "svelte/transition" import { fade } from "svelte/transition"
import { goto, params } from "@roxi/routify" import { goto, params } from "@roxi/routify"
import { Table, Modal, Heading, notifications, Layout } from "@budibase/bbui" import { Table, Heading, Layout } from "@budibase/bbui"
import { API } from "api"
import Spinner from "components/common/Spinner.svelte" import Spinner from "components/common/Spinner.svelte"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import DeleteRowsButton from "./buttons/DeleteRowsButton.svelte"
import CreateEditRow from "./modals/CreateEditRow.svelte" import CreateEditRow from "./modals/CreateEditRow.svelte"
import CreateEditUser from "./modals/CreateEditUser.svelte" import CreateEditUser from "./modals/CreateEditUser.svelte"
import CreateEditColumn from "./modals/CreateEditColumn.svelte"
import { cloneDeep } from "lodash/fp"
import { import {
TableNames, TableNames,
UNEDITABLE_USER_FIELDS, UNEDITABLE_USER_FIELDS,
@ -22,7 +17,6 @@
export let data = [] export let data = []
export let tableId export let tableId
export let title export let title
export let allowEditing = false
export let loading = false export let loading = false
export let hideAutocolumns export let hideAutocolumns
export let rowCount export let rowCount
@ -32,12 +26,7 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let selectedRows = [] let selectedRows = []
let editableColumn
let editableRow
let editRowModal
let editColumnModal
let customRenderers = [] let customRenderers = []
let confirmDelete
$: selectedRows, dispatch("selectionUpdated", selectedRows) $: selectedRows, dispatch("selectionUpdated", selectedRows)
$: isUsersTable = tableId === TableNames.USERS $: isUsersTable = tableId === TableNames.USERS
@ -92,36 +81,6 @@
`/builder/app/${$params.application}/data/table/${tableId}/relationship/${rowId}/${fieldName}` `/builder/app/${$params.application}/data/table/${tableId}/relationship/${rowId}/${fieldName}`
) )
} }
const deleteRows = async targetRows => {
try {
await API.deleteRows({
tableId,
rows: targetRows,
})
const deletedRowIds = targetRows.map(row => row._id)
data = data.filter(row => deletedRowIds.indexOf(row._id))
notifications.success(`Successfully deleted ${targetRows.length} rows`)
} catch (error) {
notifications.error("Error deleting rows")
}
}
const editRow = row => {
editableRow = row
if (row) {
editRowModal.show()
}
}
const editColumn = field => {
editableColumn = cloneDeep(schema?.[field])
if (editableColumn) {
editColumnModal.show()
}
}
</script> </script>
<Layout noPadding gap="S"> <Layout noPadding gap="S">
@ -138,16 +97,6 @@
{/if} {/if}
<div class="popovers"> <div class="popovers">
<slot /> <slot />
{#if !isUsersTable && selectedRows.length > 0}
<DeleteRowsButton
on:updaterows
{selectedRows}
deleteRows={async rows => {
await deleteRows(rows)
resetSelectedRows()
}}
/>
{/if}
</div> </div>
</Layout> </Layout>
{#key tableId} {#key tableId}
@ -160,13 +109,7 @@
{rowCount} {rowCount}
{disableSorting} {disableSorting}
{customPlaceholder} {customPlaceholder}
bind:selectedRows
allowSelectRows={allowEditing && !isUsersTable}
allowEditRows={allowEditing}
allowEditColumns={allowEditing}
showAutoColumns={!hideAutocolumns} showAutoColumns={!hideAutocolumns}
on:editcolumn={e => editColumn(e.detail)}
on:editrow={e => editRow(e.detail)}
on:clickrelationship={e => selectRelationship(e.detail)} on:clickrelationship={e => selectRelationship(e.detail)}
on:sort on:sort
> >
@ -176,42 +119,6 @@
{/key} {/key}
</Layout> </Layout>
<Modal bind:this={editRowModal}>
<svelte:component
this={editRowComponent}
on:updaterows
on:deleteRows={() => {
confirmDelete.show()
}}
row={editableRow}
/>
</Modal>
<ConfirmDialog
bind:this={confirmDelete}
okText="Delete"
onOk={async () => {
if (editableRow) {
await deleteRows([editableRow])
}
editableRow = undefined
}}
onCancel={async () => {
editRow(editableRow)
}}
title="Confirm Deletion"
>
Are you sure you want to delete this row?
</ConfirmDialog>
<Modal bind:this={editColumnModal}>
<CreateEditColumn
field={editableColumn}
on:updatecolumns
onClosed={editColumnModal.hide}
/>
</Modal>
<style> <style>
.table-title { .table-title {
height: 24px; height: 24px;

View File

@ -57,7 +57,6 @@
{data} {data}
{loading} {loading}
{type} {type}
allowEditing={false}
rowCount={10} rowCount={10}
bind:hideAutocolumns bind:hideAutocolumns
> >

View File

@ -1,25 +0,0 @@
<script>
import { ActionButton, Modal } from "@budibase/bbui"
import CreateEditColumn from "../modals/CreateEditColumn.svelte"
export let highlighted = false
export let disabled = false
let modal
export const show = () => modal?.show()
</script>
<ActionButton
{disabled}
selected={highlighted}
emphasized={highlighted}
icon="TableColumnAddRight"
quiet
on:click={modal.show}
>
Create column
</ActionButton>
<Modal bind:this={modal}>
<CreateEditColumn on:updatecolumns />
</Modal>

View File

@ -1,27 +0,0 @@
<script>
import { ActionButton, Modal } from "@budibase/bbui"
import CreateEditRow from "../modals/CreateEditRow.svelte"
export let modalContentComponent = CreateEditRow
export let title = "Create row"
export let disabled = false
export let highlighted = false
let modal
export const show = () => modal?.show()
</script>
<ActionButton
{disabled}
emphasized={highlighted}
selected={highlighted}
icon="TableRowAddBottom"
quiet
on:click={modal.show}
>
{title}
</ActionButton>
<Modal bind:this={modal}>
<svelte:component this={modalContentComponent} on:updaterows />
</Modal>

View File

@ -1,15 +0,0 @@
<script>
import { Modal, ActionButton } from "@budibase/bbui"
import CreateViewModal from "../modals/CreateViewModal.svelte"
export let disabled = false
let modal
</script>
<ActionButton {disabled} icon="CollectionAdd" quiet on:click={modal.show}>
Create view
</ActionButton>
<Modal bind:this={modal}>
<CreateViewModal />
</Modal>

View File

@ -1,10 +1,18 @@
<script> <script>
import CreateViewButton from "../CreateViewButton.svelte"
import { getContext } from "svelte" import { getContext } from "svelte"
import { Modal, ActionButton } from "@budibase/bbui"
import CreateViewModal from "../../modals/CreateViewModal.svelte"
const { rows, columns } = getContext("sheet") const { rows, columns } = getContext("sheet")
let modal
$: disabled = !$columns.length || !$rows.length $: disabled = !$columns.length || !$rows.length
</script> </script>
<CreateViewButton {disabled} /> <ActionButton {disabled} icon="CollectionAdd" quiet on:click={modal.show}>
Create view
</ActionButton>
<Modal bind:this={modal}>
<CreateViewModal />
</Modal>

View File

@ -11,7 +11,5 @@
</script> </script>
<Modal bind:this={modal}> <Modal bind:this={modal}>
<CreateEditColumn <CreateEditColumn on:updatecolumns={rows.actions.refreshTableDefinition} />
on:updatecolumns={() => rows.actions.refreshTableDefinition()}
/>
</Modal> </Modal>

View File

@ -2,16 +2,13 @@
import { getContext, onMount } from "svelte" import { getContext, onMount } from "svelte"
import { Modal } from "@budibase/bbui" import { Modal } from "@budibase/bbui"
import CreateEditColumn from "../CreateEditColumn.svelte" import CreateEditColumn from "../CreateEditColumn.svelte"
import { tables } from "stores/backend"
const { rows, subscribe } = getContext("sheet") const { rows, subscribe } = getContext("sheet")
let editableColumn let editableColumn
let editColumnModal let editColumnModal
const updateColumns = () => {
rows.actions.refreshData()
}
const editColumn = column => { const editColumn = column => {
editableColumn = column editableColumn = column
editColumnModal.show() editColumnModal.show()
@ -23,7 +20,6 @@
<Modal bind:this={editColumnModal}> <Modal bind:this={editColumnModal}>
<CreateEditColumn <CreateEditColumn
field={editableColumn} field={editableColumn}
on:updatecolumns={updateColumns} on:updatecolumns={rows.actions.refreshData}
onClosed={editColumnModal.hide}
/> />
</Modal> </Modal>

View File

@ -135,6 +135,17 @@ export function createTablesStore() {
await save(draft) await save(draft)
} }
const updateTable = table => {
const index = get(store).list.findIndex(x => x._id === table._id)
if (index === -1) {
return
}
store.update(state => {
state.list[index] = table
return state
})
}
return { return {
subscribe: derivedStore.subscribe, subscribe: derivedStore.subscribe,
fetch, fetch,
@ -145,6 +156,7 @@ export function createTablesStore() {
delete: deleteTable, delete: deleteTable,
saveField, saveField,
deleteField, deleteField,
updateTable,
} }
} }

View File

@ -8,7 +8,8 @@
export let onChange export let onChange
export let readonly = false export let readonly = false
export let api export let api
export let invert = false export let invertX = false
export let invertY = false
const { API } = getContext("sheet") const { API } = getContext("sheet")
const imageExtensions = ["png", "tiff", "gif", "raw", "jpg", "jpeg"] const imageExtensions = ["png", "tiff", "gif", "raw", "jpg", "jpeg"]
@ -89,7 +90,7 @@
</div> </div>
{#if isOpen} {#if isOpen}
<div class="dropzone" class:invert> <div class="dropzone" class:invertX class:invertY>
<Dropzone <Dropzone
{value} {value}
compact compact
@ -146,7 +147,11 @@
box-shadow: 0 0 8px 4px rgba(0, 0, 0, 0.15); box-shadow: 0 0 8px 4px rgba(0, 0, 0, 0.15);
padding: var(--cell-padding); padding: var(--cell-padding);
} }
.dropzone.invert { .dropzone.invertX {
left: auto;
right: 0;
}
.dropzone.invertY {
transform: translateY(-100%); transform: translateY(-100%);
top: 0; top: 0;
} }

View File

@ -17,7 +17,8 @@
export let row export let row
export let cellId export let cellId
export let updateRow = rows.actions.updateRow export let updateRow = rows.actions.updateRow
export let invert = false export let invertX = false
export let invertY = false
const emptyError = writable(null) const emptyError = writable(null)
@ -74,6 +75,7 @@
onChange={cellAPI.updateValue} onChange={cellAPI.updateValue}
{focused} {focused}
{readonly} {readonly}
{invert} {invertY}
{invertX}
/> />
</SheetCell> </SheetCell>

View File

@ -6,7 +6,8 @@
export let onChange export let onChange
export let readonly = false export let readonly = false
export let api export let api
export let invert = false export let invertX = false
export let invertY = false
let textarea let textarea
let isOpen = false let isOpen = false
@ -49,7 +50,8 @@
{#if isOpen} {#if isOpen}
<textarea <textarea
class:invert class:invertX
class:invertY
bind:this={textarea} bind:this={textarea}
value={value || ""} value={value || ""}
on:change={handleChange} on:change={handleChange}
@ -91,12 +93,17 @@
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
width: calc(100% + 100px); width: calc(100% + var(--max-cell-render-width-overflow));
height: calc(5 * var(--row-height) + 1px); height: var(--max-cell-render-height);
z-index: 1; z-index: 1;
border-radius: 2px; border-radius: 2px;
resize: none;
} }
textarea.invert { textarea.invertX {
left: auto;
right: 0;
}
textarea.invertY {
transform: translateY(-100%); transform: translateY(-100%);
top: calc(100% + 1px); top: calc(100% + 1px);
} }

View File

@ -10,7 +10,8 @@
export let multi = false export let multi = false
export let readonly = false export let readonly = false
export let api export let api
export let invert = false export let invertX = false
export let invertY = false
let isOpen = false let isOpen = false
let focusedOptionIdx = null let focusedOptionIdx = null
@ -97,7 +98,12 @@
</div> </div>
{/if} {/if}
{#if isOpen} {#if isOpen}
<div class="options" class:invert on:wheel={e => e.stopPropagation()}> <div
class="options"
class:invertX
class:invertY
on:wheel={e => e.stopPropagation()}
>
{#each options as option, idx} {#each options as option, idx}
{@const color = getOptionColor(option)} {@const color = getOptionColor(option)}
<div <div
@ -187,7 +193,11 @@
overflow-y: auto; overflow-y: auto;
border: var(--cell-border); border: var(--cell-border);
} }
.options.invert { .options.invertX {
left: auto;
right: 0;
}
.options.invertY {
transform: translateY(-100%); transform: translateY(-100%);
top: 0; top: 0;
} }

View File

@ -10,7 +10,8 @@
export let focused export let focused
export let schema export let schema
export let onChange export let onChange
export let invert = false export let invertX = false
export let invertY = false
const { API } = getContext("sheet") const { API } = getContext("sheet")
@ -215,7 +216,7 @@
</div> </div>
{#if isOpen} {#if isOpen}
<div class="dropdown" class:invert on:wheel|stopPropagation> <div class="dropdown" class:invertX class:invertY on:wheel|stopPropagation>
<div class="search"> <div class="search">
<Input autofocus quiet type="text" bind:value={searchString} /> <Input autofocus quiet type="text" bind:value={searchString} />
</div> </div>
@ -288,7 +289,7 @@
top: 100%; top: 100%;
left: 0; left: 0;
min-width: 100%; min-width: 100%;
max-width: calc(100% + 240px); max-width: calc(100% + var(--max-cell-render-width-overflow));
max-height: var(--max-cell-render-height); max-height: var(--max-cell-render-height);
background: var(--cell-background); background: var(--cell-background);
border: var(--cell-border); border: var(--cell-border);
@ -298,10 +299,14 @@
align-items: stretch; align-items: stretch;
background-color: var(--cell-background-hover); background-color: var(--cell-background-hover);
} }
.dropdown.invert { .dropdown.invertY {
transform: translateY(-100%); transform: translateY(-100%);
top: 0; top: 0;
} }
.dropdown.invertX {
left: auto;
right: 0;
}
.results { .results {
overflow-y: auto; overflow-y: auto;
@ -323,7 +328,7 @@
cursor: pointer; cursor: pointer;
} }
.result .badge { .result .badge {
max-width: 340px; max-width: calc(100% - 30px);
} }
.search { .search {

View File

@ -15,8 +15,10 @@
import UserAvatars from "./UserAvatars.svelte" import UserAvatars from "./UserAvatars.svelte"
import KeyboardManager from "../overlays/KeyboardManager.svelte" import KeyboardManager from "../overlays/KeyboardManager.svelte"
import { clickOutside } from "@budibase/bbui" import { clickOutside } from "@budibase/bbui"
import SheetControls from "./SheetControls.svelte" import {
import { MaxCellRenderHeight } from "../lib/constants" MaxCellRenderHeight,
MaxCellRenderWidthOverflow,
} from "../lib/constants"
import SortButton from "../controls/SortButton.svelte" import SortButton from "../controls/SortButton.svelte"
import AddColumnButton from "../controls/AddColumnButton.svelte" import AddColumnButton from "../controls/AddColumnButton.svelte"
import HideColumnsButton from "../controls/HideColumnsButton.svelte" import HideColumnsButton from "../controls/HideColumnsButton.svelte"
@ -25,11 +27,11 @@
export let API export let API
export let tableId export let tableId
export let allowAddRows = true export let allowAddRows = true
export let allowSelectRows = true
export let allowAddColumns = true export let allowAddColumns = true
export let allowEditColumns = true export let allowEditColumns = true
export let allowExpandRows = true export let allowExpandRows = true
export let allowEditRows = true export let allowEditRows = true
export let allowDeleteRows = true
// Sheet constants // Sheet constants
const gutterWidth = 72 const gutterWidth = 72
@ -39,11 +41,11 @@
const tableIdStore = writable(tableId) const tableIdStore = writable(tableId)
const config = writable({ const config = writable({
allowAddRows, allowAddRows,
allowSelectRows,
allowAddColumns, allowAddColumns,
allowEditColumns, allowEditColumns,
allowExpandRows, allowExpandRows,
allowEditRows, allowEditRows,
allowDeleteRows,
}) })
// Build up spreadsheet context // Build up spreadsheet context
@ -64,11 +66,11 @@
$: tableIdStore.set(tableId) $: tableIdStore.set(tableId)
$: config.set({ $: config.set({
allowAddRows, allowAddRows,
allowSelectRows,
allowAddColumns, allowAddColumns,
allowEditColumns, allowEditColumns,
allowExpandRows, allowExpandRows,
allowEditRows, allowEditRows,
allowDeleteRows,
}) })
// Set context for children to consume // Set context for children to consume
@ -86,13 +88,12 @@
id="sheet-{rand}" id="sheet-{rand}"
class:is-resizing={$isResizing} class:is-resizing={$isResizing}
class:is-reordering={$isReordering} class:is-reordering={$isReordering}
style="--row-height:{$rowHeight}px; --gutter-width:{gutterWidth}px; --max-cell-render-height:{MaxCellRenderHeight}px;" style="--row-height:{$rowHeight}px; --gutter-width:{gutterWidth}px; --max-cell-render-height:{MaxCellRenderHeight}px; --max-cell-render-width-overflow:{MaxCellRenderWidthOverflow}px;"
> >
<div class="controls"> <div class="controls">
<div class="controls-left"> <div class="controls-left">
<AddRowButton /> <AddRowButton />
<AddColumnButton /> <AddColumnButton />
<SheetControls />
<slot name="controls" /> <slot name="controls" />
<HideColumnsButton /> <HideColumnsButton />
<SortButton /> <SortButton />

View File

@ -2,16 +2,12 @@
import { getContext, onMount } from "svelte" import { getContext, onMount } from "svelte"
import SheetScrollWrapper from "./SheetScrollWrapper.svelte" import SheetScrollWrapper from "./SheetScrollWrapper.svelte"
import SheetRow from "./SheetRow.svelte" import SheetRow from "./SheetRow.svelte"
import { MaxCellRenderHeight } from "../lib/constants"
const { bounds, renderedRows, visualRowCapacity, rowHeight } = const { bounds, renderedRows, rowVerticalInversionIndex } =
getContext("sheet") getContext("sheet")
let body let body
$: inversionIdx =
$visualRowCapacity - Math.ceil(MaxCellRenderHeight / $rowHeight) - 2
onMount(() => { onMount(() => {
// Observe and record the height of the body // Observe and record the height of the body
const observer = new ResizeObserver(() => { const observer = new ResizeObserver(() => {
@ -27,7 +23,7 @@
<div bind:this={body} class="sheet-body"> <div bind:this={body} class="sheet-body">
<SheetScrollWrapper scrollHorizontally scrollVertically wheelInteractive> <SheetScrollWrapper scrollHorizontally scrollVertically wheelInteractive>
{#each $renderedRows as row, idx} {#each $renderedRows as row, idx}
<SheetRow {row} {idx} invert={idx >= inversionIdx} /> <SheetRow {row} {idx} invertY={idx >= $rowVerticalInversionIndex} />
{/each} {/each}
</SheetScrollWrapper> </SheetScrollWrapper>
</div> </div>

View File

@ -1,6 +0,0 @@
<script>
import SortButton from "../controls/SortButton.svelte"
import HideColumnsButton from "../controls/HideColumnsButton.svelte"
import AddRowButton from "../controls/AddRowButton.svelte"
import AddColumnButton from "../controls/AddColumnButton.svelte"
</script>

View File

@ -4,7 +4,7 @@
export let row export let row
export let idx export let idx
export let invert = false export let invertY = false
const { const {
focusedCellId, focusedCellId,
@ -16,6 +16,7 @@
selectedCellMap, selectedCellMap,
focusedRow, focusedRow,
hiddenColumnsWidth, hiddenColumnsWidth,
columnHorizontalInversionIndex,
} = getContext("sheet") } = getContext("sheet")
$: rowSelected = !!$selectedRows[row._id] $: rowSelected = !!$selectedRows[row._id]
@ -28,18 +29,18 @@
<div <div
class="row" class="row"
style={rowFocused ? foo : null}
on:focus on:focus
on:mouseenter={() => ($hoveredRowId = row._id)} on:mouseenter={() => ($hoveredRowId = row._id)}
on:mouseleave={() => ($hoveredRowId = null)} on:mouseleave={() => ($hoveredRowId = null)}
> >
{#each cols as column (column.name)} {#each $renderedColumns as column, idx (column.name)}
{@const cellId = `${row._id}-${column.name}`} {@const cellId = `${row._id}-${column.name}`}
<DataCell <DataCell
{cellId} {cellId}
{column} {column}
{row} {row}
{invert} {invertY}
invertX={idx >= $columnHorizontalInversionIndex}
{rowFocused} {rowFocused}
highlighted={rowHovered || rowFocused || reorderSource === column.name} highlighted={rowHovered || rowFocused || reorderSource === column.name}
selected={rowSelected} selected={rowSelected}

View File

@ -70,7 +70,7 @@
on:wheel={wheelInteractive ? handleWheel : null} on:wheel={wheelInteractive ? handleWheel : null}
on:click|self={() => ($focusedCellId = null)} on:click|self={() => ($focusedCellId = null)}
> >
<div {style}> <div {style} class="inner">
<slot /> <slot />
</div> </div>
</div> </div>
@ -79,5 +79,13 @@
.outer { .outer {
min-width: 100%; min-width: 100%;
min-height: 100%; min-height: 100%;
height: 100%;
width: 100%;
max-height: 100%;
max-width: 100%;
overflow: hidden;
}
.inner {
position: absolute;
} }
</style> </style>

View File

@ -61,8 +61,8 @@
<SheetCell width={gutterWidth}> <SheetCell width={gutterWidth}>
<div class="gutter"> <div class="gutter">
<div class="checkbox visible"> <div class="checkbox visible">
{#if $config.allowSelectRows} {#if $config.allowDeleteRows}
<div on:click={$config.allowSelectRows && selectAll}> <div on:click={selectAll}>
<Checkbox <Checkbox
value={rowCount && selectedRowCount === rowCount} value={rowCount && selectedRowCount === rowCount}
disabled={!$renderedRows.length} disabled={!$renderedRows.length}
@ -104,14 +104,14 @@
<div <div
on:click={() => selectRow(row._id)} on:click={() => selectRow(row._id)}
class="checkbox" class="checkbox"
class:visible={$config.allowSelectRows && class:visible={$config.allowDeleteRows &&
(rowSelected || rowHovered || rowFocused)} (rowSelected || rowHovered || rowFocused)}
> >
<Checkbox value={rowSelected} /> <Checkbox value={rowSelected} />
</div> </div>
<div <div
class="number" class="number"
class:visible={!$config.allowSelectRows || class:visible={!$config.allowDeleteRows ||
!(rowSelected || rowHovered || rowFocused)} !(rowSelected || rowHovered || rowFocused)}
> >
{row.__idx + 1} {row.__idx + 1}

View File

@ -1 +1,4 @@
export const SheetPadding = 264
export const MaxCellRenderHeight = 216 export const MaxCellRenderHeight = 216
export const MaxCellRenderWidthOverflow = 200
export const ScrollBarSize = 8

View File

@ -39,7 +39,7 @@
<Menu> <Menu>
<MenuItem <MenuItem
icon="Delete" icon="Delete"
disabled={!$config.allowEditRows} disabled={!$config.allowDeleteRows}
on:click={deleteRow}>Delete row</MenuItem on:click={deleteRow}>Delete row</MenuItem
> >
<MenuItem <MenuItem

View File

@ -1,6 +1,7 @@
<script> <script>
import { getContext } from "svelte" import { getContext } from "svelte"
import { domDebounce } from "../../../utils/utils" import { domDebounce } from "../../../utils/utils"
import { ScrollBarSize } from "../lib/constants"
const { const {
scroll, scroll,
@ -17,9 +18,6 @@
height, height,
} = getContext("sheet") } = getContext("sheet")
// Bar config
const barOffset = 8
// State for dragging bars // State for dragging bars
let initialMouse let initialMouse
let initialScroll let initialScroll
@ -28,17 +26,17 @@
// Terminology is the same for both axes: // Terminology is the same for both axes:
// renderX - the space available to render the bar in, edge to edge // renderX - the space available to render the bar in, edge to edge
// availX - the space available to render the bar in, until the edge // availX - the space available to render the bar in, until the edge
$: renderHeight = $height - 2 * barOffset $: renderHeight = $height - 2 * ScrollBarSize
$: barHeight = Math.max(50, ($height / $contentHeight) * renderHeight) $: barHeight = Math.max(50, ($height / $contentHeight) * renderHeight)
$: availHeight = renderHeight - barHeight $: availHeight = renderHeight - barHeight
$: barTop = $: barTop =
barOffset + $rowHeight + availHeight * ($scrollTop / $maxScrollTop) ScrollBarSize + $rowHeight + availHeight * ($scrollTop / $maxScrollTop)
// Calculate H scrollbar size and offset // Calculate H scrollbar size and offset
$: renderWidth = $screenWidth - 2 * barOffset $: renderWidth = $screenWidth - 2 * ScrollBarSize
$: barWidth = Math.max(50, ($screenWidth / $contentWidth) * renderWidth) $: barWidth = Math.max(50, ($screenWidth / $contentWidth) * renderWidth)
$: availWidth = renderWidth - barWidth $: availWidth = renderWidth - barWidth
$: barLeft = barOffset + availWidth * ($scrollLeft / $maxScrollLeft) $: barLeft = ScrollBarSize + availWidth * ($scrollLeft / $maxScrollLeft)
// V scrollbar drag handlers // V scrollbar drag handlers
const startVDragging = e => { const startVDragging = e => {
@ -88,14 +86,14 @@
{#if $showVScrollbar} {#if $showVScrollbar}
<div <div
class="v-scrollbar" class="v-scrollbar"
style="top:{barTop}px; height:{barHeight}px;right:{barOffset}px;" style="--size:{ScrollBarSize}px; top:{barTop}px; height:{barHeight}px;"
on:mousedown={startVDragging} on:mousedown={startVDragging}
/> />
{/if} {/if}
{#if $showHScrollbar} {#if $showHScrollbar}
<div <div
class="h-scrollbar" class="h-scrollbar"
style="left:{barLeft}px; width:{barWidth}px;bottom:{barOffset}px;" style="--size:{ScrollBarSize}px; left:{barLeft}px; width:{barWidth}px;"
on:mousedown={startHDragging} on:mousedown={startHDragging}
/> />
{/if} {/if}
@ -112,9 +110,11 @@
opacity: 1; opacity: 1;
} }
.v-scrollbar { .v-scrollbar {
width: 8px; width: var(--size);
right: var(--size);
} }
.h-scrollbar { .h-scrollbar {
height: 8px; height: var(--size);
bottom: var(--size);
} }
</style> </style>

View File

@ -1,4 +1,5 @@
import { derived, get, writable } from "svelte/store" import { derived, get, writable } from "svelte/store"
import { cloneDeep } from "lodash/fp"
export const DefaultColumnWidth = 200 export const DefaultColumnWidth = 200
@ -16,7 +17,6 @@ export const createStores = () => {
const enriched = { const enriched = {
...column, ...column,
left: offset, left: offset,
order: idx,
} }
if (column.visible) { if (column.visible) {
offset += column.width offset += column.width
@ -47,7 +47,7 @@ export const createStores = () => {
} }
export const deriveStores = context => { export const deriveStores = context => {
const { table, gutterWidth, columns, stickyColumn } = context const { table, gutterWidth, columns, stickyColumn, API, dispatch } = context
// Merge new schema fields with existing schema in order to preserve widths // Merge new schema fields with existing schema in order to preserve widths
table.subscribe($table => { table.subscribe($table => {
@ -77,21 +77,13 @@ export const deriveStores = context => {
// Update columns, removing extraneous columns and adding missing ones // Update columns, removing extraneous columns and adding missing ones
columns.set( columns.set(
fields fields
.map(field => { .map(field => ({
// Check if there is an existing column with this name so we can keep
// the width setting
let existing = currentColumns.find(x => x.name === field)
if (!existing && currentStickyColumn?.name === field) {
existing = currentStickyColumn
}
return {
name: field, name: field,
width: existing?.width || schema[field].width || DefaultColumnWidth, width: schema[field].width || DefaultColumnWidth,
schema: schema[field], schema: schema[field],
visible: existing?.visible ?? true, visible: schema[field].visible ?? true,
order: schema[field].order, order: schema[field].order,
} }))
})
.sort((a, b) => { .sort((a, b) => {
// Sort by order first // Sort by order first
const orderA = a.order const orderA = a.order
@ -136,5 +128,69 @@ export const deriveStores = context => {
}) })
}) })
return null // Updates a columns width
const updateColumnWidth = async (columnName, width) => {
const $table = get(table)
await updateTable({
...$table,
schema: {
...$table.schema,
[columnName]: {
...$table.schema[columnName],
width,
},
},
})
}
// Updates a columns visibility
const updateColumnVisibility = async (columnName, visible) => {
const $table = get(table)
await updateTable({
...$table,
schema: {
...$table.schema,
[columnName]: {
...$table.schema[columnName],
visible,
},
},
})
}
// Updates the orders of columns
const updateColumnOrders = async newColumns => {
const $table = get(table)
const newSchema = cloneDeep($table.schema)
Object.keys(newSchema).forEach(column => {
const index = newColumns.indexOf(column)
if (index !== -1) {
newSchema[column].order = index
} else {
delete newSchema[column].order
}
})
await updateTable({
...$table,
schema: newSchema,
})
}
// Updates the table definition
const updateTable = async newTable => {
table.set(newTable)
dispatch("updatetable", newTable)
await API.saveTable(newTable)
}
return {
columns: {
...columns,
actions: {
updateColumnWidth,
updateColumnVisibility,
updateColumnOrders,
},
},
}
} }

View File

@ -24,19 +24,28 @@ export const createStores = () => {
} }
export const deriveStores = context => { export const deriveStores = context => {
const { reorder, columns, scroll, bounds, stickyColumn, ui, table, API } = const {
context reorder,
columns,
visibleColumns,
scroll,
bounds,
stickyColumn,
ui,
table,
API,
} = context
// Callback when dragging on a colum header and starting reordering // Callback when dragging on a colum header and starting reordering
const startReordering = (column, e) => { const startReordering = (column, e) => {
const $columns = get(columns) const $visibleColumns = get(visibleColumns)
const $bounds = get(bounds) const $bounds = get(bounds)
const $scroll = get(scroll) const $scroll = get(scroll)
const $stickyColumn = get(stickyColumn) const $stickyColumn = get(stickyColumn)
ui.actions.blur() ui.actions.blur()
// Generate new breakpoints for the current columns // Generate new breakpoints for the current columns
let breakpoints = $columns.map(col => ({ let breakpoints = $visibleColumns.map(col => ({
x: col.left + col.width, x: col.left + col.width,
column: col.name, column: col.name,
})) }))
@ -93,6 +102,22 @@ export const deriveStores = context => {
const stopReordering = async () => { const stopReordering = async () => {
// Swap position of columns // Swap position of columns
let { sourceColumn, targetColumn } = get(reorder) let { sourceColumn, targetColumn } = get(reorder)
moveColumn(sourceColumn, targetColumn)
// Reset state
reorder.set(reorderInitialState)
// Remove event handlers
document.removeEventListener("mousemove", onReorderMouseMove)
document.removeEventListener("mouseup", stopReordering)
// Save column changes
await saveOrderChanges()
}
// Moves a column after another columns.
// An undefined target column will move the source to index 0.
const moveColumn = (sourceColumn, targetColumn) => {
let $columns = get(columns) let $columns = get(columns)
let sourceIdx = $columns.findIndex(x => x.name === sourceColumn) let sourceIdx = $columns.findIndex(x => x.name === sourceColumn)
let targetIdx = $columns.findIndex(x => x.name === targetColumn) let targetIdx = $columns.findIndex(x => x.name === targetColumn)
@ -105,61 +130,31 @@ export const deriveStores = context => {
state.splice(targetIdx, 0, removed[0]) state.splice(targetIdx, 0, removed[0])
return state.slice() return state.slice()
}) })
// Reset state
reorder.set(reorderInitialState)
// Remove event handlers
document.removeEventListener("mousemove", onReorderMouseMove)
document.removeEventListener("mouseup", stopReordering)
// Persist changes
await saveOrderChanges()
} }
// Moves a column one place left (as appears visually)
const moveColumnLeft = async column => { const moveColumnLeft = async column => {
const $columns = get(columns) const $visibleColumns = get(visibleColumns)
const sourceIdx = $columns.findIndex(x => x.name === column) const sourceIdx = $visibleColumns.findIndex(x => x.name === column)
if (sourceIdx === 0) { moveColumn(column, $visibleColumns[sourceIdx - 2]?.name)
return
}
columns.update(state => {
let tmp = state[sourceIdx]
state[sourceIdx] = state[sourceIdx - 1]
state[sourceIdx - 1] = tmp
return state.slice()
})
// Persist changes
await saveOrderChanges() await saveOrderChanges()
} }
// Moves a column one place right (as appears visually)
const moveColumnRight = async column => { const moveColumnRight = async column => {
const $columns = get(columns) const $visibleColumns = get(visibleColumns)
const sourceIdx = $columns.findIndex(x => x.name === column) const sourceIdx = $visibleColumns.findIndex(x => x.name === column)
if (sourceIdx === $columns.length - 1) { if (sourceIdx === $visibleColumns.length - 1) {
return return
} }
columns.update(state => { moveColumn(column, $visibleColumns[sourceIdx + 1]?.name)
let tmp = state[sourceIdx]
state[sourceIdx] = state[sourceIdx + 1]
state[sourceIdx + 1] = tmp
return state.slice()
})
// Persist changes
await saveOrderChanges() await saveOrderChanges()
} }
// Saves order changes as part of table metadata // Saves order changes as part of table metadata
const saveOrderChanges = async () => { const saveOrderChanges = async () => {
const $table = cloneDeep(get(table)) const newOrder = get(columns).map(column => column.name)
const $columns = get(columns) await columns.actions.updateColumnOrders(newOrder)
$columns.forEach(column => {
$table.schema[column.name].order = column.order
})
const newTable = await API.saveTable($table)
table.set(newTable)
} }
return { return {

View File

@ -23,7 +23,7 @@ export const createStores = () => {
} }
export const deriveStores = context => { export const deriveStores = context => {
const { resize, columns, stickyColumn, ui, table, API, rows } = context const { resize, columns, stickyColumn, ui } = context
// Starts resizing a certain column // Starts resizing a certain column
const startResizing = (column, e) => { const startResizing = (column, e) => {
@ -93,41 +93,25 @@ export const deriveStores = context => {
// Persist width if it changed // Persist width if it changed
if ($resize.width !== $resize.initialWidth) { if ($resize.width !== $resize.initialWidth) {
await saveNewColumnWidth($resize.column, $resize.width) await columns.actions.updateColumnWidth($resize.column, $resize.width)
} }
} }
// Resets a column size back to default // Resets a column size back to default
const resetSize = async column => { const resetSize = async column => {
let columnIdx = get(columns).findIndex(col => col.name === column.name) // let columnIdx = get(columns).findIndex(col => col.name === column.name)
if (columnIdx === -1) { // if (columnIdx === -1) {
stickyColumn.update(state => ({ // stickyColumn.update(state => ({
...state, // ...state,
width: DefaultColumnWidth, // width: DefaultColumnWidth,
})) // }))
} else { // } else {
columns.update(state => { // columns.update(state => {
state[columnIdx].width = DefaultColumnWidth // state[columnIdx].width = DefaultColumnWidth
return [...state] // return [...state]
}) // })
} // }
await saveNewColumnWidth(column.name, DefaultColumnWidth) await columns.actions.updateColumnWidth(column.name, DefaultColumnWidth)
}
// Saves a new column width as part of table metadata
const saveNewColumnWidth = async (columnName, width) => {
const $table = get(table)
const newDefinition = await API.saveTable({
...$table,
schema: {
...$table.schema,
[columnName]: {
...$table.schema[columnName],
width,
},
},
})
table.set(newDefinition)
} }
return { return {

View File

@ -121,6 +121,8 @@ export const deriveStores = context => {
instanceLoaded.set(true) instanceLoaded.set(true)
scroll.set({ top: 0, left: 0 }) scroll.set({ top: 0, left: 0 })
} else if (resetRows) { } else if (resetRows) {
table.set($fetch.definition)
// Only reset top scroll position when resetting rows // Only reset top scroll position when resetting rows
scroll.update(state => ({ ...state, top: 0 })) scroll.update(state => ({ ...state, top: 0 }))
} }

View File

@ -1,6 +1,6 @@
import { writable, derived, get } from "svelte/store" import { writable, derived, get } from "svelte/store"
import { tick } from "svelte" import { tick } from "svelte"
import { DefaultColumnWidth } from "./columns" import { SheetPadding } from "../lib/constants"
export const createStores = () => { export const createStores = () => {
const scroll = writable({ const scroll = writable({
@ -35,7 +35,6 @@ export const deriveStores = context => {
width, width,
height, height,
} = context } = context
const padding = 264
// Memoize store primitives // Memoize store primitives
const stickyColumnWidth = derived(stickyColumn, $col => $col?.width || 0, 0) const stickyColumnWidth = derived(stickyColumn, $col => $col?.width || 0, 0)
@ -43,7 +42,7 @@ export const deriveStores = context => {
// Derive vertical limits // Derive vertical limits
const contentHeight = derived( const contentHeight = derived(
[rows, rowHeight], [rows, rowHeight],
([$rows, $rowHeight]) => $rows.length * $rowHeight + padding, ([$rows, $rowHeight]) => $rows.length * $rowHeight + SheetPadding,
0 0
) )
const maxScrollTop = derived( const maxScrollTop = derived(
@ -56,7 +55,7 @@ export const deriveStores = context => {
const contentWidth = derived( const contentWidth = derived(
[visibleColumns, stickyColumnWidth], [visibleColumns, stickyColumnWidth],
([$visibleColumns, $stickyColumnWidth]) => { ([$visibleColumns, $stickyColumnWidth]) => {
let width = gutterWidth + padding + $stickyColumnWidth let width = gutterWidth + SheetPadding + $stickyColumnWidth
$visibleColumns.forEach(col => { $visibleColumns.forEach(col => {
width += col.width width += col.width
}) })
@ -146,7 +145,7 @@ export const deriveStores = context => {
const $visibleColumns = get(visibleColumns) const $visibleColumns = get(visibleColumns)
const columnName = $focusedCellId?.split("-")[1] const columnName = $focusedCellId?.split("-")[1]
const column = $visibleColumns.find(col => col.name === columnName) const column = $visibleColumns.find(col => col.name === columnName)
const horizontalOffset = DefaultColumnWidth const horizontalOffset = 50
if (!column) { if (!column) {
return return
} }

View File

@ -1,4 +1,9 @@
import { derived, get } from "svelte/store" import { derived, get } from "svelte/store"
import {
MaxCellRenderHeight,
MaxCellRenderWidthOverflow,
ScrollBarSize,
} from "../lib/constants"
export const deriveStores = context => { export const deriveStores = context => {
const { const {
@ -96,11 +101,41 @@ export const deriveStores = context => {
0 0
) )
// Determine the row index at which we should start vertically inverting cell
// dropdowns
const rowVerticalInversionIndex = derived(
[visualRowCapacity, rowHeight],
([$visualRowCapacity, $rowHeight]) => {
return (
$visualRowCapacity - Math.ceil(MaxCellRenderHeight / $rowHeight) - 2
)
}
)
// Determine the column index at which we should start horizontally inverting
// cell dropdowns
const columnHorizontalInversionIndex = derived(
[renderedColumns, scrollLeft, width],
([$renderedColumns, $scrollLeft, $width]) => {
const cutoff = $width + $scrollLeft - ScrollBarSize * 3
let inversionIdx = $renderedColumns.length
for (let i = $renderedColumns.length - 1; i >= 0; i--, inversionIdx--) {
const rightEdge = $renderedColumns[i].left + $renderedColumns[i].width
if (rightEdge + MaxCellRenderWidthOverflow < cutoff) {
break
}
}
return inversionIdx
}
)
return { return {
scrolledRowCount, scrolledRowCount,
visualRowCapacity, visualRowCapacity,
renderedRows, renderedRows,
renderedColumns, renderedColumns,
hiddenColumnsWidth, hiddenColumnsWidth,
rowVerticalInversionIndex,
columnHorizontalInversionIndex,
} }
} }

View File

@ -31,6 +31,8 @@ export interface FieldSchema {
timeOnly?: boolean timeOnly?: boolean
lastID?: number lastID?: number
useRichText?: boolean | null useRichText?: boolean | null
order?: number
width?: number
meta?: { meta?: {
toTable: string toTable: string
toKey: string toKey: string