From 81a28eb4da7285a4b43046a1109c7ab6f3694a53 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 10 Apr 2023 18:46:34 +0100 Subject: [PATCH] Large refactors to row creation, naming and sheet APIs --- packages/bbui/package.json | 2 +- packages/bbui/yarn.lock | 8 +- .../sheet/cells/AttachmentCell.svelte | 6 +- .../components/sheet/cells/BooleanCell.svelte | 4 +- .../components/sheet/cells/DataCell.svelte | 130 ++++----- .../components/sheet/cells/DateCell.svelte | 4 +- .../components/sheet/cells/HeaderCell.svelte | 1 + .../sheet/cells/LongFormCell.svelte | 6 +- .../components/sheet/cells/OptionsCell.svelte | 8 +- .../sheet/cells/RelationshipCell.svelte | 6 +- .../components/sheet/cells/SheetCell.svelte | 43 ++- .../components/sheet/cells/TextCell.svelte | 12 +- .../components/sheet/layout/HeaderRow.svelte | 2 +- .../src/components/sheet/layout/NewRow.svelte | 246 ------------------ .../components/sheet/layout/NewRowTop.svelte | 73 ++---- .../src/components/sheet/layout/Sheet.svelte | 5 +- .../components/sheet/layout/SheetRow.svelte | 29 ++- .../sheet/layout/SheetScrollWrapper.svelte | 24 +- .../sheet/layout/StickyColumn.svelte | 39 +-- .../src/components/sheet/lib/websocket.js | 6 +- .../sheet/overlays/KeyboardManager.svelte | 24 +- .../sheet/overlays/MenuOverlay.svelte | 12 +- .../sheet/overlays/ResizeOverlay.svelte | 5 +- .../src/components/sheet/stores/max-scroll.js | 14 +- .../src/components/sheet/stores/menu.js | 4 +- .../src/components/sheet/stores/rows.js | 21 +- .../src/components/sheet/stores/sheet-api.js | 66 +++++ .../src/components/sheet/stores/ui.js | 37 +-- .../src/components/sheet/stores/users.js | 4 +- .../src/components/sheet/stores/validation.js | 25 ++ .../src/components/sheet/stores/viewport.js | 18 ++ 31 files changed, 376 insertions(+), 508 deletions(-) delete mode 100644 packages/frontend-core/src/components/sheet/layout/NewRow.svelte create mode 100644 packages/frontend-core/src/components/sheet/stores/sheet-api.js create mode 100644 packages/frontend-core/src/components/sheet/stores/validation.js diff --git a/packages/bbui/package.json b/packages/bbui/package.json index b4c0241a49..ee88119d3c 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -84,7 +84,7 @@ "@spectrum-css/vars": "3.0.1", "dayjs": "^1.10.4", "easymde": "^2.16.1", - "svelte-flatpickr": "^3.2.3", + "svelte-flatpickr": "^3.3.2", "svelte-portal": "^1.0.0" }, "resolutions": { diff --git a/packages/bbui/yarn.lock b/packages/bbui/yarn.lock index 16f1feb920..09bf462781 100644 --- a/packages/bbui/yarn.lock +++ b/packages/bbui/yarn.lock @@ -2576,10 +2576,10 @@ supports-color@^7.0.0, supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -svelte-flatpickr@^3.2.3: - version "3.2.3" - resolved "https://registry.yarnpkg.com/svelte-flatpickr/-/svelte-flatpickr-3.2.3.tgz#db5dd7ad832ef83262b45e09737955ad3d591fc8" - integrity sha512-PNkqK4Napx8nTvCwkaUXdnKo8dISThaxEOK+szTUXcY6H0dQM0TSyuoMaVWY2yX7pM+PN5cpCQCcVe8YvTRFSw== +svelte-flatpickr@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/svelte-flatpickr/-/svelte-flatpickr-3.3.2.tgz#f08bcde83d439cb30df6fd07b974d87371f130c1" + integrity sha512-VNJLYyLRDplI63oWX5hJylzAJc2VhTh3z9SNecfjtuPZmP6FZPpg9Fw7rXpkEV2DPovIWj2PtaVxB6Kp9r423w== dependencies: flatpickr "^4.5.2" diff --git a/packages/frontend-core/src/components/sheet/cells/AttachmentCell.svelte b/packages/frontend-core/src/components/sheet/cells/AttachmentCell.svelte index 9cd48cfaca..e753d6e18f 100644 --- a/packages/frontend-core/src/components/sheet/cells/AttachmentCell.svelte +++ b/packages/frontend-core/src/components/sheet/cells/AttachmentCell.svelte @@ -4,7 +4,7 @@ import { Dropzone, notifications } from "@budibase/bbui" export let value - export let selected = false + export let focused = false export let onChange export let readonly = false export let api @@ -15,9 +15,9 @@ let isOpen = false - $: editable = selected && !readonly + $: editable = focused && !readonly $: { - if (!selected) { + if (!focused) { close() } } diff --git a/packages/frontend-core/src/components/sheet/cells/BooleanCell.svelte b/packages/frontend-core/src/components/sheet/cells/BooleanCell.svelte index 1b5ea6d606..46494177d8 100644 --- a/packages/frontend-core/src/components/sheet/cells/BooleanCell.svelte +++ b/packages/frontend-core/src/components/sheet/cells/BooleanCell.svelte @@ -3,12 +3,12 @@ import { Checkbox } from "@budibase/bbui" export let value - export let selected = false + export let focused = false export let onChange export let readonly = false export let api - $: editable = selected && !readonly + $: editable = focused && !readonly const handleChange = e => { onChange(e.detail) diff --git a/packages/frontend-core/src/components/sheet/cells/DataCell.svelte b/packages/frontend-core/src/components/sheet/cells/DataCell.svelte index edf604295c..35143ce732 100644 --- a/packages/frontend-core/src/components/sheet/cells/DataCell.svelte +++ b/packages/frontend-core/src/components/sheet/cells/DataCell.svelte @@ -1,15 +1,17 @@ -{#key error} - selectedCellId.set(cellId)} - on:contextmenu={e => menu.actions.open(cellId, e)} - width={column.width} - > - - -{/key} - - + focusedCellId.set(cellId)} + on:contextmenu={e => menu.actions.open(cellId, e)} + width={column.width} +> + + diff --git a/packages/frontend-core/src/components/sheet/cells/DateCell.svelte b/packages/frontend-core/src/components/sheet/cells/DateCell.svelte index b4529a02cd..0ebe78b566 100644 --- a/packages/frontend-core/src/components/sheet/cells/DateCell.svelte +++ b/packages/frontend-core/src/components/sheet/cells/DateCell.svelte @@ -5,7 +5,7 @@ export let value export let schema export let onChange - export let selected = false + export let focused = false export let readonly = false // adding the 0- will turn a string like 00:00:00 into a valid ISO @@ -18,7 +18,7 @@ : dateOnly ? "MMM D YYYY" : "MMM D YYYY, HH:mm" - $: editable = selected && !readonly + $: editable = focused && !readonly
diff --git a/packages/frontend-core/src/components/sheet/cells/HeaderCell.svelte b/packages/frontend-core/src/components/sheet/cells/HeaderCell.svelte index e4d37a8ffc..dd90ba9adb 100644 --- a/packages/frontend-core/src/components/sheet/cells/HeaderCell.svelte +++ b/packages/frontend-core/src/components/sheet/cells/HeaderCell.svelte @@ -181,6 +181,7 @@ .header-cell :global(.cell) { padding: 0 var(--cell-padding); gap: calc(2 * var(--cell-spacing)); + background: var(--spectrum-global-color-gray-100); } .header-cell.sorted :global(.cell) { background: var(--spectrum-global-color-gray-200); diff --git a/packages/frontend-core/src/components/sheet/cells/LongFormCell.svelte b/packages/frontend-core/src/components/sheet/cells/LongFormCell.svelte index b4c62cca4c..fb499499c5 100644 --- a/packages/frontend-core/src/components/sheet/cells/LongFormCell.svelte +++ b/packages/frontend-core/src/components/sheet/cells/LongFormCell.svelte @@ -2,7 +2,7 @@ import { onMount, tick } from "svelte" export let value - export let selected = false + export let focused = false export let onChange export let readonly = false export let api @@ -11,9 +11,9 @@ let textarea let isOpen = false - $: editable = selected && !readonly + $: editable = focused && !readonly $: { - if (!selected) { + if (!focused) { isOpen = false } } diff --git a/packages/frontend-core/src/components/sheet/cells/OptionsCell.svelte b/packages/frontend-core/src/components/sheet/cells/OptionsCell.svelte index 3435b2b3b6..a248d64abe 100644 --- a/packages/frontend-core/src/components/sheet/cells/OptionsCell.svelte +++ b/packages/frontend-core/src/components/sheet/cells/OptionsCell.svelte @@ -6,7 +6,7 @@ export let value export let schema export let onChange - export let selected = false + export let focused = false export let multi = false export let readonly = false export let api @@ -16,11 +16,11 @@ let focusedOptionIdx = null $: options = schema?.constraints?.inclusion || [] - $: editable = selected && !readonly + $: editable = focused && !readonly $: values = Array.isArray(value) ? value : [value].filter(x => x != null) $: { // Close when deselected - if (!selected) { + if (!focused) { close() } } @@ -180,7 +180,7 @@ left: 0; display: flex; flex-direction: column; - box-shadow: 0 0 8px 4px rgba(0, 0, 0, 0.15); + box-shadow: 4px 4px 10px 2px rgba(0, 0, 0, 0.1); justify-content: flex-start; align-items: stretch; max-height: var(--max-cell-render-height); diff --git a/packages/frontend-core/src/components/sheet/cells/RelationshipCell.svelte b/packages/frontend-core/src/components/sheet/cells/RelationshipCell.svelte index 98c45058a7..56953b02f3 100644 --- a/packages/frontend-core/src/components/sheet/cells/RelationshipCell.svelte +++ b/packages/frontend-core/src/components/sheet/cells/RelationshipCell.svelte @@ -7,7 +7,7 @@ export let value export let api export let readonly - export let selected + export let focused export let schema export let onChange export let invert = false @@ -25,12 +25,12 @@ let results $: oneRowOnly = schema?.relationshipType === "one-to-many" - $: editable = selected && !readonly + $: editable = focused && !readonly $: results = getResults(searchResults, value) $: lookupMap = buildLookupMap(value, isOpen) $: search(searchString) $: { - if (!selected) { + if (!focused) { close() } } diff --git a/packages/frontend-core/src/components/sheet/cells/SheetCell.svelte b/packages/frontend-core/src/components/sheet/cells/SheetCell.svelte index a5c2e1b5f0..a25932b1f8 100644 --- a/packages/frontend-core/src/components/sheet/cells/SheetCell.svelte +++ b/packages/frontend-core/src/components/sheet/cells/SheetCell.svelte @@ -1,7 +1,8 @@ - - -{#if firstColumn} -
- -
- -
-
- - -
-
-
($hoveredRowId = "new")} - on:mouseleave={() => ($hoveredRowId = null)} - > -
0} - > - -
- {#if $config.allowExpandRows} - - {/if} -
-
- {#if $stickyColumn} - {@const cellId = `new-${$stickyColumn.name}`} - - {/if} -
- -
- {#each $renderedColumns as column} - {@const cellId = `new-${column.name}`} - - {/each} -
-
-
-
-
-{/if} - - diff --git a/packages/frontend-core/src/components/sheet/layout/NewRowTop.svelte b/packages/frontend-core/src/components/sheet/layout/NewRowTop.svelte index 37f5218541..4bd5c3f946 100644 --- a/packages/frontend-core/src/components/sheet/layout/NewRowTop.svelte +++ b/packages/frontend-core/src/components/sheet/layout/NewRowTop.svelte @@ -4,11 +4,11 @@ import { Icon, Button } from "@budibase/bbui" import SheetScrollWrapper from "./SheetScrollWrapper.svelte" import DataCell from "../cells/DataCell.svelte" + import { fly } from "svelte/transition" const { - renderedColumns, hoveredRowId, - selectedCellId, + focusedCellId, stickyColumn, gutterWidth, scroll, @@ -19,7 +19,7 @@ showHScrollbar, tableId, subscribe, - selectedCellAPI, + sheetAPI, } = getContext("sheet") let isAdding = false @@ -28,33 +28,16 @@ $: firstColumn = $stickyColumn || $visibleColumns[0] $: rowHovered = $hoveredRowId === "new" - $: containsSelectedCell = $selectedCellId?.startsWith("new-") + $: rowFocused = $focusedCellId?.startsWith("new-") $: width = gutterWidth + ($stickyColumn?.width || 0) $: scrollLeft = $scroll.left $: $tableId, (isAdding = false) - const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) - const addRow = async () => { - // Validate new row - let allColumns = [] - if ($stickyColumn) { - allColumns.push($stickyColumn) - } - allColumns = allColumns.concat($visibleColumns) - for (let col of allColumns) { - $selectedCellId = `new-${col.name}` - await tick() - const error = $selectedCellAPI.validate() - if (error) { - return - } - } - // Create row const savedRow = await rows.actions.addRow(newRow, 0) if (savedRow && firstColumn) { - $selectedCellId = `${savedRow._id}-${firstColumn.name}` + $focusedCellId = `${savedRow._id}-${firstColumn.name}` isAdding = false } @@ -70,14 +53,10 @@ } const startAdding = () => { - scroll.set({ - left: 0, - top: 0, - }) newRow = {} isAdding = true if (firstColumn) { - $selectedCellId = `new-${firstColumn.name}` + $focusedCellId = `new-${firstColumn.name}` } } @@ -95,8 +74,8 @@ -{#if firstColumn} -
+{#if isAdding} +
0} > - +
1
{#if $config.allowExpandRows} @@ -132,8 +107,8 @@ column={$stickyColumn} row={newRow} {rowHovered} - selected={$selectedCellId === cellId} - rowSelected={containsSelectedCell} + focused={$focusedCellId === cellId} + {rowFocused} width={$stickyColumn.width} {updateRow} rowIdx={0} @@ -151,8 +126,8 @@ {column} row={newRow} {rowHovered} - selected={$selectedCellId === cellId} - rowSelected={containsSelectedCell} + focused={$focusedCellId === cellId} + {rowFocused} width={column.width} {updateRow} rowIdx={0} @@ -176,22 +151,17 @@ position: absolute; top: var(--row-height); left: 0; - transform: translateY(-100%); - transition: transform 130ms ease-out; background: linear-gradient( to bottom, var(--cell-background) 20%, transparent 100% ); width: 100%; - padding-bottom: 64px; + padding-bottom: 100px; display: flex; flex-direction: column; align-items: stretch; } - .container.visible { - transform: translateY(0); - } .content { pointer-events: all; background: var(--background); @@ -216,6 +186,7 @@ .sticky-column { display: flex; z-index: 1; + position: relative; } /* Don't show borders between cells in the sticky column */ .sticky-column :global(.cell:not(:last-child)) { @@ -228,13 +199,17 @@ } /* Add shadow when scrolled */ - .sticky.scrolled :global(.cell:last-child:after) { - content: " "; - position: absolute; + .sticky-column.scrolled { + /*box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.1);*/ + } + .sticky-column.scrolled:after { + content: ""; width: 10px; height: 100%; + background: linear-gradient(to right, rgba(0, 0, 0, 0.05), transparent); left: 100%; - background: linear-gradient(to right, rgba(0, 0, 0, 0.08), transparent); + top: 0; + position: absolute; } /* Styles for gutter */ @@ -252,7 +227,7 @@ display: flex; flex-direction: row; gap: 8px; - margin: 16px 0 0 16px; + margin: 24px 0 0 16px; pointer-events: all; align-self: flex-start; } diff --git a/packages/frontend-core/src/components/sheet/layout/Sheet.svelte b/packages/frontend-core/src/components/sheet/layout/Sheet.svelte index 03d8bb2881..22c9ed148a 100644 --- a/packages/frontend-core/src/components/sheet/layout/Sheet.svelte +++ b/packages/frontend-core/src/components/sheet/layout/Sheet.svelte @@ -15,6 +15,8 @@ import { createMenuStores } from "../stores/menu" import { createMaxScrollStores } from "../stores/max-scroll" import { createPaginationStores } from "../stores/pagination" + import { createSheetAPIStores } from "../stores/sheet-api" + import { createValidationStores } from "../stores/validation" import DeleteButton from "../controls/DeleteButton.svelte" import SheetBody from "./SheetBody.svelte" import ResizeOverlay from "../overlays/ResizeOverlay.svelte" @@ -63,11 +65,13 @@ tableId: tableIdStore, } context = { ...context, ...createEventManagers() } + context = { ...context, ...createValidationStores(context) } context = { ...context, ...createBoundsStores(context) } context = { ...context, ...createScrollStores(context) } context = { ...context, ...createRowsStore(context) } context = { ...context, ...createColumnsStores(context) } context = { ...context, ...createUIStores(context) } + context = { ...context, ...createSheetAPIStores(context) } context = { ...context, ...createResizeStores(context) } context = { ...context, ...createViewportStores(context) } context = { ...context, ...createMaxScrollStores(context) } @@ -75,7 +79,6 @@ context = { ...context, ...createUserStores(context) } context = { ...context, ...createMenuStores(context) } context = { ...context, ...createPaginationStores(context) } - context = { ...context, ...context } // Reference some stores for local use const { isResizing, isReordering, ui, loaded, rowHeight } = context diff --git a/packages/frontend-core/src/components/sheet/layout/SheetRow.svelte b/packages/frontend-core/src/components/sheet/layout/SheetRow.svelte index bd291f6ce3..8f97dea33f 100644 --- a/packages/frontend-core/src/components/sheet/layout/SheetRow.svelte +++ b/packages/frontend-core/src/components/sheet/layout/SheetRow.svelte @@ -7,41 +7,47 @@ export let invert = false const { - selectedCellId, + focusedCellId, reorder, selectedRows, + visibleColumns, renderedColumns, hoveredRowId, selectedCellMap, - selectedCellRow, + focusedRow, + hiddenColumnsWidth, } = getContext("sheet") $: rowSelected = !!$selectedRows[row._id] $: rowHovered = $hoveredRowId === row._id - $: containsSelectedCell = $selectedCellRow?._id === row._id + $: rowFocused = $focusedRow?._id === row._id + $: cols = rowFocused ? $visibleColumns : $renderedColumns + $: foo = `margin-left: ${-1 * $hiddenColumnsWidth}px;`
($hoveredRowId = row._id)} on:mouseleave={() => ($hoveredRowId = null)} > - {#each $renderedColumns as column (column.name)} + {#each cols as column (column.name)} {@const cellId = `${row._id}-${column.name}`} {/each}
@@ -50,5 +56,6 @@ .row { width: 0; display: flex; + height: var(--row-height); } diff --git a/packages/frontend-core/src/components/sheet/layout/SheetScrollWrapper.svelte b/packages/frontend-core/src/components/sheet/layout/SheetScrollWrapper.svelte index e7b61a37db..907f69aae6 100644 --- a/packages/frontend-core/src/components/sheet/layout/SheetScrollWrapper.svelte +++ b/packages/frontend-core/src/components/sheet/layout/SheetScrollWrapper.svelte @@ -5,14 +5,13 @@ const { rowHeight, scroll, - visibleColumns, - renderedColumns, - selectedCellId, + focusedCellId, renderedRows, maxScrollTop, maxScrollLeft, bounds, hoveredRowId, + hiddenColumnsWidth, } = getContext("sheet") export let scrollVertically = true @@ -20,8 +19,7 @@ export let wheelInteractive = true export let foo = false - $: hiddenWidths = calculateHiddenWidths($renderedColumns) - $: style = generateStyle($scroll, $rowHeight, hiddenWidths, foo) + $: style = generateStyle($scroll, $rowHeight, $hiddenColumnsWidth, foo) const generateStyle = (scroll, rowHeight, hiddenWidths, foo) => { let offsetX, offsetY @@ -35,20 +33,6 @@ return `transform: translate3d(${offsetX}px, ${offsetY}px, 0);` } - // Calculates with total width of all columns currently not rendered - 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 += $visibleColumns[i].width - } - } - return width - } - // Handles a wheel even and updates the scroll offsets const handleWheel = e => { e.preventDefault() @@ -84,7 +68,7 @@
($selectedCellId = null)} + on:click|self={() => ($focusedCellId = null)} >
diff --git a/packages/frontend-core/src/components/sheet/layout/StickyColumn.svelte b/packages/frontend-core/src/components/sheet/layout/StickyColumn.svelte index ef6b509951..83a2552806 100644 --- a/packages/frontend-core/src/components/sheet/layout/StickyColumn.svelte +++ b/packages/frontend-core/src/components/sheet/layout/StickyColumn.svelte @@ -11,13 +11,13 @@ selectedRows, stickyColumn, renderedRows, - selectedCellId, + focusedCellId, hoveredRowId, scroll, reorder, config, selectedCellMap, - selectedCellRow, + focusedRow, gutterWidth, dispatch, } = getContext("sheet") @@ -90,7 +90,7 @@ {#each $renderedRows as row, idx} {@const rowSelected = !!$selectedRows[row._id]} {@const rowHovered = $hoveredRowId === row._id} - {@const containsSelectedRow = $selectedCellRow?._id === row._id} + {@const rowFocused = $focusedRow?._id === row._id}
($hoveredRowId = row._id)} @@ -98,7 +98,8 @@ >
@@ -106,22 +107,19 @@ on:click={() => selectRow(row._id)} class="checkbox" class:visible={$config.allowSelectRows && - (rowSelected || rowHovered || containsSelectedRow)} + (rowSelected || rowHovered || rowFocused)} >
{row.__idx + 1}
{#if $config.allowExpandRows} -
+
{ - const { rows, tableId, users, userId, selectedCellId } = context + const { rows, tableId, users, userId, focusedCellId } = context // Determine connection info const tls = location.protocol === "https:" @@ -55,8 +55,8 @@ export const createWebsocket = context => { tableId.subscribe(connectToDataspace) // Notify selected cell changes - selectedCellId.subscribe($selectedCellId => { - socket.emit("select-cell", $selectedCellId) + focusedCellId.subscribe($focusedCellId => { + socket.emit("select-cell", $focusedCellId) }) return () => socket?.disconnect() diff --git a/packages/frontend-core/src/components/sheet/overlays/KeyboardManager.svelte b/packages/frontend-core/src/components/sheet/overlays/KeyboardManager.svelte index 21b65c4f18..f7fe18cd7f 100644 --- a/packages/frontend-core/src/components/sheet/overlays/KeyboardManager.svelte +++ b/packages/frontend-core/src/components/sheet/overlays/KeyboardManager.svelte @@ -5,16 +5,16 @@ const { rows, - selectedCellId, + focusedCellId, visibleColumns, - selectedCellRow, + focusedRow, stickyColumn, selectedCellAPI, } = getContext("sheet") const handleKeyDown = e => { // If nothing selected avoid processing further key presses - if (!$selectedCellId) { + if (!$focusedCellId) { if (e.key === "Tab") { selectFirstCell() } @@ -72,15 +72,15 @@ if (!firstColumn) { return } - selectedCellId.set(`${firstRow._id}-${firstColumn.name}`) + focusedCellId.set(`${firstRow._id}-${firstColumn.name}`) } const changeSelectedColumn = delta => { - if (!$selectedCellId) { + if (!$focusedCellId) { return } const cols = $visibleColumns - const split = $selectedCellId.split("-") + const split = $focusedCellId.split("-") const columnName = split[1] let newColumnName if (columnName === $stickyColumn?.name) { @@ -95,24 +95,24 @@ } } if (newColumnName) { - $selectedCellId = `${split[0]}-${newColumnName}` + $focusedCellId = `${split[0]}-${newColumnName}` } } const changeSelectedRow = delta => { - if (!$selectedCellRow) { + if (!$focusedRow) { return } - const newRow = $rows[$selectedCellRow.__idx + delta] + const newRow = $rows[$focusedRow.__idx + delta] if (newRow) { - const split = $selectedCellId.split("-") - $selectedCellId = `${newRow._id}-${split[1]}` + const split = $focusedCellId.split("-") + $focusedCellId = `${newRow._id}-${split[1]}` } } // Debounce to avoid holding down delete and spamming requests const deleteSelectedCell = debounce(() => { - if (!$selectedCellId) { + if (!$focusedCellId) { return } $selectedCellAPI.updateValue(null) diff --git a/packages/frontend-core/src/components/sheet/overlays/MenuOverlay.svelte b/packages/frontend-core/src/components/sheet/overlays/MenuOverlay.svelte index 5c0c43c13d..efddb96ade 100644 --- a/packages/frontend-core/src/components/sheet/overlays/MenuOverlay.svelte +++ b/packages/frontend-core/src/components/sheet/overlays/MenuOverlay.svelte @@ -3,11 +3,11 @@ import { getContext } from "svelte" const { - selectedCellRow, + focusedRow, menu, rows, columns, - selectedCellId, + focusedCellId, stickyColumn, config, } = getContext("sheet") @@ -19,20 +19,20 @@ } const deleteRow = () => { - rows.actions.deleteRows([$selectedCellRow]) + rows.actions.deleteRows([$focusedRow]) menu.actions.close() notifications.success("Deleted 1 row") } const duplicate = async () => { - let clone = { ...$selectedCellRow } + let clone = { ...$focusedRow } delete clone._id delete clone._rev delete clone.__idx - const newRow = await rows.actions.addRow(clone, $selectedCellRow.__idx + 1) + const newRow = await rows.actions.addRow(clone, $focusedRow.__idx + 1) if (newRow) { const column = $stickyColumn?.name || $columns[0].name - $selectedCellId = `${newRow._id}-${column}` + $focusedCellId = `${newRow._id}-${column}` menu.actions.close() } } diff --git a/packages/frontend-core/src/components/sheet/overlays/ResizeOverlay.svelte b/packages/frontend-core/src/components/sheet/overlays/ResizeOverlay.svelte index ea7de5edfd..20d6ecd177 100644 --- a/packages/frontend-core/src/components/sheet/overlays/ResizeOverlay.svelte +++ b/packages/frontend-core/src/components/sheet/overlays/ResizeOverlay.svelte @@ -25,7 +25,7 @@ {#if !$isReordering} {#if $stickyColumn}
resize.actions.startResizing($stickyColumn, e)} on:dblclick={() => resize.actions.resetSize($stickyColumn)} @@ -62,9 +62,6 @@ cursor: col-resize; opacity: 1; } - .resize-slider.sticky { - z-index: 3; - } .resize-indicator { margin-left: -1px; width: 2px; diff --git a/packages/frontend-core/src/components/sheet/stores/max-scroll.js b/packages/frontend-core/src/components/sheet/stores/max-scroll.js index f05dfd4c34..28759d0a77 100644 --- a/packages/frontend-core/src/components/sheet/stores/max-scroll.js +++ b/packages/frontend-core/src/components/sheet/stores/max-scroll.js @@ -9,8 +9,8 @@ export const createMaxScrollStores = context => { bounds, rowHeight, scroll, - selectedCellRow, - selectedCellId, + focusedRow, + focusedCellId, gutterWidth, } = context const padding = 264 @@ -89,18 +89,18 @@ export const createMaxScrollStores = context => { }) // Ensure the selected cell is visible - selectedCellId.subscribe(async $selectedCellId => { + focusedCellId.subscribe(async $focusedCellId => { await tick() - const $selectedCellRow = get(selectedCellRow) + const $focusedRow = get(focusedRow) const $scroll = get(scroll) const $bounds = get(bounds) const $rowHeight = get(rowHeight) const verticalOffset = $rowHeight * 1.5 // Ensure vertical position is viewable - if ($selectedCellRow) { + if ($focusedRow) { // Ensure row is not below bottom of screen - const rowYPos = $selectedCellRow.__idx * $rowHeight + const rowYPos = $focusedRow.__idx * $rowHeight const bottomCutoff = $scroll.top + $bounds.height - $rowHeight - verticalOffset let delta = rowYPos - bottomCutoff @@ -126,7 +126,7 @@ export const createMaxScrollStores = context => { // Ensure horizontal position is viewable // Check horizontal position of columns next const $visibleColumns = get(visibleColumns) - const columnName = $selectedCellId?.split("-")[1] + const columnName = $focusedCellId?.split("-")[1] const column = $visibleColumns.find(col => col.name === columnName) const horizontalOffset = 24 if (!column) { diff --git a/packages/frontend-core/src/components/sheet/stores/menu.js b/packages/frontend-core/src/components/sheet/stores/menu.js index 901ac54f95..0b5dfc0641 100644 --- a/packages/frontend-core/src/components/sheet/stores/menu.js +++ b/packages/frontend-core/src/components/sheet/stores/menu.js @@ -1,7 +1,7 @@ import { writable, get } from "svelte/store" export const createMenuStores = context => { - const { bounds, selectedCellId, stickyColumn, rowHeight } = context + const { bounds, focusedCellId, stickyColumn, rowHeight } = context const menu = writable({ x: 0, y: 0, @@ -14,7 +14,7 @@ export const createMenuStores = context => { const $stickyColumn = get(stickyColumn) const $rowHeight = get(rowHeight) e.preventDefault() - selectedCellId.set(cellId) + focusedCellId.set(cellId) menu.set({ left: e.clientX - $bounds.left + 44 + ($stickyColumn?.width || 0), top: e.clientY - $bounds.top + $rowHeight + 4, diff --git a/packages/frontend-core/src/components/sheet/stores/rows.js b/packages/frontend-core/src/components/sheet/stores/rows.js index 6e0b0b1751..df301c45c4 100644 --- a/packages/frontend-core/src/components/sheet/stores/rows.js +++ b/packages/frontend-core/src/components/sheet/stores/rows.js @@ -3,7 +3,7 @@ import { fetchData } from "../../../fetch/fetchData" import { notifications } from "@budibase/bbui" export const createRowsStore = context => { - const { tableId, API, scroll } = context + const { tableId, API, scroll, validation } = context const rows = writable([]) const table = writable(null) const filter = writable([]) @@ -120,6 +120,21 @@ export const createRowsStore = context => { return index >= 0 ? get(enrichedRows)[index] : null } + // Handles validation errors from the rows API and updates local validation + // state, storing error messages against relevant cells + const handleValidationError = (rowId, error) => { + if (error?.json?.validationErrors) { + for (let column of Object.keys(error.json.validationErrors)) { + validation.actions.setError( + `${rowId}-${column}`, + `${column} ${error.json.validationErrors[column]}` + ) + } + } else { + notifications.error(`Error saving row: ${error?.message}`) + } + } + // Adds a new row const addRow = async (row, idx) => { try { @@ -139,7 +154,7 @@ export const createRowsStore = context => { notifications.success("Row created successfully") return newRow } catch (error) { - notifications.error(`Error adding row: ${error?.message}`) + handleValidationError("new", error) } } @@ -208,7 +223,7 @@ export const createRowsStore = context => { try { await API.saveRow(newRow) } catch (error) { - notifications.error(`Error saving row: ${error?.message}`) + handleValidationError(newRow._id, error) // Revert change rows.update(state => { diff --git a/packages/frontend-core/src/components/sheet/stores/sheet-api.js b/packages/frontend-core/src/components/sheet/stores/sheet-api.js new file mode 100644 index 0000000000..45d30f9452 --- /dev/null +++ b/packages/frontend-core/src/components/sheet/stores/sheet-api.js @@ -0,0 +1,66 @@ +import { derived, get, writable } from "svelte/store" + +export const createSheetAPIStores = context => { + const { focusedCellId } = context + const cellAPIs = writable({}) + + const registerCellAPI = (cellId, api) => { + // Ignore registration if cell is not selected + const [rowId, column] = cellId.split("-") + if (rowId !== "new" && !get(focusedCellId)?.startsWith(rowId)) { + return + } + + // Store API + cellAPIs.update(state => ({ + ...state, + [column]: api, + })) + } + + const getCellAPI = column => { + return get(cellAPIs)[column] + } + + // Derive the selected cell API + const selectedCellAPI = derived( + [cellAPIs, focusedCellId], + ([$apis, $focusedCellId]) => { + if (!$focusedCellId) { + return null + } + const [, column] = $focusedCellId.split("-") + return $apis[column] + }, + null + ) + + const focusedRowAPI = derived(cellAPIs, $apis => { + return { + validate: () => { + let errors = null + for (let [column, api] of Object.entries($apis || {})) { + const error = api.validate() + if (error) { + errors = { + ...errors, + [column]: error, + } + } + } + return errors + }, + } + }) + + return { + selectedCellAPI, + focusedRowAPI, + sheetAPI: { + actions: { + registerCellAPI, + getCellAPI, + }, + }, + } +} diff --git a/packages/frontend-core/src/components/sheet/stores/ui.js b/packages/frontend-core/src/components/sheet/stores/ui.js index f3332b306d..e600a83aa3 100644 --- a/packages/frontend-core/src/components/sheet/stores/ui.js +++ b/packages/frontend-core/src/components/sheet/stores/ui.js @@ -2,17 +2,16 @@ import { writable, get, derived } from "svelte/store" export const createUIStores = context => { const { rows, rowLookupMap } = context - const selectedCellId = writable(null) + const focusedCellId = writable(null) const selectedRows = writable({}) const hoveredRowId = writable(null) - const selectedCellAPI = writable(null) const rowHeight = writable(36) // Derive the row that contains the selected cell - const selectedCellRow = derived( - [selectedCellId, rowLookupMap, rows], - ([$selectedCellId, $rowLookupMap, $rows]) => { - const rowId = $selectedCellId?.split("-")[0] + const focusedRow = derived( + [focusedCellId, rowLookupMap, rows], + ([$focusedCellId, $rowLookupMap, $rows]) => { + const rowId = $focusedCellId?.split("-")[0] const index = $rowLookupMap[rowId] return $rows[index] }, @@ -21,15 +20,15 @@ export const createUIStores = context => { // Ensure we clear invalid rows from state if they disappear rows.subscribe(() => { - const $selectedCellId = get(selectedCellId) + const $focusedCellId = get(focusedCellId) const $selectedRows = get(selectedRows) const $hoveredRowId = get(hoveredRowId) const hasRow = rows.actions.hasRow // Check selected cell - const selectedRowId = $selectedCellId?.split("-")[0] + const selectedRowId = $focusedCellId?.split("-")[0] if (selectedRowId && !hasRow(selectedRowId)) { - selectedCellId.set(null) + focusedCellId.set(null) } // Check hovered row @@ -53,7 +52,7 @@ export const createUIStores = context => { }) // Reset selected rows when selected cell changes - selectedCellId.subscribe(id => { + focusedCellId.subscribe(id => { if (id) { selectedRows.set({}) } @@ -62,37 +61,29 @@ export const createUIStores = context => { // Unset selected cell when rows are selected selectedRows.subscribe(rows => { if (Object.keys(rows || {}).length) { - selectedCellId.set(null) + focusedCellId.set(null) } }) // Callback when leaving the sheet, deselecting all focussed or selected items const blur = () => { - selectedCellId.set(null) + focusedCellId.set(null) selectedRows.set({}) hoveredRowId.set(null) } - // Remove selected cell API when no selected cell is present - selectedCellId.subscribe(cell => { - if (!cell && get(selectedCellAPI)) { - selectedCellAPI.set(null) - } - }) - // Remove hovered row when a cell is selected - selectedCellId.subscribe(cell => { + focusedCellId.subscribe(cell => { if (cell && get(hoveredRowId)) { hoveredRowId.set(null) } }) return { - selectedCellId, + focusedCellId, selectedRows, hoveredRowId, - selectedCellRow, - selectedCellAPI, + focusedRow, rowHeight, ui: { actions: { diff --git a/packages/frontend-core/src/components/sheet/stores/users.js b/packages/frontend-core/src/components/sheet/stores/users.js index cb09587b8b..5a2f74e67e 100644 --- a/packages/frontend-core/src/components/sheet/stores/users.js +++ b/packages/frontend-core/src/components/sheet/stores/users.js @@ -62,8 +62,8 @@ export const createUserStores = () => { ([$enrichedUsers, $userId]) => { let map = {} $enrichedUsers.forEach(user => { - if (user.selectedCellId && user.id !== $userId) { - map[user.selectedCellId] = user + if (user.focusedCellId && user.id !== $userId) { + map[user.focusedCellId] = user } }) return map diff --git a/packages/frontend-core/src/components/sheet/stores/validation.js b/packages/frontend-core/src/components/sheet/stores/validation.js new file mode 100644 index 0000000000..d3217038a1 --- /dev/null +++ b/packages/frontend-core/src/components/sheet/stores/validation.js @@ -0,0 +1,25 @@ +import { writable, get } from "svelte/store" + +export const createValidationStores = () => { + const validation = writable({}) + + return { + validation: { + subscribe: validation.subscribe, + actions: { + setError: (cellId, error) => { + if (!cellId) { + return + } + validation.update(state => ({ + ...state, + [cellId]: error, + })) + }, + getError: cellId => { + return get(validation)[cellId] + }, + }, + }, + } +} diff --git a/packages/frontend-core/src/components/sheet/stores/viewport.js b/packages/frontend-core/src/components/sheet/stores/viewport.js index f5f9c6ff66..d2f29dd558 100644 --- a/packages/frontend-core/src/components/sheet/stores/viewport.js +++ b/packages/frontend-core/src/components/sheet/stores/viewport.js @@ -77,10 +77,28 @@ export const createViewportStores = context => { [] ) + const hiddenColumnsWidth = derived( + [renderedColumns, visibleColumns], + ([$renderedColumns, $visibleColumns]) => { + const idx = $visibleColumns.findIndex( + col => col.name === $renderedColumns[0]?.name + ) + let width = 0 + if (idx > 0) { + for (let i = 0; i < idx; i++) { + width += $visibleColumns[i].width + } + } + return width + }, + 0 + ) + return { scrolledRowCount, visualRowCapacity, renderedRows, renderedColumns, + hiddenColumnsWidth, } }