From 69f68348862fcfd99c7e795ba221f5da5ec0d6ee Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 13 Apr 2023 12:01:16 +0100 Subject: [PATCH] Improve sheet integration with data section and add horizontal cell inversion --- packages/bbui/src/Table/Table.svelte | 2 +- .../backend/DataTable/DataTable.svelte | 8 +- .../components/backend/DataTable/Table.svelte | 95 +------------------ .../backend/DataTable/ViewDataTable.svelte | 1 - .../buttons/CreateColumnButton.svelte | 25 ----- .../DataTable/buttons/CreateRowButton.svelte | 27 ------ .../DataTable/buttons/CreateViewButton.svelte | 15 --- .../sheet/SheetCreateViewButton.svelte | 12 ++- .../sheet/SheetCreateColumnModal.svelte | 4 +- .../modals/sheet/SheetEditColumnModal.svelte | 8 +- packages/builder/src/stores/backend/tables.js | 12 +++ .../sheet/cells/AttachmentCell.svelte | 11 ++- .../components/sheet/cells/DataCell.svelte | 6 +- .../sheet/cells/LongFormCell.svelte | 17 +++- .../components/sheet/cells/OptionsCell.svelte | 16 +++- .../sheet/cells/RelationshipCell.svelte | 15 ++- .../src/components/sheet/layout/Sheet.svelte | 15 +-- .../components/sheet/layout/SheetBody.svelte | 8 +- .../sheet/layout/SheetControls.svelte | 6 -- .../components/sheet/layout/SheetRow.svelte | 9 +- .../sheet/layout/SheetScrollWrapper.svelte | 10 +- .../sheet/layout/StickyColumn.svelte | 8 +- .../src/components/sheet/lib/constants.js | 3 + .../sheet/overlays/MenuOverlay.svelte | 2 +- .../sheet/overlays/ScrollOverlay.svelte | 22 ++--- .../src/components/sheet/stores/columns.js | 92 ++++++++++++++---- .../src/components/sheet/stores/reorder.js | 85 ++++++++--------- .../src/components/sheet/stores/resize.js | 46 +++------ .../src/components/sheet/stores/rows.js | 2 + .../src/components/sheet/stores/scroll.js | 9 +- .../src/components/sheet/stores/viewport.js | 35 +++++++ packages/types/src/documents/app/table.ts | 2 + 32 files changed, 296 insertions(+), 332 deletions(-) delete mode 100644 packages/builder/src/components/backend/DataTable/buttons/CreateColumnButton.svelte delete mode 100644 packages/builder/src/components/backend/DataTable/buttons/CreateRowButton.svelte delete mode 100644 packages/builder/src/components/backend/DataTable/buttons/CreateViewButton.svelte delete mode 100644 packages/frontend-core/src/components/sheet/layout/SheetControls.svelte diff --git a/packages/bbui/src/Table/Table.svelte b/packages/bbui/src/Table/Table.svelte index 19d361c8b1..5bb4ed4523 100644 --- a/packages/bbui/src/Table/Table.svelte +++ b/packages/bbui/src/Table/Table.svelte @@ -143,7 +143,7 @@ } fields?.forEach(field => { const fieldSchema = schema[field] - if (fieldSchema.width) { + if (fieldSchema.width && typeof fieldSchema.width === "string") { style += ` ${fieldSchema.width}` } else { style += " minmax(auto, 1fr)" diff --git a/packages/builder/src/components/backend/DataTable/DataTable.svelte b/packages/builder/src/components/backend/DataTable/DataTable.svelte index 3ea27efa25..bb5e31ff5d 100644 --- a/packages/builder/src/components/backend/DataTable/DataTable.svelte +++ b/packages/builder/src/components/backend/DataTable/DataTable.svelte @@ -21,7 +21,13 @@
- + tables.updateTable(e.detail)} + > {#if isInternal} diff --git a/packages/builder/src/components/backend/DataTable/Table.svelte b/packages/builder/src/components/backend/DataTable/Table.svelte index a0e777c331..4df6e9a306 100644 --- a/packages/builder/src/components/backend/DataTable/Table.svelte +++ b/packages/builder/src/components/backend/DataTable/Table.svelte @@ -1,15 +1,10 @@ @@ -138,16 +97,6 @@ {/if}
- {#if !isUsersTable && selectedRows.length > 0} - { - await deleteRows(rows) - resetSelectedRows() - }} - /> - {/if}
{#key tableId} @@ -160,13 +109,7 @@ {rowCount} {disableSorting} {customPlaceholder} - bind:selectedRows - allowSelectRows={allowEditing && !isUsersTable} - allowEditRows={allowEditing} - allowEditColumns={allowEditing} showAutoColumns={!hideAutocolumns} - on:editcolumn={e => editColumn(e.detail)} - on:editrow={e => editRow(e.detail)} on:clickrelationship={e => selectRelationship(e.detail)} on:sort > @@ -176,42 +119,6 @@ {/key} - - { - confirmDelete.show() - }} - row={editableRow} - /> - - - { - if (editableRow) { - await deleteRows([editableRow]) - } - editableRow = undefined - }} - onCancel={async () => { - editRow(editableRow) - }} - title="Confirm Deletion" -> - Are you sure you want to delete this row? - - - - - - diff --git a/packages/frontend-core/src/components/sheet/layout/StickyColumn.svelte b/packages/frontend-core/src/components/sheet/layout/StickyColumn.svelte index 18df7a16bc..59b5b50f6b 100644 --- a/packages/frontend-core/src/components/sheet/layout/StickyColumn.svelte +++ b/packages/frontend-core/src/components/sheet/layout/StickyColumn.svelte @@ -61,8 +61,8 @@
- {#if $config.allowSelectRows} -
+ {#if $config.allowDeleteRows} +
selectRow(row._id)} class="checkbox" - class:visible={$config.allowSelectRows && + class:visible={$config.allowDeleteRows && (rowSelected || rowHovered || rowFocused)} >
{row.__idx + 1} diff --git a/packages/frontend-core/src/components/sheet/lib/constants.js b/packages/frontend-core/src/components/sheet/lib/constants.js index 3fb5ea9d05..cb4bd5a069 100644 --- a/packages/frontend-core/src/components/sheet/lib/constants.js +++ b/packages/frontend-core/src/components/sheet/lib/constants.js @@ -1 +1,4 @@ +export const SheetPadding = 264 export const MaxCellRenderHeight = 216 +export const MaxCellRenderWidthOverflow = 200 +export const ScrollBarSize = 8 diff --git a/packages/frontend-core/src/components/sheet/overlays/MenuOverlay.svelte b/packages/frontend-core/src/components/sheet/overlays/MenuOverlay.svelte index a4b9369314..ba89707d3f 100644 --- a/packages/frontend-core/src/components/sheet/overlays/MenuOverlay.svelte +++ b/packages/frontend-core/src/components/sheet/overlays/MenuOverlay.svelte @@ -39,7 +39,7 @@ Delete row import { getContext } from "svelte" import { domDebounce } from "../../../utils/utils" + import { ScrollBarSize } from "../lib/constants" const { scroll, @@ -17,9 +18,6 @@ height, } = getContext("sheet") - // Bar config - const barOffset = 8 - // State for dragging bars let initialMouse let initialScroll @@ -28,17 +26,17 @@ // Terminology is the same for both axes: // renderX - the space available to render the bar in, edge to 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) $: availHeight = renderHeight - barHeight $: barTop = - barOffset + $rowHeight + availHeight * ($scrollTop / $maxScrollTop) + ScrollBarSize + $rowHeight + availHeight * ($scrollTop / $maxScrollTop) // Calculate H scrollbar size and offset - $: renderWidth = $screenWidth - 2 * barOffset + $: renderWidth = $screenWidth - 2 * ScrollBarSize $: barWidth = Math.max(50, ($screenWidth / $contentWidth) * renderWidth) $: availWidth = renderWidth - barWidth - $: barLeft = barOffset + availWidth * ($scrollLeft / $maxScrollLeft) + $: barLeft = ScrollBarSize + availWidth * ($scrollLeft / $maxScrollLeft) // V scrollbar drag handlers const startVDragging = e => { @@ -88,14 +86,14 @@ {#if $showVScrollbar}
{/if} {#if $showHScrollbar}
{/if} @@ -112,9 +110,11 @@ opacity: 1; } .v-scrollbar { - width: 8px; + width: var(--size); + right: var(--size); } .h-scrollbar { - height: 8px; + height: var(--size); + bottom: var(--size); } diff --git a/packages/frontend-core/src/components/sheet/stores/columns.js b/packages/frontend-core/src/components/sheet/stores/columns.js index d9c8d5c06a..57919d881b 100644 --- a/packages/frontend-core/src/components/sheet/stores/columns.js +++ b/packages/frontend-core/src/components/sheet/stores/columns.js @@ -1,4 +1,5 @@ import { derived, get, writable } from "svelte/store" +import { cloneDeep } from "lodash/fp" export const DefaultColumnWidth = 200 @@ -16,7 +17,6 @@ export const createStores = () => { const enriched = { ...column, left: offset, - order: idx, } if (column.visible) { offset += column.width @@ -47,7 +47,7 @@ export const createStores = () => { } 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 table.subscribe($table => { @@ -77,21 +77,13 @@ export const deriveStores = context => { // Update columns, removing extraneous columns and adding missing ones columns.set( fields - .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, - width: existing?.width || schema[field].width || DefaultColumnWidth, - schema: schema[field], - visible: existing?.visible ?? true, - order: schema[field].order, - } - }) + .map(field => ({ + name: field, + width: schema[field].width || DefaultColumnWidth, + schema: schema[field], + visible: schema[field].visible ?? true, + order: schema[field].order, + })) .sort((a, b) => { // Sort by order first 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, + }, + }, + } } diff --git a/packages/frontend-core/src/components/sheet/stores/reorder.js b/packages/frontend-core/src/components/sheet/stores/reorder.js index 297c6e40a4..88c4bea61b 100644 --- a/packages/frontend-core/src/components/sheet/stores/reorder.js +++ b/packages/frontend-core/src/components/sheet/stores/reorder.js @@ -24,19 +24,28 @@ export const createStores = () => { } export const deriveStores = context => { - const { reorder, columns, scroll, bounds, stickyColumn, ui, table, API } = - context + const { + reorder, + columns, + visibleColumns, + scroll, + bounds, + stickyColumn, + ui, + table, + API, + } = context // Callback when dragging on a colum header and starting reordering const startReordering = (column, e) => { - const $columns = get(columns) + const $visibleColumns = get(visibleColumns) const $bounds = get(bounds) const $scroll = get(scroll) const $stickyColumn = get(stickyColumn) ui.actions.blur() // Generate new breakpoints for the current columns - let breakpoints = $columns.map(col => ({ + let breakpoints = $visibleColumns.map(col => ({ x: col.left + col.width, column: col.name, })) @@ -93,6 +102,22 @@ export const deriveStores = context => { const stopReordering = async () => { // Swap position of columns 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 sourceIdx = $columns.findIndex(x => x.name === sourceColumn) let targetIdx = $columns.findIndex(x => x.name === targetColumn) @@ -105,61 +130,31 @@ export const deriveStores = context => { state.splice(targetIdx, 0, removed[0]) 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 $columns = get(columns) - const sourceIdx = $columns.findIndex(x => x.name === column) - if (sourceIdx === 0) { - return - } - columns.update(state => { - let tmp = state[sourceIdx] - state[sourceIdx] = state[sourceIdx - 1] - state[sourceIdx - 1] = tmp - return state.slice() - }) - - // Persist changes + const $visibleColumns = get(visibleColumns) + const sourceIdx = $visibleColumns.findIndex(x => x.name === column) + moveColumn(column, $visibleColumns[sourceIdx - 2]?.name) await saveOrderChanges() } + // Moves a column one place right (as appears visually) const moveColumnRight = async column => { - const $columns = get(columns) - const sourceIdx = $columns.findIndex(x => x.name === column) - if (sourceIdx === $columns.length - 1) { + const $visibleColumns = get(visibleColumns) + const sourceIdx = $visibleColumns.findIndex(x => x.name === column) + if (sourceIdx === $visibleColumns.length - 1) { return } - columns.update(state => { - let tmp = state[sourceIdx] - state[sourceIdx] = state[sourceIdx + 1] - state[sourceIdx + 1] = tmp - return state.slice() - }) - - // Persist changes + moveColumn(column, $visibleColumns[sourceIdx + 1]?.name) await saveOrderChanges() } // Saves order changes as part of table metadata const saveOrderChanges = async () => { - const $table = cloneDeep(get(table)) - const $columns = get(columns) - $columns.forEach(column => { - $table.schema[column.name].order = column.order - }) - const newTable = await API.saveTable($table) - table.set(newTable) + const newOrder = get(columns).map(column => column.name) + await columns.actions.updateColumnOrders(newOrder) } return { diff --git a/packages/frontend-core/src/components/sheet/stores/resize.js b/packages/frontend-core/src/components/sheet/stores/resize.js index dab52a2e78..00a58737db 100644 --- a/packages/frontend-core/src/components/sheet/stores/resize.js +++ b/packages/frontend-core/src/components/sheet/stores/resize.js @@ -23,7 +23,7 @@ export const createStores = () => { } export const deriveStores = context => { - const { resize, columns, stickyColumn, ui, table, API, rows } = context + const { resize, columns, stickyColumn, ui } = context // Starts resizing a certain column const startResizing = (column, e) => { @@ -93,41 +93,25 @@ export const deriveStores = context => { // Persist width if it changed 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 const resetSize = async column => { - let columnIdx = get(columns).findIndex(col => col.name === column.name) - if (columnIdx === -1) { - stickyColumn.update(state => ({ - ...state, - width: DefaultColumnWidth, - })) - } else { - columns.update(state => { - state[columnIdx].width = DefaultColumnWidth - return [...state] - }) - } - await saveNewColumnWidth(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) + // let columnIdx = get(columns).findIndex(col => col.name === column.name) + // if (columnIdx === -1) { + // stickyColumn.update(state => ({ + // ...state, + // width: DefaultColumnWidth, + // })) + // } else { + // columns.update(state => { + // state[columnIdx].width = DefaultColumnWidth + // return [...state] + // }) + // } + await columns.actions.updateColumnWidth(column.name, DefaultColumnWidth) } return { diff --git a/packages/frontend-core/src/components/sheet/stores/rows.js b/packages/frontend-core/src/components/sheet/stores/rows.js index 27304ec996..72795f5d74 100644 --- a/packages/frontend-core/src/components/sheet/stores/rows.js +++ b/packages/frontend-core/src/components/sheet/stores/rows.js @@ -121,6 +121,8 @@ export const deriveStores = context => { instanceLoaded.set(true) scroll.set({ top: 0, left: 0 }) } else if (resetRows) { + table.set($fetch.definition) + // Only reset top scroll position when resetting rows scroll.update(state => ({ ...state, top: 0 })) } diff --git a/packages/frontend-core/src/components/sheet/stores/scroll.js b/packages/frontend-core/src/components/sheet/stores/scroll.js index 64e2cb75bd..37ad38b4ab 100644 --- a/packages/frontend-core/src/components/sheet/stores/scroll.js +++ b/packages/frontend-core/src/components/sheet/stores/scroll.js @@ -1,6 +1,6 @@ import { writable, derived, get } from "svelte/store" import { tick } from "svelte" -import { DefaultColumnWidth } from "./columns" +import { SheetPadding } from "../lib/constants" export const createStores = () => { const scroll = writable({ @@ -35,7 +35,6 @@ export const deriveStores = context => { width, height, } = context - const padding = 264 // Memoize store primitives const stickyColumnWidth = derived(stickyColumn, $col => $col?.width || 0, 0) @@ -43,7 +42,7 @@ export const deriveStores = context => { // Derive vertical limits const contentHeight = derived( [rows, rowHeight], - ([$rows, $rowHeight]) => $rows.length * $rowHeight + padding, + ([$rows, $rowHeight]) => $rows.length * $rowHeight + SheetPadding, 0 ) const maxScrollTop = derived( @@ -56,7 +55,7 @@ export const deriveStores = context => { const contentWidth = derived( [visibleColumns, stickyColumnWidth], ([$visibleColumns, $stickyColumnWidth]) => { - let width = gutterWidth + padding + $stickyColumnWidth + let width = gutterWidth + SheetPadding + $stickyColumnWidth $visibleColumns.forEach(col => { width += col.width }) @@ -146,7 +145,7 @@ export const deriveStores = context => { const $visibleColumns = get(visibleColumns) const columnName = $focusedCellId?.split("-")[1] const column = $visibleColumns.find(col => col.name === columnName) - const horizontalOffset = DefaultColumnWidth + const horizontalOffset = 50 if (!column) { return } diff --git a/packages/frontend-core/src/components/sheet/stores/viewport.js b/packages/frontend-core/src/components/sheet/stores/viewport.js index c5fd8e47f2..9da3523480 100644 --- a/packages/frontend-core/src/components/sheet/stores/viewport.js +++ b/packages/frontend-core/src/components/sheet/stores/viewport.js @@ -1,4 +1,9 @@ import { derived, get } from "svelte/store" +import { + MaxCellRenderHeight, + MaxCellRenderWidthOverflow, + ScrollBarSize, +} from "../lib/constants" export const deriveStores = context => { const { @@ -96,11 +101,41 @@ export const deriveStores = context => { 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 { scrolledRowCount, visualRowCapacity, renderedRows, renderedColumns, hiddenColumnsWidth, + rowVerticalInversionIndex, + columnHorizontalInversionIndex, } } diff --git a/packages/types/src/documents/app/table.ts b/packages/types/src/documents/app/table.ts index 929409d0e9..60b528e1b0 100644 --- a/packages/types/src/documents/app/table.ts +++ b/packages/types/src/documents/app/table.ts @@ -31,6 +31,8 @@ export interface FieldSchema { timeOnly?: boolean lastID?: number useRichText?: boolean | null + order?: number + width?: number meta?: { toTable: string toKey: string