From ae15690741481c1c438ec8642a244b63f3fe47d4 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 24 Nov 2022 14:12:40 +0000 Subject: [PATCH 001/235] Add WIP spreadsheet --- .../new/_components/componentStructure.json | 1 + packages/client/manifest.json | 31 ++ packages/client/src/components/app/index.js | 1 + .../app/spreadsheet/DateCell.svelte | 20 ++ .../app/spreadsheet/OptionsCell.svelte | 39 +++ .../app/spreadsheet/Spreadsheet.svelte | 269 ++++++++++++++++++ .../app/spreadsheet/TextCell.svelte | 16 ++ .../src/components/app/spreadsheet/index.js | 1 + 8 files changed, 378 insertions(+) create mode 100644 packages/client/src/components/app/spreadsheet/DateCell.svelte create mode 100644 packages/client/src/components/app/spreadsheet/OptionsCell.svelte create mode 100644 packages/client/src/components/app/spreadsheet/Spreadsheet.svelte create mode 100644 packages/client/src/components/app/spreadsheet/TextCell.svelte create mode 100644 packages/client/src/components/app/spreadsheet/index.js diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/new/_components/componentStructure.json b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/new/_components/componentStructure.json index 5da16d2b50..d35e7cd515 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/new/_components/componentStructure.json +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/new/_components/componentStructure.json @@ -28,6 +28,7 @@ "dataprovider", "repeater", "table", + "spreadsheet", "dynamicfilter", "daterangepicker" ] diff --git a/packages/client/manifest.json b/packages/client/manifest.json index 4f1352f338..c202cab172 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -5279,5 +5279,36 @@ "type": "schema", "suffix": "repeater" } + }, + "spreadsheet": { + "name": "Spreadsheet", + "icon": "ViewGrid", + "settings": [ + { + "key": "table", + "type": "table", + "label": "Table" + }, + { + "type": "filter", + "label": "Filtering", + "key": "filter" + }, + { + "type": "field/sortable", + "label": "Sort Column", + "key": "sortColumn" + }, + { + "type": "select", + "label": "Sort Order", + "key": "sortOrder", + "options": [ + "Ascending", + "Descending" + ], + "defaultValue": "Ascending" + } + ] } } \ No newline at end of file diff --git a/packages/client/src/components/app/index.js b/packages/client/src/components/app/index.js index 70074790ac..30d8ac2b7e 100644 --- a/packages/client/src/components/app/index.js +++ b/packages/client/src/components/app/index.js @@ -41,6 +41,7 @@ export * from "./forms" export * from "./table" export * from "./blocks" export * from "./dynamic-filter" +export * from "./spreadsheet" // Deprecated component left for compatibility in old apps export { default as navigation } from "./deprecated/Navigation.svelte" diff --git a/packages/client/src/components/app/spreadsheet/DateCell.svelte b/packages/client/src/components/app/spreadsheet/DateCell.svelte new file mode 100644 index 0000000000..c385f98a38 --- /dev/null +++ b/packages/client/src/components/app/spreadsheet/DateCell.svelte @@ -0,0 +1,20 @@ + + +
+ {parsedValue} +
+ + diff --git a/packages/client/src/components/app/spreadsheet/OptionsCell.svelte b/packages/client/src/components/app/spreadsheet/OptionsCell.svelte new file mode 100644 index 0000000000..9eccfdb42a --- /dev/null +++ b/packages/client/src/components/app/spreadsheet/OptionsCell.svelte @@ -0,0 +1,39 @@ + + +
+ {value || ""} +
+ + diff --git a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte new file mode 100644 index 0000000000..5f6d01fa7a --- /dev/null +++ b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte @@ -0,0 +1,269 @@ + + +
+
+
+
+ Filter + Group + Sort + Hide fields +
+
Sales Records
+ +
+
+
+ +
+ {#each fields as field, fieldIdx} +
+ + {field} +
+ {/each} + {#each $fetch.rows as row, rowIdx} +
+ {rowIdx + 1} +
+ {#each fields as field, fieldIdx} + {@const cellIdx = rowIdx * fields.length + fieldIdx} +
(hoveredRow = rowIdx)} + on:click={() => (selectedCell = cellIdx)} + > + +
+ {/each} + {/each} +
+
+
+ + diff --git a/packages/client/src/components/app/spreadsheet/TextCell.svelte b/packages/client/src/components/app/spreadsheet/TextCell.svelte new file mode 100644 index 0000000000..c4ea345e8b --- /dev/null +++ b/packages/client/src/components/app/spreadsheet/TextCell.svelte @@ -0,0 +1,16 @@ + + +
+ {value || ""} +
+ + diff --git a/packages/client/src/components/app/spreadsheet/index.js b/packages/client/src/components/app/spreadsheet/index.js new file mode 100644 index 0000000000..223ab462fa --- /dev/null +++ b/packages/client/src/components/app/spreadsheet/index.js @@ -0,0 +1 @@ +export { default as spreadsheet } from "./Spreadsheet.svelte" From 1c3b6e0cec3e0bff18ef3eab79f8602c13ee7db1 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 24 Nov 2022 15:59:24 +0000 Subject: [PATCH 002/235] Add footer and improve styles --- .../app/spreadsheet/Spreadsheet.svelte | 119 +++++++++++++++--- 1 file changed, 100 insertions(+), 19 deletions(-) diff --git a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte index 5f6d01fa7a..72ce139e5e 100644 --- a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte +++ b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte @@ -13,13 +13,13 @@ const { styleable, API } = getContext("sdk") const component = getContext("component") - - // Config const limit = 100 const defaultWidth = 200 + let widths let hoveredRow let selectedCell + let selectedRows = {} let horizontallyScrolled = false $: query = LuceneUtils.buildLuceneQuery(filter) @@ -34,6 +34,8 @@ $: initWidths(fields) $: gridStyles = getGridStyles(widths) $: schema = $fetch.schema + $: rowCount = $fetch.rows?.length || 0 + $: selectedRowCount = Object.values(selectedRows).filter(x => !!x).length const createFetch = datasource => { return fetchData({ @@ -57,7 +59,7 @@ if (!widths?.length) { return "--grid: 1fr;" } - return `--grid: 60px ${widths.map(x => `${x}px`).join(" ")};` + return `--grid: 50px ${widths.map(x => `${x}px`).join(" ")};` } const handleScroll = e => { @@ -86,6 +88,21 @@ } return "Text" } + + const selectRow = idx => { + selectedRows[idx] = !selectedRows[idx] + } + + const selectAll = () => { + const allSelected = selectedRowCount === rowCount + if (allSelected) { + selectedRows = {} + } else { + for (let i = 0; i < rowCount; i++) { + selectedRows[i] = true + } + } + }
@@ -107,8 +124,12 @@
-
- + +
+
{#each fields as field, fieldIdx}
{/each} + + {#each $fetch.rows as row, rowIdx} -
- {rowIdx + 1} + {@const rowSelected = !!selectedRows[rowIdx]} + {@const rowHovered = hoveredRow === rowIdx} +
(hoveredRow = rowIdx)} + on:click={() => selectRow(rowIdx)} + > + {#if rowSelected || rowHovered} + + {:else} + + {rowIdx + 1} + + {/if}
{#each fields as field, fieldIdx} {@const cellIdx = rowIdx * fields.length + fieldIdx}
{/each} {/each} + + +
+ +
+ {#each fields as field, fieldIdx} +
+ {/each} +
+
@@ -170,6 +228,12 @@ overflow: auto; height: 800px; position: relative; + padding-bottom: 100px; + padding-right: 100px; + } + + .wrapper ::-webkit-scrollbar-track { + background: var(--spectrum-global-color-gray-50); } .controls { @@ -177,7 +241,7 @@ grid-template-columns: 1fr auto 1fr; align-items: center; height: 36px; - padding: 0 16px; + padding: 0 12px; background: var(--spectrum-global-color-gray-200); gap: 8px; border-bottom: 1px solid var(--spectrum-global-color-gray-400); @@ -207,29 +271,29 @@ flex-direction: row; justify-content: flex-start; align-items: center; - background: var(--spectrum-global-color-gray-50); color: var(--spectrum-global-color-gray-900); font-size: 14px; gap: 4px; - } - .cell:last-child { - border-right: none; + background: var(--spectrum-global-color-gray-50); } .cell.hovered { background: var(--spectrum-global-color-gray-100); } .cell.selected { - box-shadow: inset 0 0 0 2px var(--primaryColorHover); + box-shadow: inset 0 0 0 2px rgb(89, 167, 246); z-index: 1; } .cell:hover { - cursor: pointer; + cursor: default; } .cell.sticky { position: sticky; - left: 60px; + left: 50px; z-index: 2; } + .cell.row-selected { + background-color: rgba(20, 122, 243, 0.05); + } .header { background: var(--spectrum-global-color-gray-200); @@ -253,9 +317,8 @@ } .label { - padding: 0 16px; + padding: 0 12px; border-right: none; - color: var(--spectrum-global-color-gray-500); position: sticky; left: 0; z-index: 2; @@ -263,7 +326,25 @@ .label.header { z-index: 4; } - .label.header input { + .label span { + min-width: 14px; + text-align: center; + color: var(--spectrum-global-color-gray-500); + } + + input[type="checkbox"] { margin: 0; } + + .footer { + height: 32px; + width: 100%; + border-top: 1px solid var(--spectrum-global-color-gray-400); + padding: 0 12px; + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + background: var(--spectrum-global-color-gray-50); + } From b19a2a3d3a01fead26632757833c56e2c4cd2ac1 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 24 Nov 2022 16:41:53 +0000 Subject: [PATCH 003/235] Refactor to use IDs and support changing text values inline --- .../app/spreadsheet/Spreadsheet.svelte | 47 +++++++++++++------ .../app/spreadsheet/TextCell.svelte | 31 ++++++++++-- 2 files changed, 61 insertions(+), 17 deletions(-) diff --git a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte index 72ce139e5e..40d9d130cb 100644 --- a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte +++ b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte @@ -21,6 +21,7 @@ let selectedCell let selectedRows = {} let horizontallyScrolled = false + let changeCache = {} $: query = LuceneUtils.buildLuceneQuery(filter) $: fetch = createFetch(table) @@ -89,8 +90,8 @@ return "Text" } - const selectRow = idx => { - selectedRows[idx] = !selectedRows[idx] + const selectRow = id => { + selectedRows[id] = !selectedRows[id] } const selectAll = () => { @@ -98,11 +99,27 @@ if (allSelected) { selectedRows = {} } else { - for (let i = 0; i < rowCount; i++) { - selectedRows[i] = true - } + $fetch.rows.forEach(row => { + selectedRows[row._id] = true + }) } } + + const handleChange = async (rowId, field, value) => { + selectedCell = null + let row = $fetch.rows.find(x => x._id === rowId) + if (!row) { + return + } + const newRow = { + ...row, + [field]: value, + } + changeCache[rowId] = { [field]: value } + await API.saveRow(newRow) + await fetch.refresh() + delete changeCache[rowId] + }
@@ -147,16 +164,17 @@ {/each} - {#each $fetch.rows as row, rowIdx} - {@const rowSelected = !!selectedRows[rowIdx]} - {@const rowHovered = hoveredRow === rowIdx} + {#each $fetch.rows as row, rowIdx (row._id)} + {@const rowSelected = !!selectedRows[row._id]} + {@const rowHovered = hoveredRow === row._id} + {@const data = { ...row, ...changeCache[row._id] }}
(hoveredRow = rowIdx)} - on:click={() => selectRow(rowIdx)} + on:mouseover={() => (hoveredRow = row._id)} + on:click={() => selectRow(row._id)} > {#if rowSelected || rowHovered} @@ -167,7 +185,7 @@ {/if}
{#each fields as field, fieldIdx} - {@const cellIdx = rowIdx * fields.length + fieldIdx} + {@const cellIdx = `${row._id}-${field}`}
(hoveredRow = rowIdx)} + on:mouseover={() => (hoveredRow = row._id)} on:click={() => (selectedCell = cellIdx)} > handleChange(row._id, field, val)} />
{/each} @@ -292,7 +311,7 @@ z-index: 2; } .cell.row-selected { - background-color: rgba(20, 122, 243, 0.05); + background-color: rgb(224, 242, 255); } .header { diff --git a/packages/client/src/components/app/spreadsheet/TextCell.svelte b/packages/client/src/components/app/spreadsheet/TextCell.svelte index c4ea345e8b..fb8b208831 100644 --- a/packages/client/src/components/app/spreadsheet/TextCell.svelte +++ b/packages/client/src/components/app/spreadsheet/TextCell.svelte @@ -1,10 +1,20 @@ -
- {value || ""} -
+{#if selected} + +{:else} +
+ {value || ""} +
+{/if} From 6471464971c1d87c88dee3742a4aa2ee12bdd6be Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 24 Nov 2022 19:28:55 +0000 Subject: [PATCH 004/235] Add inline editing of options fields --- .../app/spreadsheet/OptionsCell.svelte | 111 ++++++++++++++++-- .../app/spreadsheet/Spreadsheet.svelte | 18 ++- 2 files changed, 115 insertions(+), 14 deletions(-) diff --git a/packages/client/src/components/app/spreadsheet/OptionsCell.svelte b/packages/client/src/components/app/spreadsheet/OptionsCell.svelte index 9eccfdb42a..859a02461f 100644 --- a/packages/client/src/components/app/spreadsheet/OptionsCell.svelte +++ b/packages/client/src/components/app/spreadsheet/OptionsCell.svelte @@ -1,7 +1,12 @@ -
- {value || ""} +
+ {#if color} +
+ {value} +
+ {:else if value} +
+ {value} +
+ {/if} + {#if selected} + + {/if} + {#if open} +
+
+
+ {value} +
+ +
+ {#each options.filter(x => x !== value) as option} +
onChange(option)}> +
+ {option} +
+
+ {/each} +
+ {/if}
diff --git a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte index 40d9d130cb..7e5813b301 100644 --- a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte +++ b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte @@ -106,17 +106,18 @@ } const handleChange = async (rowId, field, value) => { - selectedCell = null let row = $fetch.rows.find(x => x._id === rowId) if (!row) { return } - const newRow = { - ...row, - [field]: value, + if (row[field] === value) { + return } changeCache[rowId] = { [field]: value } - await API.saveRow(newRow) + await API.saveRow({ + ...row, + ...changeCache[rowId], + }) await fetch.refresh() delete changeCache[rowId] } @@ -237,6 +238,7 @@ justify-content: flex-start; align-items: stretch; border: 1px solid var(--spectrum-global-color-gray-400); + border-radius: 4px; } .spreadsheet { display: grid; @@ -294,12 +296,13 @@ font-size: 14px; gap: 4px; background: var(--spectrum-global-color-gray-50); + position: relative; } .cell.hovered { background: var(--spectrum-global-color-gray-100); } .cell.selected { - box-shadow: inset 0 0 0 2px rgb(89, 167, 246); + box-shadow: inset 0 0 0 2px var(--spectrum-global-color-blue-400); z-index: 1; } .cell:hover { @@ -310,6 +313,9 @@ left: 50px; z-index: 2; } + .cell.sticky.selected { + z-index: 3; + } .cell.row-selected { background-color: rgb(224, 242, 255); } From fafb8ec9384d340054060c1d81f4d0f6f9abb4e5 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 24 Nov 2022 20:06:31 +0000 Subject: [PATCH 005/235] Add row deletion and fix sizing --- .../app/spreadsheet/OptionsCell.svelte | 3 +- .../app/spreadsheet/Spreadsheet.svelte | 90 +++++++++++++------ packages/client/src/sdk.js | 2 + 3 files changed, 64 insertions(+), 31 deletions(-) diff --git a/packages/client/src/components/app/spreadsheet/OptionsCell.svelte b/packages/client/src/components/app/spreadsheet/OptionsCell.svelte index 859a02461f..eda93cc9c8 100644 --- a/packages/client/src/components/app/spreadsheet/OptionsCell.svelte +++ b/packages/client/src/components/app/spreadsheet/OptionsCell.svelte @@ -115,8 +115,7 @@ justify-content: flex-start; align-items: stretch; box-shadow: 0 0 8px 4px rgba(0, 0, 0, 0.15); - border-radius: 4px; - max-height: 192px; + max-height: 191px; overflow-y: auto; } .option { diff --git a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte index 7e5813b301..06db8e7f9c 100644 --- a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte +++ b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte @@ -11,7 +11,8 @@ export let sortColumn export let sortOrder - const { styleable, API } = getContext("sdk") + const { styleable, API, confirmationStore, notificationStore } = + getContext("sdk") const component = getContext("component") const limit = 100 const defaultWidth = 200 @@ -121,6 +122,47 @@ await fetch.refresh() delete changeCache[rowId] } + + const deleteRows = () => { + // Fetch full row objects to be deleted + const rows = Object.entries(selectedRows) + .map(entry => { + if (entry[1] === true) { + return $fetch.rows.find(x => x._id === entry[0]) + } else { + return null + } + }) + .filter(x => x != null) + + // Deletion callback when confirmed + const performDeletion = async () => { + await API.deleteRows({ + tableId: table.tableId, + rows, + }) + await fetch.refresh() + notificationStore.actions.success( + `${selectedRowCount} row${ + selectedRowCount === 1 ? "" : "s" + } deleted successfully` + ) + + // Refresh state + selectedCell = null + hoveredRow = null + selectedRows = {} + } + + // Show confirmation + confirmationStore.actions.showConfirmation( + "Delete rows", + `Are you sure you want to delete ${selectedRowCount} row${ + selectedRowCount === 1 ? "" : "s" + }?`, + performDeletion + ) + }
@@ -133,12 +175,14 @@ Hide fields
Sales Records
-
@@ -221,13 +265,6 @@ /> {/each}
-
@@ -247,9 +284,9 @@ justify-content: flex-start; align-items: stretch; overflow: auto; - height: 800px; + max-height: 800px; position: relative; - padding-bottom: 100px; + padding-bottom: 80px; padding-right: 100px; } @@ -277,11 +314,18 @@ align-items: center; gap: 4px; } - .search { + .delete { display: flex; flex-direction: row; justify-content: flex-end; align-items: center; + color: var(--spectrum-global-color-gray-700); + } + .delete :global(.spectrum-ActionButton) { + color: var(--spectrum-global-color-red-600); + } + .delete :global(.spectrum-Icon) { + fill: var(--spectrum-global-color-red-600); } .cell { @@ -360,16 +404,4 @@ input[type="checkbox"] { margin: 0; } - - .footer { - height: 32px; - width: 100%; - border-top: 1px solid var(--spectrum-global-color-gray-400); - padding: 0 12px; - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - background: var(--spectrum-global-color-gray-50); - } diff --git a/packages/client/src/sdk.js b/packages/client/src/sdk.js index 264cc85626..c9ff1eba36 100644 --- a/packages/client/src/sdk.js +++ b/packages/client/src/sdk.js @@ -12,6 +12,7 @@ import { environmentStore, sidePanelStore, dndIsDragging, + confirmationStore, } from "stores" import { styleable } from "utils/styleable" import { linkable } from "utils/linkable" @@ -35,6 +36,7 @@ export default { sidePanelStore, dndIsDragging, currentRole, + confirmationStore, styleable, linkable, getAction, From 152d157aaa2fc43cf8c0af39914943f5bbaac92e Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 25 Nov 2022 08:34:44 +0000 Subject: [PATCH 006/235] Add ability to add new rows --- .../app/spreadsheet/OptionsCell.svelte | 19 ++++--- .../app/spreadsheet/Spreadsheet.svelte | 50 +++++++++++++++---- 2 files changed, 51 insertions(+), 18 deletions(-) diff --git a/packages/client/src/components/app/spreadsheet/OptionsCell.svelte b/packages/client/src/components/app/spreadsheet/OptionsCell.svelte index eda93cc9c8..fcde18bbc1 100644 --- a/packages/client/src/components/app/spreadsheet/OptionsCell.svelte +++ b/packages/client/src/components/app/spreadsheet/OptionsCell.svelte @@ -52,9 +52,9 @@
{value}
- {:else if value} + {:else}
- {value} + {value || ""}
{/if} {#if selected} @@ -62,12 +62,17 @@ {/if} {#if open}
-
-
- {value} + {#if value} +
+
+ {value} +
+
- -
+ {/if} {#each options.filter(x => x !== value) as option}
onChange(option)}>
diff --git a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte index 06db8e7f9c..267a8a7d85 100644 --- a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte +++ b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte @@ -23,6 +23,7 @@ let selectedRows = {} let horizontallyScrolled = false let changeCache = {} + let newRows = [] $: query = LuceneUtils.buildLuceneQuery(filter) $: fetch = createFetch(table) @@ -38,6 +39,7 @@ $: schema = $fetch.schema $: rowCount = $fetch.rows?.length || 0 $: selectedRowCount = Object.values(selectedRows).filter(x => !!x).length + $: rows = getSortedRows($fetch.rows, newRows) const createFetch = datasource => { return fetchData({ @@ -100,14 +102,14 @@ if (allSelected) { selectedRows = {} } else { - $fetch.rows.forEach(row => { + rows.forEach(row => { selectedRows[row._id] = true }) } } const handleChange = async (rowId, field, value) => { - let row = $fetch.rows.find(x => x._id === rowId) + let row = rows.find(x => x._id === rowId) if (!row) { return } @@ -125,10 +127,10 @@ const deleteRows = () => { // Fetch full row objects to be deleted - const rows = Object.entries(selectedRows) + const rowsToDelete = Object.entries(selectedRows) .map(entry => { if (entry[1] === true) { - return $fetch.rows.find(x => x._id === entry[0]) + return rows.find(x => x._id === entry[0]) } else { return null } @@ -139,7 +141,7 @@ const performDeletion = async () => { await API.deleteRows({ tableId: table.tableId, - rows, + rows: rowsToDelete, }) await fetch.refresh() notificationStore.actions.success( @@ -163,6 +165,23 @@ performDeletion ) } + + const addRow = async field => { + const res = await API.saveRow({ tableId: table.tableId }) + selectedCell = `${res._id}-${field}` + newRows.push(res._id) + await fetch.refresh() + } + + const getSortedRows = (rows, newRows) => { + let sortedRows = rows.slice() + sortedRows.sort((a, b) => { + const aIndex = newRows.indexOf(a._id) + const bIndex = newRows.indexOf(b._id) + return aIndex < bIndex ? -1 : 1 + }) + return sortedRows + }
@@ -185,7 +204,12 @@ {/if}
-
+
(selectedCell = null)} + >
- {#each $fetch.rows as row, rowIdx (row._id)} + {#each rows as row, rowIdx (row._id)} {@const rowSelected = !!selectedRows[row._id]} {@const rowHovered = hoveredRow === row._id} {@const data = { ...row, ...changeCache[row._id] }} @@ -254,14 +278,15 @@ {/each} -
+
{#each fields as field, fieldIdx}
addRow(field)} /> {/each}
@@ -284,9 +309,9 @@ justify-content: flex-start; align-items: stretch; overflow: auto; - max-height: 800px; + max-height: 1014px; position: relative; - padding-bottom: 80px; + padding-bottom: 180px; padding-right: 100px; } @@ -363,6 +388,9 @@ .cell.row-selected { background-color: rgb(224, 242, 255); } + .cell.new:hover { + cursor: pointer; + } .header { background: var(--spectrum-global-color-gray-200); From adb03cefb25257ce770e3521994af27f3aa3dd29 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 25 Nov 2022 08:35:08 +0000 Subject: [PATCH 007/235] Fix z-index issue with option cells --- .../client/src/components/app/spreadsheet/OptionsCell.svelte | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/client/src/components/app/spreadsheet/OptionsCell.svelte b/packages/client/src/components/app/spreadsheet/OptionsCell.svelte index fcde18bbc1..9c0f4c497d 100644 --- a/packages/client/src/components/app/spreadsheet/OptionsCell.svelte +++ b/packages/client/src/components/app/spreadsheet/OptionsCell.svelte @@ -122,6 +122,7 @@ box-shadow: 0 0 8px 4px rgba(0, 0, 0, 0.15); max-height: 191px; overflow-y: auto; + z-index: 1; } .option { flex: 0 0 32px; From db13f7a2c39de709d0614006055dd494192df0cb Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 25 Nov 2022 08:43:44 +0000 Subject: [PATCH 008/235] Remove deletion notification and fix selection --- .../app/spreadsheet/Spreadsheet.svelte | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte index 267a8a7d85..1936bbbab0 100644 --- a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte +++ b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte @@ -144,11 +144,11 @@ rows: rowsToDelete, }) await fetch.refresh() - notificationStore.actions.success( - `${selectedRowCount} row${ - selectedRowCount === 1 ? "" : "s" - } deleted successfully` - ) + // notificationStore.actions.success( + // `${selectedRowCount} row${ + // selectedRowCount === 1 ? "" : "s" + // } deleted successfully` + // ) // Refresh state selectedCell = null @@ -278,15 +278,24 @@ {/each} -
- +
(hoveredRow = "new")} + class:hovered={hoveredRow === "new"} + > +
{#each fields as field, fieldIdx}
addRow(field)} + on:focus + on:mouseover={() => (hoveredRow = "new")} /> {/each}
@@ -374,6 +383,9 @@ box-shadow: inset 0 0 0 2px var(--spectrum-global-color-blue-400); z-index: 1; } + .cell:not(.selected) { + user-select: none; + } .cell:hover { cursor: default; } From 4e3400eb7f3d66677d90f0442029f46b19c607eb Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 25 Nov 2022 08:48:18 +0000 Subject: [PATCH 009/235] Add gap between items in an options cell --- .../client/src/components/app/spreadsheet/OptionsCell.svelte | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/client/src/components/app/spreadsheet/OptionsCell.svelte b/packages/client/src/components/app/spreadsheet/OptionsCell.svelte index 9c0f4c497d..a29db5f817 100644 --- a/packages/client/src/components/app/spreadsheet/OptionsCell.svelte +++ b/packages/client/src/components/app/spreadsheet/OptionsCell.svelte @@ -131,6 +131,7 @@ flex-direction: row; justify-content: space-between; align-items: center; + gap: 8px; background-color: var(--spectrum-global-color-gray-50); } .option:hover { From e40e82fd5fe766f36f47d6848a4bad5d5cb1c486 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 25 Nov 2022 10:17:03 +0000 Subject: [PATCH 010/235] Tweak options cell to be pixel perfect --- .../client/src/components/app/spreadsheet/OptionsCell.svelte | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/client/src/components/app/spreadsheet/OptionsCell.svelte b/packages/client/src/components/app/spreadsheet/OptionsCell.svelte index a29db5f817..69d874d363 100644 --- a/packages/client/src/components/app/spreadsheet/OptionsCell.svelte +++ b/packages/client/src/components/app/spreadsheet/OptionsCell.svelte @@ -131,9 +131,12 @@ flex-direction: row; justify-content: space-between; align-items: center; - gap: 8px; + gap: 4px; background-color: var(--spectrum-global-color-gray-50); } + .option:first-child { + flex: 0 0 31px; + } .option:hover { background-color: var(--spectrum-global-color-gray-100); } From 1cbd427d701434f124017c8899c053beead4b336 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 9 Feb 2023 08:50:22 +0000 Subject: [PATCH 011/235] Fix padding around sheet not working when scrolling --- .../app/spreadsheet/Spreadsheet.svelte | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte index 1936bbbab0..040e2d30dc 100644 --- a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte +++ b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte @@ -63,7 +63,7 @@ if (!widths?.length) { return "--grid: 1fr;" } - return `--grid: 50px ${widths.map(x => `${x}px`).join(" ")};` + return `--grid: 50px ${widths.map(x => `${x}px`).join(" ")} 180px;` } const handleScroll = e => { @@ -231,6 +231,8 @@ {field}
{/each} + +
{#each rows as row, rowIdx (row._id)} @@ -275,6 +277,8 @@ />
{/each} + +
{/each} @@ -298,6 +302,11 @@ on:mouseover={() => (hoveredRow = "new")} /> {/each} + +
+ + +
@@ -314,14 +323,16 @@ .spreadsheet { display: grid; grid-template-columns: var(--grid); - flex-direction: column; justify-content: flex-start; align-items: stretch; overflow: auto; - max-height: 1014px; + max-height: 800px; position: relative; - padding-bottom: 180px; - padding-right: 100px; + cursor: default; + } + .vertical-spacer { + grid-column: 1/-1; + height: 180px; } .wrapper ::-webkit-scrollbar-track { From 23450d245c10c455f9ad59f22ac8707d2f42d15d Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 10 Feb 2023 08:17:18 +0000 Subject: [PATCH 012/235] Add resizable columns and add support for all themes --- .../app/spreadsheet/OptionsCell.svelte | 14 +--- .../app/spreadsheet/Spreadsheet.svelte | 74 ++++++++++++++++++- 2 files changed, 74 insertions(+), 14 deletions(-) diff --git a/packages/client/src/components/app/spreadsheet/OptionsCell.svelte b/packages/client/src/components/app/spreadsheet/OptionsCell.svelte index 69d874d363..a39556c140 100644 --- a/packages/client/src/components/app/spreadsheet/OptionsCell.svelte +++ b/packages/client/src/components/app/spreadsheet/OptionsCell.svelte @@ -7,17 +7,6 @@ export let onChange const options = schema?.constraints?.inclusion || [] - const colors = [ - "rgb(207, 223, 255)", - "rgb(208, 240, 253)", - "rgb(194, 245, 233)", - "rgb(209, 247, 196)", - "rgb(255, 234, 182)", - "rgb(254, 226, 213)", - "rgb(255, 220, 229)", - "rgb(255, 218, 246)", - "rgb(237, 226, 254)", - ] let open = false @@ -34,7 +23,7 @@ if (!value || index === -1) { return null } - return colors[index % colors.length] + return `hsla(${((index + 1) * 222) % 360}, 90%, 75%, 0.3)` } const toggle = () => { @@ -107,7 +96,6 @@ padding: 2px 8px; background: var(--color); border-radius: 8px; - color: #2c2c2c; user-select: none; } .options { diff --git a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte index 040e2d30dc..b4437bc8e9 100644 --- a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte +++ b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte @@ -16,6 +16,7 @@ const component = getContext("component") const limit = 100 const defaultWidth = 200 + const minWidth = 100 let widths let hoveredRow @@ -35,6 +36,7 @@ }) $: fields = Object.keys($fetch.schema || {}) $: initWidths(fields) + $: sliderPositions = getSliderPositions(widths) $: gridStyles = getGridStyles(widths) $: schema = $fetch.schema $: rowCount = $fetch.rows?.length || 0 @@ -66,6 +68,13 @@ return `--grid: 50px ${widths.map(x => `${x}px`).join(" ")} 180px;` } + const getSliderPositions = widths => { + let offset = 50 + return widths.map(width => { + return (offset += width) + }) + } + const handleScroll = e => { const nextHorizontallyScrolled = e.target.scrollLeft > 0 if (nextHorizontallyScrolled !== horizontallyScrolled) { @@ -182,6 +191,28 @@ }) return sortedRows } + + let resizeInitialX + let resizeInitialWidth + let resizeFieldIndex + + const startResizing = (fieldIdx, e) => { + resizeInitialX = e.clientX + resizeInitialWidth = widths[fieldIdx] + resizeFieldIndex = fieldIdx + document.addEventListener("mousemove", onResize) + document.addEventListener("mouseup", stopResizing) + } + + const onResize = e => { + const dx = e.clientX - resizeInitialX + widths[resizeFieldIndex] = Math.max(minWidth, resizeInitialWidth + dx) + } + + const stopResizing = () => { + document.removeEventListener("mousemove", onResize) + document.removeEventListener("mouseup", stopResizing) + }
@@ -228,12 +259,23 @@ name={getIconForField(field)} color="var(--spectrum-global-color-gray-600)" /> - {field} + + {field} +
{/each}
+ + {#each sliderPositions as left, idx} +
startResizing(idx, e)} + style="--left: {left}px" + /> + {/each} + {#each rows as row, rowIdx (row._id)} {@const rowSelected = !!selectedRows[row._id]} @@ -423,10 +465,40 @@ z-index: 3; border-color: var(--spectrum-global-color-gray-400); } + .header span { + flex: 1 1 auto; + width: 0; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } .header.sticky { z-index: 4; } + /* Column resizing */ + .slider { + position: absolute; + z-index: 5; + left: var(--left); + top: 0; + width: 16px; + height: 32px; + transform: translateX(-50%); + } + .slider:hover:after { + content: " "; + position: absolute; + width: 2px; + left: 50%; + height: 100%; + background: var(--spectrum-global-color-blue-400); + transform: translateX(-50%); + } + .slider:hover { + cursor: col-resize; + } + .sticky.shadow:after { content: " "; position: absolute; From c627cb60c3bf8f2d7ad8b66f4087ca0791718002 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 20 Feb 2023 18:45:35 +0000 Subject: [PATCH 013/235] Allow multiselect component and field to support text values --- packages/bbui/src/Form/Core/Multiselect.svelte | 9 +++++---- packages/bbui/src/Table/ArrayRenderer.svelte | 5 +++-- .../components/app/spreadsheet/MultiSelectCell.svelte | 0 3 files changed, 8 insertions(+), 6 deletions(-) create mode 100644 packages/client/src/components/app/spreadsheet/MultiSelectCell.svelte diff --git a/packages/bbui/src/Form/Core/Multiselect.svelte b/packages/bbui/src/Form/Core/Multiselect.svelte index d6c4dc23ac..d76dba96d3 100644 --- a/packages/bbui/src/Form/Core/Multiselect.svelte +++ b/packages/bbui/src/Form/Core/Multiselect.svelte @@ -17,12 +17,13 @@ const dispatch = createEventDispatcher() - $: selectedLookupMap = getSelectedLookupMap(value) + $: arrayValue = Array.isArray(value) ? value : [value].filter(x => !!x) + $: selectedLookupMap = getSelectedLookupMap(arrayValue) $: optionLookupMap = getOptionLookupMap(options) - $: fieldText = getFieldText(value, optionLookupMap, placeholder) + $: fieldText = getFieldText(arrayValue, optionLookupMap, placeholder) $: isOptionSelected = optionValue => selectedLookupMap[optionValue] === true - $: toggleOption = makeToggleOption(selectedLookupMap, value) + $: toggleOption = makeToggleOption(selectedLookupMap, arrayValue) const getFieldText = (value, map, placeholder) => { if (Array.isArray(value) && value.length > 0) { @@ -81,7 +82,7 @@ {readonly} {fieldText} {options} - isPlaceholder={!value?.length} + isPlaceholder={!arrayValue.length} {autocomplete} {isOptionSelected} {getOptionLabel} diff --git a/packages/bbui/src/Table/ArrayRenderer.svelte b/packages/bbui/src/Table/ArrayRenderer.svelte index 3755850666..637454dbca 100644 --- a/packages/bbui/src/Table/ArrayRenderer.svelte +++ b/packages/bbui/src/Table/ArrayRenderer.svelte @@ -5,8 +5,9 @@ const displayLimit = 5 - $: badges = Array.isArray(value) ? value.slice(0, displayLimit) : [] - $: leftover = (value?.length ?? 0) - badges.length + $: arrayValue = Array.isArray(value) ? value : [value].filter(x => !!x) + $: badges = arrayValue.slice(0, displayLimit) + $: leftover = arrayValue.length - badges.length {#each badges as badge} diff --git a/packages/client/src/components/app/spreadsheet/MultiSelectCell.svelte b/packages/client/src/components/app/spreadsheet/MultiSelectCell.svelte new file mode 100644 index 0000000000..e69de29bb2 From 654c348e4e75bff6098542d16a90a5b670ce73f3 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 20 Feb 2023 18:46:06 +0000 Subject: [PATCH 014/235] Generate inclusion schema when importing multiselect columns --- packages/server/src/api/controllers/table/utils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/server/src/api/controllers/table/utils.ts b/packages/server/src/api/controllers/table/utils.ts index bbccde467b..866b0a8f42 100644 --- a/packages/server/src/api/controllers/table/utils.ts +++ b/packages/server/src/api/controllers/table/utils.ts @@ -111,7 +111,8 @@ export function importToRows(data: any, table: any, user: any = {}) { for ([fieldName, schema] of Object.entries(table.schema)) { // check whether the options need to be updated for inclusion as part of the data import if ( - schema.type === FieldTypes.OPTIONS && + (schema.type === FieldTypes.OPTIONS || + schema.type === FieldTypes.ARRAY) && row[fieldName] && (!schema.constraints.inclusion || schema.constraints.inclusion.indexOf(row[fieldName]) === -1) From e26163e274bfb3e0080181e4ce91ad00668ee564 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 20 Feb 2023 19:04:22 +0000 Subject: [PATCH 015/235] Add support for multiselect type --- .../app/spreadsheet/MultiSelectCell.svelte | 5 ++ .../app/spreadsheet/OptionsCell.svelte | 68 +++++++++++++------ .../app/spreadsheet/Spreadsheet.svelte | 50 ++++++-------- 3 files changed, 74 insertions(+), 49 deletions(-) diff --git a/packages/client/src/components/app/spreadsheet/MultiSelectCell.svelte b/packages/client/src/components/app/spreadsheet/MultiSelectCell.svelte index e69de29bb2..77eb95c9e7 100644 --- a/packages/client/src/components/app/spreadsheet/MultiSelectCell.svelte +++ b/packages/client/src/components/app/spreadsheet/MultiSelectCell.svelte @@ -0,0 +1,5 @@ + + + diff --git a/packages/client/src/components/app/spreadsheet/OptionsCell.svelte b/packages/client/src/components/app/spreadsheet/OptionsCell.svelte index a39556c140..08c7699f62 100644 --- a/packages/client/src/components/app/spreadsheet/OptionsCell.svelte +++ b/packages/client/src/components/app/spreadsheet/OptionsCell.svelte @@ -5,12 +5,14 @@ export let schema export let selected = false export let onChange + export let multi = false const options = schema?.constraints?.inclusion || [] let open = false - $: color = getColor(value) + $: values = Array.isArray(value) ? value : [value].filter(x => x != null) + $: unselectedOptions = options.filter(x => !values.includes(x)) $: { // Close when deselected if (!selected) { @@ -26,44 +28,59 @@ return `hsla(${((index + 1) * 222) % 360}, 90%, 75%, 0.3)` } - const toggle = () => { - open = !open + const toggleOption = option => { + if (!multi) { + onChange(option) + } else { + if (values.includes(option)) { + onChange(values.filter(x => x !== option)) + } else { + onChange([...values, option]) + } + } }
(open = true) : null} > - {#if color} -
- {value} -
- {:else} -
- {value || ""} -
- {/if} +
+ {#each values as val (val)} + {@const color = getColor(val)} + {#if color} +
+ {val} +
+ {:else} +
+ {val || ""} +
+ {/if} + {/each} +
{#if selected} {/if} {#if open}
- {#if value} -
+ {#each values as val (val)} + {@const color = getColor(val)} +
toggleOption(val)}>
- {value} + {val}
- {/if} - {#each options.filter(x => x !== value) as option} -
onChange(option)}> + {/each} + {#each unselectedOptions as option (option)} +
toggleOption(option)}>
{option}
@@ -87,11 +104,24 @@ .container.selected:hover { cursor: pointer; } + .values { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + flex: 1 1 auto; + width: 0; + gap: 4px; + overflow: hidden; + } .text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .multi .text { + flex: 0 0 auto; + } .badge { padding: 2px 8px; background: var(--color); diff --git a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte index b4437bc8e9..e20e5879ff 100644 --- a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte +++ b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte @@ -5,6 +5,7 @@ import TextCell from "./TextCell.svelte" import OptionsCell from "./OptionsCell.svelte" import DateCell from "./DateCell.svelte" + import MultiSelectCell from "./MultiSelectCell.svelte" export let table export let filter @@ -15,7 +16,7 @@ getContext("sdk") const component = getContext("component") const limit = 100 - const defaultWidth = 200 + const defaultWidth = 160 const minWidth = 100 let widths @@ -36,7 +37,6 @@ }) $: fields = Object.keys($fetch.schema || {}) $: initWidths(fields) - $: sliderPositions = getSliderPositions(widths) $: gridStyles = getGridStyles(widths) $: schema = $fetch.schema $: rowCount = $fetch.rows?.length || 0 @@ -65,14 +65,7 @@ if (!widths?.length) { return "--grid: 1fr;" } - return `--grid: 50px ${widths.map(x => `${x}px`).join(" ")} 180px;` - } - - const getSliderPositions = widths => { - let offset = 50 - return widths.map(width => { - return (offset += width) - }) + return `--grid: 40px ${widths.map(x => `${x}px`).join(" ")} 180px;` } const handleScroll = e => { @@ -88,6 +81,8 @@ return OptionsCell } else if (type === "datetime") { return DateCell + } else if (type === "array") { + return MultiSelectCell } return TextCell } @@ -262,20 +257,12 @@ {field} +
startResizing(fieldIdx, e)} />
{/each}
- - {#each sliderPositions as left, idx} -
startResizing(idx, e)} - style="--left: {left}px" - /> - {/each} - {#each rows as row, rowIdx (row._id)} {@const rowSelected = !!selectedRows[row._id]} @@ -368,7 +355,7 @@ justify-content: flex-start; align-items: stretch; overflow: auto; - max-height: 800px; + max-height: 600px; position: relative; cursor: default; } @@ -444,7 +431,7 @@ } .cell.sticky { position: sticky; - left: 50px; + left: 40px; z-index: 2; } .cell.sticky.selected { @@ -479,25 +466,28 @@ /* Column resizing */ .slider { position: absolute; - z-index: 5; - left: var(--left); top: 0; + right: 0; width: 16px; - height: 32px; - transform: translateX(-50%); + height: 100%; } - .slider:hover:after { + .slider:after { + opacity: 0; content: " "; position: absolute; - width: 2px; - left: 50%; + width: 4px; + right: 0; + top: 0; height: 100%; - background: var(--spectrum-global-color-blue-400); - transform: translateX(-50%); + background: var(--spectrum-global-color-gray-600); + transition: opacity 130ms ease-out; } .slider:hover { cursor: col-resize; } + .slider:hover:after { + opacity: 1; + } .sticky.shadow:after { content: " "; From 8316692ecfe84081b0880836c5118fbb270eb839 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 21 Feb 2023 10:00:18 +0000 Subject: [PATCH 016/235] Add number cell --- .../client/src/components/app/spreadsheet/NumberCell.svelte | 5 +++++ .../client/src/components/app/spreadsheet/Spreadsheet.svelte | 3 +++ .../client/src/components/app/spreadsheet/TextCell.svelte | 3 ++- 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 packages/client/src/components/app/spreadsheet/NumberCell.svelte diff --git a/packages/client/src/components/app/spreadsheet/NumberCell.svelte b/packages/client/src/components/app/spreadsheet/NumberCell.svelte new file mode 100644 index 0000000000..2e0ef27c39 --- /dev/null +++ b/packages/client/src/components/app/spreadsheet/NumberCell.svelte @@ -0,0 +1,5 @@ + + + diff --git a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte index e20e5879ff..6275934b3a 100644 --- a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte +++ b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte @@ -6,6 +6,7 @@ import OptionsCell from "./OptionsCell.svelte" import DateCell from "./DateCell.svelte" import MultiSelectCell from "./MultiSelectCell.svelte" + import NumberCell from "./NumberCell.svelte" export let table export let filter @@ -83,6 +84,8 @@ return DateCell } else if (type === "array") { return MultiSelectCell + } else if (type === "number") { + return NumberCell } return TextCell } diff --git a/packages/client/src/components/app/spreadsheet/TextCell.svelte b/packages/client/src/components/app/spreadsheet/TextCell.svelte index fb8b208831..47e30aae2d 100644 --- a/packages/client/src/components/app/spreadsheet/TextCell.svelte +++ b/packages/client/src/components/app/spreadsheet/TextCell.svelte @@ -2,6 +2,7 @@ export let value export let selected = false export let onChange + export let type = "text" const handleChange = e => { onChange(e.target.value) @@ -9,7 +10,7 @@ {#if selected} - + {:else}
{value || ""} From dbf8494c4bf86c212df32064ac1457db001566eb Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 21 Feb 2023 10:23:55 +0000 Subject: [PATCH 017/235] Add functional date cell --- .../app/spreadsheet/DateCell.svelte | 60 +++++++++++++++++-- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/packages/client/src/components/app/spreadsheet/DateCell.svelte b/packages/client/src/components/app/spreadsheet/DateCell.svelte index c385f98a38..903738ca35 100644 --- a/packages/client/src/components/app/spreadsheet/DateCell.svelte +++ b/packages/client/src/components/app/spreadsheet/DateCell.svelte @@ -1,20 +1,72 @@ -
- {parsedValue} +
+
+ {dayjs(timeOnly ? time : value).format(format)} +
+ {#if selected} + + {/if}
+{#if selected} +
+ onChange(e.detail)} + appendTo={document.getElementById("flatpickr-root")} + enableTime={!dateOnly} + {timeOnly} + time24hr + ignoreTimezones={schema.ignoreTimezones} + /> +
+{/if} + From 8c81d5916b32657fe25074e4275b8ffb88ec5857 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 21 Feb 2023 10:46:38 +0000 Subject: [PATCH 018/235] Disable editing autocolumns --- .../src/components/app/spreadsheet/DateCell.svelte | 8 +++++--- .../components/app/spreadsheet/OptionsCell.svelte | 12 +++++++----- .../components/app/spreadsheet/Spreadsheet.svelte | 6 ++++-- .../src/components/app/spreadsheet/TextCell.svelte | 5 ++++- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/packages/client/src/components/app/spreadsheet/DateCell.svelte b/packages/client/src/components/app/spreadsheet/DateCell.svelte index 903738ca35..a57ee4e10a 100644 --- a/packages/client/src/components/app/spreadsheet/DateCell.svelte +++ b/packages/client/src/components/app/spreadsheet/DateCell.svelte @@ -4,8 +4,9 @@ export let value export let schema - export let selected export let onChange + export let selected = false + export let readonly = false // adding the 0- will turn a string like 00:00:00 into a valid ISO // date, but will make actual ISO dates invalid @@ -17,18 +18,19 @@ : dateOnly ? "MMM D YYYY" : "MMM D YYYY, HH:mm" + $: editable = selected && !readonly
{dayjs(timeOnly ? time : value).format(format)}
- {#if selected} + {#if editable} {/if}
-{#if selected} +{#if editable}
x != null) $: unselectedOptions = options.filter(x => !values.includes(x)) $: { @@ -44,9 +46,9 @@
(open = true) : null} + on:click={editable ? () => (open = true) : null} >
{#each values as val (val)} @@ -62,7 +64,7 @@ {/if} {/each}
- {#if selected} + {#if editable} {/if} {#if open} @@ -101,7 +103,7 @@ overflow: hidden; gap: 4px; } - .container.selected:hover { + .container.editable:hover { cursor: pointer; } .values { diff --git a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte index 6275934b3a..d772e88505 100644 --- a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte +++ b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte @@ -37,7 +37,8 @@ limit, }) $: fields = Object.keys($fetch.schema || {}) - $: initWidths(fields) + $: fieldCount = fields.length + $: fieldCount, initWidths() $: gridStyles = getGridStyles(widths) $: schema = $fetch.schema $: rowCount = $fetch.rows?.length || 0 @@ -58,7 +59,7 @@ }) } - const initWidths = fields => { + const initWidths = () => { widths = fields.map(() => defaultWidth) } @@ -306,6 +307,7 @@ schema={schema[field]} selected={selectedCell === cellIdx} onChange={val => handleChange(row._id, field, val)} + readonly={schema[field]?.autocolumn} />
{/each} diff --git a/packages/client/src/components/app/spreadsheet/TextCell.svelte b/packages/client/src/components/app/spreadsheet/TextCell.svelte index 47e30aae2d..19531c88aa 100644 --- a/packages/client/src/components/app/spreadsheet/TextCell.svelte +++ b/packages/client/src/components/app/spreadsheet/TextCell.svelte @@ -3,13 +3,16 @@ export let selected = false export let onChange export let type = "text" + export let readonly = false + + $: editable = selected && !readonly const handleChange = e => { onChange(e.target.value) } -{#if selected} +{#if editable} {:else}
From 8f5c5cc75835d813dbf1c5acdb5f23825765f562 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 21 Feb 2023 11:10:26 +0000 Subject: [PATCH 019/235] Make sticky column the primary display and fix opening options cells --- .../app/spreadsheet/OptionsCell.svelte | 10 +++++++++- .../app/spreadsheet/Spreadsheet.svelte | 17 +++++++++++++---- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/packages/client/src/components/app/spreadsheet/OptionsCell.svelte b/packages/client/src/components/app/spreadsheet/OptionsCell.svelte index ee75ededac..1d4fc0c85b 100644 --- a/packages/client/src/components/app/spreadsheet/OptionsCell.svelte +++ b/packages/client/src/components/app/spreadsheet/OptionsCell.svelte @@ -41,6 +41,14 @@ } } } + + const toggleOpen = () => { + if (multi) { + open = true + } else { + open = !open + } + }
(open = true) : null} + on:click={editable ? toggleOpen : null} >
{#each values as val (val)} diff --git a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte index d772e88505..dc7f715d90 100644 --- a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte +++ b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte @@ -36,11 +36,12 @@ query, limit, }) - $: fields = Object.keys($fetch.schema || {}) + $: schema = $fetch.schema + $: primaryDisplay = $fetch.definition?.primaryDisplay + $: fields = getFields(schema, primaryDisplay) $: fieldCount = fields.length $: fieldCount, initWidths() $: gridStyles = getGridStyles(widths) - $: schema = $fetch.schema $: rowCount = $fetch.rows?.length || 0 $: selectedRowCount = Object.values(selectedRows).filter(x => !!x).length $: rows = getSortedRows($fetch.rows, newRows) @@ -59,6 +60,14 @@ }) } + const getFields = (schema, primaryDisplay) => { + let fields = Object.keys(schema || {}) + if (primaryDisplay) { + fields = [primaryDisplay, ...fields.filter(x => x !== primaryDisplay)] + } + return fields + } + const initWidths = () => { widths = fields.map(() => defaultWidth) } @@ -293,7 +302,7 @@
addRow(field)} From 57cfc9d84c139064947a003ff8b717a9b73196f4 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 21 Feb 2023 11:46:20 +0000 Subject: [PATCH 020/235] Improve display of relationship cell and options cell --- .../app/spreadsheet/OptionsCell.svelte | 25 +++++++++--- .../app/spreadsheet/RelationshipCell.svelte | 40 +++++++++++++++++++ .../app/spreadsheet/Spreadsheet.svelte | 3 ++ 3 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 packages/client/src/components/app/spreadsheet/RelationshipCell.svelte diff --git a/packages/client/src/components/app/spreadsheet/OptionsCell.svelte b/packages/client/src/components/app/spreadsheet/OptionsCell.svelte index 1d4fc0c85b..0494f951bc 100644 --- a/packages/client/src/components/app/spreadsheet/OptionsCell.svelte +++ b/packages/client/src/components/app/spreadsheet/OptionsCell.svelte @@ -73,7 +73,9 @@ {/each}
{#if editable} - +
+ +
{/if} {#if open}
@@ -105,11 +107,9 @@ display: flex; flex-direction: row; justify-content: space-between; - align-items: center; - padding: 0 8px; + align-items: stretch; + align-self: stretch; flex: 1 1 auto; - overflow: hidden; - gap: 4px; } .container.editable:hover { cursor: pointer; @@ -123,6 +123,7 @@ width: 0; gap: 4px; overflow: hidden; + padding: 0 8px; } .text { overflow: hidden; @@ -138,6 +139,20 @@ border-radius: 8px; user-select: none; } + .arrow { + position: absolute; + right: 2px; + top: 2px; + bottom: 2px; + padding: 0 6px 0 16px; + display: grid; + place-items: center; + background: linear-gradient( + to right, + transparent 0%, + var(--background) 40% + ); + } .options { min-width: 100%; position: absolute; diff --git a/packages/client/src/components/app/spreadsheet/RelationshipCell.svelte b/packages/client/src/components/app/spreadsheet/RelationshipCell.svelte new file mode 100644 index 0000000000..e47633d603 --- /dev/null +++ b/packages/client/src/components/app/spreadsheet/RelationshipCell.svelte @@ -0,0 +1,40 @@ + + +
+ {#each value || [] as relationship, idx} +
+ {relationship.primaryDisplay} +
+ {/each} +
+ + diff --git a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte index dc7f715d90..0474d784f1 100644 --- a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte +++ b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte @@ -7,6 +7,7 @@ import DateCell from "./DateCell.svelte" import MultiSelectCell from "./MultiSelectCell.svelte" import NumberCell from "./NumberCell.svelte" + import RelationshipCell from "./RelationshipCell.svelte" export let table export let filter @@ -96,6 +97,8 @@ return MultiSelectCell } else if (type === "number") { return NumberCell + } else if (type === "link") { + return RelationshipCell } return TextCell } From 3abc2ddbd1918da4e31071eb475706e326127ffa Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 21 Feb 2023 11:51:52 +0000 Subject: [PATCH 021/235] Support empty dates and use CSS variables for easier styling --- .../src/components/app/spreadsheet/DateCell.svelte | 4 +++- .../src/components/app/spreadsheet/OptionsCell.svelte | 2 +- .../src/components/app/spreadsheet/Spreadsheet.svelte | 11 ++++++++--- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/client/src/components/app/spreadsheet/DateCell.svelte b/packages/client/src/components/app/spreadsheet/DateCell.svelte index a57ee4e10a..5f0c0cdfa1 100644 --- a/packages/client/src/components/app/spreadsheet/DateCell.svelte +++ b/packages/client/src/components/app/spreadsheet/DateCell.svelte @@ -23,7 +23,9 @@
- {dayjs(timeOnly ? time : value).format(format)} + {#if value} + {dayjs(timeOnly ? time : value).format(format)} + {/if}
{#if editable} diff --git a/packages/client/src/components/app/spreadsheet/OptionsCell.svelte b/packages/client/src/components/app/spreadsheet/OptionsCell.svelte index 0494f951bc..46d3552ad2 100644 --- a/packages/client/src/components/app/spreadsheet/OptionsCell.svelte +++ b/packages/client/src/components/app/spreadsheet/OptionsCell.svelte @@ -150,7 +150,7 @@ background: linear-gradient( to right, transparent 0%, - var(--background) 40% + var(--cell-background) 40% ); } .options { diff --git a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte index 0474d784f1..60eeda6898 100644 --- a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte +++ b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte @@ -365,6 +365,10 @@ align-items: stretch; border: 1px solid var(--spectrum-global-color-gray-400); border-radius: 4px; + + /* Variables */ + --cell-background: var(--spectrum-global-color-gray-50); + --cell-background-hover: var(--spectrum-global-color-gray-100); } .spreadsheet { display: grid; @@ -382,7 +386,7 @@ } .wrapper ::-webkit-scrollbar-track { - background: var(--spectrum-global-color-gray-50); + background: var(--cell-background); } .controls { @@ -430,11 +434,12 @@ color: var(--spectrum-global-color-gray-900); font-size: 14px; gap: 4px; - background: var(--spectrum-global-color-gray-50); + background: var(--cell-background); position: relative; } .cell.hovered { - background: var(--spectrum-global-color-gray-100); + background: var(--cell-background-hover); + --cell-background: var(--cell-background-hover); } .cell.selected { box-shadow: inset 0 0 0 2px var(--spectrum-global-color-blue-400); From 8da1e507b12d3b4023e4dd5aa4dfdd0c9029fdc6 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 21 Feb 2023 12:05:16 +0000 Subject: [PATCH 022/235] Use more CSS variables and add utils to spreadsheets --- .../app/spreadsheet/DateCell.svelte | 4 +- .../app/spreadsheet/OptionsCell.svelte | 43 +++++++++++-------- .../app/spreadsheet/RelationshipCell.svelte | 19 +++----- .../app/spreadsheet/Spreadsheet.svelte | 15 ++++--- .../app/spreadsheet/TextCell.svelte | 6 +-- .../src/components/app/spreadsheet/utils.js | 6 +++ 6 files changed, 50 insertions(+), 43 deletions(-) create mode 100644 packages/client/src/components/app/spreadsheet/utils.js diff --git a/packages/client/src/components/app/spreadsheet/DateCell.svelte b/packages/client/src/components/app/spreadsheet/DateCell.svelte index 5f0c0cdfa1..887bf11c65 100644 --- a/packages/client/src/components/app/spreadsheet/DateCell.svelte +++ b/packages/client/src/components/app/spreadsheet/DateCell.svelte @@ -48,13 +48,13 @@ diff --git a/packages/client/src/components/app/spreadsheet/RelationshipCell.svelte b/packages/client/src/components/app/spreadsheet/RelationshipCell.svelte index e47633d603..4c1a32ca16 100644 --- a/packages/client/src/components/app/spreadsheet/RelationshipCell.svelte +++ b/packages/client/src/components/app/spreadsheet/RelationshipCell.svelte @@ -1,14 +1,7 @@
@@ -24,17 +17,17 @@ display: flex; flex-direction: row; align-items: center; - padding: 0 8px; + padding: 0 var(--cell-padding); flex: 1 1 auto; width: 0; - gap: 4px; + gap: var(--cell-spacing); overflow: hidden; } .badge { flex: 0 0 auto; - padding: 2px 8px; + padding: 2px var(--cell-padding); background: var(--color); - border-radius: 8px; + border-radius: var(--cell-padding); user-select: none; } diff --git a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte index 60eeda6898..6a5cec15d2 100644 --- a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte +++ b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte @@ -369,6 +369,10 @@ /* Variables */ --cell-background: var(--spectrum-global-color-gray-50); --cell-background-hover: var(--spectrum-global-color-gray-100); + --cell-padding: 8px; + --cell-spacing: 4px; + --cell-height: 32px; + --cell-font-size: 14px; } .spreadsheet { display: grid; @@ -407,7 +411,7 @@ flex-direction: row; justify-content: flex-start; align-items: center; - gap: 4px; + gap: var(--cell-spacing); } .delete { display: flex; @@ -424,7 +428,7 @@ } .cell { - height: 32px; + height: var(--cell-height); border-bottom: 1px solid var(--spectrum-global-color-gray-300); border-right: 1px solid var(--spectrum-global-color-gray-300); display: flex; @@ -432,14 +436,13 @@ justify-content: flex-start; align-items: center; color: var(--spectrum-global-color-gray-900); - font-size: 14px; - gap: 4px; + font-size: var(--cell-font-size); + gap: var(--cell-spacing); background: var(--cell-background); position: relative; } .cell.hovered { background: var(--cell-background-hover); - --cell-background: var(--cell-background-hover); } .cell.selected { box-shadow: inset 0 0 0 2px var(--spectrum-global-color-blue-400); @@ -470,7 +473,7 @@ background: var(--spectrum-global-color-gray-200); position: sticky; top: 0; - padding: 0 8px; + padding: 0 var(--cell-padding); z-index: 3; border-color: var(--spectrum-global-color-gray-400); } diff --git a/packages/client/src/components/app/spreadsheet/TextCell.svelte b/packages/client/src/components/app/spreadsheet/TextCell.svelte index 19531c88aa..40d15818f8 100644 --- a/packages/client/src/components/app/spreadsheet/TextCell.svelte +++ b/packages/client/src/components/app/spreadsheet/TextCell.svelte @@ -22,7 +22,7 @@ From ca7aed617f6cb0fd14538a93f7daeed2d1d1e9a0 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Wed, 22 Feb 2023 15:42:20 +0000 Subject: [PATCH 024/235] Break out reordering logic into new stores --- .../spreadsheet/ReorderingPlaceholder.svelte | 32 ++ .../app/spreadsheet/Spreadsheet.svelte | 276 +++++++----------- .../spreadsheet/{ => cells}/DateCell.svelte | 0 .../{ => cells}/MultiSelectCell.svelte | 0 .../spreadsheet/{ => cells}/NumberCell.svelte | 0 .../{ => cells}/OptionsCell.svelte | 2 +- .../{ => cells}/RelationshipCell.svelte | 2 +- .../spreadsheet/{ => cells}/TextCell.svelte | 0 .../app/spreadsheet/stores/reordering.js | 134 +++++++++ 9 files changed, 273 insertions(+), 173 deletions(-) create mode 100644 packages/client/src/components/app/spreadsheet/ReorderingPlaceholder.svelte rename packages/client/src/components/app/spreadsheet/{ => cells}/DateCell.svelte (100%) rename packages/client/src/components/app/spreadsheet/{ => cells}/MultiSelectCell.svelte (100%) rename packages/client/src/components/app/spreadsheet/{ => cells}/NumberCell.svelte (100%) rename packages/client/src/components/app/spreadsheet/{ => cells}/OptionsCell.svelte (99%) rename packages/client/src/components/app/spreadsheet/{ => cells}/RelationshipCell.svelte (94%) rename packages/client/src/components/app/spreadsheet/{ => cells}/TextCell.svelte (100%) create mode 100644 packages/client/src/components/app/spreadsheet/stores/reordering.js diff --git a/packages/client/src/components/app/spreadsheet/ReorderingPlaceholder.svelte b/packages/client/src/components/app/spreadsheet/ReorderingPlaceholder.svelte new file mode 100644 index 0000000000..d57a299f7f --- /dev/null +++ b/packages/client/src/components/app/spreadsheet/ReorderingPlaceholder.svelte @@ -0,0 +1,32 @@ + + +{#if $reorderingPlaceholder.x != null} +
+{/if} + + diff --git a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte index 2ccd029e25..ddf71b3b57 100644 --- a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte +++ b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte @@ -1,32 +1,55 @@ diff --git a/packages/client/src/components/app/spreadsheet/TextCell.svelte b/packages/client/src/components/app/spreadsheet/cells/TextCell.svelte similarity index 100% rename from packages/client/src/components/app/spreadsheet/TextCell.svelte rename to packages/client/src/components/app/spreadsheet/cells/TextCell.svelte diff --git a/packages/client/src/components/app/spreadsheet/stores/reordering.js b/packages/client/src/components/app/spreadsheet/stores/reordering.js new file mode 100644 index 0000000000..71cbeaa151 --- /dev/null +++ b/packages/client/src/components/app/spreadsheet/stores/reordering.js @@ -0,0 +1,134 @@ +import { get, writable } from "svelte/store" + +export const createReorderingStores = context => { + const { columns, rand, rows } = context + const reorderingInitialState = { + columnIdx: null, + swapColumnIdx: null, + breakpoints: [], + initialMouseX: null, + } + const reordering = writable(reorderingInitialState) + + // This is broken into its own store as it is rapidly updated, and we want to + // ensure good performance by avoiding updating other components which depend + // on other reordering state + const placeholderInitialState = { + x: null, + initialX: null, + width: null, + height: null, + } + const placeholder = writable(placeholderInitialState) + + // Callback when dragging on a colum header and starting reordering + const startReordering = (columnIdx, e) => { + // Generate new breakpoints for the current columns + let breakpoints = [] + const cols = get(columns) + console.log(cols) + cols.forEach((col, idx) => { + const header = document.getElementById(`sheet-${rand}-header-${idx}`) + const bounds = header.getBoundingClientRect() + breakpoints.push(bounds.x) + if (idx === cols.length - 1) { + breakpoints.push(bounds.x + bounds.width) + } + }) + + // Get bounds of the selected header and sheet body + const self = document.getElementById(`sheet-${rand}-header-${columnIdx}`) + const selfBounds = self.getBoundingClientRect() + const body = document.getElementById(`sheet-${rand}-body`) + const bodyBounds = body.getBoundingClientRect() + + // Update state + reordering.set({ + columnIdx, + breakpoints, + swapColumnIdx: null, + initialMouseX: e.clientX, + }) + placeholder.set({ + initialX: selfBounds.x - bodyBounds.x, + x: selfBounds.x - bodyBounds.x, + width: selfBounds.width, + height: (get(rows).length + 2) * 32, + }) + + // Add listeners to handle mouse movement + document.addEventListener("mousemove", onReorderMouseMove) + document.addEventListener("mouseup", stopReordering) + + // Trigger a move event immediately so ensure a candidate column is chosen + onReorderMouseMove(e) + } + + // Callback when moving the mouse when reordering columns + const onReorderMouseMove = e => { + const $reordering = get(reordering) + if ($reordering.columnIdx == null) { + return + } + + // Compute new placeholder position + const $placeholder = get(placeholder) + let newX = e.clientX - $reordering.initialMouseX + $placeholder.initialX + newX = Math.max(0, newX) + + // Compute the closest breakpoint to the current position + let swapColumnIdx + let minDistance = Number.MAX_SAFE_INTEGER + $reordering.breakpoints.forEach((point, idx) => { + const distance = Math.abs(point - e.clientX) + if (distance < minDistance) { + minDistance = distance + swapColumnIdx = idx + } + }) + + // Update state + placeholder.update(state => { + state.x = newX + return state + }) + if (swapColumnIdx !== $reordering.swapColumnIdx) { + reordering.update(state => { + state.swapColumnIdx = swapColumnIdx + return state + }) + } + } + + // Callback when stopping reordering columns + const stopReordering = () => { + // Swap position of columns + let { columnIdx, swapColumnIdx } = get(reordering) + const newColumns = get(columns).slice() + const removed = newColumns.splice(columnIdx, 1) + if (--swapColumnIdx < columnIdx) { + swapColumnIdx++ + } + newColumns.splice(swapColumnIdx, 0, removed[0]) + columns.set(newColumns) + + // Reset state + reordering.set(reorderingInitialState) + placeholder.set(placeholderInitialState) + + // Remove event handlers + document.removeEventListener("mousemove", onReorderMouseMove) + document.removeEventListener("mouseup", stopReordering) + } + + return { + reordering: { + ...reordering, + actions: { + startReordering, + stopReordering, + }, + }, + reorderingPlaceholder: placeholder, + } +} From fc06811b2c5b562518d463a8976d490f3bc8454e Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Wed, 22 Feb 2023 15:48:17 +0000 Subject: [PATCH 025/235] Rename reordering to reorder --- ...older.svelte => ReorderPlaceholder.svelte} | 6 +-- .../app/spreadsheet/Spreadsheet.svelte | 39 +++++++++---------- .../stores/{reordering.js => reorder.js} | 31 +++++++-------- .../app/spreadsheet/stores/resize.js | 0 4 files changed, 37 insertions(+), 39 deletions(-) rename packages/client/src/components/app/spreadsheet/{ReorderingPlaceholder.svelte => ReorderPlaceholder.svelte} (76%) rename packages/client/src/components/app/spreadsheet/stores/{reordering.js => reorder.js} (83%) create mode 100644 packages/client/src/components/app/spreadsheet/stores/resize.js diff --git a/packages/client/src/components/app/spreadsheet/ReorderingPlaceholder.svelte b/packages/client/src/components/app/spreadsheet/ReorderPlaceholder.svelte similarity index 76% rename from packages/client/src/components/app/spreadsheet/ReorderingPlaceholder.svelte rename to packages/client/src/components/app/spreadsheet/ReorderPlaceholder.svelte index d57a299f7f..b3cd754cd8 100644 --- a/packages/client/src/components/app/spreadsheet/ReorderingPlaceholder.svelte +++ b/packages/client/src/components/app/spreadsheet/ReorderPlaceholder.svelte @@ -1,9 +1,9 @@ -{#if $reorderingPlaceholder.x != null} +{#if $reorderPlaceholder.x != null}
{/if} diff --git a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte index ddf71b3b57..2419c65564 100644 --- a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte +++ b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte @@ -10,8 +10,8 @@ import NumberCell from "./cells/NumberCell.svelte" import RelationshipCell from "./cells/RelationshipCell.svelte" import { getColor } from "./utils.js" - import { createReorderingStores } from "./stores/reordering" - import ReorderingPlaceholder from "./ReorderingPlaceholder.svelte" + import { createReorderStores } from "./stores/reorder" + import ReorderPlaceholder from "./ReorderPlaceholder.svelte" export let table export let filter @@ -43,11 +43,11 @@ selectedCellId, selectedRows, } - const { reordering, reorderingPlaceholder } = createReorderingStores(context) + const { reorder, reorderPlaceholder } = createReorderStores(context) setContext("spreadsheet", { ...context, - reordering, - reorderingPlaceholder, + reorder, + reorderPlaceholder, }) let horizontallyScrolled = false @@ -301,9 +301,9 @@ class="header cell" class:sticky={fieldIdx === 0} class:shadow={horizontallyScrolled} - class:reordering-source={$reordering.columnIdx === fieldIdx} - class:reordering-target={$reordering.swapColumnIdx === fieldIdx} - on:mousedown={e => reordering.actions.startReordering(fieldIdx, e)} + class:reorder-source={$reorder.columnIdx === fieldIdx} + class:reorder-target={$reorder.swapColumnIdx === fieldIdx} + on:mousedown={e => reorder.actions.startReordering(fieldIdx, e)} id={`sheet-${rand}-header-${fieldIdx}`} >
@@ -354,8 +354,8 @@ class:hovered={rowHovered} class:selected={$selectedCellId === cellIdx} class:shadow={horizontallyScrolled} - class:reordering-source={$reordering.columnIdx === fieldIdx} - class:reordering-target={$reordering.swapColumnIdx === fieldIdx} + class:reorder-source={$reorder.columnIdx === fieldIdx} + class:reorder-target={$reorder.swapColumnIdx === fieldIdx} on:focus on:mouseover={() => ($hoveredRowId = row._id)} on:click={() => ($selectedCellId = cellIdx)} @@ -374,8 +374,7 @@
{/each} @@ -395,8 +394,8 @@ class:sticky={fieldIdx === 0} class:shadow={horizontallyScrolled} class:hovered={$hoveredRowId === "new"} - class:reordering-source={$reordering.columnIdx === fieldIdx} - class:reordering-target={$reordering.swapColumnIdx === fieldIdx} + class:reorder-source={$reorder.columnIdx === fieldIdx} + class:reorder-target={$reorder.swapColumnIdx === fieldIdx} on:click={() => addRow(field)} on:focus on:mouseover={() => ($hoveredRowId = "new")} @@ -405,7 +404,7 @@
@@ -413,7 +412,7 @@
- +
@@ -573,11 +572,11 @@ background: linear-gradient(to right, rgba(0, 0, 0, 0.08), transparent); } - /* Reordering styles */ - .cell.reordering-source { + /* Reorder styles */ + .cell.reorder-source { background: var(--spectrum-global-color-gray-200); } - .cell.reordering-target { + .cell.reorder-target { border-left-color: var(--spectrum-global-color-blue-400); } diff --git a/packages/client/src/components/app/spreadsheet/stores/reordering.js b/packages/client/src/components/app/spreadsheet/stores/reorder.js similarity index 83% rename from packages/client/src/components/app/spreadsheet/stores/reordering.js rename to packages/client/src/components/app/spreadsheet/stores/reorder.js index 71cbeaa151..972db3a6ce 100644 --- a/packages/client/src/components/app/spreadsheet/stores/reordering.js +++ b/packages/client/src/components/app/spreadsheet/stores/reorder.js @@ -1,14 +1,14 @@ import { get, writable } from "svelte/store" -export const createReorderingStores = context => { +export const createReorderStores = context => { const { columns, rand, rows } = context - const reorderingInitialState = { + const reorderInitialState = { columnIdx: null, swapColumnIdx: null, breakpoints: [], initialMouseX: null, } - const reordering = writable(reorderingInitialState) + const reorder = writable(reorderInitialState) // This is broken into its own store as it is rapidly updated, and we want to // ensure good performance by avoiding updating other components which depend @@ -26,7 +26,6 @@ export const createReorderingStores = context => { // Generate new breakpoints for the current columns let breakpoints = [] const cols = get(columns) - console.log(cols) cols.forEach((col, idx) => { const header = document.getElementById(`sheet-${rand}-header-${idx}`) const bounds = header.getBoundingClientRect() @@ -43,7 +42,7 @@ export const createReorderingStores = context => { const bodyBounds = body.getBoundingClientRect() // Update state - reordering.set({ + reorder.set({ columnIdx, breakpoints, swapColumnIdx: null, @@ -66,20 +65,20 @@ export const createReorderingStores = context => { // Callback when moving the mouse when reordering columns const onReorderMouseMove = e => { - const $reordering = get(reordering) - if ($reordering.columnIdx == null) { + const $reorder = get(reorder) + if ($reorder.columnIdx == null) { return } // Compute new placeholder position const $placeholder = get(placeholder) - let newX = e.clientX - $reordering.initialMouseX + $placeholder.initialX + let newX = e.clientX - $reorder.initialMouseX + $placeholder.initialX newX = Math.max(0, newX) // Compute the closest breakpoint to the current position let swapColumnIdx let minDistance = Number.MAX_SAFE_INTEGER - $reordering.breakpoints.forEach((point, idx) => { + $reorder.breakpoints.forEach((point, idx) => { const distance = Math.abs(point - e.clientX) if (distance < minDistance) { minDistance = distance @@ -92,8 +91,8 @@ export const createReorderingStores = context => { state.x = newX return state }) - if (swapColumnIdx !== $reordering.swapColumnIdx) { - reordering.update(state => { + if (swapColumnIdx !== $reorder.swapColumnIdx) { + reorder.update(state => { state.swapColumnIdx = swapColumnIdx return state }) @@ -103,7 +102,7 @@ export const createReorderingStores = context => { // Callback when stopping reordering columns const stopReordering = () => { // Swap position of columns - let { columnIdx, swapColumnIdx } = get(reordering) + let { columnIdx, swapColumnIdx } = get(reorder) const newColumns = get(columns).slice() const removed = newColumns.splice(columnIdx, 1) if (--swapColumnIdx < columnIdx) { @@ -113,7 +112,7 @@ export const createReorderingStores = context => { columns.set(newColumns) // Reset state - reordering.set(reorderingInitialState) + reorder.set(reorderInitialState) placeholder.set(placeholderInitialState) // Remove event handlers @@ -122,13 +121,13 @@ export const createReorderingStores = context => { } return { - reordering: { - ...reordering, + reorder: { + ...reorder, actions: { startReordering, stopReordering, }, }, - reorderingPlaceholder: placeholder, + reorderPlaceholder: placeholder, } } diff --git a/packages/client/src/components/app/spreadsheet/stores/resize.js b/packages/client/src/components/app/spreadsheet/stores/resize.js new file mode 100644 index 0000000000..e69de29bb2 From c834a236b703829b122ffb0dd33bd0181a99ea2f Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 23 Feb 2023 07:44:59 +0000 Subject: [PATCH 026/235] Break out other components from spreadsheet for cleaner code --- .../components/app/spreadsheet/Header.svelte | 108 +++++++++++ .../app/spreadsheet/ResizeSlider.svelte | 36 ++++ .../app/spreadsheet/Spreadsheet.svelte | 174 +++--------------- .../app/spreadsheet/stores/resize.js | 60 ++++++ 4 files changed, 228 insertions(+), 150 deletions(-) create mode 100644 packages/client/src/components/app/spreadsheet/Header.svelte create mode 100644 packages/client/src/components/app/spreadsheet/ResizeSlider.svelte diff --git a/packages/client/src/components/app/spreadsheet/Header.svelte b/packages/client/src/components/app/spreadsheet/Header.svelte new file mode 100644 index 0000000000..35f81b950d --- /dev/null +++ b/packages/client/src/components/app/spreadsheet/Header.svelte @@ -0,0 +1,108 @@ + + +
+
+ Filter + Group + Sort + Hide fields +
+
Sales Records
+
+ {#if selectedRowCount} + + Delete {selectedRowCount} row{selectedRowCount === 1 ? "" : "s"} + + {:else} + {rowCount} row{rowCount === 1 ? "" : "s"} + {/if} +
+
+ + diff --git a/packages/client/src/components/app/spreadsheet/ResizeSlider.svelte b/packages/client/src/components/app/spreadsheet/ResizeSlider.svelte new file mode 100644 index 0000000000..7e4c4c9a12 --- /dev/null +++ b/packages/client/src/components/app/spreadsheet/ResizeSlider.svelte @@ -0,0 +1,36 @@ + + +
resize.actions.startResizing(columnIdx, e)} /> + + diff --git a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte index 2419c65564..2c484849df 100644 --- a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte +++ b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte @@ -2,16 +2,18 @@ import { getContext, setContext } from "svelte" import { writable } from "svelte/store" import { fetchData, LuceneUtils } from "@budibase/frontend-core" - import { Icon, ActionButton } from "@budibase/bbui" + import { Icon } from "@budibase/bbui" import TextCell from "./cells/TextCell.svelte" import OptionsCell from "./cells/OptionsCell.svelte" import DateCell from "./cells/DateCell.svelte" import MultiSelectCell from "./cells/MultiSelectCell.svelte" import NumberCell from "./cells/NumberCell.svelte" import RelationshipCell from "./cells/RelationshipCell.svelte" - import { getColor } from "./utils.js" import { createReorderStores } from "./stores/reorder" + import { createResizeStore } from "./stores/resize" import ReorderPlaceholder from "./ReorderPlaceholder.svelte" + import ResizeSlider from "./ResizeSlider.svelte" + import Header from "./Header.svelte" export let table export let filter @@ -24,15 +26,15 @@ // Sheet constants const limit = 100 const defaultWidth = 160 - const minWidth = 100 const rand = Math.random() // State stores + const rows = writable([]) const columns = writable([]) const hoveredRowId = writable(null) const selectedCellId = writable(null) const selectedRows = writable({}) - const rows = writable([]) + const tableId = writable(table?.tableId) // Build up spreadsheet context and additional stores const context = { @@ -42,23 +44,30 @@ hoveredRowId, selectedCellId, selectedRows, + tableId, } const { reorder, reorderPlaceholder } = createReorderStores(context) + const resize = createResizeStore(context) + + // API for children to consume + const spreadsheetAPI = { + refreshData: () => fetch?.refresh(), + } + + // Set context for children to consume setContext("spreadsheet", { ...context, reorder, reorderPlaceholder, + resize, + spreadsheetAPI, }) let horizontallyScrolled = false let changeCache = {} let newRows = [] - // State for resizing columns - let resizeInitialX - let resizeInitialWidth - let resizeFieldIndex - + $: tableId.set(table?.tableId) $: query = LuceneUtils.buildLuceneQuery(filter) $: fetch = createFetch(table) $: fetch.update({ @@ -183,42 +192,6 @@ delete changeCache[rowId] } - const deleteRows = () => { - // Fetch full row objects to be deleted - const rowsToDelete = Object.entries($selectedRows) - .map(entry => { - if (entry[1] === true) { - return $rows.find(x => x._id === entry[0]) - } else { - return null - } - }) - .filter(x => x != null) - - // Deletion callback when confirmed - const performDeletion = async () => { - await API.deleteRows({ - tableId: table.tableId, - rows: rowsToDelete, - }) - await fetch.refresh() - - // Refresh state - $selectedCellId = null - $hoveredRowId = null - $selectedRows = {} - } - - // Show confirmation - confirmationStore.actions.showConfirmation( - "Delete rows", - `Are you sure you want to delete ${selectedRowCount} row${ - selectedRowCount === 1 ? "" : "s" - }?`, - performDeletion - ) - } - const addRow = async field => { const res = await API.saveRow({ tableId: table.tableId }) $selectedCellId = `${res._id}-${field.name}` @@ -235,53 +208,11 @@ }) $rows = sortedRows } - - const startResizing = (fieldIdx, e) => { - e.stopPropagation() - resizeInitialX = e.clientX - resizeInitialWidth = $columns[fieldIdx].width - resizeFieldIndex = fieldIdx - document.addEventListener("mousemove", onResizeMove) - document.addEventListener("mouseup", stopResizing) - } - - const onResizeMove = e => { - const dx = e.clientX - resizeInitialX - columns.update(state => { - state[resizeFieldIndex].width = Math.max( - minWidth, - resizeInitialWidth + dx - ) - return state - }) - } - - const stopResizing = () => { - document.removeEventListener("mousemove", onResizeMove) - document.removeEventListener("mouseup", stopResizing) - }
-
-
-
- Filter - Group - Sort - Hide fields -
-
Sales Records
-
- {#if selectedRowCount} - - Delete {selectedRowCount} row{selectedRowCount === 1 ? "" : "s"} - - {:else} - {rowCount} row{rowCount === 1 ? "" : "s"} - {/if} -
-
+
+
{field.name} -
startResizing(fieldIdx, e)} /> +
{/each} @@ -434,6 +365,9 @@ --cell-height: 32px; --cell-font-size: 14px; } + .wrapper.resize *:hover { + cursor: col-resize; + } .spreadsheet { display: grid; grid-template-columns: var(--grid); @@ -453,40 +387,6 @@ background: var(--cell-background); } - .controls { - display: grid; - grid-template-columns: 1fr auto 1fr; - align-items: center; - height: 36px; - padding: 0 12px; - background: var(--spectrum-global-color-gray-200); - gap: 8px; - border-bottom: 1px solid var(--spectrum-global-color-gray-400); - } - .title { - font-weight: 600; - } - .buttons { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: var(--cell-spacing); - } - .delete { - display: flex; - flex-direction: row; - justify-content: flex-end; - align-items: center; - color: var(--spectrum-global-color-gray-700); - } - .delete :global(.spectrum-ActionButton) { - color: var(--spectrum-global-color-red-600); - } - .delete :global(.spectrum-Icon) { - fill: var(--spectrum-global-color-red-600); - } - /* Cells */ .cell { height: var(--cell-height); @@ -580,32 +480,6 @@ border-left-color: var(--spectrum-global-color-blue-400); } - /* Column resizing */ - .slider { - position: absolute; - top: 0; - right: 0; - width: 16px; - height: 100%; - } - .slider:after { - opacity: 0; - content: " "; - position: absolute; - width: 4px; - right: 0; - top: 0; - height: 100%; - background: var(--spectrum-global-color-gray-600); - transition: opacity 130ms ease-out; - } - .slider:hover { - cursor: col-resize; - } - .slider:hover:after { - opacity: 1; - } - .label { padding: 0 12px; border-right: none; diff --git a/packages/client/src/components/app/spreadsheet/stores/resize.js b/packages/client/src/components/app/spreadsheet/stores/resize.js index e69de29bb2..9b4f8619a0 100644 --- a/packages/client/src/components/app/spreadsheet/stores/resize.js +++ b/packages/client/src/components/app/spreadsheet/stores/resize.js @@ -0,0 +1,60 @@ +import { writable, get } from "svelte/store" + +const MinColumnWidth = 100 + +export const createResizeStore = context => { + const { columns } = context + const initialState = { + initialMouseX: null, + initialWidth: null, + columnIdx: null, + } + const resize = writable(initialState) + + const startResizing = (columnIdx, e) => { + // Prevent propagation to stop reordering triggering + e.stopPropagation() + + // Update state + resize.set({ + columnIdx, + initialWidth: get(columns)[columnIdx].width, + initialMouseX: e.clientX, + }) + + // Add mouse event listeners to handle resizing + document.addEventListener("mousemove", onResizeMouseMove) + document.addEventListener("mouseup", stopResizing) + } + + const onResizeMouseMove = e => { + const $resize = get(resize) + const dx = e.clientX - $resize.initialMouseX + const width = get(columns)[$resize.columnIdx].width + const newWidth = Math.max(MinColumnWidth, $resize.initialWidth + dx) + + // Skip small updates + if (Math.abs(width - newWidth) < 10) { + return + } + + // Update width of column + columns.update(state => { + state[$resize.columnIdx].width = newWidth + return state + }) + } + + const stopResizing = () => { + resize.set(initialState) + document.removeEventListener("mousemove", onResizeMouseMove) + document.removeEventListener("mouseup", stopResizing) + } + + return { + ...resize, + actions: { + startResizing, + }, + } +} From 0060025cca77e1a6c49f507e895a08924c062377 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 23 Feb 2023 08:43:37 +0000 Subject: [PATCH 027/235] Break out spreadsheet body into its own component --- .../app/spreadsheet/Spreadsheet.svelte | 73 ++++--------------- .../app/spreadsheet/SpreadsheetBody.svelte | 58 +++++++++++++++ ...Header.svelte => SpreadsheetHeader.svelte} | 0 3 files changed, 72 insertions(+), 59 deletions(-) create mode 100644 packages/client/src/components/app/spreadsheet/SpreadsheetBody.svelte rename packages/client/src/components/app/spreadsheet/{Header.svelte => SpreadsheetHeader.svelte} (100%) diff --git a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte index 2c484849df..53b6b6fe06 100644 --- a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte +++ b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte @@ -13,14 +13,15 @@ import { createResizeStore } from "./stores/resize" import ReorderPlaceholder from "./ReorderPlaceholder.svelte" import ResizeSlider from "./ResizeSlider.svelte" - import Header from "./Header.svelte" + import SpreadsheetHeader from "./SpreadsheetHeader.svelte" + import SpreadsheetBody from "./SpreadsheetBody.svelte" export let table export let filter export let sortColumn export let sortOrder - const { styleable, API, confirmationStore } = getContext("sdk") + const { styleable, API } = getContext("sdk") const component = getContext("component") // Sheet constants @@ -63,7 +64,6 @@ spreadsheetAPI, }) - let horizontallyScrolled = false let changeCache = {} let newRows = [] @@ -77,7 +77,6 @@ limit, }) $: generateColumns($fetch) - $: gridStyles = getGridStyles($columns) $: rowCount = $rows.length $: selectedRowCount = Object.values($selectedRows).filter(x => !!x).length $: updateSortedRows($fetch.rows, newRows) @@ -113,21 +112,6 @@ } } - const getGridStyles = columns => { - const widths = columns?.map(x => x.width) - if (!widths?.length) { - return "--grid: 1fr;" - } - return `--grid: 40px ${widths.map(x => `${x}px`).join(" ")} 180px;` - } - - const handleScroll = e => { - const nextHorizontallyScrolled = e.target.scrollLeft > 0 - if (nextHorizontallyScrolled !== horizontallyScrolled) { - horizontallyScrolled = nextHorizontallyScrolled - } - } - const getCellForField = field => { const type = field.schema.type if (type === "options") { @@ -212,14 +196,8 @@
-
-
($selectedCellId = null)} - id={`sheet-${rand}-body`} - > + +
reorder.actions.startReordering(fieldIdx, e)} @@ -284,7 +261,6 @@ class:sticky={fieldIdx === 0} class:hovered={rowHovered} class:selected={$selectedCellId === cellIdx} - class:shadow={horizontallyScrolled} class:reorder-source={$reorder.columnIdx === fieldIdx} class:reorder-target={$reorder.swapColumnIdx === fieldIdx} on:focus @@ -323,7 +299,6 @@
-
+ @@ -368,21 +343,6 @@ .wrapper.resize *:hover { cursor: col-resize; } - .spreadsheet { - display: grid; - grid-template-columns: var(--grid); - justify-content: flex-start; - align-items: stretch; - overflow: auto; - max-height: 600px; - position: relative; - cursor: default; - } - .vertical-spacer { - grid-column: 1/-1; - height: 180px; - } - .wrapper ::-webkit-scrollbar-track { background: var(--cell-background); } @@ -425,10 +385,6 @@ .cell.new:hover { cursor: pointer; } - .cell.spacer { - background: none; - border-bottom: none; - } /* Header cells */ .header { @@ -460,16 +416,15 @@ .sticky.selected { z-index: 3; } - .sticky.shadow { - border-right-width: 1px; + + /* Spacer cells */ + .spacer { + background: none; + border-bottom: none; } - .sticky.shadow:after { - content: " "; - position: absolute; - width: 10px; - left: 100%; - height: 100%; - background: linear-gradient(to right, rgba(0, 0, 0, 0.08), transparent); + .vertical-spacer { + grid-column: 1/-1; + height: 180px; } /* Reorder styles */ diff --git a/packages/client/src/components/app/spreadsheet/SpreadsheetBody.svelte b/packages/client/src/components/app/spreadsheet/SpreadsheetBody.svelte new file mode 100644 index 0000000000..839e45b8e8 --- /dev/null +++ b/packages/client/src/components/app/spreadsheet/SpreadsheetBody.svelte @@ -0,0 +1,58 @@ + + +
($selectedCellId = null)} + id={`sheet-${rand}-body`} +> + +
+ + diff --git a/packages/client/src/components/app/spreadsheet/Header.svelte b/packages/client/src/components/app/spreadsheet/SpreadsheetHeader.svelte similarity index 100% rename from packages/client/src/components/app/spreadsheet/Header.svelte rename to packages/client/src/components/app/spreadsheet/SpreadsheetHeader.svelte From ddb70ba3f68bf74f6c6afd1faeb7974912b0d911 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 23 Feb 2023 16:40:48 +0000 Subject: [PATCH 028/235] Split into more modular components and try virtual rendering --- .../app/spreadsheet/SpacerCell.svelte | 13 + .../app/spreadsheet/Spreadsheet.svelte | 319 ++++-------------- .../app/spreadsheet/SpreadsheetBody.svelte | 33 +- .../app/spreadsheet/SpreadsheetCell.svelte | 138 ++++++++ .../app/spreadsheet/SpreadsheetRow.svelte | 107 ++++++ .../app/spreadsheet/VerticalSpacer.svelte | 17 + .../app/spreadsheet/stores/resize.js | 2 +- 7 files changed, 379 insertions(+), 250 deletions(-) create mode 100644 packages/client/src/components/app/spreadsheet/SpacerCell.svelte create mode 100644 packages/client/src/components/app/spreadsheet/SpreadsheetCell.svelte create mode 100644 packages/client/src/components/app/spreadsheet/SpreadsheetRow.svelte create mode 100644 packages/client/src/components/app/spreadsheet/VerticalSpacer.svelte diff --git a/packages/client/src/components/app/spreadsheet/SpacerCell.svelte b/packages/client/src/components/app/spreadsheet/SpacerCell.svelte new file mode 100644 index 0000000000..d4a9a87470 --- /dev/null +++ b/packages/client/src/components/app/spreadsheet/SpacerCell.svelte @@ -0,0 +1,13 @@ + + + ($selectedCellId = null)} + on:mouseenter={() => ($hoveredRowId = null)} +/> diff --git a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte index 53b6b6fe06..dde5566dd7 100644 --- a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte +++ b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte @@ -3,18 +3,16 @@ import { writable } from "svelte/store" import { fetchData, LuceneUtils } from "@budibase/frontend-core" import { Icon } from "@budibase/bbui" - import TextCell from "./cells/TextCell.svelte" - import OptionsCell from "./cells/OptionsCell.svelte" - import DateCell from "./cells/DateCell.svelte" - import MultiSelectCell from "./cells/MultiSelectCell.svelte" - import NumberCell from "./cells/NumberCell.svelte" - import RelationshipCell from "./cells/RelationshipCell.svelte" import { createReorderStores } from "./stores/reorder" import { createResizeStore } from "./stores/resize" import ReorderPlaceholder from "./ReorderPlaceholder.svelte" import ResizeSlider from "./ResizeSlider.svelte" import SpreadsheetHeader from "./SpreadsheetHeader.svelte" import SpreadsheetBody from "./SpreadsheetBody.svelte" + import SpreadsheetCell from "./SpreadsheetCell.svelte" + import SpacerCell from "./SpacerCell.svelte" + import VerticalSpacer from "./VerticalSpacer.svelte" + import SpreadsheetRow from "./SpreadsheetRow.svelte" export let table export let filter @@ -25,6 +23,7 @@ const component = getContext("component") // Sheet constants + const cellHeight = 32 const limit = 100 const defaultWidth = 160 const rand = Math.random() @@ -36,6 +35,9 @@ const selectedCellId = writable(null) const selectedRows = writable({}) const tableId = writable(table?.tableId) + const changeCache = writable({}) + const newRows = writable([]) + const visibleRows = writable([0, 0]) // Build up spreadsheet context and additional stores const context = { @@ -46,27 +48,14 @@ selectedCellId, selectedRows, tableId, + changeCache, + newRows, + cellHeight, + visibleRows, } const { reorder, reorderPlaceholder } = createReorderStores(context) const resize = createResizeStore(context) - // API for children to consume - const spreadsheetAPI = { - refreshData: () => fetch?.refresh(), - } - - // Set context for children to consume - setContext("spreadsheet", { - ...context, - reorder, - reorderPlaceholder, - resize, - spreadsheetAPI, - }) - - let changeCache = {} - let newRows = [] - $: tableId.set(table?.tableId) $: query = LuceneUtils.buildLuceneQuery(filter) $: fetch = createFetch(table) @@ -79,7 +68,7 @@ $: generateColumns($fetch) $: rowCount = $rows.length $: selectedRowCount = Object.values($selectedRows).filter(x => !!x).length - $: updateSortedRows($fetch.rows, newRows) + $: updateSortedRows($fetch.rows, $newRows) const createFetch = datasource => { return fetchData({ @@ -112,22 +101,6 @@ } } - const getCellForField = field => { - const type = field.schema.type - if (type === "options") { - return OptionsCell - } else if (type === "datetime") { - return DateCell - } else if (type === "array") { - return MultiSelectCell - } else if (type === "number") { - return NumberCell - } else if (type === "link") { - return RelationshipCell - } - return TextCell - } - const getIconForField = field => { const type = field.schema.type if (type === "options") { @@ -138,13 +111,6 @@ return "Text" } - const selectRow = id => { - selectedRows.update(state => { - state[id] = !state[id] - return state - }) - } - const selectAll = () => { const allSelected = selectedRowCount === rowCount if (allSelected) { @@ -159,7 +125,7 @@ } } - const handleChange = async (rowId, field, value) => { + const updateValue = async (rowId, field, value) => { let row = $rows.find(x => x._id === rowId) if (!row) { return @@ -167,19 +133,25 @@ if (row[field.name] === value) { return } - changeCache[rowId] = { [field.name]: value } + changeCache.update(state => { + state[rowId] = { [field.name]: value } + return state + }) await API.saveRow({ ...row, - ...changeCache[rowId], + ...$changeCache[rowId], }) await fetch.refresh() - delete changeCache[rowId] + changeCache.update(state => { + delete state[rowId] + return state + }) } const addRow = async field => { const res = await API.saveRow({ tableId: table.tableId }) $selectedCellId = `${res._id}-${field.name}` - newRows.push(res._id) + newRows.update(state => [...state, res._id]) await fetch.refresh() } @@ -192,25 +164,44 @@ }) $rows = sortedRows } + + // API for children to consume + const spreadsheetAPI = { + refreshData: () => fetch?.refresh(), + updateValue, + } + + // Set context for children to consume + setContext("spreadsheet", { + ...context, + reorder, + reorderPlaceholder, + resize, + spreadsheetAPI, + })
-
+
-
+ -
+ {#each $columns as field, fieldIdx} -
reorder.actions.startReordering(fieldIdx, e)} id={`sheet-${rand}-header-${fieldIdx}`} > @@ -223,101 +214,44 @@ {field.name} -
+ {/each} - -
{#each $rows as row, rowIdx (row._id)} - {@const rowSelected = !!$selectedRows[row._id]} - {@const rowHovered = $hoveredRowId === row._id} - {@const data = { ...row, ...changeCache[row._id] }} -
($hoveredRowId = row._id)} - on:click={() => selectRow(row._id)} - > - {#if rowSelected || rowHovered} - - {:else} - - {rowIdx + 1} - - {/if} -
- {#each $columns as field, fieldIdx} - {@const cellIdx = `${row._id}-${field.name}`} - {#key cellIdx} -
($hoveredRowId = row._id)} - on:click={() => ($selectedCellId = cellIdx)} - > - handleChange(row._id, field, val)} - readonly={field.schema.autocolumn} - /> -
- {/key} - {/each} - -
+ {/each} -
($hoveredRowId = "new")} - class:hovered={$hoveredRowId === "new"} + on:mouseenter={() => ($hoveredRowId = "new")} + rowHovered={$hoveredRowId === "new"} > -
+ {#each $columns as field, fieldIdx} -
addRow(field)} - on:focus - on:mouseover={() => ($hoveredRowId = "new")} + on:mouseenter={() => ($hoveredRowId = "new")} /> {/each} - -
+ - -
+ + - +
@@ -337,121 +271,12 @@ --cell-background-hover: var(--spectrum-global-color-gray-100); --cell-padding: 8px; --cell-spacing: 4px; - --cell-height: 32px; --cell-font-size: 14px; } .wrapper.resize *:hover { cursor: col-resize; } - .wrapper ::-webkit-scrollbar-track { + .wrapper::-webkit-scrollbar-track { background: var(--cell-background); } - - /* Cells */ - .cell { - height: var(--cell-height); - border-style: solid; - border-color: var(--spectrum-global-color-gray-300); - border-width: 0; - border-bottom-width: 1px; - border-left-width: 1px; - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - color: var(--spectrum-global-color-gray-900); - font-size: var(--cell-font-size); - gap: var(--cell-spacing); - background: var(--cell-background); - position: relative; - transition: border-color 130ms ease-out; - } - .cell.hovered { - background: var(--cell-background-hover); - } - .cell.selected { - box-shadow: inset 0 0 0 2px var(--spectrum-global-color-blue-400); - z-index: 1; - } - .cell:not(.selected) { - user-select: none; - } - .cell:hover { - cursor: default; - } - .cell.row-selected { - background-color: rgb(224, 242, 255); - } - .cell.new:hover { - cursor: pointer; - } - - /* Header cells */ - .header { - background: var(--spectrum-global-color-gray-200); - position: sticky; - top: 0; - padding: 0 var(--cell-padding); - z-index: 3; - border-color: var(--spectrum-global-color-gray-400); - } - .header span { - flex: 1 1 auto; - width: 0; - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - } - .header.sticky { - z-index: 4; - } - - /* Sticky styles */ - .sticky { - position: sticky; - left: 40px; - z-index: 2; - border-left-color: transparent; - } - .sticky.selected { - z-index: 3; - } - - /* Spacer cells */ - .spacer { - background: none; - border-bottom: none; - } - .vertical-spacer { - grid-column: 1/-1; - height: 180px; - } - - /* Reorder styles */ - .cell.reorder-source { - background: var(--spectrum-global-color-gray-200); - } - .cell.reorder-target { - border-left-color: var(--spectrum-global-color-blue-400); - } - - .label { - padding: 0 12px; - border-right: none; - position: sticky; - left: 0; - z-index: 2; - } - .label.header { - z-index: 4; - } - .label span { - min-width: 14px; - text-align: center; - color: var(--spectrum-global-color-gray-500); - } - - input[type="checkbox"] { - margin: 0; - } diff --git a/packages/client/src/components/app/spreadsheet/SpreadsheetBody.svelte b/packages/client/src/components/app/spreadsheet/SpreadsheetBody.svelte index 839e45b8e8..40084f0066 100644 --- a/packages/client/src/components/app/spreadsheet/SpreadsheetBody.svelte +++ b/packages/client/src/components/app/spreadsheet/SpreadsheetBody.svelte @@ -1,11 +1,17 @@
+ export let header = false + export let label = false + export let spacer = false + export let rowHovered = false + export let rowSelected = false + export let sticky = false + export let selected = false + export let reorderSource = false + export let reorderTarget = false + export let id = null + + +
+ +
+ + diff --git a/packages/client/src/components/app/spreadsheet/SpreadsheetRow.svelte b/packages/client/src/components/app/spreadsheet/SpreadsheetRow.svelte new file mode 100644 index 0000000000..2311ffab60 --- /dev/null +++ b/packages/client/src/components/app/spreadsheet/SpreadsheetRow.svelte @@ -0,0 +1,107 @@ + + +{#if !visible} +
+{:else} + ($hoveredRowId = row._id)} + on:click={() => selectRow(row._id)} + > + {#if rowSelected || rowHovered} + + {:else} + + {rowIdx + 1} + + {/if} + + {#each $columns as field, fieldIdx} + {@const cellIdx = `${row._id}-${field.name}`} + {#key cellIdx} + ($hoveredRowId = row._id)} + on:click={() => ($selectedCellId = cellIdx)} + > + spreadsheetAPI.updateValue(row._id, field, val)} + readonly={field.schema.autocolumn} + /> + + {/key} + {/each} + +{/if} + + diff --git a/packages/client/src/components/app/spreadsheet/VerticalSpacer.svelte b/packages/client/src/components/app/spreadsheet/VerticalSpacer.svelte new file mode 100644 index 0000000000..fcde0fcce9 --- /dev/null +++ b/packages/client/src/components/app/spreadsheet/VerticalSpacer.svelte @@ -0,0 +1,17 @@ + + +
($selectedCellId = null)} + on:mouseenter={() => ($hoveredRowId = null)} +/> + + diff --git a/packages/client/src/components/app/spreadsheet/stores/resize.js b/packages/client/src/components/app/spreadsheet/stores/resize.js index 9b4f8619a0..58c6151640 100644 --- a/packages/client/src/components/app/spreadsheet/stores/resize.js +++ b/packages/client/src/components/app/spreadsheet/stores/resize.js @@ -34,7 +34,7 @@ export const createResizeStore = context => { const newWidth = Math.max(MinColumnWidth, $resize.initialWidth + dx) // Skip small updates - if (Math.abs(width - newWidth) < 10) { + if (Math.abs(width - newWidth) < 20) { return } From 3ab0e95032a85f8ae89cf509b37ab7f42c027aa8 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 24 Feb 2023 09:46:44 +0000 Subject: [PATCH 029/235] Test absolute positioning --- .../app/spreadsheet/Spreadsheet.svelte | 120 ++++++++++-------- .../app/spreadsheet/SpreadsheetBody.svelte | 56 ++++---- .../app/spreadsheet/SpreadsheetCell.svelte | 6 +- .../app/spreadsheet/SpreadsheetRow.svelte | 57 +++++---- .../app/spreadsheet/stores/resize.js | 67 +++++++--- 5 files changed, 185 insertions(+), 121 deletions(-) diff --git a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte index dde5566dd7..49dbbf00d8 100644 --- a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte +++ b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte @@ -92,9 +92,10 @@ if (primaryDisplay) { fields = [primaryDisplay, ...fields.filter(x => x !== primaryDisplay)] } - $columns = fields.map(field => ({ + $columns = fields.map((field, idx) => ({ name: field, width: defaultWidth, + left: 40 + idx * defaultWidth, schema: schema[field], primaryDisplay: field === primaryDisplay, })) @@ -179,47 +180,56 @@ resize, spreadsheetAPI, }) + + let sheetStyles = "" + let left = 40 + for (let i = 0; i < 20; i++) { + sheetStyles += `--col-${i}-width:${160}px; --col-${i}-left:${left}px;` + left += 160 + }
- - - - - {#each $columns as field, fieldIdx} - reorder.actions.startReordering(fieldIdx, e)} - id={`sheet-${rand}-header-${fieldIdx}`} - > - + + + - - {field.name} - - - {/each} - + {#each $columns as field, fieldIdx} + reorder.actions.startReordering(fieldIdx, e)} + id={`sheet-${rand}-header-${fieldIdx}`} + width={field.width} + left={field.left} + column={fieldIdx} + > + + + {field.name} + + + + {/each} +
{#each $rows as row, rowIdx (row._id)} @@ -227,28 +237,30 @@ {/each} - ($hoveredRowId = "new")} - rowHovered={$hoveredRowId === "new"} - > - - - {#each $columns as field, fieldIdx} +
addRow(field)} + label + on:click={addRow} on:mouseenter={() => ($hoveredRowId = "new")} - /> - {/each} - - - - + rowHovered={$hoveredRowId === "new"} + width="40" + left="0" + > + + + {#each $columns as field, fieldIdx} + addRow(field)} + on:mouseenter={() => ($hoveredRowId = "new")} + width={field.width} + left={field.left} + /> + {/each} +
@@ -279,4 +291,10 @@ .wrapper::-webkit-scrollbar-track { background: var(--cell-background); } + + .row { + display: flex; + position: sticky; + width: 100%; + } diff --git a/packages/client/src/components/app/spreadsheet/SpreadsheetBody.svelte b/packages/client/src/components/app/spreadsheet/SpreadsheetBody.svelte index 40084f0066..5853ddceb4 100644 --- a/packages/client/src/components/app/spreadsheet/SpreadsheetBody.svelte +++ b/packages/client/src/components/app/spreadsheet/SpreadsheetBody.svelte @@ -2,18 +2,31 @@ import { getContext, onMount } from "svelte" import { Utils } from "@budibase/frontend-core" - const { columns, selectedCellId, rand, visibleRows, cellHeight } = + const { columns, selectedCellId, rand, visibleRows, cellHeight, rows } = getContext("spreadsheet") let ref - let height = 0 + let height = 600 let horizontallyScrolled = false let scrollTop = 0 $: gridStyles = getGridStyles($columns) $: computeVisibleRows(scrollTop, height) + $: contentHeight = ($rows.length + 2) * cellHeight + 180 + $: contentWidth = computeWidth($columns) + $: console.log("new height") + + const computeWidth = columns => { + console.log("width") + let width = 220 + columns.forEach(col => { + width += col.width + }) + return width + } const getGridStyles = columns => { + console.log("grid") const widths = columns?.map(x => x.width) if (!widths?.length) { return "--grid: 1fr;" @@ -30,11 +43,11 @@ scrollTop = e.target.scrollTop } - const computeVisibleRows = Utils.debounce((scrollTop, height) => { - const rows = Math.ceil(height / cellHeight) + 16 - const firstRow = Math.max(0, Math.floor(scrollTop / cellHeight) - 8) + const computeVisibleRows = (scrollTop, height) => { + const rows = Math.ceil(height / cellHeight) + 8 + const firstRow = Math.max(0, Math.floor(scrollTop / cellHeight) - 4) visibleRows.set([firstRow, firstRow + rows]) - }, 50) + } // Observe and record the height of the body onMount(() => { @@ -53,35 +66,28 @@ class="spreadsheet" class:horizontally-scrolled={horizontallyScrolled} on:scroll={handleScroll} - style={gridStyles} on:click|self={() => ($selectedCellId = null)} id={`sheet-${rand}-body`} > - +
+ +
diff --git a/packages/client/src/components/app/spreadsheet/SpreadsheetCell.svelte b/packages/client/src/components/app/spreadsheet/SpreadsheetCell.svelte index 1ce19744f7..2501d47726 100644 --- a/packages/client/src/components/app/spreadsheet/SpreadsheetCell.svelte +++ b/packages/client/src/components/app/spreadsheet/SpreadsheetCell.svelte @@ -9,6 +9,7 @@ export let reorderSource = false export let reorderTarget = false export let id = null + export let column
@@ -48,7 +50,7 @@ font-size: var(--cell-font-size); gap: var(--cell-spacing); background: var(--cell-background); - position: relative; + position: absolute; transition: border-color 130ms ease-out; } .cell.row-hovered { @@ -71,8 +73,6 @@ /* Header cells */ .cell.header { background: var(--spectrum-global-color-gray-200); - position: sticky; - top: 0; padding: 0 var(--cell-padding); z-index: 3; border-color: var(--spectrum-global-color-gray-400); diff --git a/packages/client/src/components/app/spreadsheet/SpreadsheetRow.svelte b/packages/client/src/components/app/spreadsheet/SpreadsheetRow.svelte index 2311ffab60..c64deaa63f 100644 --- a/packages/client/src/components/app/spreadsheet/SpreadsheetRow.svelte +++ b/packages/client/src/components/app/spreadsheet/SpreadsheetRow.svelte @@ -21,6 +21,7 @@ changeCache, spreadsheetAPI, visibleRows, + cellHeight, } = getContext("spreadsheet") $: rowSelected = !!$selectedRows[row._id] @@ -52,27 +53,27 @@ } -{#if !visible} -
-{:else} - ($hoveredRowId = row._id)} - on:click={() => selectRow(row._id)} - > - {#if rowSelected || rowHovered} - - {:else} - - {rowIdx + 1} - - {/if} - - {#each $columns as field, fieldIdx} - {@const cellIdx = `${row._id}-${field.name}`} - {#key cellIdx} +{#if visible} +
+ ($hoveredRowId = row._id)} + on:click={() => selectRow(row._id)} + width="40" + left="0" + > + {#if rowSelected || rowHovered} + + {:else} + + {rowIdx + 1} + + {/if} + + {#each $columns as field, fieldIdx} + {@const cellIdx = `${row._id}-${field.name}`} ($hoveredRowId = row._id)} on:click={() => ($selectedCellId = cellIdx)} + width={field.width} + left={field.left} + column={fieldIdx} > - {/key} - {/each} - + {/each} +
{/if} diff --git a/packages/client/src/components/app/spreadsheet/stores/resize.js b/packages/client/src/components/app/spreadsheet/stores/resize.js index 58c6151640..aab716df99 100644 --- a/packages/client/src/components/app/spreadsheet/stores/resize.js +++ b/packages/client/src/components/app/spreadsheet/stores/resize.js @@ -1,26 +1,39 @@ import { writable, get } from "svelte/store" +import { domDebounce } from "../../../../utils/domDebounce.js" const MinColumnWidth = 100 export const createResizeStore = context => { - const { columns } = context + const { columns, rand } = context const initialState = { initialMouseX: null, initialWidth: null, columnIdx: null, } + + let initialWidth = 0 + let width = 0 + let left = 0 + let initialMouseX = null + let sheet + let columnIdx = 0 + let columnCount = 0 + let styles + const resize = writable(initialState) - const startResizing = (columnIdx, e) => { + const startResizing = (idx, e) => { // Prevent propagation to stop reordering triggering e.stopPropagation() - // Update state - resize.set({ - columnIdx, - initialWidth: get(columns)[columnIdx].width, - initialMouseX: e.clientX, - }) + sheet = document.getElementById(`sheet-${rand}`) + styles = getComputedStyle(sheet) + width = parseInt(styles.getPropertyValue(`--col-${idx}-width`)) + left = parseInt(styles.getPropertyValue(`--col-${idx}-left`)) + initialWidth = width + initialMouseX = e.clientX + columnIdx = idx + columnCount = get(columns).length // Add mouse event listeners to handle resizing document.addEventListener("mousemove", onResizeMouseMove) @@ -28,21 +41,39 @@ export const createResizeStore = context => { } const onResizeMouseMove = e => { - const $resize = get(resize) - const dx = e.clientX - $resize.initialMouseX - const width = get(columns)[$resize.columnIdx].width - const newWidth = Math.max(MinColumnWidth, $resize.initialWidth + dx) + const dx = e.clientX - initialMouseX + const newWidth = Math.round(Math.max(MinColumnWidth, initialWidth + dx)) - // Skip small updates - if (Math.abs(width - newWidth) < 20) { + if (Math.abs(width - newWidth) < 10) { return } + + let newStyle = `--col-${columnIdx}-width:${newWidth}px;` + + let offset = left + newWidth + for (let i = columnIdx + 1; i < columnCount; i++) { + const colWidth = 160//parseInt(styles.getPropertyValue(`--col-${i}-width`)) + newStyle += `--col-${i}-left:${offset}px;` + offset += colWidth + } + + sheet.style.cssText += newStyle + width = newWidth + // Update width of column - columns.update(state => { - state[$resize.columnIdx].width = newWidth - return state - }) + // columns.update(state => { + // state[$resize.columnIdx].width = Math.round(newWidth) + // + // // Update left offset of other columns + // let offset = 40 + // state.forEach(col => { + // col.left = offset + // offset += col.width + // }) + // + // return state + // }) } const stopResizing = () => { From 4d3f669ae7c5f4429ee79d7a021874e9db98d1f7 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 24 Feb 2023 13:53:23 +0000 Subject: [PATCH 030/235] Optimise virtual rendering for both columns and rows to handle infinitely large datasets --- .../app/spreadsheet/Spreadsheet.svelte | 71 ++++++----- .../app/spreadsheet/SpreadsheetBody.svelte | 117 +++++++++++++----- .../app/spreadsheet/SpreadsheetCell.svelte | 13 +- .../app/spreadsheet/SpreadsheetRow.svelte | 106 ++++++++-------- .../app/spreadsheet/stores/reorder.js | 33 ++--- .../app/spreadsheet/stores/resize.js | 62 ++++++++-- 6 files changed, 251 insertions(+), 151 deletions(-) diff --git a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte index 49dbbf00d8..d57da63b19 100644 --- a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte +++ b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte @@ -10,8 +10,6 @@ import SpreadsheetHeader from "./SpreadsheetHeader.svelte" import SpreadsheetBody from "./SpreadsheetBody.svelte" import SpreadsheetCell from "./SpreadsheetCell.svelte" - import SpacerCell from "./SpacerCell.svelte" - import VerticalSpacer from "./VerticalSpacer.svelte" import SpreadsheetRow from "./SpreadsheetRow.svelte" export let table @@ -37,7 +35,7 @@ const tableId = writable(table?.tableId) const changeCache = writable({}) const newRows = writable([]) - const visibleRows = writable([0, 0]) + const visibleCells = writable({ y: [0, 0], x: [0, 0] }) // Build up spreadsheet context and additional stores const context = { @@ -51,7 +49,7 @@ changeCache, newRows, cellHeight, - visibleRows, + visibleCells, } const { reorder, reorderPlaceholder } = createReorderStores(context) const resize = createResizeStore(context) @@ -68,7 +66,8 @@ $: generateColumns($fetch) $: rowCount = $rows.length $: selectedRowCount = Object.values($selectedRows).filter(x => !!x).length - $: updateSortedRows($fetch.rows, $newRows) + $: updateSortedRows($fetch, $newRows) + $: visibleRows = $rows.slice($visibleCells.y[0], $visibleCells.y[1]) const createFetch = datasource => { return fetchData({ @@ -92,13 +91,16 @@ if (primaryDisplay) { fields = [primaryDisplay, ...fields.filter(x => x !== primaryDisplay)] } - $columns = fields.map((field, idx) => ({ - name: field, - width: defaultWidth, - left: 40 + idx * defaultWidth, - schema: schema[field], - primaryDisplay: field === primaryDisplay, - })) + $columns = fields.map((field, idx) => { + return { + idx, + name: field, + width: defaultWidth, + left: 40 + idx * defaultWidth, + schema: schema[field], + primaryDisplay: field === primaryDisplay, + } + }) } } @@ -157,13 +159,17 @@ } const updateSortedRows = (unsortedRows, newRows) => { - let sortedRows = unsortedRows.slice() - sortedRows.sort((a, b) => { - const aIndex = newRows.indexOf(a._id) - const bIndex = newRows.indexOf(b._id) - return aIndex < bIndex ? -1 : 1 - }) - $rows = sortedRows + let foo = unsortedRows.rows + for (let i = 0; i < 10; i++) { + foo = foo.concat(foo.map(x => ({ ...x, _id: x._id + "x" }))) + } + // let sortedRows = foo.slice() + // sortedRows.sort((a, b) => { + // const aIndex = newRows.indexOf(a._id) + // const bIndex = newRows.indexOf(b._id) + // return aIndex < bIndex ? -1 : 1 + // }) + $rows = foo.slice() } // API for children to consume @@ -180,25 +186,18 @@ resize, spreadsheetAPI, }) - - let sheetStyles = "" - let left = 40 - for (let i = 0; i < 20; i++) { - sheetStyles += `--col-${i}-width:${160}px; --col-${i}-left:${left}px;` - left += 160 - }
-
+
reorder.actions.startReordering(fieldIdx, e)} - id={`sheet-${rand}-header-${fieldIdx}`} width={field.width} left={field.left} - column={fieldIdx} > - {#each $rows as row, rowIdx (row._id)} - + {#each visibleRows as row, rowIdx (row._id)} + {/each} -
+
diff --git a/packages/client/src/components/app/spreadsheet/SpreadsheetBody.svelte b/packages/client/src/components/app/spreadsheet/SpreadsheetBody.svelte index 5853ddceb4..185dc518f5 100644 --- a/packages/client/src/components/app/spreadsheet/SpreadsheetBody.svelte +++ b/packages/client/src/components/app/spreadsheet/SpreadsheetBody.svelte @@ -1,57 +1,85 @@
($selectedCellId = null)} id={`sheet-${rand}-body`} + style={sheetStyles} >
diff --git a/packages/client/src/components/app/spreadsheet/SpreadsheetCell.svelte b/packages/client/src/components/app/spreadsheet/SpreadsheetCell.svelte index 2501d47726..b8138a501d 100644 --- a/packages/client/src/components/app/spreadsheet/SpreadsheetCell.svelte +++ b/packages/client/src/components/app/spreadsheet/SpreadsheetCell.svelte @@ -8,8 +8,8 @@ export let selected = false export let reorderSource = false export let reorderTarget = false - export let id = null - export let column + export let left + export let width
@@ -52,6 +51,8 @@ background: var(--cell-background); position: absolute; transition: border-color 130ms ease-out; + width: var(--width); + transform: translateX(var(--left)); } .cell.row-hovered { background: var(--cell-background-hover); @@ -88,9 +89,10 @@ /* Sticky styles */ .cell.sticky { position: sticky; - left: 40px; z-index: 2; border-left-color: transparent; + transform: none; + left: 40px; } .cell.sticky.selected { z-index: 3; @@ -112,6 +114,7 @@ /* Label cells */ .cell.label { + width: 40px; padding: 0 12px; border-left-width: 0; position: sticky; diff --git a/packages/client/src/components/app/spreadsheet/SpreadsheetRow.svelte b/packages/client/src/components/app/spreadsheet/SpreadsheetRow.svelte index c64deaa63f..fcba9caf88 100644 --- a/packages/client/src/components/app/spreadsheet/SpreadsheetRow.svelte +++ b/packages/client/src/components/app/spreadsheet/SpreadsheetRow.svelte @@ -20,14 +20,18 @@ selectedRows, changeCache, spreadsheetAPI, - visibleRows, + visibleCells, cellHeight, } = getContext("spreadsheet") $: rowSelected = !!$selectedRows[row._id] $: rowHovered = $hoveredRowId === row._id $: data = { ...row, ...$changeCache[row._id] } - $: visible = rowIdx >= $visibleRows[0] && rowIdx <= $visibleRows[1] + $: visibleColumns = [ + $columns[0], + ...$columns.slice($visibleCells.x[0], $visibleCells.x[1]), + ] + $: containsSelectedCell = $selectedCellId?.split("-")[0] === row._id const getCellForField = field => { const type = field.schema.type @@ -53,64 +57,66 @@ } -{#if visible} -
+
+ ($hoveredRowId = row._id)} + on:click={() => selectRow(row._id)} + > + {#if rowSelected || rowHovered} + + {:else} + + {rowIdx + 1} + + {/if} + + {#each visibleColumns as column (column.name)} + {@const cellIdx = `${row._id}-${column.name}`} ($hoveredRowId = row._id)} - on:click={() => selectRow(row._id)} - width="40" - left="0" + on:click={() => ($selectedCellId = cellIdx)} + width={column.width} + left={column.left} + column={column.idx} > - {#if rowSelected || rowHovered} - - {:else} - - {rowIdx + 1} - - {/if} - - {#each $columns as field, fieldIdx} - {@const cellIdx = `${row._id}-${field.name}`} - ($hoveredRowId = row._id)} - on:click={() => ($selectedCellId = cellIdx)} - width={field.width} - left={field.left} - column={fieldIdx} - > - spreadsheetAPI.updateValue(row._id, field, val)} - readonly={field.schema.autocolumn} - /> - - {/each} -
-{/if} + onChange={val => spreadsheetAPI.updateValue(row._id, column, val)} + readonly={column.schema.autocolumn} + /> + + {/each} +
diff --git a/packages/client/src/components/app/spreadsheet/stores/reorder.js b/packages/client/src/components/app/spreadsheet/stores/reorder.js index 972db3a6ce..db6d1831ec 100644 --- a/packages/client/src/components/app/spreadsheet/stores/reorder.js +++ b/packages/client/src/components/app/spreadsheet/stores/reorder.js @@ -27,17 +27,16 @@ export const createReorderStores = context => { let breakpoints = [] const cols = get(columns) cols.forEach((col, idx) => { - const header = document.getElementById(`sheet-${rand}-header-${idx}`) - const bounds = header.getBoundingClientRect() - breakpoints.push(bounds.x) + breakpoints.push(col.left) if (idx === cols.length - 1) { - breakpoints.push(bounds.x + bounds.width) + breakpoints.push(col.left + col.width) } }) + console.log(breakpoints, e.clientX) // Get bounds of the selected header and sheet body - const self = document.getElementById(`sheet-${rand}-header-${columnIdx}`) - const selfBounds = self.getBoundingClientRect() + + const self = cols[columnIdx] const body = document.getElementById(`sheet-${rand}-body`) const bodyBounds = body.getBoundingClientRect() @@ -49,9 +48,9 @@ export const createReorderStores = context => { initialMouseX: e.clientX, }) placeholder.set({ - initialX: selfBounds.x - bodyBounds.x, - x: selfBounds.x - bodyBounds.x, - width: selfBounds.width, + initialX: self.left - bodyBounds.x, + x: self.left - bodyBounds.x, + width: self.width, height: (get(rows).length + 2) * 32, }) @@ -103,13 +102,15 @@ export const createReorderStores = context => { const stopReordering = () => { // Swap position of columns let { columnIdx, swapColumnIdx } = get(reorder) - const newColumns = get(columns).slice() - const removed = newColumns.splice(columnIdx, 1) - if (--swapColumnIdx < columnIdx) { - swapColumnIdx++ - } - newColumns.splice(swapColumnIdx, 0, removed[0]) - columns.set(newColumns) + columns.update(state => { + const removed = state.splice(columnIdx, 1) + if (--swapColumnIdx < columnIdx) { + swapColumnIdx++ + } + state.splice(swapColumnIdx, 0, removed[0]) + state = state.map((col, idx) => ({ ...col, idx })) + return state + }) // Reset state reorder.set(reorderInitialState) diff --git a/packages/client/src/components/app/spreadsheet/stores/resize.js b/packages/client/src/components/app/spreadsheet/stores/resize.js index aab716df99..8327f0e79d 100644 --- a/packages/client/src/components/app/spreadsheet/stores/resize.js +++ b/packages/client/src/components/app/spreadsheet/stores/resize.js @@ -23,17 +23,16 @@ export const createResizeStore = context => { const resize = writable(initialState) const startResizing = (idx, e) => { + const $columns = get(columns) // Prevent propagation to stop reordering triggering e.stopPropagation() - sheet = document.getElementById(`sheet-${rand}`) - styles = getComputedStyle(sheet) - width = parseInt(styles.getPropertyValue(`--col-${idx}-width`)) - left = parseInt(styles.getPropertyValue(`--col-${idx}-left`)) + width = $columns[idx].width + left = $columns[idx].left initialWidth = width initialMouseX = e.clientX columnIdx = idx - columnCount = get(columns).length + columnCount = $columns.length // Add mouse event listeners to handle resizing document.addEventListener("mousemove", onResizeMouseMove) @@ -48,17 +47,54 @@ export const createResizeStore = context => { return } + columns.update(state => { + state[columnIdx].width = newWidth + let offset = state[columnIdx].left + newWidth + for (let i = columnIdx + 1; i < state.length; i++) { + state[i].left = offset + offset += state[i].width + } + return state + }) + + // let newStyle = `--col-${columnIdx}-width:${newWidth}px;` + // + // let offset = left + newWidth + // for (let i = columnIdx + 1; i < columnCount; i++) { + // const colWidth = parseInt(styles.getPropertyValue(`--col-${i}-width`)) + // newStyle += `--col-${i}-left:${offset}px;` + // offset += colWidth + // } + // + // sheet.style.cssText += newStyle + + + // let cells = sheet.querySelectorAll(`[data-col="${columnIdx}"]`) + // let left + // cells.forEach(cell => { + // cell.style.width = `${newWidth}px` + // cell.dataset.width = newWidth + // if (!left) { + // left = parseInt(cell.dataset.left) + // } + // }) + // + // let offset = left + newWidth + // for (let i = columnIdx + 1; i < columnCount; i++) { + // cells = sheet.querySelectorAll(`[data-col="${i}"]`) + // let colWidth + // cells.forEach(cell => { + // cell.style.transform = `translateX(${offset}px)` + // cell.dataset.left = offset + // if (!colWidth) { + // colWidth = parseInt(cell.dataset.width) + // } + // }) + // offset += colWidth + // } - let newStyle = `--col-${columnIdx}-width:${newWidth}px;` - let offset = left + newWidth - for (let i = columnIdx + 1; i < columnCount; i++) { - const colWidth = 160//parseInt(styles.getPropertyValue(`--col-${i}-width`)) - newStyle += `--col-${i}-left:${offset}px;` - offset += colWidth - } - sheet.style.cssText += newStyle width = newWidth // Update width of column From 0eadca9acbf68e3b47a4e7fb220baf2fb7aea9ac Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Sat, 25 Feb 2023 16:33:20 +0000 Subject: [PATCH 031/235] Optimise scrolling and virtual rendering performance --- .../app/spreadsheet/Spreadsheet.svelte | 28 +++--- .../app/spreadsheet/SpreadsheetBody.svelte | 96 ++++++++++--------- .../app/spreadsheet/SpreadsheetRow.svelte | 13 ++- packages/client/src/utils/domDebounce.js | 12 ++- 4 files changed, 81 insertions(+), 68 deletions(-) diff --git a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte index d57da63b19..2808b2c572 100644 --- a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte +++ b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte @@ -35,7 +35,8 @@ const tableId = writable(table?.tableId) const changeCache = writable({}) const newRows = writable([]) - const visibleCells = writable({ y: [0, 0], x: [0, 0] }) + const visibleRows = writable([0, 0]) + const visibleColumns = writable([0, 0]) // Build up spreadsheet context and additional stores const context = { @@ -49,7 +50,8 @@ changeCache, newRows, cellHeight, - visibleCells, + visibleRows, + visibleColumns, } const { reorder, reorderPlaceholder } = createReorderStores(context) const resize = createResizeStore(context) @@ -67,7 +69,7 @@ $: rowCount = $rows.length $: selectedRowCount = Object.values($selectedRows).filter(x => !!x).length $: updateSortedRows($fetch, $newRows) - $: visibleRows = $rows.slice($visibleCells.y[0], $visibleCells.y[1]) + $: renderedRows = $rows.slice($visibleRows[0], $visibleRows[1]) const createFetch = datasource => { return fetchData({ @@ -160,16 +162,16 @@ const updateSortedRows = (unsortedRows, newRows) => { let foo = unsortedRows.rows - for (let i = 0; i < 10; i++) { + for (let i = 0; i < 3; i++) { foo = foo.concat(foo.map(x => ({ ...x, _id: x._id + "x" }))) } - // let sortedRows = foo.slice() - // sortedRows.sort((a, b) => { - // const aIndex = newRows.indexOf(a._id) - // const bIndex = newRows.indexOf(b._id) - // return aIndex < bIndex ? -1 : 1 - // }) - $rows = foo.slice() + let sortedRows = foo.slice() + sortedRows.sort((a, b) => { + const aIndex = newRows.indexOf(a._id) + const bIndex = newRows.indexOf(b._id) + return aIndex < bIndex ? -1 : 1 + }) + $rows = sortedRows } // API for children to consume @@ -229,8 +231,8 @@
- {#each visibleRows as row, rowIdx (row._id)} - + {#each renderedRows as row, rowIdx (row._id)} + {/each} diff --git a/packages/client/src/components/app/spreadsheet/SpreadsheetBody.svelte b/packages/client/src/components/app/spreadsheet/SpreadsheetBody.svelte index 185dc518f5..a8a5985f4c 100644 --- a/packages/client/src/components/app/spreadsheet/SpreadsheetBody.svelte +++ b/packages/client/src/components/app/spreadsheet/SpreadsheetBody.svelte @@ -1,8 +1,16 @@
($selectedCellId = null)} id={`sheet-${rand}-body`} - style={sheetStyles} + on:scroll={handleScroll} >
.spreadsheet { display: block; - overflow: auto; height: 800px; position: relative; cursor: default; + overflow: auto; } .content { min-width: 100%; diff --git a/packages/client/src/components/app/spreadsheet/SpreadsheetRow.svelte b/packages/client/src/components/app/spreadsheet/SpreadsheetRow.svelte index fcba9caf88..595f2d1742 100644 --- a/packages/client/src/components/app/spreadsheet/SpreadsheetRow.svelte +++ b/packages/client/src/components/app/spreadsheet/SpreadsheetRow.svelte @@ -1,7 +1,6 @@
- { - selectedRows = e.detail - }} - customPlaceholder - > -
-
- - {#if !isUsersTable} - - {/if} - {#if isInternal} - - {/if} -
-
- - {#if isUsersTable} - - {/if} - {#if !isInternal} - - {/if} - - - - {#key id} - - {/key} -
-
-
- - {#if !hasCols} - Let's create some columns - - Start building out your table structure
- by adding some columns - - {:else} - Now let's add a row - - Add some data to your table
- by adding some rows - - {/if} -
-
-
- {#key id} -
- -
- {/key} +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/client/src/components/app/spreadsheet/ResizeSlider.svelte b/packages/client/src/components/app/spreadsheet/ResizeSlider.svelte deleted file mode 100644 index 7e4c4c9a12..0000000000 --- a/packages/client/src/components/app/spreadsheet/ResizeSlider.svelte +++ /dev/null @@ -1,36 +0,0 @@ - - -
resize.actions.startResizing(columnIdx, e)} /> - - diff --git a/packages/client/src/components/app/spreadsheet/SpacerCell.svelte b/packages/client/src/components/app/spreadsheet/SpacerCell.svelte deleted file mode 100644 index d4a9a87470..0000000000 --- a/packages/client/src/components/app/spreadsheet/SpacerCell.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - - ($selectedCellId = null)} - on:mouseenter={() => ($hoveredRowId = null)} -/> diff --git a/packages/client/src/components/app/spreadsheet/SpreadsheetHeader.svelte b/packages/client/src/components/app/spreadsheet/SpreadsheetHeader.svelte deleted file mode 100644 index 35f81b950d..0000000000 --- a/packages/client/src/components/app/spreadsheet/SpreadsheetHeader.svelte +++ /dev/null @@ -1,108 +0,0 @@ - - -
-
- Filter - Group - Sort - Hide fields -
-
Sales Records
-
- {#if selectedRowCount} - - Delete {selectedRowCount} row{selectedRowCount === 1 ? "" : "s"} - - {:else} - {rowCount} row{rowCount === 1 ? "" : "s"} - {/if} -
-
- - diff --git a/packages/client/src/components/app/spreadsheet/VerticalSpacer.svelte b/packages/client/src/components/app/spreadsheet/VerticalSpacer.svelte deleted file mode 100644 index fcde0fcce9..0000000000 --- a/packages/client/src/components/app/spreadsheet/VerticalSpacer.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - -
($selectedCellId = null)} - on:mouseenter={() => ($hoveredRowId = null)} -/> - - diff --git a/packages/client/src/components/app/spreadsheet/index.js b/packages/client/src/components/app/spreadsheet/index.js deleted file mode 100644 index 223ab462fa..0000000000 --- a/packages/client/src/components/app/spreadsheet/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default as spreadsheet } from "./Spreadsheet.svelte" diff --git a/packages/frontend-core/src/components/index.js b/packages/frontend-core/src/components/index.js index 7ca21c4ff9..8e5959ef14 100644 --- a/packages/frontend-core/src/components/index.js +++ b/packages/frontend-core/src/components/index.js @@ -1,2 +1,3 @@ export { default as SplitPage } from "./SplitPage.svelte" export { default as TestimonialPage } from "./TestimonialPage.svelte" +export { default as Sheet } from "./sheet/Sheet.svelte" diff --git a/packages/client/src/components/app/spreadsheet/stores/resize.js b/packages/frontend-core/src/components/sheet/ResizeOverlay.svelte similarity index 67% rename from packages/client/src/components/app/spreadsheet/stores/resize.js rename to packages/frontend-core/src/components/sheet/ResizeOverlay.svelte index 8327f0e79d..aca8f59ac1 100644 --- a/packages/client/src/components/app/spreadsheet/stores/resize.js +++ b/packages/frontend-core/src/components/sheet/ResizeOverlay.svelte @@ -1,26 +1,16 @@ -import { writable, get } from "svelte/store" -import { domDebounce } from "../../../../utils/domDebounce.js" + + +{#each $columns as col} +
startResizing(col.idx, e)} + style="--left:{col.left + col.width}px;" + > +
+
+{/each} + + diff --git a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte b/packages/frontend-core/src/components/sheet/Sheet.svelte similarity index 52% rename from packages/client/src/components/app/spreadsheet/Spreadsheet.svelte rename to packages/frontend-core/src/components/sheet/Sheet.svelte index ee1d75d0af..4c3c58e5e1 100644 --- a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte +++ b/packages/frontend-core/src/components/sheet/Sheet.svelte @@ -1,27 +1,24 @@ -
-
- - -
- - - - - {#each $columns as field, fieldIdx} - reorder.actions.startReordering(fieldIdx, e)} - width={field.width} - left={field.left} - > - - - {field.name} - - - - {/each} -
- - - {#each renderedRows as row, rowIdx (row._id)} - - {/each} - - -
+
+ + +
+ + + + + {#each $columns as field, fieldIdx} ($hoveredRowId = "new")} - rowHovered={$hoveredRowId === "new"} - width="40" - left="0" + header + sticky={fieldIdx === 0} + reorderSource={$reorder.columnIdx === fieldIdx} + reorderTarget={$reorder.swapColumnIdx === fieldIdx} + on:mousedown={e => reorder.actions.startReordering(fieldIdx, e)} + width={field.width} + left={field.left} > - - - {#each $columns as field, fieldIdx} - addRow(field)} - on:mouseenter={() => ($hoveredRowId = "new")} - width={field.width} - left={field.left} + - {/each} -
-
+ + {field.name} + + + {/each} +
- - -
+ + {#each renderedRows as row, rowIdx (row._id)} + + {/each} + + +
+ ($hoveredRowId = "new")} + rowHovered={$hoveredRowId === "new"} + width="40" + left="0" + > + + + {#each $columns as field, fieldIdx} + addRow(field)} + on:mouseenter={() => ($hoveredRowId = "new")} + width={field.width} + left={field.left} + /> + {/each} +
+
+
diff --git a/packages/client/src/components/app/spreadsheet/SpreadsheetBody.svelte b/packages/frontend-core/src/components/sheet/SheetBody.svelte similarity index 94% rename from packages/client/src/components/app/spreadsheet/SpreadsheetBody.svelte rename to packages/frontend-core/src/components/sheet/SheetBody.svelte index b6fd5e94ae..12bd2dff70 100644 --- a/packages/client/src/components/app/spreadsheet/SpreadsheetBody.svelte +++ b/packages/frontend-core/src/components/sheet/SheetBody.svelte @@ -1,6 +1,6 @@
+ import { getContext } from "svelte" + import { ActionButton, Modal, ModalContent } from "@budibase/bbui" + + const { + selectedRows, + rows, + selectedCellId, + hoveredRowId, + tableId, + spreadsheetAPI, + API, + } = getContext("spreadsheet") + + let modal + + $: selectedRowCount = Object.values($selectedRows).filter(x => !!x).length + $: rowCount = $rows.length + $: rowsToDelete = Object.entries($selectedRows) + .map(entry => { + if (entry[1] === true) { + return $rows.find(x => x._id === entry[0]) + } else { + return null + } + }) + .filter(x => x != null) + + // Deletion callback when confirmed + const performDeletion = async () => { + await API.deleteRows({ + tableId, + rows: rowsToDelete, + }) + await spreadsheetAPI.refreshData() + + // Refresh state + $selectedCellId = null + $hoveredRowId = null + $selectedRows = {} + } + + +
+
+ Filter + Group + Sort + Hide fields +
+
+
+ {#if selectedRowCount} + + Delete {selectedRowCount} row{selectedRowCount === 1 ? "" : "s"} + + {:else} + {rowCount} row{rowCount === 1 ? "" : "s"} + {/if} +
+
+ + + + Are you sure you want to delete {selectedRowCount} + row{selectedRowCount === 1 ? "" : "s"}? + + + + diff --git a/packages/client/src/components/app/spreadsheet/SpreadsheetRow.svelte b/packages/frontend-core/src/components/sheet/SheetRow.svelte similarity index 98% rename from packages/client/src/components/app/spreadsheet/SpreadsheetRow.svelte rename to packages/frontend-core/src/components/sheet/SheetRow.svelte index 595f2d1742..0a174e1548 100644 --- a/packages/client/src/components/app/spreadsheet/SpreadsheetRow.svelte +++ b/packages/frontend-core/src/components/sheet/SheetRow.svelte @@ -1,6 +1,6 @@ -{#each $columns as col} +{#each $visibleColumns as col}
startResizing(col.idx, e)} - style="--left:{col.left + col.width}px;" + style="--left:{col.left + + col.width - + (col.idx === 0 ? 0 : $scroll.left)}px;" >
diff --git a/packages/frontend-core/src/components/sheet/Sheet.svelte b/packages/frontend-core/src/components/sheet/Sheet.svelte index 4c3c58e5e1..637b824cb6 100644 --- a/packages/frontend-core/src/components/sheet/Sheet.svelte +++ b/packages/frontend-core/src/components/sheet/Sheet.svelte @@ -5,6 +5,7 @@ import { LuceneUtils } from "../../utils" import { Icon } from "@budibase/bbui" import { createReorderStores } from "./stores/reorder" + import { createViewportStores } from "./stores/viewport" import SpreadsheetHeader from "./SheetHeader.svelte" import SpreadsheetBody from "./SheetBody.svelte" import SpreadsheetCell from "./SheetCell.svelte" @@ -31,8 +32,6 @@ const selectedRows = writable({}) const changeCache = writable({}) const newRows = writable([]) - const visibleRows = writable([0, 0]) - const visibleColumns = writable([0, 0]) const scroll = writable({ left: 0, top: 0, @@ -57,12 +56,11 @@ changeCache, newRows, cellHeight, - visibleRows, - visibleColumns, bounds, scroll, } const { reorder, reorderPlaceholder } = createReorderStores(context) + const { visibleRows, visibleColumns } = createViewportStores(context) $: query = LuceneUtils.buildLuceneQuery(filter) $: fetch = createFetch(tableId) @@ -76,7 +74,6 @@ $: rowCount = $rows.length $: selectedRowCount = Object.values($selectedRows).filter(x => !!x).length $: updateSortedRows($fetch, $newRows) - $: renderedRows = $rows.slice($visibleRows[0], $visibleRows[1]) const createFetch = tableId => { return fetchData({ @@ -192,7 +189,7 @@ const bIndex = newRows.indexOf(b._id) return aIndex < bIndex ? -1 : 1 }) - $rows = sortedRows + $rows = sortedRows.map((x, idx) => ({ ...x, __idx: idx })) } // API for children to consume @@ -206,6 +203,8 @@ ...context, reorder, reorderPlaceholder, + visibleRows, + visibleColumns, spreadsheetAPI, }) @@ -221,31 +220,31 @@ checked={rowCount && selectedRowCount === rowCount} /> - {#each $columns as field, fieldIdx} + {#each $visibleColumns as column} reorder.actions.startReordering(fieldIdx, e)} - width={field.width} - left={field.left} + sticky={column.idx === 0} + reorderSource={$reorder.columnIdx === column.idx} + reorderTarget={$reorder.swapColumnIdx === column.idx} + on:mousedown={e => reorder.actions.startReordering(column.idx, e)} + width={column.width} + left={column.left} > - {field.name} + {column.name} {/each}
- {#each renderedRows as row, rowIdx (row._id)} - + {#each $visibleRows as row (row._id)} + {/each} @@ -260,16 +259,16 @@ > - {#each $columns as field, fieldIdx} + {#each $visibleColumns as column} addRow(field)} + reorderSource={$reorder.columnIdx === column.idx} + reorderTarget={$reorder.swapColumnIdx === column.idx} + on:click={() => addRow(column)} on:mouseenter={() => ($hoveredRowId = "new")} - width={field.width} - left={field.left} + width={column.width} + left={column.left} /> {/each}
diff --git a/packages/frontend-core/src/components/sheet/SheetBody.svelte b/packages/frontend-core/src/components/sheet/SheetBody.svelte index 12bd2dff70..fc5ea694ac 100644 --- a/packages/frontend-core/src/components/sheet/SheetBody.svelte +++ b/packages/frontend-core/src/components/sheet/SheetBody.svelte @@ -2,104 +2,30 @@ import { getContext, onMount } from "svelte" import { Utils } from "../../utils" - const { - columns, - selectedCellId, - rand, - visibleRows, - visibleColumns, - cellHeight, - rows, - bounds, - scroll, - } = getContext("spreadsheet") + const { columns, selectedCellId, rand, cellHeight, rows, bounds, scroll } = + getContext("spreadsheet") const padding = 180 let ref - let scrollLeft = 0 - let scrollTop = 0 - $: updateVisibleRows($columns, scrollTop, $bounds.height) - $: updateVisibleColumns($columns, scrollLeft, $bounds.width) - $: contentHeight = ($rows.length + 2) * cellHeight + padding + $: contentHeight = ($rows.length + 2) * cellHeight $: contentWidth = computeContentWidth($columns) - $: horizontallyScrolled = scrollLeft > 0 const computeContentWidth = columns => { - let total = 40 + padding - columns.forEach(col => { - total += col.width - }) - return total + if (!columns.length) { + return 0 + } + const last = columns[columns.length - 1] + return last.left + last.width } - // Store the current scroll position - // let lastTop - // let lastLeft - // let ticking = false - // const handleScroll = e => { - // lastTop = e.target.scrollTop - // lastLeft = e.target.scrollLeft - // if (!ticking) { - // ticking = true - // requestAnimationFrame(() => { - // if (Math.abs(lastTop - scrollTop) > 100) { - // scrollTop = lastTop - // } - // if (lastLeft === 0 || Math.abs(lastLeft - scrollLeft) > 100) { - // scrollLeft = lastLeft - // } - // ticking = false - // }) - // } - // } + const updateScrollStore = Utils.domDebounce((left, top) => { + scroll.set({ left, top }) + }) - const handleScroll = Utils.domDebounce( - ({ left, top }) => { - // Only update local state when big changes occur - if (Math.abs(top - scrollTop) > 100) { - scrollTop = top - } - if (left === 0 || Math.abs(left - scrollLeft) > 100) { - scrollLeft = left - } - - // Always update store - scroll.set({ left, top }) - }, - e => ({ left: e.target.scrollLeft, top: e.target.scrollTop }) - ) - - const updateVisibleRows = (columns, scrollTop, height) => { - if (!columns.length) { - return - } - // Compute row visibility - const rows = Math.ceil(height / cellHeight) + 8 - const firstRow = Math.max(0, Math.floor(scrollTop / cellHeight) - 4) - visibleRows.set([firstRow, firstRow + rows]) - } - - const updateVisibleColumns = (columns, scrollLeft, width) => { - if (!columns.length) { - return - } - - // Compute column visibility - let startColIdx = 1 - let rightEdge = columns[1].width - while (rightEdge < scrollLeft) { - startColIdx++ - rightEdge += columns[startColIdx].width - } - let endColIdx = startColIdx + 1 - let leftEdge = columns[0].width + 40 + rightEdge - while (leftEdge < width + scrollLeft) { - leftEdge += columns[endColIdx]?.width - endColIdx++ - } - visibleColumns.set([Math.max(1, startColIdx - 2), endColIdx + 2]) + const handleScroll = e => { + updateScrollStore(e.target.scrollLeft, e.target.scrollTop) } onMount(() => { @@ -117,16 +43,22 @@
0} on:click|self={() => ($selectedCellId = null)} id={`sheet-${rand}-body`} on:scroll={handleScroll} >
- +
+ +
@@ -140,11 +72,19 @@ height: 0; } .sheet-body::-webkit-scrollbar-track { - background: var(--cell-background); + background: transparent; + } + .content, + .data-content { + position: absolute; } .content { min-width: 100%; min-height: 100%; + background: var(--background-alt); + } + .data-content { + background: var(--cell-background); } /* Add shadow to sticky cells when horizontally scrolled */ diff --git a/packages/frontend-core/src/components/sheet/SheetRow.svelte b/packages/frontend-core/src/components/sheet/SheetRow.svelte index 0a174e1548..03c6336f95 100644 --- a/packages/frontend-core/src/components/sheet/SheetRow.svelte +++ b/packages/frontend-core/src/components/sheet/SheetRow.svelte @@ -9,13 +9,11 @@ import TextCell from "./cells/TextCell.svelte" export let row - export let rowIdx const { selectedCellId, reorder, hoveredRowId, - columns, selectedRows, changeCache, spreadsheetAPI, @@ -26,10 +24,6 @@ $: rowSelected = !!$selectedRows[row._id] $: rowHovered = $hoveredRowId === row._id $: data = { ...row, ...$changeCache[row._id] } - $: renderedColumns = [ - $columns[0], - ...$columns.slice($visibleColumns[0], $visibleColumns[1]), - ] $: containsSelectedCell = $selectedCellId?.split("-")[0] === row._id const getCellForField = field => { @@ -58,7 +52,7 @@
{:else} - {rowIdx + 1} + {row.__idx + 1} {/if} - {#each renderedColumns as column (column.name)} + {#each $visibleColumns as column (column.name)} {@const cellIdx = `${row._id}-${column.name}`} { + const { cellHeight, columns, rows, scroll, bounds } = context + + // Use local variables to avoid needing to invoke 2 svelte getters each time + // scroll state changes, but also use stores to allow use of derived stores + let scrollTop = 0 + let scrollLeft = 0 + const scrollTopStore = writable(0) + const scrollLeftStore = writable(0) + + // Derive height and width as primitives to avoid wasted computation + const width = derived(bounds, $bounds => $bounds.width) + const height = derived(bounds, $bounds => $bounds.height) + + // Debounce scroll updates so we can slow down visible row computation + scroll.subscribe(({ left, top }) => { + // Only update local state when big changes occur + if (Math.abs(top - scrollTop) > cellHeight * 2) { + scrollTop = top + scrollTopStore.set(top) + } + if (Math.abs(left - scrollLeft) > 100) { + scrollLeft = left + scrollLeftStore.set(left) + } + }) + + // Derive visible rows + const visibleRows = derived( + [rows, scrollTopStore, height], + ([$rows, $scrollTop, $height]) => { + console.log("new rows") + const maxRows = Math.ceil($height / cellHeight) + 8 + const firstRow = Math.max(0, Math.floor($scrollTop / cellHeight) - 4) + return $rows.slice(firstRow, firstRow + maxRows) + } + ) + + // Derive visible columns + const visibleColumns = derived( + [columns, scrollLeftStore, width], + ([$columns, $scrollLeft, $width]) => { + console.log("new columns") + if (!$columns.length) { + return [] + } + let startColIdx = 1 + let rightEdge = $columns[1].width + while (rightEdge < $scrollLeft) { + startColIdx++ + rightEdge += $columns[startColIdx].width + } + let endColIdx = startColIdx + 1 + let leftEdge = $columns[0].width + 40 + rightEdge + while (leftEdge < $width + $scrollLeft) { + leftEdge += $columns[endColIdx]?.width + endColIdx++ + } + return [ + $columns[0], + ...$columns.slice(Math.max(1, startColIdx - 2), endColIdx + 2), + ] + } + ) + + return { visibleRows, visibleColumns } +} diff --git a/packages/frontend-core/src/utils/utils.js b/packages/frontend-core/src/utils/utils.js index 5b23fa4d93..46d8395f77 100644 --- a/packages/frontend-core/src/utils/utils.js +++ b/packages/frontend-core/src/utils/utils.js @@ -90,18 +90,17 @@ export const throttle = (callback, minDelay = 1000) => { /** * Utility to debounce DOM activities using requestAnimationFrame * @param callback the function to run - * @param extractParams * @returns {Function} */ -export const domDebounce = (callback, extractParams = x => x) => { +export const domDebounce = callback => { let active = false let lastParams return (...params) => { - lastParams = extractParams(...params) + lastParams = params if (!active) { active = true requestAnimationFrame(() => { - callback(lastParams) + callback(...lastParams) active = false }) } From b1f2fe326a38aeaddc7ecbff6d9d9a9b8d3ff6db Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 27 Feb 2023 13:59:35 +0000 Subject: [PATCH 035/235] Fix multiple issues with z-index, reordering and resizing --- .../src/components/sheet/ResizeOverlay.svelte | 28 ++++---- .../src/components/sheet/Sheet.svelte | 30 ++++---- .../src/components/sheet/SheetCell.svelte | 68 +++++-------------- .../src/components/sheet/SheetRow.svelte | 60 ++++++++-------- .../components/sheet/cells/OptionsCell.svelte | 8 --- .../src/components/sheet/stores/reorder.js | 55 ++------------- .../src/components/sheet/stores/viewport.js | 25 ++++--- 7 files changed, 98 insertions(+), 176 deletions(-) diff --git a/packages/frontend-core/src/components/sheet/ResizeOverlay.svelte b/packages/frontend-core/src/components/sheet/ResizeOverlay.svelte index 2c40f34f7d..6ed31268e6 100644 --- a/packages/frontend-core/src/components/sheet/ResizeOverlay.svelte +++ b/packages/frontend-core/src/components/sheet/ResizeOverlay.svelte @@ -1,5 +1,4 @@ {#each $visibleColumns as col} -
startResizing(col.idx, e)} - style="--left:{col.left + - col.width - - (col.idx === 0 ? 0 : $scroll.left)}px;" - > -
-
+ {#if col.idx === 0 || col.left + col.width > cutoff} +
startResizing(col.idx, e)} + style="--left:{col.left + + col.width - + (col.idx === 0 ? 0 : $scroll.left)}px;" + > +
+
+ {/if} {/each} diff --git a/packages/frontend-core/src/components/sheet/SheetCell.svelte b/packages/frontend-core/src/components/sheet/SheetCell.svelte index 7a40bf92ed..f4d5bbb1f7 100644 --- a/packages/frontend-core/src/components/sheet/SheetCell.svelte +++ b/packages/frontend-core/src/components/sheet/SheetCell.svelte @@ -1,8 +1,6 @@ -
- ($hoveredRowId = row._id)} - on:click={() => selectRow(row._id)} - > - {#if rowSelected || rowHovered} +
+ selectRow(row._id)}> +
- {:else} - - {row.__idx + 1} - - {/if} +
+
+ {row.__idx + 1} +
{#each $visibleColumns as column (column.name)} {@const cellIdx = `${row._id}-${column.name}`} ($hoveredRowId = row._id)} on:click={() => ($selectedCellId = cellIdx)} width={column.width} left={column.left} @@ -101,14 +85,36 @@ .row { display: flex; position: absolute; - top: 0; - transform: translateY(var(--top)); + top: var(--top); width: inherit; } - .row.contains-selected-cell { - z-index: 1; + .row:hover :global(.cell) { + background: var(--cell-background-hover); } + /* Styles for label cell */ + .checkbox { + display: none; + } + input[type="checkbox"] { + margin: 0; + } + .number { + display: none; + min-width: 14px; + text-align: center; + color: var(--spectrum-global-color-gray-500); + } + .row:hover .checkbox, + .checkbox.visible, + .number.visible { + display: block; + } + .row:hover .number { + display: none; + } + + /* Add right border to last cell */ .row :global(> :last-child) { border-right-width: 1px; } diff --git a/packages/frontend-core/src/components/sheet/cells/OptionsCell.svelte b/packages/frontend-core/src/components/sheet/cells/OptionsCell.svelte index 754bac35a2..6f4e3dea42 100644 --- a/packages/frontend-core/src/components/sheet/cells/OptionsCell.svelte +++ b/packages/frontend-core/src/components/sheet/cells/OptionsCell.svelte @@ -151,13 +151,6 @@ var(--cell-background) 40% ); } - :global(.cell.hovered) .arrow { - background: linear-gradient( - to right, - transparent 0%, - var(--cell-background-hover) 40% - ); - } .options { min-width: 100%; position: absolute; @@ -170,7 +163,6 @@ align-items: stretch; max-height: calc(6 * var(--cell-height) - 1px); overflow-y: auto; - z-index: 1; } .option { flex: 0 0 var(--cell-height); diff --git a/packages/frontend-core/src/components/sheet/stores/reorder.js b/packages/frontend-core/src/components/sheet/stores/reorder.js index 8549452aff..dd07cd043a 100644 --- a/packages/frontend-core/src/components/sheet/stores/reorder.js +++ b/packages/frontend-core/src/components/sheet/stores/reorder.js @@ -1,7 +1,7 @@ import { get, writable } from "svelte/store" export const createReorderStores = context => { - const { columns, bounds, rows, scroll, rand } = context + const { columns, visibleColumns, rand, scroll, bounds } = context const reorderInitialState = { columnIdx: null, swapColumnIdx: null, @@ -10,33 +10,14 @@ export const createReorderStores = context => { } const reorder = writable(reorderInitialState) - // This is broken into its own store as it is rapidly updated, and we want to - // ensure good performance by avoiding updating other components which depend - // on other reordering state - const placeholderInitialState = { - x: null, - initialX: null, - width: null, - height: null, - } - const placeholder = writable(placeholderInitialState) - // Callback when dragging on a colum header and starting reordering const startReordering = (columnIdx, e) => { const $columns = get(columns) const $bounds = get(bounds) - const $rows = get(rows) const $scroll = get(scroll) // Generate new breakpoints for the current columns - let breakpoints = [] - $columns.forEach((col, idx) => { - breakpoints.push(col.left) - if (idx === $columns.length - 1) { - breakpoints.push(col.left + col.width) - } - }) - const self = $columns[columnIdx] + let breakpoints = $columns.map(col => col.left + col.width) // Update state reorder.set({ @@ -44,15 +25,8 @@ export const createReorderStores = context => { breakpoints, swapColumnIdx: null, initialMouseX: e.clientX, - }) - placeholder.set({ - initialX: self.left, - x: self.left, - width: self.width, - height: ($rows.length + 2) * 32, - sheetLeft: $bounds.left, - maxX: $bounds.width - self.width, scrollLeft: $scroll.left, + sheetLeft: $bounds.left, }) // Add listeners to handle mouse movement @@ -71,34 +45,18 @@ export const createReorderStores = context => { return } - // Compute new placeholder position - const $placeholder = get(placeholder) - let newX = - e.clientX - - $reorder.initialMouseX + - $placeholder.initialX - - $placeholder.scrollLeft - newX = Math.max(0, newX) - newX = Math.min($placeholder.maxX, newX) - // Compute the closest breakpoint to the current position let swapColumnIdx let minDistance = Number.MAX_SAFE_INTEGER + const mouseX = e.clientX - $reorder.sheetLeft + $reorder.scrollLeft $reorder.breakpoints.forEach((point, idx) => { - const distance = Math.abs( - point - e.clientX + $placeholder.sheetLeft - $placeholder.scrollLeft - ) + const distance = Math.abs(point - mouseX) if (distance < minDistance) { minDistance = distance swapColumnIdx = idx } }) - // Update state - placeholder.update(state => { - state.x = newX - return state - }) if (swapColumnIdx !== $reorder.swapColumnIdx) { reorder.update(state => { state.swapColumnIdx = swapColumnIdx @@ -111,6 +69,7 @@ export const createReorderStores = context => { const stopReordering = () => { // Swap position of columns let { columnIdx, swapColumnIdx } = get(reorder) + swapColumnIdx++ columns.update(state => { const removed = state.splice(columnIdx, 1) if (--swapColumnIdx < columnIdx) { @@ -131,7 +90,6 @@ export const createReorderStores = context => { // Reset state reorder.set(reorderInitialState) - placeholder.set(placeholderInitialState) // Remove event handlers document.removeEventListener("mousemove", onReorderMouseMove) @@ -147,6 +105,5 @@ export const createReorderStores = context => { stopReordering, }, }, - reorderPlaceholder: placeholder, } } diff --git a/packages/frontend-core/src/components/sheet/stores/viewport.js b/packages/frontend-core/src/components/sheet/stores/viewport.js index ad501147da..f4801dcfba 100644 --- a/packages/frontend-core/src/components/sheet/stores/viewport.js +++ b/packages/frontend-core/src/components/sheet/stores/viewport.js @@ -1,4 +1,5 @@ import { writable, derived } from "svelte/store" +import { Utils } from "../../../utils" export const createViewportStores = context => { const { cellHeight, columns, rows, scroll, bounds } = context @@ -16,15 +17,17 @@ export const createViewportStores = context => { // Debounce scroll updates so we can slow down visible row computation scroll.subscribe(({ left, top }) => { - // Only update local state when big changes occur - if (Math.abs(top - scrollTop) > cellHeight * 2) { - scrollTop = top - scrollTopStore.set(top) - } - if (Math.abs(left - scrollLeft) > 100) { - scrollLeft = left - scrollLeftStore.set(left) - } + window.requestAnimationFrame(() => { + // Only update local state when big changes occur + if (Math.abs(top - scrollTop) > cellHeight * 2) { + scrollTop = top + scrollTopStore.set(top) + } + if (Math.abs(left - scrollLeft) > 100) { + scrollLeft = left + scrollLeftStore.set(left) + } + }) }) // Derive visible rows @@ -32,8 +35,8 @@ export const createViewportStores = context => { [rows, scrollTopStore, height], ([$rows, $scrollTop, $height]) => { console.log("new rows") - const maxRows = Math.ceil($height / cellHeight) + 8 - const firstRow = Math.max(0, Math.floor($scrollTop / cellHeight) - 4) + const maxRows = Math.ceil($height / cellHeight) + 16 + const firstRow = Math.max(0, Math.floor($scrollTop / cellHeight) - 8) return $rows.slice(firstRow, firstRow + maxRows) } ) From 30e1ecd67f545f384609de7b8ec5c9d58d718f1a Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 27 Feb 2023 15:41:35 +0000 Subject: [PATCH 036/235] Fix date cells in sheets --- packages/frontend-core/src/components/sheet/Sheet.svelte | 2 +- .../frontend-core/src/components/sheet/cells/DateCell.svelte | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/frontend-core/src/components/sheet/Sheet.svelte b/packages/frontend-core/src/components/sheet/Sheet.svelte index 7ae47d4d53..7b616706cc 100644 --- a/packages/frontend-core/src/components/sheet/Sheet.svelte +++ b/packages/frontend-core/src/components/sheet/Sheet.svelte @@ -224,7 +224,7 @@ sticky={column.idx === 0} reorderSource={$reorder.columnIdx === column.idx} reorderTarget={$reorder.swapColumnIdx === column.idx} - on:mousedown={column.idx === 123 + on:mousedown={column.idx === 0 ? null : e => reorder.actions.startReordering(column.idx, 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 887bf11c65..b4529a02cd 100644 --- a/packages/frontend-core/src/components/sheet/cells/DateCell.svelte +++ b/packages/frontend-core/src/components/sheet/cells/DateCell.svelte @@ -37,7 +37,7 @@ onChange(e.detail)} - appendTo={document.getElementById("flatpickr-root")} + appendTo={document.documentElement} enableTime={!dateOnly} {timeOnly} time24hr From b45ba0eba752a62f871dc4c00b392936de4bfc80 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 27 Feb 2023 19:01:23 +0000 Subject: [PATCH 037/235] Separate data fetching logic from main sheet and tidy up --- .../src/components/sheet/HeaderRow.svelte | 87 +++++++ .../src/components/sheet/NewRow.svelte | 43 ++++ .../src/components/sheet/Sheet.svelte | 223 ++---------------- .../src/components/sheet/SheetBody.svelte | 3 +- .../src/components/sheet/SheetHeader.svelte | 17 +- .../src/components/sheet/SheetRow.svelte | 8 +- .../src/components/sheet/stores/reorder.js | 2 +- .../src/components/sheet/stores/rows.js | 125 ++++++++++ .../src/components/sheet/stores/viewport.js | 1 - 9 files changed, 285 insertions(+), 224 deletions(-) create mode 100644 packages/frontend-core/src/components/sheet/HeaderRow.svelte create mode 100644 packages/frontend-core/src/components/sheet/NewRow.svelte create mode 100644 packages/frontend-core/src/components/sheet/stores/rows.js diff --git a/packages/frontend-core/src/components/sheet/HeaderRow.svelte b/packages/frontend-core/src/components/sheet/HeaderRow.svelte new file mode 100644 index 0000000000..f41b31715a --- /dev/null +++ b/packages/frontend-core/src/components/sheet/HeaderRow.svelte @@ -0,0 +1,87 @@ + + +
+ + + + + {#each $visibleColumns as column} + reorder.actions.startReordering(column.idx, e)} + width={column.width} + left={column.left} + > + + + {column.name} + + + {/each} +
+ + diff --git a/packages/frontend-core/src/components/sheet/NewRow.svelte b/packages/frontend-core/src/components/sheet/NewRow.svelte new file mode 100644 index 0000000000..800f300b46 --- /dev/null +++ b/packages/frontend-core/src/components/sheet/NewRow.svelte @@ -0,0 +1,43 @@ + + +
+ + + + {#each $visibleColumns as column} + addRow(column)} + width={column.width} + left={column.left} + /> + {/each} +
+ + diff --git a/packages/frontend-core/src/components/sheet/Sheet.svelte b/packages/frontend-core/src/components/sheet/Sheet.svelte index 7b616706cc..01e24b2948 100644 --- a/packages/frontend-core/src/components/sheet/Sheet.svelte +++ b/packages/frontend-core/src/components/sheet/Sheet.svelte @@ -1,16 +1,16 @@
- - -
- - - - - {#each $visibleColumns as column} - reorder.actions.startReordering(column.idx, e)} - width={column.width} - left={column.left} - > - - - {column.name} - - - {/each} -
- - + + + {#each $visibleRows as row (row._id)} - + {/each} - - -
- - - - {#each $visibleColumns as column} - addRow(column)} - width={column.width} - left={column.left} - /> - {/each} -
-
+ +
@@ -289,22 +130,4 @@ .sheet :global(*) { box-sizing: border-box; } - - .row { - display: flex; - position: sticky; - top: 0; - width: inherit; - z-index: 10; - } - .row.new { - position: absolute; - transform: translateY(var(--top)); - } - .row :global(> :last-child) { - border-right-width: 1px; - } - input[type="checkbox"] { - margin: 0; - } diff --git a/packages/frontend-core/src/components/sheet/SheetBody.svelte b/packages/frontend-core/src/components/sheet/SheetBody.svelte index fc5ea694ac..308d17a726 100644 --- a/packages/frontend-core/src/components/sheet/SheetBody.svelte +++ b/packages/frontend-core/src/components/sheet/SheetBody.svelte @@ -2,7 +2,7 @@ import { getContext, onMount } from "svelte" import { Utils } from "../../utils" - const { columns, selectedCellId, rand, cellHeight, rows, bounds, scroll } = + const { columns, selectedCellId, cellHeight, rows, bounds, scroll } = getContext("spreadsheet") const padding = 180 @@ -45,7 +45,6 @@ class="sheet-body" class:horizontally-scrolled={$scroll.left > 0} on:click|self={() => ($selectedCellId = null)} - id={`sheet-${rand}-body`} on:scroll={handleScroll} >
{ - await API.deleteRows({ - tableId, - rows: rowsToDelete, - }) - await spreadsheetAPI.refreshData() + await rows.actions.deleteRows(rowsToDelete) // Refresh state $selectedCellId = null - $hoveredRowId = null $selectedRows = {} } diff --git a/packages/frontend-core/src/components/sheet/SheetRow.svelte b/packages/frontend-core/src/components/sheet/SheetRow.svelte index f73cf1a5c4..602a23c4b1 100644 --- a/packages/frontend-core/src/components/sheet/SheetRow.svelte +++ b/packages/frontend-core/src/components/sheet/SheetRow.svelte @@ -14,14 +14,12 @@ selectedCellId, reorder, selectedRows, - changeCache, - spreadsheetAPI, + rows, visibleColumns, cellHeight, } = getContext("spreadsheet") $: rowSelected = !!$selectedRows[row._id] - $: data = { ...row, ...$changeCache[row._id] } const getCellForField = field => { const type = field.schema.type @@ -71,10 +69,10 @@ > spreadsheetAPI.updateValue(row._id, column, val)} + onChange={val => rows.actions.updateRow(row._id, column, val)} readonly={column.schema.autocolumn} /> diff --git a/packages/frontend-core/src/components/sheet/stores/reorder.js b/packages/frontend-core/src/components/sheet/stores/reorder.js index dd07cd043a..9a50e05476 100644 --- a/packages/frontend-core/src/components/sheet/stores/reorder.js +++ b/packages/frontend-core/src/components/sheet/stores/reorder.js @@ -1,7 +1,7 @@ import { get, writable } from "svelte/store" export const createReorderStores = context => { - const { columns, visibleColumns, rand, scroll, bounds } = context + const { columns, rand, scroll, bounds } = context const reorderInitialState = { columnIdx: null, swapColumnIdx: null, diff --git a/packages/frontend-core/src/components/sheet/stores/rows.js b/packages/frontend-core/src/components/sheet/stores/rows.js new file mode 100644 index 0000000000..4b35f45a7e --- /dev/null +++ b/packages/frontend-core/src/components/sheet/stores/rows.js @@ -0,0 +1,125 @@ +import { writable, derived, get } from "svelte/store" +import { buildLuceneQuery } from "../../../utils/lucene" +import { fetchData } from "../../../fetch/fetchData" + +export const createRowsStore = context => { + const { tableId, filter, API } = context + const rows = writable([]) + const schema = writable({}) + const primaryDisplay = writable(null) + const query = derived(filter, $filter => buildLuceneQuery($filter)) + const fetch = derived(tableId, $tableId => { + return fetchData({ + API, + datasource: { + type: "table", + tableId: $tableId, + }, + options: { + sortColumn: null, + sortOrder: null, + query: get(query), + limit: 100, + paginate: true, + }, + }) + }) + + // Update fetch when query changes + query.subscribe($query => { + get(fetch).update({ + query: $query, + }) + }) + + // Observe each data fetch and extract some data + fetch.subscribe($fetch => { + $fetch.subscribe($$fetch => { + console.log("new fetch") + rows.set($$fetch.rows.map((row, idx) => ({ ...row, __idx: idx }))) + schema.set($$fetch.schema) + primaryDisplay.set($$fetch.definition?.primaryDisplay) + }) + }) + + // Adds a new empty row + const addRow = async () => { + let newRow = await API.saveRow({ tableId: get(tableId) }) + newRow.__idx = get(rows).length + rows.update(state => { + state.push(newRow) + return state + }) + return newRow + } + + // Updates a value of a row + const updateRow = async (rowId, column, value) => { + const $rows = get(rows) + const index = $rows.findIndex(x => x._id === rowId) + const row = $rows[index] + if (index === -1 || row?.[column.name] === value) { + return + } + + // Immediately update state so that the change is reflected + let newRow = { ...row, [column.name]: value } + rows.update(state => { + state[index] = { ...newRow } + return state + }) + + // Save change + delete newRow.__idx + await API.saveRow(newRow) + + // Fetch row from the server again + newRow = await API.fetchRow({ + tableId: get(tableId), + rowId: row._id, + }) + + // Update state again with this row + newRow = { ...newRow, __idx: row.__idx } + rows.update(state => { + state[index] = newRow + return state + }) + + return newRow + } + + // Deletes an array of rows + const deleteRows = async rowsToDelete => { + const deletedIds = rowsToDelete.map(row => row._id) + + // Actually delete rows + rowsToDelete.forEach(row => { + delete row.__idx + }) + await API.deleteRows({ + tableId: get(tableId), + rows: rowsToDelete, + }) + + // Update state + rows.update(state => { + return state + .filter(row => !deletedIds.includes(row._id)) + .map((row, idx) => ({ ...row, __idx: idx })) + }) + } + + return { + rows: { + ...rows, + actions: { + addRow, + updateRow, + deleteRows, + }, + }, + schema, + primaryDisplay, + } +} diff --git a/packages/frontend-core/src/components/sheet/stores/viewport.js b/packages/frontend-core/src/components/sheet/stores/viewport.js index f4801dcfba..934b0e8c1a 100644 --- a/packages/frontend-core/src/components/sheet/stores/viewport.js +++ b/packages/frontend-core/src/components/sheet/stores/viewport.js @@ -1,5 +1,4 @@ import { writable, derived } from "svelte/store" -import { Utils } from "../../../utils" export const createViewportStores = context => { const { cellHeight, columns, rows, scroll, bounds } = context From 385e9eadb0b46bcd44b4c5579b368acffbfbdc18 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 28 Feb 2023 11:48:25 +0000 Subject: [PATCH 038/235] Add infinite scroll, improve row fetching, add error handling, fix svelte store updates --- .../backend/DataTable/DataTable.svelte | 252 +++++++++--------- .../src/components/sheet/HeaderRow.svelte | 9 +- .../src/components/sheet/NewRow.svelte | 4 +- .../src/components/sheet/Sheet.svelte | 6 +- .../src/components/sheet/SheetCell.svelte | 2 + .../src/components/sheet/SheetRow.svelte | 14 +- .../src/components/sheet/stores/reorder.js | 8 +- .../src/components/sheet/stores/rows.js | 101 ++++++- .../src/components/sheet/stores/viewport.js | 14 +- 9 files changed, 252 insertions(+), 158 deletions(-) diff --git a/packages/builder/src/components/backend/DataTable/DataTable.svelte b/packages/builder/src/components/backend/DataTable/DataTable.svelte index d921c94fb4..f590429565 100644 --- a/packages/builder/src/components/backend/DataTable/DataTable.svelte +++ b/packages/builder/src/components/backend/DataTable/DataTable.svelte @@ -24,132 +24,132 @@ import { fetchData, Sheet } from "@budibase/frontend-core" import { API } from "api" - let hideAutocolumns = true - let filters - - $: isUsersTable = $tables.selected?._id === TableNames.USERS - $: type = $tables.selected?.type - $: isInternal = type !== "external" - $: schema = $tables.selected?.schema - $: enrichedSchema = enrichSchema($tables.selected?.schema) - $: id = $tables.selected?._id - $: fetch = createFetch(id) - $: hasCols = checkHasCols(schema) - $: hasRows = !!$fetch.rows?.length - $: showError($fetch.error) - $: id, (filters = null) - - let appliedFilter - let rawFilter - let appliedSort - let selectedRows = [] - - $: enrichedSchema, - () => { - appliedFilter = null - rawFilter = null - appliedSort = null - selectedRows = [] - } - - $: if (Number.isInteger($fetch.pageNumber)) { - selectedRows = [] - } - - const showError = error => { - if (error) { - notifications.error(error?.message || "Unable to fetch data.") - } - } - - const enrichSchema = schema => { - let tempSchema = { ...schema } - tempSchema._id = { - type: "internal", - editable: false, - displayName: "ID", - autocolumn: true, - } - if (isInternal) { - tempSchema._rev = { - type: "internal", - editable: false, - displayName: "Revision", - autocolumn: true, - } - } - - return tempSchema - } - - const checkHasCols = schema => { - if (!schema || Object.keys(schema).length === 0) { - return false - } - let fields = Object.values(schema) - for (let field of fields) { - if (!field.autocolumn) { - return true - } - } - return false - } - - // Fetches new data whenever the table changes - const createFetch = tableId => { - return fetchData({ - API, - datasource: { - tableId, - type: "table", - }, - options: { - schema, - limit: 10, - paginate: true, - }, - }) - } - - // Fetch data whenever sorting option changes - const onSort = async e => { - const sort = { - sortColumn: e.detail.column, - sortOrder: e.detail.order, - } - await fetch.update(sort) - appliedSort = { ...sort } - appliedSort.sortOrder = appliedSort.sortOrder.toLowerCase() - selectedRows = [] - } - - // Fetch data whenever filters change - const onFilter = e => { - filters = e.detail - fetch.update({ - filter: filters, - }) - appliedFilter = e.detail - } - - // Fetch data whenever schema changes - const onUpdateColumns = () => { - selectedRows = [] - fetch.refresh() - } - - // Fetch data whenever rows are modified. Unfortunately we have to lose - // our pagination place, as our bookmarks will have shifted. - const onUpdateRows = () => { - selectedRows = [] - fetch.refresh() - } - - // When importing new rows it is better to reinitialise request/paging data. - // Not doing so causes inconsistency in paging behaviour and content. - const onImportData = () => { - fetch.getInitialData() - } + // let hideAutocolumns = true + // let filters + // + // $: isUsersTable = $tables.selected?._id === TableNames.USERS + // $: type = $tables.selected?.type + // $: isInternal = type !== "external" + // $: schema = $tables.selected?.schema + // $: enrichedSchema = enrichSchema($tables.selected?.schema) + // $: id = $tables.selected?._id + // $: fetch = createFetch(id) + // $: hasCols = checkHasCols(schema) + // $: hasRows = !!$fetch.rows?.length + // $: showError($fetch.error) + // $: id, (filters = null) + // + // let appliedFilter + // let rawFilter + // let appliedSort + // let selectedRows = [] + // + // $: enrichedSchema, + // () => { + // appliedFilter = null + // rawFilter = null + // appliedSort = null + // selectedRows = [] + // } + // + // $: if (Number.isInteger($fetch.pageNumber)) { + // selectedRows = [] + // } + // + // const showError = error => { + // if (error) { + // notifications.error(error?.message || "Unable to fetch data.") + // } + // } + // + // const enrichSchema = schema => { + // let tempSchema = { ...schema } + // tempSchema._id = { + // type: "internal", + // editable: false, + // displayName: "ID", + // autocolumn: true, + // } + // if (isInternal) { + // tempSchema._rev = { + // type: "internal", + // editable: false, + // displayName: "Revision", + // autocolumn: true, + // } + // } + // + // return tempSchema + // } + // + // const checkHasCols = schema => { + // if (!schema || Object.keys(schema).length === 0) { + // return false + // } + // let fields = Object.values(schema) + // for (let field of fields) { + // if (!field.autocolumn) { + // return true + // } + // } + // return false + // } + // + // // Fetches new data whenever the table changes + // const createFetch = tableId => { + // return fetchData({ + // API, + // datasource: { + // tableId, + // type: "table", + // }, + // options: { + // schema, + // limit: 10, + // paginate: true, + // }, + // }) + // } + // + // // Fetch data whenever sorting option changes + // const onSort = async e => { + // const sort = { + // sortColumn: e.detail.column, + // sortOrder: e.detail.order, + // } + // await fetch.update(sort) + // appliedSort = { ...sort } + // appliedSort.sortOrder = appliedSort.sortOrder.toLowerCase() + // selectedRows = [] + // } + // + // // Fetch data whenever filters change + // const onFilter = e => { + // filters = e.detail + // fetch.update({ + // filter: filters, + // }) + // appliedFilter = e.detail + // } + // + // // Fetch data whenever schema changes + // const onUpdateColumns = () => { + // selectedRows = [] + // fetch.refresh() + // } + // + // // Fetch data whenever rows are modified. Unfortunately we have to lose + // // our pagination place, as our bookmarks will have shifted. + // const onUpdateRows = () => { + // selectedRows = [] + // fetch.refresh() + // } + // + // // When importing new rows it is better to reinitialise request/paging data. + // // Not doing so causes inconsistency in paging behaviour and content. + // const onImportData = () => { + // fetch.getInitialData() + // }
diff --git a/packages/frontend-core/src/components/sheet/HeaderRow.svelte b/packages/frontend-core/src/components/sheet/HeaderRow.svelte index f41b31715a..3802fc2664 100644 --- a/packages/frontend-core/src/components/sheet/HeaderRow.svelte +++ b/packages/frontend-core/src/components/sheet/HeaderRow.svelte @@ -24,12 +24,11 @@ if (allSelected) { $selectedRows = {} } else { - selectedRows.update(state => { - $rows.forEach(row => { - state[row._id] = true - }) - return state + let allRows = {} + $rows.forEach(row => { + allRows[row._id] = true }) + $selectedRows = allRows } } diff --git a/packages/frontend-core/src/components/sheet/NewRow.svelte b/packages/frontend-core/src/components/sheet/NewRow.svelte index 800f300b46..bea996ecab 100644 --- a/packages/frontend-core/src/components/sheet/NewRow.svelte +++ b/packages/frontend-core/src/components/sheet/NewRow.svelte @@ -8,7 +8,9 @@ const addRow = async field => { const newRow = await rows.actions.addRow() - $selectedCellId = `${newRow._id}-${field.name}` + if (newRow) { + $selectedCellId = `${newRow._id}-${field.name}` + } } diff --git a/packages/frontend-core/src/components/sheet/Sheet.svelte b/packages/frontend-core/src/components/sheet/Sheet.svelte index 01e24b2948..72d648c32c 100644 --- a/packages/frontend-core/src/components/sheet/Sheet.svelte +++ b/packages/frontend-core/src/components/sheet/Sheet.svelte @@ -1,3 +1,5 @@ + + @@ -104,7 +106,9 @@ color: var(--spectrum-global-color-gray-500); } .row:hover .checkbox, - .checkbox.visible, + .checkbox.visible { + display: flex; + } .number.visible { display: block; } diff --git a/packages/frontend-core/src/components/sheet/stores/reorder.js b/packages/frontend-core/src/components/sheet/stores/reorder.js index 9a50e05476..f327a5ef2d 100644 --- a/packages/frontend-core/src/components/sheet/stores/reorder.js +++ b/packages/frontend-core/src/components/sheet/stores/reorder.js @@ -58,10 +58,10 @@ export const createReorderStores = context => { }) if (swapColumnIdx !== $reorder.swapColumnIdx) { - reorder.update(state => { - state.swapColumnIdx = swapColumnIdx - return state - }) + reorder.update(state => ({ + ...state, + swapColumnIdx: swapColumnIdx, + })) } } diff --git a/packages/frontend-core/src/components/sheet/stores/rows.js b/packages/frontend-core/src/components/sheet/stores/rows.js index 4b35f45a7e..1ce2cf23dc 100644 --- a/packages/frontend-core/src/components/sheet/stores/rows.js +++ b/packages/frontend-core/src/components/sheet/stores/rows.js @@ -1,14 +1,34 @@ import { writable, derived, get } from "svelte/store" import { buildLuceneQuery } from "../../../utils/lucene" import { fetchData } from "../../../fetch/fetchData" +import { notifications } from "@budibase/bbui" export const createRowsStore = context => { const { tableId, filter, API } = context + + // Flag for whether this is the first time loading our fetch + let loaded = false + + // Local cache of row IDs to speed up checking if a row exists + let rowCacheMap = {} + + // Exported stores const rows = writable([]) const schema = writable({}) const primaryDisplay = writable(null) + + // Local stores for managing fetching data const query = derived(filter, $filter => buildLuceneQuery($filter)) const fetch = derived(tableId, $tableId => { + if (!$tableId) { + return null + } + // Wipe state and fully hydrate next time our fetch returns data + loaded = false + rowCacheMap = {} + rows.set([]) + + // Create fetch and load initial data return fetchData({ API, datasource: { @@ -27,30 +47,66 @@ export const createRowsStore = context => { // Update fetch when query changes query.subscribe($query => { - get(fetch).update({ + get(fetch)?.update({ query: $query, }) }) // Observe each data fetch and extract some data fetch.subscribe($fetch => { + if (!$fetch) { + return + } $fetch.subscribe($$fetch => { - console.log("new fetch") - rows.set($$fetch.rows.map((row, idx) => ({ ...row, __idx: idx }))) - schema.set($$fetch.schema) - primaryDisplay.set($$fetch.definition?.primaryDisplay) + if ($$fetch.loaded) { + if (!loaded) { + // Hydrate initial data + loaded = true + console.log("instantiate new fetch data") + schema.set($$fetch.schema) + primaryDisplay.set($$fetch.definition?.primaryDisplay) + } + + // Process new rows + handleNewRows($$fetch.rows) + } }) }) + // Local handler to process new rows inside the fetch, and append any new + // rows to state that we haven't encountered before + const handleNewRows = newRows => { + let rowsToAppend = [] + let newRow + for (let i = 0; i < newRows.length; i++) { + newRow = newRows[i] + if (!rowCacheMap[newRow._id]) { + rowCacheMap[newRow._id] = true + rowsToAppend.push(newRow) + } + } + if (rowsToAppend.length) { + rows.update($rows => { + return [ + ...$rows, + ...rowsToAppend.map((row, idx) => ({ + ...row, + __idx: $rows.length + idx, + })), + ] + }) + } + } + // Adds a new empty row const addRow = async () => { - let newRow = await API.saveRow({ tableId: get(tableId) }) - newRow.__idx = get(rows).length - rows.update(state => { - state.push(newRow) - return state - }) - return newRow + try { + const newRow = await API.saveRow({ tableId: get(tableId) }) + handleNewRows([newRow]) + return newRow + } catch (error) { + notifications.error(`Error adding row: ${error?.message}`) + } } // Updates a value of a row @@ -71,7 +127,11 @@ export const createRowsStore = context => { // Save change delete newRow.__idx - await API.saveRow(newRow) + try { + await API.saveRow(newRow) + } catch (error) { + notifications.error(`Error saving row: ${error?.message}`) + } // Fetch row from the server again newRow = await API.fetchRow({ @@ -103,11 +163,25 @@ export const createRowsStore = context => { }) // Update state + // We deliberately do not remove IDs from the cache map as the data may + // still exist inside the fetch, but we don't want to add it again rows.update(state => { return state .filter(row => !deletedIds.includes(row._id)) .map((row, idx) => ({ ...row, __idx: idx })) }) + + // If we ended up with no rows, try getting the next page + if (!get(rows).length) { + loadNextPage() + } + } + + // Loads the next page of data if available + const loadNextPage = () => { + const $fetch = get(fetch) + console.log("fetch next page") + $fetch?.nextPage() } return { @@ -117,6 +191,7 @@ export const createRowsStore = context => { addRow, updateRow, deleteRows, + loadNextPage, }, }, schema, diff --git a/packages/frontend-core/src/components/sheet/stores/viewport.js b/packages/frontend-core/src/components/sheet/stores/viewport.js index 934b0e8c1a..4a1698f873 100644 --- a/packages/frontend-core/src/components/sheet/stores/viewport.js +++ b/packages/frontend-core/src/components/sheet/stores/viewport.js @@ -1,4 +1,4 @@ -import { writable, derived } from "svelte/store" +import { writable, derived, get } from "svelte/store" export const createViewportStores = context => { const { cellHeight, columns, rows, scroll, bounds } = context @@ -18,7 +18,7 @@ export const createViewportStores = context => { scroll.subscribe(({ left, top }) => { window.requestAnimationFrame(() => { // Only update local state when big changes occur - if (Math.abs(top - scrollTop) > cellHeight * 2) { + if (Math.abs(top - scrollTop) > cellHeight * 4) { scrollTop = top scrollTopStore.set(top) } @@ -67,5 +67,15 @@ export const createViewportStores = context => { } ) + // Fetch next page when approaching end of data + visibleRows.subscribe($visibleRows => { + const lastVisible = $visibleRows[$visibleRows.length - 1] + const $rows = get(rows) + const lastRow = $rows[$rows.length - 1] + if (lastVisible && lastRow && lastVisible._id === lastRow._id) { + rows.actions.loadNextPage() + } + }) + return { visibleRows, visibleColumns } } From e5b2fdfcf67e72fafd36b0c6f67ffbb756e8ea22 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 28 Feb 2023 11:51:00 +0000 Subject: [PATCH 039/235] Fix overly thin scrollbars in firefox --- packages/builder/src/global.css | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/builder/src/global.css b/packages/builder/src/global.css index 5e9a35706e..bc1f55d9d3 100644 --- a/packages/builder/src/global.css +++ b/packages/builder/src/global.css @@ -70,7 +70,6 @@ a { background: var(--spectrum-alias-background-color-default); } html * { - scrollbar-width: thin; scrollbar-color: var(--spectrum-global-color-gray-400) var(--spectrum-alias-background-color-default); } From 26ca96eaa9398c797d2e9a7d8fae4db162fe8c97 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 28 Feb 2023 14:17:06 +0000 Subject: [PATCH 040/235] Use nicer checkboxes and fix some hover styles --- .../src/components/sheet/HeaderRow.svelte | 13 ++----- .../src/components/sheet/NewRow.svelte | 2 +- .../src/components/sheet/SheetCell.svelte | 6 +++- .../src/components/sheet/SheetRow.svelte | 35 ++++++------------- 4 files changed, 19 insertions(+), 37 deletions(-) diff --git a/packages/frontend-core/src/components/sheet/HeaderRow.svelte b/packages/frontend-core/src/components/sheet/HeaderRow.svelte index 3802fc2664..61152f35e8 100644 --- a/packages/frontend-core/src/components/sheet/HeaderRow.svelte +++ b/packages/frontend-core/src/components/sheet/HeaderRow.svelte @@ -2,6 +2,7 @@ import SheetCell from "./SheetCell.svelte" import { getContext } from "svelte" import { Icon } from "@budibase/bbui" + import { Checkbox } from "@budibase/bbui" const { visibleColumns, reorder, selectedRows, rows } = getContext("spreadsheet") @@ -36,10 +37,7 @@
- + {#each $visibleColumns as column} :last-child) { border-right-width: 1px; } - input[type="checkbox"] { - margin: 0; - } diff --git a/packages/frontend-core/src/components/sheet/NewRow.svelte b/packages/frontend-core/src/components/sheet/NewRow.svelte index bea996ecab..09121d636b 100644 --- a/packages/frontend-core/src/components/sheet/NewRow.svelte +++ b/packages/frontend-core/src/components/sheet/NewRow.svelte @@ -35,7 +35,7 @@ width: inherit; position: absolute; } - .row:hover :global(.cell) { + :global(.sheet:not(.is-resizing):not(.is-reordering) .row:hover .cell) { background: var(--cell-background-hover); cursor: pointer; } diff --git a/packages/frontend-core/src/components/sheet/SheetCell.svelte b/packages/frontend-core/src/components/sheet/SheetCell.svelte index 2fc20bf8b7..8d177d39af 100644 --- a/packages/frontend-core/src/components/sheet/SheetCell.svelte +++ b/packages/frontend-core/src/components/sheet/SheetCell.svelte @@ -76,7 +76,7 @@ gap: calc(2 * var(--cell-spacing)); z-index: 10; } - .cell.header :global(span) { + .cell.header :global(> span) { flex: 1 1 auto; width: 0; white-space: nowrap; @@ -127,5 +127,9 @@ position: sticky; left: 0; z-index: 5; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; } diff --git a/packages/frontend-core/src/components/sheet/SheetRow.svelte b/packages/frontend-core/src/components/sheet/SheetRow.svelte index 9ecc134e20..67a479963a 100644 --- a/packages/frontend-core/src/components/sheet/SheetRow.svelte +++ b/packages/frontend-core/src/components/sheet/SheetRow.svelte @@ -9,6 +9,7 @@ import NumberCell from "./cells/NumberCell.svelte" import RelationshipCell from "./cells/RelationshipCell.svelte" import TextCell from "./cells/TextCell.svelte" + import { Checkbox } from "@budibase/bbui" export let row @@ -20,25 +21,16 @@ visibleColumns, cellHeight, } = getContext("spreadsheet") + const TypeComponentMap = { + options: OptionsCell, + datetime: DateCell, + array: MultiSelectCell, + number: NumberCell, + link: RelationshipCell, + } $: rowSelected = !!$selectedRows[row._id] - const getCellForField = field => { - const type = field.schema.type - if (type === "options") { - return OptionsCell - } else if (type === "datetime") { - return DateCell - } else if (type === "array") { - return MultiSelectCell - } else if (type === "number") { - return NumberCell - } else if (type === "link") { - return RelationshipCell - } - return TextCell - } - const selectRow = id => { selectedRows.update(state => ({ ...state, @@ -50,7 +42,7 @@
selectRow(row._id)}>
- +
{row.__idx + 1} @@ -70,7 +62,7 @@ column={column.idx} > Date: Tue, 28 Feb 2023 14:31:58 +0000 Subject: [PATCH 041/235] Fix issue reordering columns in firefox and increase performance --- .../src/components/sheet/NewRow.svelte | 4 +++- .../src/components/sheet/ResizeOverlay.svelte | 20 +++++++++++++------ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/frontend-core/src/components/sheet/NewRow.svelte b/packages/frontend-core/src/components/sheet/NewRow.svelte index 09121d636b..78a990ff58 100644 --- a/packages/frontend-core/src/components/sheet/NewRow.svelte +++ b/packages/frontend-core/src/components/sheet/NewRow.svelte @@ -3,7 +3,7 @@ import { Icon } from "@budibase/bbui" import { getContext } from "svelte" - const { visibleColumns, cellHeight, rows, selectedCellId } = + const { visibleColumns, cellHeight, rows, selectedCellId, reorder } = getContext("spreadsheet") const addRow = async field => { @@ -24,6 +24,8 @@ on:click={() => addRow(column)} width={column.width} left={column.left} + reorderSource={$reorder.columnIdx === column.idx} + reorderTarget={$reorder.swapColumnIdx === column.idx} /> {/each}
diff --git a/packages/frontend-core/src/components/sheet/ResizeOverlay.svelte b/packages/frontend-core/src/components/sheet/ResizeOverlay.svelte index 6ed31268e6..b099c25a07 100644 --- a/packages/frontend-core/src/components/sheet/ResizeOverlay.svelte +++ b/packages/frontend-core/src/components/sheet/ResizeOverlay.svelte @@ -1,7 +1,8 @@ {#each $visibleColumns as col} @@ -65,9 +75,7 @@ class="resize-slider" class:visible={columnIdx === col.idx} on:mousedown={e => startResizing(col.idx, e)} - style="--left:{col.left + - col.width - - (col.idx === 0 ? 0 : $scroll.left)}px;" + style={getStyle(col, scrollLeft, rowCount)} >
@@ -90,7 +98,7 @@ .resize-slider.visible { cursor: col-resize; opacity: 1; - height: calc(100% - var(--controls-height)); + height: min(var(--content-height), calc(100% - var(--controls-height))); } .resize-indicator { margin-left: -1px; From c83286cb61c07c53928c311306a942b1ab833789 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 28 Feb 2023 14:40:36 +0000 Subject: [PATCH 042/235] Tidy up --- .../frontend-core/src/components/sheet/ResizeOverlay.svelte | 1 - packages/frontend-core/src/components/sheet/SheetBody.svelte | 3 ++- packages/frontend-core/src/components/sheet/stores/rows.js | 5 +---- .../frontend-core/src/components/sheet/stores/viewport.js | 2 -- 4 files changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/frontend-core/src/components/sheet/ResizeOverlay.svelte b/packages/frontend-core/src/components/sheet/ResizeOverlay.svelte index b099c25a07..d6665777d8 100644 --- a/packages/frontend-core/src/components/sheet/ResizeOverlay.svelte +++ b/packages/frontend-core/src/components/sheet/ResizeOverlay.svelte @@ -62,7 +62,6 @@ } const getStyle = (col, scrollLeft, rowCount) => { - console.log("stye") const left = col.left + col.width - (col.idx === 0 ? 0 : scrollLeft) const contentHeight = (rowCount + 2) * cellHeight return `--left:${left}px; --content-height:${contentHeight}px;` diff --git a/packages/frontend-core/src/components/sheet/SheetBody.svelte b/packages/frontend-core/src/components/sheet/SheetBody.svelte index 308d17a726..f7cf7c63ab 100644 --- a/packages/frontend-core/src/components/sheet/SheetBody.svelte +++ b/packages/frontend-core/src/components/sheet/SheetBody.svelte @@ -11,6 +11,7 @@ $: contentHeight = ($rows.length + 2) * cellHeight $: contentWidth = computeContentWidth($columns) + $: scrollLeft = $scroll.left const computeContentWidth = columns => { if (!columns.length) { @@ -43,7 +44,7 @@
0} + class:horizontally-scrolled={scrollLeft > 0} on:click|self={() => ($selectedCellId = null)} on:scroll={handleScroll} > diff --git a/packages/frontend-core/src/components/sheet/stores/rows.js b/packages/frontend-core/src/components/sheet/stores/rows.js index 1ce2cf23dc..fbfe15822b 100644 --- a/packages/frontend-core/src/components/sheet/stores/rows.js +++ b/packages/frontend-core/src/components/sheet/stores/rows.js @@ -62,7 +62,6 @@ export const createRowsStore = context => { if (!loaded) { // Hydrate initial data loaded = true - console.log("instantiate new fetch data") schema.set($$fetch.schema) primaryDisplay.set($$fetch.definition?.primaryDisplay) } @@ -179,9 +178,7 @@ export const createRowsStore = context => { // Loads the next page of data if available const loadNextPage = () => { - const $fetch = get(fetch) - console.log("fetch next page") - $fetch?.nextPage() + get(fetch)?.nextPage() } return { diff --git a/packages/frontend-core/src/components/sheet/stores/viewport.js b/packages/frontend-core/src/components/sheet/stores/viewport.js index 4a1698f873..9379c1670b 100644 --- a/packages/frontend-core/src/components/sheet/stores/viewport.js +++ b/packages/frontend-core/src/components/sheet/stores/viewport.js @@ -33,7 +33,6 @@ export const createViewportStores = context => { const visibleRows = derived( [rows, scrollTopStore, height], ([$rows, $scrollTop, $height]) => { - console.log("new rows") const maxRows = Math.ceil($height / cellHeight) + 16 const firstRow = Math.max(0, Math.floor($scrollTop / cellHeight) - 8) return $rows.slice(firstRow, firstRow + maxRows) @@ -44,7 +43,6 @@ export const createViewportStores = context => { const visibleColumns = derived( [columns, scrollLeftStore, width], ([$columns, $scrollLeft, $width]) => { - console.log("new columns") if (!$columns.length) { return [] } From 4c70959327cb4e08cb956b512661b7e58b4fe779 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 28 Feb 2023 14:53:11 +0000 Subject: [PATCH 043/235] Use search endpoint instead of get endpoint to fetch individual rows so that relationship enrichment occurs --- .../src/components/sheet/stores/rows.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/frontend-core/src/components/sheet/stores/rows.js b/packages/frontend-core/src/components/sheet/stores/rows.js index fbfe15822b..e46bc36c50 100644 --- a/packages/frontend-core/src/components/sheet/stores/rows.js +++ b/packages/frontend-core/src/components/sheet/stores/rows.js @@ -133,10 +133,19 @@ export const createRowsStore = context => { } // Fetch row from the server again - newRow = await API.fetchRow({ + const res = await API.searchTable({ tableId: get(tableId), - rowId: row._id, + limit: 1, + query: { + equal: { + _id: row._id, + }, + }, + paginate: false, }) + if (res?.rows?.[0]) { + newRow = res?.rows?.[0] + } // Update state again with this row newRow = { ...newRow, __idx: row.__idx } From d2bc4d8fdccbb1d5ea8c39b430faa84941afe80d Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 28 Feb 2023 14:53:31 +0000 Subject: [PATCH 044/235] Tidy up --- packages/frontend-core/src/components/sheet/stores/rows.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend-core/src/components/sheet/stores/rows.js b/packages/frontend-core/src/components/sheet/stores/rows.js index e46bc36c50..1396b5f753 100644 --- a/packages/frontend-core/src/components/sheet/stores/rows.js +++ b/packages/frontend-core/src/components/sheet/stores/rows.js @@ -144,7 +144,7 @@ export const createRowsStore = context => { paginate: false, }) if (res?.rows?.[0]) { - newRow = res?.rows?.[0] + newRow = res.rows[0] } // Update state again with this row From 9be7d042a934090c56df6dab205b612fb1a34c20 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 28 Feb 2023 14:56:44 +0000 Subject: [PATCH 045/235] Fix relationship issues when creating rows --- .../src/components/sheet/stores/rows.js | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/frontend-core/src/components/sheet/stores/rows.js b/packages/frontend-core/src/components/sheet/stores/rows.js index 1396b5f753..c49da287a7 100644 --- a/packages/frontend-core/src/components/sheet/stores/rows.js +++ b/packages/frontend-core/src/components/sheet/stores/rows.js @@ -100,7 +100,26 @@ export const createRowsStore = context => { // Adds a new empty row const addRow = async () => { try { - const newRow = await API.saveRow({ tableId: get(tableId) }) + // Create row + let newRow = await API.saveRow({ tableId: get(tableId) }) + + // Use search endpoint to fetch the row again, ensuring relationships are + // properly enriched + const res = await API.searchTable({ + tableId: get(tableId), + limit: 1, + query: { + equal: { + _id: newRow._id, + }, + }, + paginate: false, + }) + if (res?.rows?.[0]) { + newRow = res.rows[0] + } + + // Update state handleNewRows([newRow]) return newRow } catch (error) { From db469711cfe1f609cd1386ded1e86f36c7b0be81 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 28 Feb 2023 15:00:10 +0000 Subject: [PATCH 046/235] Optimise resetting data to smoothly transition when changing datasource --- packages/frontend-core/src/components/sheet/stores/rows.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/frontend-core/src/components/sheet/stores/rows.js b/packages/frontend-core/src/components/sheet/stores/rows.js index c49da287a7..2fa58f4c76 100644 --- a/packages/frontend-core/src/components/sheet/stores/rows.js +++ b/packages/frontend-core/src/components/sheet/stores/rows.js @@ -25,8 +25,6 @@ export const createRowsStore = context => { } // Wipe state and fully hydrate next time our fetch returns data loaded = false - rowCacheMap = {} - rows.set([]) // Create fetch and load initial data return fetchData({ @@ -62,6 +60,8 @@ export const createRowsStore = context => { if (!loaded) { // Hydrate initial data loaded = true + rowCacheMap = {} + rows.set([]) schema.set($$fetch.schema) primaryDisplay.set($$fetch.definition?.primaryDisplay) } From 43eadf2ec6c9629c789cde666ce42abf32cc8b2b Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Wed, 1 Mar 2023 08:44:02 +0000 Subject: [PATCH 047/235] Add WIP virtual dom implementation to massively increase performance --- .../src/components/sheet/NewRow.svelte | 4 +- .../src/components/sheet/ResizeOverlay.svelte | 2 +- .../src/components/sheet/ScrollOverlay.svelte | 124 ++++++++++++++++++ .../src/components/sheet/Sheet.svelte | 5 +- .../src/components/sheet/SheetBody.svelte | 46 ++++--- .../src/components/sheet/SheetRow.svelte | 7 +- .../src/components/sheet/stores/viewport.js | 19 +-- 7 files changed, 168 insertions(+), 39 deletions(-) create mode 100644 packages/frontend-core/src/components/sheet/ScrollOverlay.svelte diff --git a/packages/frontend-core/src/components/sheet/NewRow.svelte b/packages/frontend-core/src/components/sheet/NewRow.svelte index 78a990ff58..11f99de5a6 100644 --- a/packages/frontend-core/src/components/sheet/NewRow.svelte +++ b/packages/frontend-core/src/components/sheet/NewRow.svelte @@ -14,7 +14,7 @@ } -
+
@@ -33,9 +33,7 @@ diff --git a/packages/frontend-core/src/components/sheet/Sheet.svelte b/packages/frontend-core/src/components/sheet/Sheet.svelte index 72d648c32c..920964e758 100644 --- a/packages/frontend-core/src/components/sheet/Sheet.svelte +++ b/packages/frontend-core/src/components/sheet/Sheet.svelte @@ -100,9 +100,10 @@
+ + - - {#each $visibleRows as row (row._id)} + {#each $visibleRows as row} {/each} diff --git a/packages/frontend-core/src/components/sheet/SheetBody.svelte b/packages/frontend-core/src/components/sheet/SheetBody.svelte index f7cf7c63ab..6a5bfe68d5 100644 --- a/packages/frontend-core/src/components/sheet/SheetBody.svelte +++ b/packages/frontend-core/src/components/sheet/SheetBody.svelte @@ -1,6 +1,7 @@ -
+
selectRow(row._id)}>
@@ -76,8 +78,7 @@ diff --git a/packages/frontend-core/src/components/sheet/ResizeOverlay.svelte b/packages/frontend-core/src/components/sheet/ResizeOverlay.svelte index 32edc77016..a51d0b11f4 100644 --- a/packages/frontend-core/src/components/sheet/ResizeOverlay.svelte +++ b/packages/frontend-core/src/components/sheet/ResizeOverlay.svelte @@ -1,8 +1,15 @@ +{#if $stickyColumn} +
startResizing("sticky", e)} + style="--left:{40 + + $stickyColumn.width}px; --content-height:{contentHeight}px;" + > +
+
+{/if} {#each $visibleColumns as col} - {#if col.idx === 0 || col.left + col.width > cutoff} -
startResizing(col.idx, e)} - style={getStyle(col, scrollLeft, rowCount)} - > -
-
- {/if} +
startResizing(col.idx, e)} + style={getStyle(col, offset, scrollLeft, contentHeight)} + > +
+
{/each} diff --git a/packages/frontend-core/src/components/sheet/SheetBody.svelte b/packages/frontend-core/src/components/sheet/SheetBody.svelte index 6a5bfe68d5..49c0ca3a16 100644 --- a/packages/frontend-core/src/components/sheet/SheetBody.svelte +++ b/packages/frontend-core/src/components/sheet/SheetBody.svelte @@ -1,7 +1,7 @@
@@ -39,7 +38,7 @@ border-color: var(--spectrum-global-color-gray-200); border-width: 0; border-bottom-width: 1px; - border-left-width: 1px; + border-right-width: 1px; display: flex; flex-direction: row; justify-content: flex-start; @@ -48,10 +47,11 @@ font-size: var(--cell-font-size); gap: var(--cell-spacing); background: var(--cell-background); - position: absolute; transition: border-color 130ms ease-out; - width: var(--width); + flex: 0 0 var(--width); + position: absolute; left: var(--left); + width: var(--width); } .cell.selected { box-shadow: inset 0 0 0 2px var(--spectrum-global-color-blue-400); @@ -91,18 +91,6 @@ z-index: 11; } - /* Sticky styles */ - .cell.sticky { - position: sticky; - border-left-width: 0; - transform: none; - left: 40px; - z-index: 5; - } - .cell.selected.sticky { - z-index: 6; - } - /* Reorder styles */ .cell.reorder-source { background: var(--spectrum-global-color-gray-100); @@ -122,8 +110,8 @@ /* Label cells */ .cell.label { padding: var(--cell-padding); - width: 40px; - border-left-width: 0; + flex: 0 0 40px; + border-right-width: 0; position: sticky; left: 0; z-index: 5; diff --git a/packages/frontend-core/src/components/sheet/SheetHeader.svelte b/packages/frontend-core/src/components/sheet/SheetControls.svelte similarity index 100% rename from packages/frontend-core/src/components/sheet/SheetHeader.svelte rename to packages/frontend-core/src/components/sheet/SheetControls.svelte diff --git a/packages/frontend-core/src/components/sheet/SheetRow.svelte b/packages/frontend-core/src/components/sheet/SheetRow.svelte index 5129ec0b98..ff1940abcb 100644 --- a/packages/frontend-core/src/components/sheet/SheetRow.svelte +++ b/packages/frontend-core/src/components/sheet/SheetRow.svelte @@ -2,14 +2,8 @@
- selectRow(row._id)}> -
- -
-
- {row.__idx + 1} -
-
{#each $visibleColumns as column (column.name)} {@const cellIdx = `${row._id}-${column.name}`} - ($selectedCellId = cellIdx)} width={column.width} left={column.left} - column={column.idx} > rows.actions.updateRow(row._id, column, val)} readonly={column.schema.autocolumn} /> - + {/each}
@@ -80,32 +50,9 @@ display: flex; position: relative; width: inherit; + height: var(--cell-height); } :global(.sheet:not(.is-resizing):not(.is-reordering) .row:hover .cell) { background: var(--cell-background-hover); } - - /* Styles for label cell */ - .checkbox { - display: none; - } - .number { - display: none; - color: var(--spectrum-global-color-gray-500); - } - .row:hover .checkbox, - .checkbox.visible { - display: flex; - } - .number.visible { - display: block; - } - .row:hover .number { - display: none; - } - - /* Add right border to last cell */ - .row :global(> :last-child) { - border-right-width: 1px; - } diff --git a/packages/frontend-core/src/components/sheet/SheetScrollWrapper.svelte b/packages/frontend-core/src/components/sheet/SheetScrollWrapper.svelte new file mode 100644 index 0000000000..c6f5798246 --- /dev/null +++ b/packages/frontend-core/src/components/sheet/SheetScrollWrapper.svelte @@ -0,0 +1,82 @@ + + +
+ +
+ + diff --git a/packages/frontend-core/src/components/sheet/StickyColumn.svelte b/packages/frontend-core/src/components/sheet/StickyColumn.svelte new file mode 100644 index 0000000000..1669460e53 --- /dev/null +++ b/packages/frontend-core/src/components/sheet/StickyColumn.svelte @@ -0,0 +1,153 @@ + + +
+
+ + + + + + {#if $stickyColumn} + + + + {$stickyColumn.name} + + + {/if} +
+ + + {#each $visibleRows as row} + {@const rowSelected = !!$selectedRows[row._id]} +
+ selectRow(row._id)} + width="40" + > +
+ +
+
+ {row.__idx + 1} +
+
+ + {#if $stickyColumn} + {@const cellIdx = `${row._id}-${$stickyColumn.name}`} + ($selectedCellId = cellIdx)} + width={$stickyColumn.width} + left="40" + > + + rows.actions.updateRow(row._id, $stickyColumn, val)} + readonly={$stickyColumn.schema.autocolumn} + /> + + {/if} +
+ {/each} + +
+ + + + {#if $stickyColumn} + + {/if} +
+
+
+ + diff --git a/packages/frontend-core/src/components/sheet/stores/columns.js b/packages/frontend-core/src/components/sheet/stores/columns.js new file mode 100644 index 0000000000..a5728f639d --- /dev/null +++ b/packages/frontend-core/src/components/sheet/stores/columns.js @@ -0,0 +1,64 @@ +import { get, writable } from "svelte/store" + +export const createColumnsStores = context => { + const { schema } = context + const defaultWidth = 200 + const columns = writable([]) + const stickyColumn = writable(null) + + schema.subscribe($schema => { + const currentColumns = get(columns) + if (!$schema) { + columns.set([]) + return + } + + // Get field list + let fields = [] + Object.entries($schema || {}).forEach(([field, fieldSchema]) => { + if (!fieldSchema.primaryDisplay) { + fields.push(field) + } + }) + + // Update columns, removing extraneous columns and adding missing ones + let offset = 0 + columns.set( + fields.map((field, idx) => { + const existing = currentColumns.find(x => x.name === field) + const newCol = { + idx, + name: field, + width: existing?.width || defaultWidth, + left: offset, + schema: $schema[field], + } + offset += newCol.width + return newCol + }) + ) + }) + + schema.subscribe($schema => { + const primaryDisplay = Object.entries($schema).find(entry => { + return entry[1].primaryDisplay + }) + if (!primaryDisplay) { + stickyColumn.set(null) + return + } + const existingWidth = get(stickyColumn)?.width + const same = primaryDisplay[0] === get(stickyColumn)?.name + stickyColumn.set({ + name: primaryDisplay[0], + width: same ? existingWidth : defaultWidth, + left: 40, + schema: primaryDisplay[1], + }) + }) + + return { + columns, + stickyColumn, + } +} diff --git a/packages/frontend-core/src/components/sheet/stores/reorder.js b/packages/frontend-core/src/components/sheet/stores/reorder.js index f327a5ef2d..4d4659fe8d 100644 --- a/packages/frontend-core/src/components/sheet/stores/reorder.js +++ b/packages/frontend-core/src/components/sheet/stores/reorder.js @@ -76,7 +76,7 @@ export const createReorderStores = context => { swapColumnIdx++ } state.splice(swapColumnIdx, 0, removed[0]) - let offset = 40 + let offset = 0 return state.map((col, idx) => { const newCol = { ...col, diff --git a/packages/frontend-core/src/components/sheet/stores/rows.js b/packages/frontend-core/src/components/sheet/stores/rows.js index 2fa58f4c76..840798f1e2 100644 --- a/packages/frontend-core/src/components/sheet/stores/rows.js +++ b/packages/frontend-core/src/components/sheet/stores/rows.js @@ -15,7 +15,6 @@ export const createRowsStore = context => { // Exported stores const rows = writable([]) const schema = writable({}) - const primaryDisplay = writable(null) // Local stores for managing fetching data const query = derived(filter, $filter => buildLuceneQuery($filter)) @@ -62,8 +61,12 @@ export const createRowsStore = context => { loaded = true rowCacheMap = {} rows.set([]) - schema.set($$fetch.schema) - primaryDisplay.set($$fetch.definition?.primaryDisplay) + let newSchema = $$fetch.schema + const primaryDisplay = $$fetch.definition?.primaryDisplay + if (primaryDisplay && newSchema[primaryDisplay]) { + newSchema[primaryDisplay].primaryDisplay = true + } + schema.set(newSchema) } // Process new rows @@ -220,6 +223,5 @@ export const createRowsStore = context => { }, }, schema, - primaryDisplay, } } diff --git a/packages/frontend-core/src/components/sheet/stores/viewport.js b/packages/frontend-core/src/components/sheet/stores/viewport.js index 82df7739e7..56987006a7 100644 --- a/packages/frontend-core/src/components/sheet/stores/viewport.js +++ b/packages/frontend-core/src/components/sheet/stores/viewport.js @@ -39,25 +39,26 @@ export const createViewportStores = context => { if (!$columns.length) { return [] } - let startColIdx = 1 - let rightEdge = $columns[1].width + let startColIdx = 0 + let rightEdge = $columns[0].width while (rightEdge < $scrollLeft) { startColIdx++ rightEdge += $columns[startColIdx].width } let endColIdx = startColIdx + 1 - let leftEdge = $columns[0].width + 40 + rightEdge + let leftEdge = rightEdge while (leftEdge < $width + $scrollLeft) { leftEdge += $columns[endColIdx]?.width endColIdx++ } - return [ - $columns[0], - ...$columns.slice(Math.max(1, startColIdx - 2), endColIdx + 2), - ] + return $columns.slice(Math.max(0, startColIdx - 1), endColIdx + 1) } ) + // visibleColumns.subscribe(state => { + // console.log(state) + // }) + // Fetch next page when approaching end of data visibleRows.subscribe($visibleRows => { const lastVisible = $visibleRows[$visibleRows.length - 1] diff --git a/packages/frontend-core/src/components/sheet/utils.js b/packages/frontend-core/src/components/sheet/utils.js index 9c708c3237..895dd99cfc 100644 --- a/packages/frontend-core/src/components/sheet/utils.js +++ b/packages/frontend-core/src/components/sheet/utils.js @@ -1,6 +1,34 @@ +import OptionsCell from "./cells/OptionsCell.svelte" +import DateCell from "./cells/DateCell.svelte" +import MultiSelectCell from "./cells/MultiSelectCell.svelte" +import NumberCell from "./cells/NumberCell.svelte" +import RelationshipCell from "./cells/RelationshipCell.svelte" +import TextCell from "./cells/TextCell.svelte" + export const getColor = idx => { if (idx == null || idx === -1) { return null } return `hsla(${((idx + 1) * 222) % 360}, 90%, 75%, 0.3)` } + +export const getIconForField = field => { + const type = field.schema.type + if (type === "options") { + return "ChevronDown" + } else if (type === "datetime") { + return "Date" + } + return "Text" +} + +const TypeComponentMap = { + options: OptionsCell, + datetime: DateCell, + array: MultiSelectCell, + number: NumberCell, + link: RelationshipCell, +} +export const getCellComponent = column => { + return TypeComponentMap[column?.schema?.type] || TextCell +} From 524c46a554acc834ddc2ec0e99dac0d80c4f1460 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Wed, 1 Mar 2023 16:10:24 +0000 Subject: [PATCH 049/235] Fix multiple issues, clean up rendering, improve performance --- .../src/components/sheet/HeaderRow.svelte | 8 --- .../src/components/sheet/NewRow.svelte | 2 - .../src/components/sheet/ScrollOverlay.svelte | 35 ++++++++++--- .../src/components/sheet/Sheet.svelte | 2 - .../src/components/sheet/SheetCell.svelte | 13 ++--- .../src/components/sheet/SheetRow.svelte | 3 -- .../sheet/SheetScrollWrapper.svelte | 28 +++++++++-- .../src/components/sheet/stores/rows.js | 7 ++- .../src/components/sheet/stores/viewport.js | 49 +++++++------------ 9 files changed, 79 insertions(+), 68 deletions(-) diff --git a/packages/frontend-core/src/components/sheet/HeaderRow.svelte b/packages/frontend-core/src/components/sheet/HeaderRow.svelte index 71fd1237cc..01a50d07f2 100644 --- a/packages/frontend-core/src/components/sheet/HeaderRow.svelte +++ b/packages/frontend-core/src/components/sheet/HeaderRow.svelte @@ -38,13 +38,5 @@ diff --git a/packages/frontend-core/src/components/sheet/NewRow.svelte b/packages/frontend-core/src/components/sheet/NewRow.svelte index 764c207228..63985febf0 100644 --- a/packages/frontend-core/src/components/sheet/NewRow.svelte +++ b/packages/frontend-core/src/components/sheet/NewRow.svelte @@ -29,8 +29,6 @@ diff --git a/packages/frontend-core/src/components/sheet/stores/rows.js b/packages/frontend-core/src/components/sheet/stores/rows.js index 840798f1e2..0c4785ddba 100644 --- a/packages/frontend-core/src/components/sheet/stores/rows.js +++ b/packages/frontend-core/src/components/sheet/stores/rows.js @@ -4,7 +4,7 @@ import { fetchData } from "../../../fetch/fetchData" import { notifications } from "@budibase/bbui" export const createRowsStore = context => { - const { tableId, filter, API } = context + const { tableId, filter, API, scroll } = context // Flag for whether this is the first time loading our fetch let loaded = false @@ -61,12 +61,17 @@ export const createRowsStore = context => { loaded = true rowCacheMap = {} rows.set([]) + + // Enrich primary display into schema let newSchema = $$fetch.schema const primaryDisplay = $$fetch.definition?.primaryDisplay if (primaryDisplay && newSchema[primaryDisplay]) { newSchema[primaryDisplay].primaryDisplay = true } schema.set(newSchema) + + // Reset scroll state for fresh dataset + scroll.set({ left: 0, top: 0 }) } // Process new rows diff --git a/packages/frontend-core/src/components/sheet/stores/viewport.js b/packages/frontend-core/src/components/sheet/stores/viewport.js index 56987006a7..ef95965ff7 100644 --- a/packages/frontend-core/src/components/sheet/stores/viewport.js +++ b/packages/frontend-core/src/components/sheet/stores/viewport.js @@ -1,64 +1,53 @@ -import { writable, derived, get } from "svelte/store" +import { derived, get } from "svelte/store" export const createViewportStores = context => { const { cellHeight, columns, rows, scroll, bounds } = context - - // Use local variables to avoid needing to invoke 2 svelte getters each time - // scroll state changes, but also use stores to allow use of derived stores - let scrollTop = 0 - let scrollLeft = 0 - const scrollTopStore = writable(0) - const scrollLeftStore = writable(0) + const scrollTop = derived(scroll, $scroll => $scroll.top, 0) + const scrollLeft = derived(scroll, $scroll => $scroll.left, 0) // Derive height and width as primitives to avoid wasted computation const width = derived(bounds, $bounds => $bounds.width) const height = derived(bounds, $bounds => $bounds.height) - // Debounce scroll updates so we can slow down visible row computation - scroll.subscribe(({ left, top }) => { - scrollTop = top - scrollTopStore.set(top) - scrollLeft = left - scrollLeftStore.set(left) - }) - // Derive visible rows + // Split into multiple stores containing primitives to optimise invalidation + // as mich as possible + const firstRowIdx = derived(scrollTop, $scrollTop => { + return Math.floor($scrollTop / cellHeight) + }) + const visibleRowCount = derived(height, $height => { + return Math.ceil($height / cellHeight) + }) const visibleRows = derived( - [rows, scrollTopStore, height], - ([$rows, $scrollTop, $height]) => { - const maxRows = Math.ceil($height / cellHeight) + 1 - const firstRow = Math.max(0, Math.floor($scrollTop / cellHeight)) - return $rows.slice(firstRow, firstRow + maxRows) + [rows, firstRowIdx, visibleRowCount], + ([$rows, $firstRowIdx, $visibleRowCount]) => { + return $rows.slice($firstRowIdx, $firstRowIdx + $visibleRowCount) } ) // Derive visible columns const visibleColumns = derived( - [columns, scrollLeftStore, width], + [columns, scrollLeft, width], ([$columns, $scrollLeft, $width]) => { if (!$columns.length) { return [] } let startColIdx = 0 let rightEdge = $columns[0].width - while (rightEdge < $scrollLeft) { + while (rightEdge < $scrollLeft && startColIdx < $columns.length - 1) { startColIdx++ rightEdge += $columns[startColIdx].width } let endColIdx = startColIdx + 1 let leftEdge = rightEdge - while (leftEdge < $width + $scrollLeft) { - leftEdge += $columns[endColIdx]?.width + while (leftEdge < $width + $scrollLeft && endColIdx < $columns.length) { + leftEdge += $columns[endColIdx].width endColIdx++ } - return $columns.slice(Math.max(0, startColIdx - 1), endColIdx + 1) + return $columns.slice(startColIdx, endColIdx) } ) - // visibleColumns.subscribe(state => { - // console.log(state) - // }) - // Fetch next page when approaching end of data visibleRows.subscribe($visibleRows => { const lastVisible = $visibleRows[$visibleRows.length - 1] From ca96a61cdea035fb42e8ac851aff8f9cc539f7ba Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Wed, 1 Mar 2023 16:12:35 +0000 Subject: [PATCH 050/235] Tune cell sizes --- .../frontend-core/src/components/sheet/Sheet.svelte | 2 +- .../src/components/sheet/SheetRow.svelte | 12 ++---------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/frontend-core/src/components/sheet/Sheet.svelte b/packages/frontend-core/src/components/sheet/Sheet.svelte index 2e5d2b68e7..0065ba08ca 100644 --- a/packages/frontend-core/src/components/sheet/Sheet.svelte +++ b/packages/frontend-core/src/components/sheet/Sheet.svelte @@ -24,7 +24,7 @@ export let API // Sheet constants - const cellHeight = 36 + const cellHeight = 40 const rand = Math.random() // State stores diff --git a/packages/frontend-core/src/components/sheet/SheetRow.svelte b/packages/frontend-core/src/components/sheet/SheetRow.svelte index 830ec9b900..ab5756b4f9 100644 --- a/packages/frontend-core/src/components/sheet/SheetRow.svelte +++ b/packages/frontend-core/src/components/sheet/SheetRow.svelte @@ -7,16 +7,8 @@ export let row - const { - selectedCellId, - reorder, - selectedRows, - rows, - visibleColumns, - cellHeight, - } = getContext("spreadsheet") - - console.log("mount") + const { selectedCellId, reorder, selectedRows, rows, visibleColumns } = + getContext("spreadsheet") $: rowSelected = !!$selectedRows[row._id] From 15dffb0f401c26966ec57523cfba199228dc3546 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Wed, 1 Mar 2023 18:32:23 +0000 Subject: [PATCH 051/235] Fix some scroll issues and add shadow to sticky column --- .../src/components/sheet/HeaderRow.svelte | 3 +- .../src/components/sheet/NewRow.svelte | 11 +- .../src/components/sheet/Sheet.svelte | 5 +- .../src/components/sheet/SheetBody.svelte | 27 +--- .../src/components/sheet/SheetCell.svelte | 8 +- .../src/components/sheet/SheetRow.svelte | 17 ++- .../sheet/SheetScrollWrapper.svelte | 4 +- .../src/components/sheet/StickyColumn.svelte | 133 +++++++++++------- .../src/components/sheet/stores/columns.js | 1 + 9 files changed, 117 insertions(+), 92 deletions(-) diff --git a/packages/frontend-core/src/components/sheet/HeaderRow.svelte b/packages/frontend-core/src/components/sheet/HeaderRow.svelte index 01a50d07f2..c5ff8e73f1 100644 --- a/packages/frontend-core/src/components/sheet/HeaderRow.svelte +++ b/packages/frontend-core/src/components/sheet/HeaderRow.svelte @@ -5,8 +5,7 @@ import { getIconForField } from "./utils" import SheetScrollWrapper from "./SheetScrollWrapper.svelte" - const { visibleColumns, reorder, selectedRows, rows } = - getContext("spreadsheet") + const { visibleColumns, reorder } = getContext("spreadsheet")
diff --git a/packages/frontend-core/src/components/sheet/NewRow.svelte b/packages/frontend-core/src/components/sheet/NewRow.svelte index 63985febf0..09173e68ed 100644 --- a/packages/frontend-core/src/components/sheet/NewRow.svelte +++ b/packages/frontend-core/src/components/sheet/NewRow.svelte @@ -1,11 +1,12 @@ -
+
($hoveredRowId = "new")}> {#each $visibleColumns as column} addRow(column)} width={column.width} left={column.left} @@ -30,8 +32,7 @@ .row { display: flex; } - :global(.sheet:not(.is-resizing):not(.is-reordering) .row:hover .cell) { - background: var(--cell-background-hover); + .row:hover :global(.cell) { cursor: pointer; } diff --git a/packages/frontend-core/src/components/sheet/Sheet.svelte b/packages/frontend-core/src/components/sheet/Sheet.svelte index 0065ba08ca..91f1c3f681 100644 --- a/packages/frontend-core/src/components/sheet/Sheet.svelte +++ b/packages/frontend-core/src/components/sheet/Sheet.svelte @@ -31,6 +31,7 @@ const tableIdStore = writable() const selectedCellId = writable() const selectedRows = writable({}) + const hoveredRowId = writable() const scroll = writable({ left: 0, top: 0, @@ -51,6 +52,7 @@ cellHeight, bounds, scroll, + hoveredRowId, tableId: tableIdStore, } const { rows, schema } = createRowsStore(context) @@ -114,7 +116,7 @@ display: flex; flex-direction: row; justify-items: flex-start; - align-items: stretch; + align-items: flex-start; overflow: hidden; height: 0; position: relative; @@ -124,5 +126,6 @@ overflow: hidden; display: flex; flex-direction: column; + align-self: stretch; } diff --git a/packages/frontend-core/src/components/sheet/SheetBody.svelte b/packages/frontend-core/src/components/sheet/SheetBody.svelte index 49c0ca3a16..841bb7bbd3 100644 --- a/packages/frontend-core/src/components/sheet/SheetBody.svelte +++ b/packages/frontend-core/src/components/sheet/SheetBody.svelte @@ -1,18 +1,11 @@ -
+
($hoveredRowId = row._id)}> {#each $visibleColumns as column (column.name)} {@const cellIdx = `${row._id}-${column.name}`} diff --git a/packages/frontend-core/src/components/sheet/SheetScrollWrapper.svelte b/packages/frontend-core/src/components/sheet/SheetScrollWrapper.svelte index 5018135334..f8c2340f4d 100644 --- a/packages/frontend-core/src/components/sheet/SheetScrollWrapper.svelte +++ b/packages/frontend-core/src/components/sheet/SheetScrollWrapper.svelte @@ -69,11 +69,11 @@ const offset = deltaY * step let newScrollTop = scrollTop newScrollTop += offset - newScrollTop = Math.max(0, newScrollTop) newScrollTop = Math.min( newScrollTop, ($rows.length + 1) * cellHeight - $bounds.height ) + newScrollTop = Math.max(0, newScrollTop) scroll.update(state => ({ ...state, top: newScrollTop, @@ -91,6 +91,8 @@ diff --git a/packages/frontend-core/src/components/sheet/SheetRow.svelte b/packages/frontend-core/src/components/sheet/SheetRow.svelte index a665addbea..eee6f2cb1b 100644 --- a/packages/frontend-core/src/components/sheet/SheetRow.svelte +++ b/packages/frontend-core/src/components/sheet/SheetRow.svelte @@ -1,9 +1,7 @@ - - -
- +
+
+ +
diff --git a/packages/frontend-core/src/components/sheet/StickyColumn.svelte b/packages/frontend-core/src/components/sheet/StickyColumn.svelte index 3a2e2f65dc..c8f6ee4f93 100644 --- a/packages/frontend-core/src/components/sheet/StickyColumn.svelte +++ b/packages/frontend-core/src/components/sheet/StickyColumn.svelte @@ -155,7 +155,7 @@ flex: 0 0 var(--width); z-index: 20; overflow: visible; - border-right: 1px solid var(--spectrum-global-color-gray-200); + border-right: var(--cell-border); } .sticky-column.scrolled { box-shadow: 1px -4px 8px rgba(0, 0, 0, 0.1); From 2c1a5ae0e851d4680eb90390da502bf68c88c6c8 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Wed, 1 Mar 2023 20:14:50 +0000 Subject: [PATCH 057/235] Update hovered row on wheel --- .../src/components/sheet/SheetScrollWrapper.svelte | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/frontend-core/src/components/sheet/SheetScrollWrapper.svelte b/packages/frontend-core/src/components/sheet/SheetScrollWrapper.svelte index 0e7900204f..2ad169e8dc 100644 --- a/packages/frontend-core/src/components/sheet/SheetScrollWrapper.svelte +++ b/packages/frontend-core/src/components/sheet/SheetScrollWrapper.svelte @@ -9,6 +9,7 @@ columns, visibleRows, visibleColumns, + hoveredRowId, } = getContext("spreadsheet") export let scrollVertically = true @@ -78,6 +79,11 @@ ...state, top: newScrollTop, })) + + // Hover row under cursor + const y = e.clientY - $bounds.top + (newScrollTop % cellHeight) + const hoveredRow = $visibleRows[Math.floor(y / cellHeight)] + $hoveredRowId = hoveredRow?._id } From 1620b81e96badaf6cac09555cbb7b224adc0c050 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 2 Mar 2023 09:27:16 +0000 Subject: [PATCH 058/235] Update scroll styles and z-index --- .../src/components/sheet/ScrollOverlay.svelte | 11 +-- .../src/components/sheet/Sheet.svelte | 3 +- .../src/components/sheet/SheetCell.svelte | 2 +- .../sheet/SheetScrollWrapper.svelte | 74 +++++++++++++++---- .../src/components/sheet/StickyColumn.svelte | 22 ++++-- 5 files changed, 83 insertions(+), 29 deletions(-) diff --git a/packages/frontend-core/src/components/sheet/ScrollOverlay.svelte b/packages/frontend-core/src/components/sheet/ScrollOverlay.svelte index 86e0c3bd55..7506318072 100644 --- a/packages/frontend-core/src/components/sheet/ScrollOverlay.svelte +++ b/packages/frontend-core/src/components/sheet/ScrollOverlay.svelte @@ -7,6 +7,7 @@ // Bar config const barOffset = 4 + const padding = 180 // State for dragging bars let initialMouse @@ -20,25 +21,25 @@ // Calculate V scrollbar size and offset // Terminology is the same for both axes: - // contentX - the size of the rendered content + // contentX - the size of the rendered content, including padding // renderX - the space available to render the bar in, edge to edge // barX - the length of the bar // availX - the space available to render the bar in, until the edge // barX - the offset of the bar - $: contentHeight = ($rows.length + 1) * cellHeight + $: contentHeight = ($rows.length + 1) * cellHeight + padding $: renderHeight = height - 2 * barOffset $: barHeight = Math.max(50, (height / contentHeight) * renderHeight) $: availHeight = renderHeight - barHeight - $: maxScrollTop = Math.max(contentHeight - height + 180, 0) + $: maxScrollTop = Math.max(contentHeight - height, 0) $: barTop = barOffset + cellHeight + availHeight * (scrollTop / maxScrollTop) // Calculate H scrollbar size and offset - $: contentWidth = calculateContentWidth($columns, $stickyColumn) + $: contentWidth = calculateContentWidth($columns, $stickyColumn) + padding $: totalWidth = width + 40 + $stickyColumn?.width || 0 $: renderWidth = totalWidth - 2 * barOffset $: barWidth = Math.max(50, (totalWidth / contentWidth) * renderWidth) $: availWidth = renderWidth - barWidth - $: maxScrollLeft = Math.max(contentWidth - totalWidth + 180, 0) + $: maxScrollLeft = Math.max(contentWidth - totalWidth, 0) $: barLeft = barOffset + availWidth * (scrollLeft / maxScrollLeft) // Calculate whether to show scrollbars or not diff --git a/packages/frontend-core/src/components/sheet/Sheet.svelte b/packages/frontend-core/src/components/sheet/Sheet.svelte index 678405f8f8..f5bdfbf18c 100644 --- a/packages/frontend-core/src/components/sheet/Sheet.svelte +++ b/packages/frontend-core/src/components/sheet/Sheet.svelte @@ -117,7 +117,7 @@ display: flex; flex-direction: row; justify-items: flex-start; - align-items: stretch; + align-items: flex-start; overflow: hidden; height: 0; position: relative; @@ -127,5 +127,6 @@ overflow: hidden; display: flex; flex-direction: column; + align-self: stretch; } diff --git a/packages/frontend-core/src/components/sheet/SheetCell.svelte b/packages/frontend-core/src/components/sheet/SheetCell.svelte index f3b3a9af39..a918fb5e98 100644 --- a/packages/frontend-core/src/components/sheet/SheetCell.svelte +++ b/packages/frontend-core/src/components/sheet/SheetCell.svelte @@ -68,7 +68,7 @@ background: var(--background); padding: 0 var(--cell-padding); gap: calc(2 * var(--cell-spacing)); - z-index: 10; + z-index: 25; border-bottom: none; } .cell.header :global(> span) { diff --git a/packages/frontend-core/src/components/sheet/SheetScrollWrapper.svelte b/packages/frontend-core/src/components/sheet/SheetScrollWrapper.svelte index 2ad169e8dc..c3093d1b0f 100644 --- a/packages/frontend-core/src/components/sheet/SheetScrollWrapper.svelte +++ b/packages/frontend-core/src/components/sheet/SheetScrollWrapper.svelte @@ -18,22 +18,68 @@ $: scrollTop = $scroll.top $: scrollLeft = $scroll.left - $: offsetY = scrollVertically ? -1 * (scrollTop % cellHeight) : 0 + $: offsetY = -1 * (scrollTop % cellHeight) $: hiddenWidths = calculateHiddenWidths($visibleColumns) - $: offsetX = scrollHorizontally ? -1 * scrollLeft + hiddenWidths : 0 + $: offsetX = -1 * scrollLeft + hiddenWidths $: rowCount = $visibleRows.length $: contentWidth = calculateContentWidth($visibleColumns, scrollHorizontally) $: contentHeight = calculateContentHeight(rowCount, scrollVertically) - $: style = getStyle(offsetX, offsetY, contentWidth, contentHeight) + $: innerStyle = getInnerStyle( + offsetX, + offsetY, + contentWidth, + contentHeight, + scrollHorizontally, + scrollVertically + ) + $: outerStyle = getOuterStyle( + offsetX, + offsetY, + contentWidth, + contentHeight, + scrollHorizontally, + scrollVertically + ) - const getStyle = (offsetX, offsetY, contentWidth, contentHeight) => { - let style = `--offset-y:${offsetY}px; --offset-x:${offsetX}px;` - if (contentWidth) { - style += `--width:${contentWidth}px;` + const getInnerStyle = ( + offsetX, + offsetY, + contentWidth, + contentHeight, + scrollH, + scrollV + ) => { + if (!scrollH) { + offsetX = 0 } - if (contentHeight) { - style += `--height:${contentHeight}px;` + if (!scrollV) { + offsetY = 0 } + let style = `--offset-x:${offsetX}px;--offset-y:${offsetY}px;` + // if (scrollH && contentWidth) { + // style += `width:${contentWidth}px;` + // } + // if (scrollV && contentHeight) { + // style += `height:${contentHeight}px;` + // } + return style + } + + const getOuterStyle = ( + offsetX, + offsetY, + contentWidth, + contentHeight, + scrollH, + scrollV + ) => { + let style = "" + // if (scrollV) { + // style += `height:${contentHeight + offsetY}px;` + // } + // if (scrollH) { + // style += `width:${contentWidth + offsetX}px;` + // } return style } @@ -87,20 +133,20 @@ } -
-
+
+
diff --git a/packages/frontend-core/src/components/sheet/StickyColumn.svelte b/packages/frontend-core/src/components/sheet/StickyColumn.svelte index c8f6ee4f93..f397b25ff2 100644 --- a/packages/frontend-core/src/components/sheet/StickyColumn.svelte +++ b/packages/frontend-core/src/components/sheet/StickyColumn.svelte @@ -152,20 +152,26 @@ diff --git a/packages/frontend-core/src/components/sheet/SheetCell.svelte b/packages/frontend-core/src/components/sheet/SheetCell.svelte index cb72080598..0e2415200d 100644 --- a/packages/frontend-core/src/components/sheet/SheetCell.svelte +++ b/packages/frontend-core/src/components/sheet/SheetCell.svelte @@ -7,7 +7,7 @@ export let selected = false export let reorderSource = false export let reorderTarget = false - export let width + export let width = ""
@@ -42,7 +42,6 @@ font-size: var(--cell-font-size); gap: var(--cell-spacing); background: var(--cell-background); - flex: 0 0 var(--width); position: relative; width: 0; } diff --git a/packages/frontend-core/src/components/sheet/SheetRow.svelte b/packages/frontend-core/src/components/sheet/SheetRow.svelte index 605b57ab66..33c4a1b5d5 100644 --- a/packages/frontend-core/src/components/sheet/SheetRow.svelte +++ b/packages/frontend-core/src/components/sheet/SheetRow.svelte @@ -33,7 +33,6 @@ reorderTarget={$reorder.targetColumn === column.name} on:click={() => ($selectedCellId = cellIdx)} width={column.width} - left={column.left} > import { getContext } from "svelte" + import { domDebounce } from "../../utils/utils" const { cellHeight, @@ -20,8 +21,13 @@ $: hiddenWidths = calculateHiddenWidths($visibleColumns) $: scrollLeft = $scroll.left $: scrollTop = $scroll.top - $: offsetX = scrollHorizontally ? -1 * scrollLeft + hiddenWidths : 0 - $: offsetY = scrollVertically ? -1 * (scrollTop % cellHeight) : 0 + $: style = generateStyle($scroll, hiddenWidths) + + const generateStyle = (scroll, hiddenWidths) => { + const offsetX = scrollHorizontally ? -1 * scroll.left + hiddenWidths : 0 + const offsetY = scrollVertically ? -1 * (scroll.top % cellHeight) : 0 + return `transform: translate3d(${offsetX}px, ${offsetY}px, 0);` + } // Calculates with total width of all columns currently not rendered const calculateHiddenWidths = visibleColumns => { @@ -38,13 +44,15 @@ // Handles a wheel even and updates the scroll offsets const handleWheel = e => { e.preventDefault() - + debouncedHandleWheel(e.deltaX, e.deltaY, e.clientY) + } + const debouncedHandleWheel = domDebounce((deltaX, deltaY, clientY) => { // Calculate new scroll top - let newScrollTop = scrollTop + e.deltaY + let newScrollTop = scrollTop + deltaY newScrollTop = Math.max(0, Math.min(newScrollTop, $maxScrollTop)) // Calculate new scroll left - let newScrollLeft = scrollLeft + e.deltaX + let newScrollLeft = scrollLeft + deltaX newScrollLeft = Math.max(0, Math.min(newScrollLeft, $maxScrollLeft)) // Update state @@ -54,14 +62,14 @@ }) // Hover row under cursor - const y = e.clientY - $bounds.top + (newScrollTop % cellHeight) + const y = clientY - $bounds.top + (newScrollTop % cellHeight) const hoveredRow = $visibleRows[Math.floor(y / cellHeight)] $hoveredRowId = hoveredRow?._id - } + })
-
+
@@ -71,7 +79,4 @@ min-width: 100%; min-height: 100%; } - .inner { - transform: translate3d(var(--offset-x), var(--offset-y), 0); - } diff --git a/packages/frontend-core/src/components/sheet/StickyColumn.svelte b/packages/frontend-core/src/components/sheet/StickyColumn.svelte index 7926251b5b..b7cb321b96 100644 --- a/packages/frontend-core/src/components/sheet/StickyColumn.svelte +++ b/packages/frontend-core/src/components/sheet/StickyColumn.svelte @@ -57,7 +57,7 @@ >
- + @@ -66,7 +66,6 @@ header sticky width={$stickyColumn.width} - left={$stickyColumn.left} reorderTarget={$reorder.targetColumn === $stickyColumn.name} > ($selectedCellId = cellIdx)} width={$stickyColumn.width} - left="40" reorderTarget={$reorder.targetColumn === $stickyColumn.name} > Date: Thu, 2 Mar 2023 13:39:47 +0000 Subject: [PATCH 063/235] Improve performance by removing keyed each blocks and fix reorder target styling --- .../frontend-core/src/components/sheet/SheetCell.svelte | 2 +- .../src/components/sheet/cells/OptionsCell.svelte | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/frontend-core/src/components/sheet/SheetCell.svelte b/packages/frontend-core/src/components/sheet/SheetCell.svelte index 0e2415200d..cdc078befc 100644 --- a/packages/frontend-core/src/components/sheet/SheetCell.svelte +++ b/packages/frontend-core/src/components/sheet/SheetCell.svelte @@ -93,7 +93,7 @@ right: 0; background: var(--spectrum-global-color-blue-400); width: 2px; - height: calc(var(--cell-height) + 1px); + height: calc(var(--cell-height) + 2px); } /* Label cells */ diff --git a/packages/frontend-core/src/components/sheet/cells/OptionsCell.svelte b/packages/frontend-core/src/components/sheet/cells/OptionsCell.svelte index 6f4e3dea42..8754105c61 100644 --- a/packages/frontend-core/src/components/sheet/cells/OptionsCell.svelte +++ b/packages/frontend-core/src/components/sheet/cells/OptionsCell.svelte @@ -57,7 +57,7 @@ on:click={editable ? toggleOpen : null} >
- {#each values as val (val)} + {#each values as val} {@const color = getOptionColor(val)} {#if color}
@@ -77,7 +77,7 @@ {/if} {#if open}
- {#each values as val (val)} + {#each values as val} {@const color = getOptionColor(val)}
toggleOption(val)}>
@@ -89,7 +89,7 @@ />
{/each} - {#each unselectedOptions as option (option)} + {#each unselectedOptions as option}
toggleOption(option)}>
{option} From 3a8d223a7780bdd430a8cc5bf601c44eaa5f2fe3 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 2 Mar 2023 15:45:55 +0000 Subject: [PATCH 064/235] Ensure scroll top is always properly reset and add config store --- .../src/components/sheet/Sheet.svelte | 21 +++++++---- .../src/components/sheet/SheetCell.svelte | 2 +- .../src/components/sheet/stores/rows.js | 3 +- .../src/components/sheet/stores/scroll.js | 37 +++++++++++-------- 4 files changed, 39 insertions(+), 24 deletions(-) diff --git a/packages/frontend-core/src/components/sheet/Sheet.svelte b/packages/frontend-core/src/components/sheet/Sheet.svelte index fa01a155a4..dcf0cf6f38 100644 --- a/packages/frontend-core/src/components/sheet/Sheet.svelte +++ b/packages/frontend-core/src/components/sheet/Sheet.svelte @@ -16,18 +16,20 @@ import ScrollOverlay from "./ScrollOverlay.svelte" import StickyColumn from "./StickyColumn.svelte" - export let tableId - export let filter - export let sortColumn - export let sortOrder export let API + export let tableId + export let allowAddColumns + export let allowAddRows + // export let filter + // export let sortColumn + // export let sortOrder // Sheet constants const cellHeight = 40 const rand = Math.random() // State stores - const tableIdStore = writable() + const config = writable({ tableId, allowAddRows, allowAddColumns }) const selectedCellId = writable() const selectedRows = writable({}) const hoveredRowId = writable() @@ -52,7 +54,7 @@ bounds, scroll, hoveredRowId, - tableId: tableIdStore, + config, } const { rows, schema } = createRowsStore(context) context = { ...context, rows, schema } @@ -64,7 +66,12 @@ const { reorder } = createReorderStores(context) context = { ...context, reorder } - $: tableIdStore.set(tableId) + // Keep config store up to date + $: config.set({ + tableId, + allowAddColumns, + allowAddRows, + }) // Set context for children to consume setContext("spreadsheet", context) diff --git a/packages/frontend-core/src/components/sheet/SheetCell.svelte b/packages/frontend-core/src/components/sheet/SheetCell.svelte index cdc078befc..5ce9b84da9 100644 --- a/packages/frontend-core/src/components/sheet/SheetCell.svelte +++ b/packages/frontend-core/src/components/sheet/SheetCell.svelte @@ -38,7 +38,7 @@ flex-direction: row; justify-content: flex-start; align-items: center; - color: var(--spectrum-global-color-gray-900); + color: var(--spectrum-global-color-gray-800); font-size: var(--cell-font-size); gap: var(--cell-spacing); background: var(--cell-background); diff --git a/packages/frontend-core/src/components/sheet/stores/rows.js b/packages/frontend-core/src/components/sheet/stores/rows.js index 0c4785ddba..9af6ed91cf 100644 --- a/packages/frontend-core/src/components/sheet/stores/rows.js +++ b/packages/frontend-core/src/components/sheet/stores/rows.js @@ -4,7 +4,8 @@ import { fetchData } from "../../../fetch/fetchData" import { notifications } from "@budibase/bbui" export const createRowsStore = context => { - const { tableId, filter, API, scroll } = context + const { config, filter, API, scroll } = context + const tableId = derived(config, $config => $config.tableId) // Flag for whether this is the first time loading our fetch let loaded = false diff --git a/packages/frontend-core/src/components/sheet/stores/scroll.js b/packages/frontend-core/src/components/sheet/stores/scroll.js index ee08f415dd..8d528c6967 100644 --- a/packages/frontend-core/src/components/sheet/stores/scroll.js +++ b/packages/frontend-core/src/components/sheet/stores/scroll.js @@ -1,4 +1,4 @@ -import { derived } from "svelte/store" +import { derived, get } from "svelte/store" export const createScrollStores = context => { const { scroll, rows, columns, stickyColumn, bounds, cellHeight } = context @@ -49,25 +49,32 @@ export const createScrollStores = context => { // Ensure scroll state never goes invalid, which can happen when changing // rows or tables - derived([scrollTop, maxScrollTop], ([$scrollTop, $maxScrollTop]) => { - console.log($scrollTop, $maxScrollTop, "check") - if ($scrollTop > $maxScrollTop) { + const overscrollTop = derived( + [scrollTop, maxScrollTop], + ([$scrollTop, $maxScrollTop]) => $scrollTop > $maxScrollTop, + false + ) + const overscrollLeft = derived( + [scrollLeft, maxScrollLeft], + ([$scrollLeft, $maxScrollLeft]) => $scrollLeft > $maxScrollLeft, + false + ) + overscrollTop.subscribe(overscroll => { + if (overscroll) { scroll.update(state => ({ ...state, - top: $maxScrollTop, + top: get(maxScrollTop), + })) + } + }) + overscrollLeft.subscribe(overscroll => { + if (overscroll) { + scroll.update(state => ({ + ...state, + left: get(maxScrollLeft), })) } }) - // $: { - // if (scrollLeft > maxScrollLeft) { - // setTimeout(() => { - // scroll.update(state => ({ - // ...state, - // left: maxScrollLeft, - // })) - // }) - // } - // } return { contentHeight, From 540906cf62a36ac37a5860a5ae1ef0b5bb028aa6 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 2 Mar 2023 16:07:14 +0000 Subject: [PATCH 065/235] Allow configuring selecting rows and adding rows --- .../src/components/sheet/Sheet.svelte | 17 +++-- .../src/components/sheet/StickyColumn.svelte | 70 +++++++++++-------- 2 files changed, 53 insertions(+), 34 deletions(-) diff --git a/packages/frontend-core/src/components/sheet/Sheet.svelte b/packages/frontend-core/src/components/sheet/Sheet.svelte index dcf0cf6f38..365afab8fd 100644 --- a/packages/frontend-core/src/components/sheet/Sheet.svelte +++ b/packages/frontend-core/src/components/sheet/Sheet.svelte @@ -18,8 +18,9 @@ export let API export let tableId - export let allowAddColumns - export let allowAddRows + export let allowAddColumns = true + export let allowAddRows = true + export let allowSelectRows = true // export let filter // export let sortColumn // export let sortOrder @@ -29,7 +30,12 @@ const rand = Math.random() // State stores - const config = writable({ tableId, allowAddRows, allowAddColumns }) + const config = writable({ + tableId, + allowAddRows, + allowAddColumns, + allowSelectRows, + }) const selectedCellId = writable() const selectedRows = writable({}) const hoveredRowId = writable() @@ -71,6 +77,7 @@ tableId, allowAddColumns, allowAddRows, + allowSelectRows, }) // Set context for children to consume @@ -87,7 +94,9 @@ {#each $visibleRows as row} {/each} - + {#if allowAddRows} + + {/if}
diff --git a/packages/frontend-core/src/components/sheet/StickyColumn.svelte b/packages/frontend-core/src/components/sheet/StickyColumn.svelte index b7cb321b96..6bbbc113b6 100644 --- a/packages/frontend-core/src/components/sheet/StickyColumn.svelte +++ b/packages/frontend-core/src/components/sheet/StickyColumn.svelte @@ -15,6 +15,7 @@ hoveredRowId, scroll, reorder, + config, } = getContext("spreadsheet") $: scrollLeft = $scroll.left @@ -57,8 +58,15 @@ >
- - + + {#if $config.allowSelectRows} + + {/if} {#if $stickyColumn} @@ -86,17 +94,20 @@ {@const rowSelected = !!$selectedRows[row._id]} {@const rowHovered = $hoveredRowId === row._id}
($hoveredRowId = row._id)}> - selectRow(row._id)} - width="40" - > -
+ +
selectRow(row._id)} + class="checkbox" + class:visible={$config.allowSelectRows && + (rowSelected || rowHovered)} + >
-
+
{row.__idx + 1}
@@ -126,24 +137,26 @@
{/each} -
($hoveredRowId = "new")}> - - - - {#if $stickyColumn} + {#if $config.allowAddRows} +
($hoveredRowId = "new")}> - {/if} -
+ label + on:click={addRow} + width="40" + > + + + {#if $stickyColumn} + + {/if} +
+ {/if}
@@ -195,7 +208,4 @@ .number.visible { display: flex; } - .row:hover .number { - display: none; - } From 5b590a59769f3341cc9ca567437adafc4a8f44d7 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 2 Mar 2023 17:20:51 +0000 Subject: [PATCH 066/235] Integrate sheet into data section better --- .../backend/DataTable/DataTable.svelte | 331 ++++++++---------- .../src/components/sheet/HeaderRow.svelte | 16 +- .../src/components/sheet/Sheet.svelte | 10 +- .../src/components/sheet/SheetCell.svelte | 5 + .../src/components/sheet/StickyColumn.svelte | 2 +- .../src/components/sheet/stores/rows.js | 12 +- 6 files changed, 178 insertions(+), 198 deletions(-) diff --git a/packages/builder/src/components/backend/DataTable/DataTable.svelte b/packages/builder/src/components/backend/DataTable/DataTable.svelte index f590429565..71e81254bd 100644 --- a/packages/builder/src/components/backend/DataTable/DataTable.svelte +++ b/packages/builder/src/components/backend/DataTable/DataTable.svelte @@ -18,142 +18,146 @@ Pagination, Heading, Body, + Modal, Layout, notifications, } from "@budibase/bbui" import { fetchData, Sheet } from "@budibase/frontend-core" import { API } from "api" + import CreateEditColumn from "components/backend/DataTable/modals/CreateEditColumn.svelte" - // let hideAutocolumns = true - // let filters - // - // $: isUsersTable = $tables.selected?._id === TableNames.USERS - // $: type = $tables.selected?.type - // $: isInternal = type !== "external" - // $: schema = $tables.selected?.schema - // $: enrichedSchema = enrichSchema($tables.selected?.schema) - // $: id = $tables.selected?._id - // $: fetch = createFetch(id) - // $: hasCols = checkHasCols(schema) - // $: hasRows = !!$fetch.rows?.length - // $: showError($fetch.error) - // $: id, (filters = null) - // - // let appliedFilter - // let rawFilter - // let appliedSort - // let selectedRows = [] - // - // $: enrichedSchema, - // () => { - // appliedFilter = null - // rawFilter = null - // appliedSort = null - // selectedRows = [] - // } - // - // $: if (Number.isInteger($fetch.pageNumber)) { - // selectedRows = [] - // } - // - // const showError = error => { - // if (error) { - // notifications.error(error?.message || "Unable to fetch data.") - // } - // } - // - // const enrichSchema = schema => { - // let tempSchema = { ...schema } - // tempSchema._id = { - // type: "internal", - // editable: false, - // displayName: "ID", - // autocolumn: true, - // } - // if (isInternal) { - // tempSchema._rev = { - // type: "internal", - // editable: false, - // displayName: "Revision", - // autocolumn: true, - // } - // } - // - // return tempSchema - // } - // - // const checkHasCols = schema => { - // if (!schema || Object.keys(schema).length === 0) { - // return false - // } - // let fields = Object.values(schema) - // for (let field of fields) { - // if (!field.autocolumn) { - // return true - // } - // } - // return false - // } - // - // // Fetches new data whenever the table changes - // const createFetch = tableId => { - // return fetchData({ - // API, - // datasource: { - // tableId, - // type: "table", - // }, - // options: { - // schema, - // limit: 10, - // paginate: true, - // }, - // }) - // } - // - // // Fetch data whenever sorting option changes - // const onSort = async e => { - // const sort = { - // sortColumn: e.detail.column, - // sortOrder: e.detail.order, - // } - // await fetch.update(sort) - // appliedSort = { ...sort } - // appliedSort.sortOrder = appliedSort.sortOrder.toLowerCase() - // selectedRows = [] - // } - // - // // Fetch data whenever filters change - // const onFilter = e => { - // filters = e.detail - // fetch.update({ - // filter: filters, - // }) - // appliedFilter = e.detail - // } - // - // // Fetch data whenever schema changes - // const onUpdateColumns = () => { - // selectedRows = [] - // fetch.refresh() - // } - // - // // Fetch data whenever rows are modified. Unfortunately we have to lose - // // our pagination place, as our bookmarks will have shifted. - // const onUpdateRows = () => { - // selectedRows = [] - // fetch.refresh() - // } - // - // // When importing new rows it is better to reinitialise request/paging data. - // // Not doing so causes inconsistency in paging behaviour and content. - // const onImportData = () => { - // fetch.getInitialData() - // } + let createColumnModal + + let hideAutocolumns = true + let filters + let hasRows = true + + $: isUsersTable = $tables.selected?._id === TableNames.USERS + $: type = $tables.selected?.type + $: isInternal = type !== "external" + $: schema = $tables.selected?.schema + $: enrichedSchema = enrichSchema($tables.selected?.schema) + $: id = $tables.selected?._id + $: hasCols = checkHasCols(schema) + $: id, (filters = null) + + let appliedFilter + let rawFilter + let appliedSort + let selectedRows = [] + + $: enrichedSchema, + () => { + appliedFilter = null + rawFilter = null + appliedSort = null + selectedRows = [] + } + + const enrichSchema = schema => { + let tempSchema = { ...schema } + tempSchema._id = { + type: "internal", + editable: false, + displayName: "ID", + autocolumn: true, + } + if (isInternal) { + tempSchema._rev = { + type: "internal", + editable: false, + displayName: "Revision", + autocolumn: true, + } + } + + return tempSchema + } + + const checkHasCols = schema => { + if (!schema || Object.keys(schema).length === 0) { + return false + } + let fields = Object.values(schema) + for (let field of fields) { + if (!field.autocolumn) { + return true + } + } + return false + } + + // Fetch data whenever sorting option changes + const onSort = async e => { + const sort = { + sortColumn: e.detail.column, + sortOrder: e.detail.order, + } + appliedSort = { ...sort } + appliedSort.sortOrder = appliedSort.sortOrder.toLowerCase() + selectedRows = [] + } + + // Fetch data whenever filters change + const onFilter = e => { + filters = e.detail + appliedFilter = e.detail + } -
- +
+
+
+ + {#if isInternal} + + {/if} +
+
+ + {#if isUsersTable} + + {/if} + {#if !isInternal} + + {/if} + + + {#key id} + + {/key} +
+
+
+ +
@@ -175,60 +179,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -262,12 +212,23 @@ + + + + + diff --git a/packages/frontend-core/src/components/sheet/Sheet.svelte b/packages/frontend-core/src/components/sheet/Sheet.svelte index 365afab8fd..0b043d34f3 100644 --- a/packages/frontend-core/src/components/sheet/Sheet.svelte +++ b/packages/frontend-core/src/components/sheet/Sheet.svelte @@ -1,5 +1,5 @@
- +
diff --git a/packages/frontend-core/src/components/sheet/SheetCell.svelte b/packages/frontend-core/src/components/sheet/SheetCell.svelte index 5ce9b84da9..4ea67d36cd 100644 --- a/packages/frontend-core/src/components/sheet/SheetCell.svelte +++ b/packages/frontend-core/src/components/sheet/SheetCell.svelte @@ -8,6 +8,7 @@ export let reorderSource = false export let reorderTarget = false export let width = "" + export let center = false
- + {#if $stickyColumn} { - const { config, filter, API, scroll } = context + const { config, API, scroll } = context const tableId = derived(config, $config => $config.tableId) + const filter = derived(config, $config => $config.filter) // Flag for whether this is the first time loading our fetch let loaded = false @@ -19,7 +20,7 @@ export const createRowsStore = context => { // Local stores for managing fetching data const query = derived(filter, $filter => buildLuceneQuery($filter)) - const fetch = derived(tableId, $tableId => { + const fetch = derived([tableId, filter], ([$tableId, $filter]) => { if (!$tableId) { return null } @@ -43,13 +44,6 @@ export const createRowsStore = context => { }) }) - // Update fetch when query changes - query.subscribe($query => { - get(fetch)?.update({ - query: $query, - }) - }) - // Observe each data fetch and extract some data fetch.subscribe($fetch => { if (!$fetch) { From b82e7582dbb58325af0dd9b8906c0c4ae44303f1 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 3 Mar 2023 08:34:26 +0000 Subject: [PATCH 067/235] Add back in functional delete row button --- .../backend/DataTable/DataTable.svelte | 9 +++ .../src/components/sheet/DeleteButton.svelte | 65 +++++++++++++++++++ .../src/components/sheet/Sheet.svelte | 3 +- .../src/components/sheet/SheetControls.svelte | 60 +---------------- 4 files changed, 79 insertions(+), 58 deletions(-) create mode 100644 packages/frontend-core/src/components/sheet/DeleteButton.svelte diff --git a/packages/builder/src/components/backend/DataTable/DataTable.svelte b/packages/builder/src/components/backend/DataTable/DataTable.svelte index 71e81254bd..8c9690fa8b 100644 --- a/packages/builder/src/components/backend/DataTable/DataTable.svelte +++ b/packages/builder/src/components/backend/DataTable/DataTable.svelte @@ -112,6 +112,15 @@ highlighted={!hasCols || !hasRows} on:updatecolumns={null} /> + {#if !isUsersTable} + + {/if} {#if isInternal} {/if} diff --git a/packages/frontend-core/src/components/sheet/DeleteButton.svelte b/packages/frontend-core/src/components/sheet/DeleteButton.svelte new file mode 100644 index 0000000000..3abf7e1530 --- /dev/null +++ b/packages/frontend-core/src/components/sheet/DeleteButton.svelte @@ -0,0 +1,65 @@ + + +{#if selectedRowCount} +
+ +
+{/if} + + + + Are you sure you want to delete {selectedRowCount} + row{selectedRowCount === 1 ? "" : "s"}? + + + + diff --git a/packages/frontend-core/src/components/sheet/Sheet.svelte b/packages/frontend-core/src/components/sheet/Sheet.svelte index 0b043d34f3..e747d115c5 100644 --- a/packages/frontend-core/src/components/sheet/Sheet.svelte +++ b/packages/frontend-core/src/components/sheet/Sheet.svelte @@ -6,7 +6,7 @@ import { createRowsStore } from "./stores/rows" import { createColumnsStores } from "./stores/columns" import { createScrollStores } from "./stores/scroll" - import SheetControls from "./SheetControls.svelte" + import DeleteButton from "./DeleteButton.svelte" import SheetBody from "./SheetBody.svelte" import SheetRow from "./SheetRow.svelte" import ResizeOverlay from "./ResizeOverlay.svelte" @@ -103,6 +103,7 @@ {/if}
+
diff --git a/packages/frontend-core/src/components/sheet/SheetControls.svelte b/packages/frontend-core/src/components/sheet/SheetControls.svelte index 34141a80cf..f994a2654a 100644 --- a/packages/frontend-core/src/components/sheet/SheetControls.svelte +++ b/packages/frontend-core/src/components/sheet/SheetControls.svelte @@ -1,31 +1,10 @@
@@ -37,29 +16,10 @@
- {#if selectedRowCount} - - Delete {selectedRowCount} row{selectedRowCount === 1 ? "" : "s"} - - {:else} - {rowCount} row{rowCount === 1 ? "" : "s"} - {/if} + {rowCount} row{rowCount === 1 ? "" : "s"}
- - - Are you sure you want to delete {selectedRowCount} - row{selectedRowCount === 1 ? "" : "s"}? - - - From 37393c4e2a02a6923b0f66e441adc661635f8cfb Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 3 Mar 2023 09:02:19 +0000 Subject: [PATCH 068/235] Refactor stores and make state more modular --- .../src/components/sheet/DeleteButton.svelte | 4 -- .../src/components/sheet/Sheet.svelte | 46 +++++-------------- .../src/components/sheet/SheetBody.svelte | 12 ++++- .../src/components/sheet/StickyColumn.svelte | 14 ++++-- .../src/components/sheet/stores/bounds.js | 11 +++++ .../src/components/sheet/stores/interface.js | 42 +++++++++++++++++ .../src/components/sheet/stores/rows.js | 5 +- .../src/components/sheet/stores/scroll.js | 9 +++- 8 files changed, 92 insertions(+), 51 deletions(-) create mode 100644 packages/frontend-core/src/components/sheet/stores/bounds.js create mode 100644 packages/frontend-core/src/components/sheet/stores/interface.js diff --git a/packages/frontend-core/src/components/sheet/DeleteButton.svelte b/packages/frontend-core/src/components/sheet/DeleteButton.svelte index 3abf7e1530..a7295c88f3 100644 --- a/packages/frontend-core/src/components/sheet/DeleteButton.svelte +++ b/packages/frontend-core/src/components/sheet/DeleteButton.svelte @@ -20,10 +20,6 @@ // Deletion callback when confirmed const performDeletion = async () => { await rows.actions.deleteRows(rowsToDelete) - - // Refresh state - $selectedCellId = null - $selectedRows = {} } diff --git a/packages/frontend-core/src/components/sheet/Sheet.svelte b/packages/frontend-core/src/components/sheet/Sheet.svelte index e747d115c5..5fedba5c1b 100644 --- a/packages/frontend-core/src/components/sheet/Sheet.svelte +++ b/packages/frontend-core/src/components/sheet/Sheet.svelte @@ -6,6 +6,8 @@ import { createRowsStore } from "./stores/rows" import { createColumnsStores } from "./stores/columns" import { createScrollStores } from "./stores/scroll" + import { createBoundsStores } from "./stores/bounds" + import { createInterfaceStores } from "./stores/interface" import DeleteButton from "./DeleteButton.svelte" import SheetBody from "./SheetBody.svelte" import SheetRow from "./SheetRow.svelte" @@ -38,42 +40,23 @@ allowAddColumns, allowSelectRows, }) - const selectedCellId = writable() - const selectedRows = writable({}) - const hoveredRowId = writable() - const scroll = writable({ - left: 0, - top: 0, - }) - const bounds = writable({ - left: 0, - top: 0, - width: 0, - height: 0, - }) - // Build up spreadsheet context and additional stores + // Build up spreadsheet context + // Stores are listed in order of dependency on each other let context = { API: API || createAPIClient(), rand, - selectedCellId, - selectedRows, cellHeight, - bounds, - scroll, - hoveredRowId, config, dispatch, } - const { rows, schema } = createRowsStore(context) - context = { ...context, rows, schema } - const { columns, stickyColumn } = createColumnsStores(context) - context = { ...context, columns, stickyColumn } + context = { ...context, ...createRowsStore(context) } + context = { ...context, ...createColumnsStores(context) } + context = { ...context, ...createBoundsStores(context) } context = { ...context, ...createScrollStores(context) } - const { visibleRows, visibleColumns } = createViewportStores(context) - context = { ...context, visibleRows, visibleColumns } - const { reorder } = createReorderStores(context) - context = { ...context, reorder } + context = { ...context, ...createViewportStores(context) } + context = { ...context, ...createReorderStores(context) } + context = { ...context, ...createInterfaceStores(context) } // Keep config store up to date $: config.set({ @@ -94,14 +77,7 @@
- - {#each $visibleRows as row} - - {/each} - {#if allowAddRows} - - {/if} - +
diff --git a/packages/frontend-core/src/components/sheet/SheetBody.svelte b/packages/frontend-core/src/components/sheet/SheetBody.svelte index 9f2acb0501..b636f4eabe 100644 --- a/packages/frontend-core/src/components/sheet/SheetBody.svelte +++ b/packages/frontend-core/src/components/sheet/SheetBody.svelte @@ -1,8 +1,11 @@
@@ -32,11 +32,6 @@ {/each}
-
dispatch("add-column")}> - - - -
diff --git a/packages/frontend-core/src/components/sheet/Sheet.svelte b/packages/frontend-core/src/components/sheet/Sheet.svelte index a61474501a..c9fcb09f0d 100644 --- a/packages/frontend-core/src/components/sheet/Sheet.svelte +++ b/packages/frontend-core/src/components/sheet/Sheet.svelte @@ -18,7 +18,6 @@ export let API export let tableId - export let allowAddColumns = true export let allowAddRows = true export let allowSelectRows = true export let filter @@ -33,7 +32,6 @@ tableId, filter, allowAddRows, - allowAddColumns, allowSelectRows, }) @@ -58,7 +56,6 @@ $: config.set({ tableId, filter, - allowAddColumns, allowAddRows, allowSelectRows, }) From 01867f573655b4955b6d5afdc4d161e00e3b2a0d Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 3 Mar 2023 09:16:00 +0000 Subject: [PATCH 071/235] Fix options cells being unable to scroll --- .../frontend-core/src/components/sheet/cells/OptionsCell.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend-core/src/components/sheet/cells/OptionsCell.svelte b/packages/frontend-core/src/components/sheet/cells/OptionsCell.svelte index 8754105c61..f3112a1252 100644 --- a/packages/frontend-core/src/components/sheet/cells/OptionsCell.svelte +++ b/packages/frontend-core/src/components/sheet/cells/OptionsCell.svelte @@ -76,7 +76,7 @@
{/if} {#if open} -
+
e.stopPropagation()}> {#each values as val} {@const color = getOptionColor(val)}
toggleOption(val)}> From 36c953443f9da1bd47d9785fdced13f95bc7c339 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Sun, 5 Mar 2023 18:57:05 +0000 Subject: [PATCH 072/235] Add WIP initial multi-user websocket implementation for sheets --- .../src/components/start/AppRow.svelte | 16 +-- .../builder/app/[application]/_layout.svelte | 1 + packages/builder/yarn.lock | 51 +++++++- packages/frontend-core/package.json | 1 + .../src/components/sheet/Sheet.svelte | 8 +- .../src/components/sheet/stores/rows.js | 121 ++++++++++-------- .../src/components/sheet/websocket.js | 57 +++++++++ packages/frontend-core/yarn.lock | 61 +++++++++ packages/server/package.json | 3 +- .../src/api/controllers/plugin/index.ts | 6 +- .../server/src/api/controllers/row/index.ts | 11 ++ packages/server/src/app.ts | 4 +- packages/server/src/middleware/builder.ts | 8 +- packages/server/src/websocket.ts | 26 ---- packages/server/src/websockets/client.ts | 11 ++ packages/server/src/websockets/dataspace.ts | 29 +++++ packages/server/src/websockets/index.ts | 14 ++ packages/server/src/websockets/websocket.ts | 73 +++++++++++ packages/server/yarn.lock | 52 ++++---- 19 files changed, 431 insertions(+), 122 deletions(-) create mode 100644 packages/frontend-core/src/components/sheet/websocket.js delete mode 100644 packages/server/src/websocket.ts create mode 100644 packages/server/src/websockets/client.ts create mode 100644 packages/server/src/websockets/dataspace.ts create mode 100644 packages/server/src/websockets/index.ts create mode 100644 packages/server/src/websockets/websocket.ts diff --git a/packages/builder/src/components/start/AppRow.svelte b/packages/builder/src/components/start/AppRow.svelte index 96c0e2154a..c6fb0b9531 100644 --- a/packages/builder/src/components/start/AppRow.svelte +++ b/packages/builder/src/components/start/AppRow.svelte @@ -15,12 +15,12 @@ } const goToBuilder = () => { - if (app.lockedOther) { - notifications.error( - `App locked by ${app.lockedBy.email}. Please allow lock to expire or have them unlock this app.` - ) - return - } + // if (app.lockedOther) { + // notifications.error( + // `App locked by ${app.lockedBy.email}. Please allow lock to expire or have them unlock this app.` + // ) + // return + // } $goto(`../../app/${app.devId}`) } @@ -59,9 +59,7 @@
- +
diff --git a/packages/builder/src/pages/builder/app/[application]/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/_layout.svelte index f561bd8ecd..c5c35bb3fd 100644 --- a/packages/builder/src/pages/builder/app/[application]/_layout.svelte +++ b/packages/builder/src/pages/builder/app/[application]/_layout.svelte @@ -89,6 +89,7 @@ } onMount(async () => { + return if (!hasSynced && application) { try { await API.syncApp(application) diff --git a/packages/builder/yarn.lock b/packages/builder/yarn.lock index 7035ca1765..56bfd6ee8c 100644 --- a/packages/builder/yarn.lock +++ b/packages/builder/yarn.lock @@ -1467,6 +1467,11 @@ dependencies: "@sinonjs/commons" "^1.7.0" +"@socket.io/component-emitter@~3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" + integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg== + "@spectrum-css/accordion@^3.0.24": version "3.0.30" resolved "https://registry.yarnpkg.com/@spectrum-css/accordion/-/accordion-3.0.30.tgz#0893a6db28bab984bf5adaf7e1ba194e741db615" @@ -2581,7 +2586,7 @@ dayjs@^1.10.4, dayjs@^1.11.2: resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.7.tgz#4b296922642f70999544d1144a2c25730fce63e2" integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ== -debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: +debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -2774,6 +2779,22 @@ end-of-stream@^1.1.0: dependencies: once "^1.4.0" +engine.io-client@~6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.4.0.tgz#88cd3082609ca86d7d3c12f0e746d12db4f47c91" + integrity sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + engine.io-parser "~5.0.3" + ws "~8.11.0" + xmlhttprequest-ssl "~2.0.0" + +engine.io-parser@~5.0.3: + version "5.0.6" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.6.tgz#7811244af173e157295dec9b2718dfe42a64ef45" + integrity sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw== + enquirer@^2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" @@ -5896,6 +5917,24 @@ snapdragon@^0.8.1: source-map-resolve "^0.5.0" use "^3.1.0" +socket.io-client@^4.6.1: + version "4.6.1" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.6.1.tgz#80d97d5eb0feca448a0fb6d69a7b222d3d547eab" + integrity sha512-5UswCV6hpaRsNg5kkEHVcbBIXEYoVbMQaHJBXJCyEQ+CiFPV1NIOY0XOFWG4XR4GZcB8Kn6AsRs/9cy9TbqVMQ== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.2" + engine.io-client "~6.4.0" + socket.io-parser "~4.2.1" + +socket.io-parser@~4.2.1: + version "4.2.2" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.2.tgz#1dd384019e25b7a3d374877f492ab34f2ad0d206" + integrity sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + source-map-js@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" @@ -6713,6 +6752,11 @@ ws@^7.4.6: resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== +ws@~8.11.0: + version "8.11.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" + integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== + xml-name-validator@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" @@ -6723,6 +6767,11 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== +xmlhttprequest-ssl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67" + integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A== + y18n@^4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" diff --git a/packages/frontend-core/package.json b/packages/frontend-core/package.json index 927f40c568..002d7814dc 100644 --- a/packages/frontend-core/package.json +++ b/packages/frontend-core/package.json @@ -8,6 +8,7 @@ "dependencies": { "@budibase/bbui": "2.3.18-alpha.15", "lodash": "^4.17.21", + "socket.io-client": "^4.6.1", "svelte": "^3.46.2" } } diff --git a/packages/frontend-core/src/components/sheet/Sheet.svelte b/packages/frontend-core/src/components/sheet/Sheet.svelte index c9fcb09f0d..ee8ed020b1 100644 --- a/packages/frontend-core/src/components/sheet/Sheet.svelte +++ b/packages/frontend-core/src/components/sheet/Sheet.svelte @@ -1,5 +1,5 @@
diff --git a/packages/frontend-core/src/components/sheet/stores/rows.js b/packages/frontend-core/src/components/sheet/stores/rows.js index 4e05e1f525..0b7bd01368 100644 --- a/packages/frontend-core/src/components/sheet/stores/rows.js +++ b/packages/frontend-core/src/components/sheet/stores/rows.js @@ -72,31 +72,6 @@ export const createRowsStore = context => { }) }) - // Local handler to process new rows inside the fetch, and append any new - // rows to state that we haven't encountered before - const handleNewRows = newRows => { - let rowsToAppend = [] - let newRow - for (let i = 0; i < newRows.length; i++) { - newRow = newRows[i] - if (!rowCacheMap[newRow._id]) { - rowCacheMap[newRow._id] = true - rowsToAppend.push(newRow) - } - } - if (rowsToAppend.length) { - rows.update($rows => { - return [ - ...$rows, - ...rowsToAppend.map((row, idx) => ({ - ...row, - __idx: $rows.length + idx, - })), - ] - }) - } - } - // Adds a new empty row const addRow = async () => { try { @@ -127,6 +102,43 @@ export const createRowsStore = context => { } } + // Refreshes a specific row, handling updates, addition or deletion + const refreshRow = async id => { + // Get index of row to check if it exists + const $rows = get(rows) + const index = $rows.findIndex(row => row._id === id) + + // Fetch row from the server again + const res = await API.searchTable({ + tableId: get(tableId), + limit: 1, + query: { + equal: { + _id: id, + }, + }, + paginate: false, + }) + let newRow = res?.rows?.[0] + + // Process as either an update, addition or deletion + if (newRow) { + if (index !== -1) { + // An existing row was updated + rows.update(state => { + state[index] = { ...newRow, __idx: index } + return state + }) + } else { + // A new row was created + handleNewRows([newRow]) + } + } else if (index !== -1) { + // A row was removed + handleRemoveRows([$rows[index]]) + } + } + // Updates a value of a row const updateRow = async (rowId, column, value) => { const $rows = get(rows) @@ -151,35 +163,11 @@ export const createRowsStore = context => { notifications.error(`Error saving row: ${error?.message}`) } - // Fetch row from the server again - const res = await API.searchTable({ - tableId: get(tableId), - limit: 1, - query: { - equal: { - _id: row._id, - }, - }, - paginate: false, - }) - if (res?.rows?.[0]) { - newRow = res.rows[0] - } - - // Update state again with this row - newRow = { ...newRow, __idx: row.__idx } - rows.update(state => { - state[index] = newRow - return state - }) - - return newRow + return await refreshRow(row._id) } // Deletes an array of rows const deleteRows = async rowsToDelete => { - const deletedIds = rowsToDelete.map(row => row._id) - // Actually delete rows rowsToDelete.forEach(row => { delete row.__idx @@ -190,6 +178,38 @@ export const createRowsStore = context => { }) // Update state + handleRemoveRows(rowsToDelete) + } + + // Local handler to process new rows inside the fetch, and append any new + // rows to state that we haven't encountered before + const handleNewRows = newRows => { + let rowsToAppend = [] + let newRow + for (let i = 0; i < newRows.length; i++) { + newRow = newRows[i] + if (!rowCacheMap[newRow._id]) { + rowCacheMap[newRow._id] = true + rowsToAppend.push(newRow) + } + } + if (rowsToAppend.length) { + rows.update($rows => { + return [ + ...$rows, + ...rowsToAppend.map((row, idx) => ({ + ...row, + __idx: $rows.length + idx, + })), + ] + }) + } + } + + // Local handler to remove rows from state + const handleRemoveRows = rowsToRemove => { + const deletedIds = rowsToRemove.map(row => row._id) + // We deliberately do not remove IDs from the cache map as the data may // still exist inside the fetch, but we don't want to add it again rows.update(state => { @@ -217,6 +237,7 @@ export const createRowsStore = context => { updateRow, deleteRows, loadNextPage, + refreshRow, }, }, schema, diff --git a/packages/frontend-core/src/components/sheet/websocket.js b/packages/frontend-core/src/components/sheet/websocket.js new file mode 100644 index 0000000000..254619b36d --- /dev/null +++ b/packages/frontend-core/src/components/sheet/websocket.js @@ -0,0 +1,57 @@ +import { derived, get } from "svelte/store" +import { io } from "socket.io-client" + +export const createWebsocket = context => { + const { rows, config } = context + const tableId = derived(config, $config => $config.tableId) + + // Determine connection info + const tls = location.protocol === "https:" + const proto = tls ? "wss:" : "ws:" + const host = location.hostname + const port = location.port || (tls ? 443 : 80) + const socket = io(`${proto}//${host}:${port}`, { + path: "/socket/dataspace", + // Cap reconnection attempts to 3 (total of 15 seconds before giving up) + reconnectionAttempts: 3, + // Delay reconnection attempt by 5 seconds + reconnectionDelay: 5000, + reconnectionDelayMax: 5000, + // Timeout after 4 seconds so we never stack requests + timeout: 4000, + }) + + const connectToDataspace = tableId => { + if (!socket.connected) { + return + } + console.log("Idenifying dataspace", tableId) + + // Identify which dataspace we are editing + socket.emit("identify", tableId, response => { + // handle initial connection info + console.log("response", response) + }) + } + + // Event handlers + socket.on("connect", () => { + connectToDataspace(get(tableId)) + }) + + socket.on("row-update", data => { + if (data.id) { + rows.actions.refreshRow(data.id) + } + console.log(data) + }) + + socket.on("connect_error", err => { + console.log("Failed to connect to websocket:", err.message) + }) + + // Change websocket connection when dataspace changes + tableId.subscribe(connectToDataspace) + + return () => socket?.disconnect() +} diff --git a/packages/frontend-core/yarn.lock b/packages/frontend-core/yarn.lock index 7a2e6b7ba4..65f615d919 100644 --- a/packages/frontend-core/yarn.lock +++ b/packages/frontend-core/yarn.lock @@ -2,12 +2,73 @@ # yarn lockfile v1 +"@socket.io/component-emitter@~3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" + integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg== + +debug@~4.3.1, debug@~4.3.2: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +engine.io-client@~6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.4.0.tgz#88cd3082609ca86d7d3c12f0e746d12db4f47c91" + integrity sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + engine.io-parser "~5.0.3" + ws "~8.11.0" + xmlhttprequest-ssl "~2.0.0" + +engine.io-parser@~5.0.3: + version "5.0.6" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.6.tgz#7811244af173e157295dec9b2718dfe42a64ef45" + integrity sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw== + lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +socket.io-client@^4.6.1: + version "4.6.1" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.6.1.tgz#80d97d5eb0feca448a0fb6d69a7b222d3d547eab" + integrity sha512-5UswCV6hpaRsNg5kkEHVcbBIXEYoVbMQaHJBXJCyEQ+CiFPV1NIOY0XOFWG4XR4GZcB8Kn6AsRs/9cy9TbqVMQ== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.2" + engine.io-client "~6.4.0" + socket.io-parser "~4.2.1" + +socket.io-parser@~4.2.1: + version "4.2.2" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.2.tgz#1dd384019e25b7a3d374877f492ab34f2ad0d206" + integrity sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + svelte@^3.46.2: version "3.49.0" resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.49.0.tgz#5baee3c672306de1070c3b7888fc2204e36a4029" integrity sha512-+lmjic1pApJWDfPCpUUTc1m8azDqYCG1JN9YEngrx/hUyIcFJo6VZhj0A1Ai0wqoHcEIuQy+e9tk+4uDgdtsFA== + +ws@~8.11.0: + version "8.11.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" + integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== + +xmlhttprequest-ssl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67" + integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A== diff --git a/packages/server/package.json b/packages/server/package.json index 39210f7d4a..3aadfb3702 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -62,6 +62,7 @@ "bull": "4.10.1", "chmodr": "1.2.0", "chokidar": "3.5.3", + "cookies": "0.8.0", "csvtojson": "2.0.10", "curlconverter": "3.21.0", "dd-trace": "3.13.2", @@ -107,7 +108,7 @@ "redis": "4", "server-destroy": "1.0.1", "snowflake-promise": "^4.5.0", - "socket.io": "^4.5.1", + "socket.io": "4.6.1", "svelte": "3.49.0", "swagger-parser": "10.0.3", "tar": "6.1.11", diff --git a/packages/server/src/api/controllers/plugin/index.ts b/packages/server/src/api/controllers/plugin/index.ts index faecbc1fd8..76411ddf48 100644 --- a/packages/server/src/api/controllers/plugin/index.ts +++ b/packages/server/src/api/controllers/plugin/index.ts @@ -7,7 +7,7 @@ import { } from "@budibase/backend-core" import { PluginType, FileType, PluginSource, Plugin } from "@budibase/types" import env from "../../../environment" -import { ClientAppSocket } from "../../../websocket" +import { clientAppSocket } from "../../../websockets" import { sdk as pro } from "@budibase/pro" export async function getPlugins(type?: PluginType) { @@ -91,7 +91,7 @@ export async function create(ctx: any) { const doc = await pro.plugins.storePlugin(metadata, directory, source) - ClientAppSocket.emit("plugins-update", { name, hash: doc.hash }) + clientAppSocket.emit("plugins-update", { name, hash: doc.hash }) ctx.body = { message: "Plugin uploaded successfully", plugins: [doc], @@ -133,6 +133,6 @@ export async function processUploadedPlugin( } const doc = await pro.plugins.storePlugin(metadata, directory, source) - ClientAppSocket.emit("plugin-update", { name: doc.name, hash: doc.hash }) + clientAppSocket.emit("plugin-update", { name: doc.name, hash: doc.hash }) return doc } diff --git a/packages/server/src/api/controllers/row/index.ts b/packages/server/src/api/controllers/row/index.ts index b59f245098..fa10b63522 100644 --- a/packages/server/src/api/controllers/row/index.ts +++ b/packages/server/src/api/controllers/row/index.ts @@ -2,6 +2,7 @@ import { quotas } from "@budibase/pro" import * as internal from "./internal" import * as external from "./external" import { isExternalTable } from "../../../integrations/utils" +import { dataspaceSocket } from "../../../websockets" function pickApi(tableId: any) { if (isExternalTable(tableId)) { @@ -45,6 +46,9 @@ export async function patch(ctx: any): Promise { ctx.eventEmitter.emitRow(`row:update`, appId, row, table) ctx.message = `${table.name} updated successfully.` ctx.body = row + + // Notify websocket change + dataspaceSocket.emit("row-update", { id: row._id }) } catch (err) { ctx.throw(400, err) } @@ -67,6 +71,9 @@ export const save = async (ctx: any) => { ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:save`, appId, row, table) ctx.message = `${table.name} saved successfully` ctx.body = row + + // Notify websocket change + dataspaceSocket.emit("row-update", { id: row._id }) } export async function fetchView(ctx: any) { const tableId = getTableId(ctx) @@ -105,6 +112,8 @@ export async function destroy(ctx: any) { response = rows for (let row of rows) { ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, row) + // Notify websocket change + dataspaceSocket.emit("row-update", { id: row._id }) } } else { let resp = await quotas.addQuery(() => pickApi(tableId).destroy(ctx), { @@ -114,6 +123,8 @@ export async function destroy(ctx: any) { response = resp.response row = resp.row ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, row) + // Notify websocket change + dataspaceSocket.emit("row-update", { id: row._id }) } ctx.status = 200 // for automations include the row that was deleted diff --git a/packages/server/src/app.ts b/packages/server/src/app.ts index 00f2aca7fc..18265eaa26 100644 --- a/packages/server/src/app.ts +++ b/packages/server/src/app.ts @@ -28,7 +28,7 @@ import * as automations from "./automations" import { Thread } from "./threads" import * as redis from "./utilities/redis" import { events, logging, middleware } from "@budibase/backend-core" -import { initialise as initialiseWebsockets } from "./websocket" +import { initialise as initialiseWebsockets } from "./websockets" import { startup } from "./startup" const Sentry = require("@sentry/node") const destroyable = require("server-destroy") @@ -72,7 +72,7 @@ if (env.isProd()) { const server = http.createServer(app.callback()) destroyable(server) -initialiseWebsockets(server) +initialiseWebsockets(app, server) let shuttingDown = false, errCode = 0 diff --git a/packages/server/src/middleware/builder.ts b/packages/server/src/middleware/builder.ts index 5174f618a0..ad89d0c658 100644 --- a/packages/server/src/middleware/builder.ts +++ b/packages/server/src/middleware/builder.ts @@ -35,12 +35,12 @@ async function checkDevAppLocks(ctx: BBContext) { if (!appId || !appId.startsWith(APP_DEV_PREFIX)) { return } - if (!(await doesUserHaveLock(appId, ctx.user))) { - ctx.throw(400, "User does not hold app lock.") - } + // if (!(await doesUserHaveLock(appId, ctx.user))) { + // ctx.throw(400, "User does not hold app lock.") + // } // they do have lock, update it - await updateLock(appId, ctx.user) + // await updateLock(appId, ctx.user) } async function updateAppUpdatedAt(ctx: BBContext) { diff --git a/packages/server/src/websocket.ts b/packages/server/src/websocket.ts deleted file mode 100644 index d6d91b0ca0..0000000000 --- a/packages/server/src/websocket.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Server } from "socket.io" -import http from "http" - -class Websocket { - socketServer: Server - - constructor(server: http.Server, path: string) { - this.socketServer = new Server(server, { - path, - }) - } - - // Emit an event to all sockets - emit(event: string, payload: any) { - this.socketServer.sockets.emit(event, payload) - } -} - -// Likely to be more socket instances in future -let ClientAppSocket: Websocket - -export const initialise = (server: http.Server) => { - ClientAppSocket = new Websocket(server, "/socket/client") -} - -export { ClientAppSocket } diff --git a/packages/server/src/websockets/client.ts b/packages/server/src/websockets/client.ts new file mode 100644 index 0000000000..d59325f66e --- /dev/null +++ b/packages/server/src/websockets/client.ts @@ -0,0 +1,11 @@ +import Socket from "./websocket" +import authorized from "../middleware/authorized" +import http from "http" +import Koa from "koa" +import { permissions } from "@budibase/backend-core" + +export default class ClientAppWebsocket extends Socket { + constructor(app: Koa, server: http.Server) { + super(app, server, "/socket/client", [authorized(permissions.BUILDER)]) + } +} diff --git a/packages/server/src/websockets/dataspace.ts b/packages/server/src/websockets/dataspace.ts new file mode 100644 index 0000000000..e5cd73b8f4 --- /dev/null +++ b/packages/server/src/websockets/dataspace.ts @@ -0,0 +1,29 @@ +import authorized from "../middleware/authorized" +import Socket from "./websocket" +import { permissions } from "@budibase/backend-core" +import http from "http" +import Koa from "koa" + +export default class DataspaceSocket extends Socket { + constructor(app: Koa, server: http.Server) { + super(app, server, "/socket/dataspace", [authorized(permissions.BUILDER)]) + + this.io.on("connection", socket => { + const user = socket.data.user + console.log(`Dataspace user connected: ${user?._id}`) + + // Initial identification of conneted dataspace + socket.on("identify", async (tableId, callback) => { + socket.join(tableId) + + const sockets = await this.io.in(tableId).fetchSockets() + callback(sockets.map(socket => socket.data.user)) + }) + + // Disconnection cleanup + socket.on("disconnect", reason => { + console.log(`Disconnecting ${user.email} because of ${reason}`) + }) + }) + } +} diff --git a/packages/server/src/websockets/index.ts b/packages/server/src/websockets/index.ts new file mode 100644 index 0000000000..551044ef8e --- /dev/null +++ b/packages/server/src/websockets/index.ts @@ -0,0 +1,14 @@ +import http from "http" +import Koa from "koa" +import DataspaceSocket from "./dataspace" +import ClientAppSocket from "./client" + +let clientAppSocket: ClientAppSocket +let dataspaceSocket: DataspaceSocket + +export const initialise = (app: Koa, server: http.Server) => { + clientAppSocket = new ClientAppSocket(app, server) + dataspaceSocket = new DataspaceSocket(app, server) +} + +export { clientAppSocket, dataspaceSocket } diff --git a/packages/server/src/websockets/websocket.ts b/packages/server/src/websockets/websocket.ts new file mode 100644 index 0000000000..085d583f48 --- /dev/null +++ b/packages/server/src/websockets/websocket.ts @@ -0,0 +1,73 @@ +import { Server } from "socket.io" +import http from "http" +import Koa from "koa" +import Cookies from "cookies" +import { userAgent } from "koa-useragent" +import { auth } from "@budibase/backend-core" + +export default class Socket { + io: Server + + constructor( + app: Koa, + server: http.Server, + path: string, + additionalMiddlewares?: any[] + ) { + this.io = new Server(server, { + path, + }) + + // Attach default middlewares + const authenticate = auth.buildAuthMiddleware([], { + publicAllowed: true, + }) + const middlewares = [ + userAgent, + authenticate, + ...(additionalMiddlewares || []), + ] + + // Apply middlewares + this.io.use(async (socket, next) => { + // Build fake koa context + const res = new http.ServerResponse(socket.request) + const ctx: any = { + ...app.createContext(socket.request, res), + + // Additional overrides needed to make our middlewares work with this + // fake koa context + cookies: new Cookies(socket.request, res), + get: (field: string) => socket.request.headers[field], + throw: (code: number, message: string) => { + throw new Error(message) + }, + + // Needed for koa-useragent middleware + header: socket.request.headers, + } + + // Run all koa middlewares + try { + for (let [idx, middleware] of middlewares.entries()) { + await middleware(ctx, () => { + if (idx === middlewares.length - 1) { + // Middlewares are finished. + // Extract some data from our enriched koa context to persist + // as metadata for the socket + socket.data.user = ctx.user + next() + } + }) + } + } catch (error: any) { + next(error) + } + }) + } + + // Emit an event to all sockets + emit(event: string, payload: any) { + this.io.sockets.emit(event, payload) + } +} diff --git a/packages/server/yarn.lock b/packages/server/yarn.lock index 465b849997..477d649f28 100644 --- a/packages/server/yarn.lock +++ b/packages/server/yarn.lock @@ -5647,7 +5647,7 @@ cookiejar@^2.1.3: resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b" integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw== -cookies@~0.8.0: +cookies@0.8.0, cookies@~0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.8.0.tgz#1293ce4b391740a8406e3c9870e828c4b54f3f90" integrity sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow== @@ -6462,10 +6462,10 @@ engine.io-parser@~5.0.3: resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.4.tgz#0b13f704fa9271b3ec4f33112410d8f3f41d0fc0" integrity sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg== -engine.io@~6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.2.0.tgz#003bec48f6815926f2b1b17873e576acd54f41d0" - integrity sha512-4KzwW3F3bk+KlzSOY57fj/Jx6LyRQ1nbcyIadehl+AnXjKT7gDO0ORdRi/84ixvMKTym6ZKuxvbzN62HDDU1Lg== +engine.io@~6.4.1: + version "6.4.1" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.4.1.tgz#8056b4526a88e779f9c280d820422d4e3eeaaae5" + integrity sha512-JFYQurD/nbsA5BSPmbaOSLa3tSVj8L6o4srSwXXY3NqE+gGUNmmPTbhn8tjzcCtSqhFgIeqef81ngny8JM25hw== dependencies: "@types/cookie" "^0.4.1" "@types/cors" "^2.8.12" @@ -6476,7 +6476,7 @@ engine.io@~6.2.0: cors "~2.8.5" debug "~4.3.1" engine.io-parser "~5.0.3" - ws "~8.2.3" + ws "~8.11.0" enhanced-resolve@^5.9.3: version "5.9.3" @@ -13943,30 +13943,32 @@ snowflake-sdk@^1.6.0: uuid "^3.3.2" winston "^3.1.0" -socket.io-adapter@~2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.4.0.tgz#b50a4a9ecdd00c34d4c8c808224daa1a786152a6" - integrity sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg== +socket.io-adapter@~2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz#5de9477c9182fdc171cd8c8364b9a8894ec75d12" + integrity sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA== + dependencies: + ws "~8.11.0" -socket.io-parser@~4.2.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.1.tgz#01c96efa11ded938dcb21cbe590c26af5eff65e5" - integrity sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g== +socket.io-parser@~4.2.1: + version "4.2.2" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.2.tgz#1dd384019e25b7a3d374877f492ab34f2ad0d206" + integrity sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw== dependencies: "@socket.io/component-emitter" "~3.1.0" debug "~4.3.1" -socket.io@^4.5.1: - version "4.5.2" - resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.5.2.tgz#1eb25fd380ab3d63470aa8279f8e48d922d443ac" - integrity sha512-6fCnk4ARMPZN448+SQcnn1u8OHUC72puJcNtSgg2xS34Cu7br1gQ09YKkO1PFfDn/wyUE9ZgMAwosJed003+NQ== +socket.io@4.6.1: + version "4.6.1" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.6.1.tgz#62ec117e5fce0692fa50498da9347cfb52c3bc70" + integrity sha512-KMcaAi4l/8+xEjkRICl6ak8ySoxsYG+gG6/XfRCPJPQ/haCRIJBTL4wIl8YCsmtaBovcAXGLOShyVWQ/FG8GZA== dependencies: accepts "~1.3.4" base64id "~2.0.0" debug "~4.3.2" - engine.io "~6.2.0" - socket.io-adapter "~2.4.0" - socket.io-parser "~4.2.0" + engine.io "~6.4.1" + socket.io-adapter "~2.5.2" + socket.io-parser "~4.2.1" socks@^2.7.0: version "2.7.0" @@ -15936,10 +15938,10 @@ ws@^5.2.0: dependencies: async-limiter "~1.0.0" -ws@~8.2.3: - version "8.2.3" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba" - integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA== +ws@~8.11.0: + version "8.11.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" + integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== x3-linkedlist@1.2.0: version "1.2.0" From 3e907af8b55ce9d652a8c1f73aca61b8579ddfad Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 6 Mar 2023 07:43:45 +0000 Subject: [PATCH 073/235] Add WIP multi-user UI for sheet interface --- .../src/components/sheet/Sheet.svelte | 3 ++ .../src/components/sheet/SheetCell.svelte | 20 +++++++++ .../src/components/sheet/SheetRow.svelte | 2 + .../src/components/sheet/stores/users.js | 41 +++++++++++++++++++ .../src/components/sheet/websocket.js | 18 ++++++-- packages/server/src/websockets/dataspace.ts | 25 +++++++++-- packages/server/src/websockets/websocket.ts | 6 ++- 7 files changed, 107 insertions(+), 8 deletions(-) create mode 100644 packages/frontend-core/src/components/sheet/stores/users.js diff --git a/packages/frontend-core/src/components/sheet/Sheet.svelte b/packages/frontend-core/src/components/sheet/Sheet.svelte index ee8ed020b1..bacdd27f69 100644 --- a/packages/frontend-core/src/components/sheet/Sheet.svelte +++ b/packages/frontend-core/src/components/sheet/Sheet.svelte @@ -8,6 +8,7 @@ import { createScrollStores } from "./stores/scroll" import { createBoundsStores } from "./stores/bounds" import { createInterfaceStores } from "./stores/interface" + export { createUserStores } from "./stores/users" import DeleteButton from "./DeleteButton.svelte" import SheetBody from "./SheetBody.svelte" import ResizeOverlay from "./ResizeOverlay.svelte" @@ -16,6 +17,7 @@ import ScrollOverlay from "./ScrollOverlay.svelte" import StickyColumn from "./StickyColumn.svelte" import { createWebsocket } from "./websocket" + import { createUserStores } from "./stores/users" export let API export let tableId @@ -52,6 +54,7 @@ context = { ...context, ...createViewportStores(context) } context = { ...context, ...createReorderStores(context) } context = { ...context, ...createInterfaceStores(context) } + context = { ...context, ...createUserStores(context) } // Keep config store up to date $: config.set({ diff --git a/packages/frontend-core/src/components/sheet/SheetCell.svelte b/packages/frontend-core/src/components/sheet/SheetCell.svelte index 4ea67d36cd..821ef30b65 100644 --- a/packages/frontend-core/src/components/sheet/SheetCell.svelte +++ b/packages/frontend-core/src/components/sheet/SheetCell.svelte @@ -9,6 +9,7 @@ export let reorderTarget = false export let width = "" export let center = false + export let selectedUser = null
+ {#if selectedUser} +
+ {selectedUser.email} +
+ {/if}
@@ -51,6 +58,9 @@ box-shadow: inset 0 0 0 2px var(--spectrum-global-color-blue-400); z-index: 1; } + .cell.selected-other { + box-shadow: inset 0 0 0 2px var(--spectrum-global-color-purple-400); + } .cell:not(.selected) { user-select: none; } @@ -110,4 +120,14 @@ justify-content: center; align-items: center; } + + .name { + position: absolute; + bottom: 100%; + background: var(--spectrum-global-color-purple-400); + padding: 1px 4px 0 4px; + border-radius: 2px; + font-size: 12px; + font-weight: 600; + } diff --git a/packages/frontend-core/src/components/sheet/SheetRow.svelte b/packages/frontend-core/src/components/sheet/SheetRow.svelte index 632047467f..920cd48c0c 100644 --- a/packages/frontend-core/src/components/sheet/SheetRow.svelte +++ b/packages/frontend-core/src/components/sheet/SheetRow.svelte @@ -12,6 +12,7 @@ rows, visibleColumns, hoveredRowId, + selectedCellMap, } = getContext("spreadsheet") $: rowSelected = !!$selectedRows[row._id] @@ -30,6 +31,7 @@ {rowSelected} {rowHovered} selected={$selectedCellId === cellIdx} + selectedUser={$selectedCellMap[cellIdx]} reorderSource={$reorder.sourceColumn === column.name} reorderTarget={$reorder.targetColumn === column.name} on:click={() => ($selectedCellId = cellIdx)} diff --git a/packages/frontend-core/src/components/sheet/stores/users.js b/packages/frontend-core/src/components/sheet/stores/users.js new file mode 100644 index 0000000000..d79b74899d --- /dev/null +++ b/packages/frontend-core/src/components/sheet/stores/users.js @@ -0,0 +1,41 @@ +import { writable, get, derived } from "svelte/store" + +export const createUserStores = () => { + const users = writable([]) + const userId = writable(null) + + const updateUser = user => { + const $users = get(users) + const index = $users.findIndex(x => x.id === user.id) + if (index === -1) { + users.set([...$users, user]) + } else { + users.update(state => { + state[index] = user + return state.slice() + }) + } + } + + const selectedCellMap = derived([users, userId], ([$users, $userId]) => { + let map = {} + $users.forEach(user => { + if (user.selectedCellId && user.id !== $userId) { + map[user.selectedCellId] = user + } + }) + console.log(map) + return map + }) + + return { + users: { + ...users, + actions: { + updateUser, + }, + }, + selectedCellMap, + userId, + } +} diff --git a/packages/frontend-core/src/components/sheet/websocket.js b/packages/frontend-core/src/components/sheet/websocket.js index 254619b36d..7df00bd6d9 100644 --- a/packages/frontend-core/src/components/sheet/websocket.js +++ b/packages/frontend-core/src/components/sheet/websocket.js @@ -2,7 +2,7 @@ import { derived, get } from "svelte/store" import { io } from "socket.io-client" export const createWebsocket = context => { - const { rows, config } = context + const { rows, config, users, userId, selectedCellId } = context const tableId = derived(config, $config => $config.tableId) // Determine connection info @@ -28,9 +28,11 @@ export const createWebsocket = context => { console.log("Idenifying dataspace", tableId) // Identify which dataspace we are editing - socket.emit("identify", tableId, response => { + socket.emit("select-dataspace", tableId, response => { // handle initial connection info console.log("response", response) + users.set(response.users) + userId.set(response.id) }) } @@ -40,10 +42,15 @@ export const createWebsocket = context => { }) socket.on("row-update", data => { + console.log("row-update:", data.id) if (data.id) { rows.actions.refreshRow(data.id) } - console.log(data) + }) + + socket.on("user-update", user => { + console.log("user-update", user) + users.actions.updateUser(user) }) socket.on("connect_error", err => { @@ -53,5 +60,10 @@ export const createWebsocket = context => { // Change websocket connection when dataspace changes tableId.subscribe(connectToDataspace) + // Notify selected cell changes + selectedCellId.subscribe($selectedCellId => { + socket.emit("select-cell", $selectedCellId) + }) + return () => socket?.disconnect() } diff --git a/packages/server/src/websockets/dataspace.ts b/packages/server/src/websockets/dataspace.ts index e5cd73b8f4..7c4ff38c90 100644 --- a/packages/server/src/websockets/dataspace.ts +++ b/packages/server/src/websockets/dataspace.ts @@ -10,14 +10,31 @@ export default class DataspaceSocket extends Socket { this.io.on("connection", socket => { const user = socket.data.user - console.log(`Dataspace user connected: ${user?._id}`) + console.log(`Dataspace user connected: ${user?.id}`) + + // Socket state + let currentRoom: string // Initial identification of conneted dataspace - socket.on("identify", async (tableId, callback) => { + socket.on("select-dataspace", async (tableId, callback) => { + if (currentRoom) { + socket.leave(currentRoom) + } socket.join(tableId) - + currentRoom = tableId const sockets = await this.io.in(tableId).fetchSockets() - callback(sockets.map(socket => socket.data.user)) + socket.broadcast.emit("user-update", socket.data.user) + + callback({ + users: sockets.map(socket => socket.data.user), + id: user.id, + }) + }) + + socket.on("select-cell", cellId => { + console.log("cell update for " + user.id + " to " + cellId) + socket.data.user.selectedCellId = cellId + socket.broadcast.emit("user-update", socket.data.user) }) // Disconnection cleanup diff --git a/packages/server/src/websockets/websocket.ts b/packages/server/src/websockets/websocket.ts index 085d583f48..c58419748c 100644 --- a/packages/server/src/websockets/websocket.ts +++ b/packages/server/src/websockets/websocket.ts @@ -55,7 +55,11 @@ export default class Socket { // Middlewares are finished. // Extract some data from our enriched koa context to persist // as metadata for the socket - socket.data.user = ctx.user + socket.data.user = { + id: ctx.user._id, + email: ctx.user.email, + selectedCellId: null, + } next() } }) From 4647e1bc07843c74cd7703a5dd9a9b0634b2d4c1 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 6 Mar 2023 11:20:47 +0000 Subject: [PATCH 074/235] Fix issues with not disconnecting users when swapping datasource and improve multi-user UI --- .../backend/DataTable/DataTable.svelte | 53 +++---------- .../src/components/sheet/Avatar.svelte | 24 ++++++ .../src/components/sheet/DeleteButton.svelte | 2 +- .../src/components/sheet/HeaderRow.svelte | 2 +- .../src/components/sheet/NewRow.svelte | 2 +- .../src/components/sheet/ResizeOverlay.svelte | 2 +- .../src/components/sheet/ScrollOverlay.svelte | 2 +- .../src/components/sheet/Sheet.svelte | 42 ++++++++-- .../src/components/sheet/SheetBody.svelte | 7 +- .../src/components/sheet/SheetCell.svelte | 56 ++++++++++--- .../src/components/sheet/SheetControls.svelte | 2 +- .../src/components/sheet/SheetRow.svelte | 4 +- .../sheet/SheetScrollWrapper.svelte | 2 +- .../src/components/sheet/StickyColumn.svelte | 7 +- .../src/components/sheet/UserAvatars.svelte | 21 +++++ .../components/sheet/cells/OptionsCell.svelte | 3 +- .../src/components/sheet/stores/users.js | 79 ++++++++++++++++--- .../src/components/sheet/utils.js | 4 +- .../src/components/sheet/websocket.js | 12 ++- packages/server/src/websockets/dataspace.ts | 27 ++++--- 20 files changed, 249 insertions(+), 104 deletions(-) create mode 100644 packages/frontend-core/src/components/sheet/Avatar.svelte create mode 100644 packages/frontend-core/src/components/sheet/UserAvatars.svelte diff --git a/packages/builder/src/components/backend/DataTable/DataTable.svelte b/packages/builder/src/components/backend/DataTable/DataTable.svelte index 8c9690fa8b..ced118d5e2 100644 --- a/packages/builder/src/components/backend/DataTable/DataTable.svelte +++ b/packages/builder/src/components/backend/DataTable/DataTable.svelte @@ -106,8 +106,13 @@
-
-
+ + {/if} -
-
{#if isUsersTable} @@ -157,16 +160,8 @@ tableId={id} /> {/key} -
-
-
- -
+ +
@@ -234,34 +229,4 @@ flex-direction: column; background: var(--background); } - .sheet { - flex: 1 1 auto; - display: flex; - flex-direction: column; - } - .pagination { - display: flex; - flex-direction: row; - justify-content: flex-end; - align-items: center; - margin-top: var(--spacing-xl); - } - .buttons { - flex: 0 0 48px; - border-bottom: 2px solid var(--spectrum-global-color-gray-200); - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - flex-wrap: wrap; - padding: 0 8px; - } - .left-buttons, - .right-buttons { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: var(--spacing-m); - } diff --git a/packages/frontend-core/src/components/sheet/Avatar.svelte b/packages/frontend-core/src/components/sheet/Avatar.svelte new file mode 100644 index 0000000000..e3eec9f830 --- /dev/null +++ b/packages/frontend-core/src/components/sheet/Avatar.svelte @@ -0,0 +1,24 @@ + + +
+ {user.email[0]} +
+ + diff --git a/packages/frontend-core/src/components/sheet/DeleteButton.svelte b/packages/frontend-core/src/components/sheet/DeleteButton.svelte index 26b9d6cfa6..b27a750a68 100644 --- a/packages/frontend-core/src/components/sheet/DeleteButton.svelte +++ b/packages/frontend-core/src/components/sheet/DeleteButton.svelte @@ -2,7 +2,7 @@ import { Modal, ModalContent, Button } from "@budibase/bbui" import { getContext } from "svelte" - const { selectedRows, rows } = getContext("spreadsheet") + const { selectedRows, rows } = getContext("sheet") let modal diff --git a/packages/frontend-core/src/components/sheet/HeaderRow.svelte b/packages/frontend-core/src/components/sheet/HeaderRow.svelte index 6e1e799ec3..553ee6430a 100644 --- a/packages/frontend-core/src/components/sheet/HeaderRow.svelte +++ b/packages/frontend-core/src/components/sheet/HeaderRow.svelte @@ -5,7 +5,7 @@ import { getIconForField } from "./utils" import SheetScrollWrapper from "./SheetScrollWrapper.svelte" - const { visibleColumns, reorder } = getContext("spreadsheet") + const { visibleColumns, reorder } = getContext("sheet")
diff --git a/packages/frontend-core/src/components/sheet/NewRow.svelte b/packages/frontend-core/src/components/sheet/NewRow.svelte index bb52767705..7d01f6010c 100644 --- a/packages/frontend-core/src/components/sheet/NewRow.svelte +++ b/packages/frontend-core/src/components/sheet/NewRow.svelte @@ -3,7 +3,7 @@ import { getContext } from "svelte" const { visibleColumns, hoveredRowId, rows, selectedCellId, reorder } = - getContext("spreadsheet") + getContext("sheet") $: rowHovered = $hoveredRowId === "new" diff --git a/packages/frontend-core/src/components/sheet/ResizeOverlay.svelte b/packages/frontend-core/src/components/sheet/ResizeOverlay.svelte index f38038fdd4..3bf3ea35a3 100644 --- a/packages/frontend-core/src/components/sheet/ResizeOverlay.svelte +++ b/packages/frontend-core/src/components/sheet/ResizeOverlay.svelte @@ -9,7 +9,7 @@ visibleColumns, cellHeight, stickyColumn, - } = getContext("spreadsheet") + } = getContext("sheet") const MinColumnWidth = 100 let initialMouseX = null diff --git a/packages/frontend-core/src/components/sheet/ScrollOverlay.svelte b/packages/frontend-core/src/components/sheet/ScrollOverlay.svelte index 12030ff4a5..79b663f385 100644 --- a/packages/frontend-core/src/components/sheet/ScrollOverlay.svelte +++ b/packages/frontend-core/src/components/sheet/ScrollOverlay.svelte @@ -11,7 +11,7 @@ maxScrollTop, contentWidth, maxScrollLeft, - } = getContext("spreadsheet") + } = getContext("sheet") // Bar config const barOffset = 4 diff --git a/packages/frontend-core/src/components/sheet/Sheet.svelte b/packages/frontend-core/src/components/sheet/Sheet.svelte index bacdd27f69..3df2d27287 100644 --- a/packages/frontend-core/src/components/sheet/Sheet.svelte +++ b/packages/frontend-core/src/components/sheet/Sheet.svelte @@ -1,6 +1,7 @@
- +
+
+ +
+
+ +
+
@@ -128,4 +134,24 @@ flex-direction: column; align-self: stretch; } + + /* Controls */ + .controls { + height: var(--controls-height); + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + border-bottom: 2px solid var(--spectrum-global-color-gray-200); + padding: var(--cell-padding); + gap: var(--cell-spacing); + } + .controls-left, + .controls-right { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: var(--cell-spacing); + } diff --git a/packages/frontend-core/src/components/sheet/SheetBody.svelte b/packages/frontend-core/src/components/sheet/SheetBody.svelte index b636f4eabe..586aaafb2f 100644 --- a/packages/frontend-core/src/components/sheet/SheetBody.svelte +++ b/packages/frontend-core/src/components/sheet/SheetBody.svelte @@ -4,8 +4,7 @@ import NewRow from "./NewRow.svelte" import SheetRow from "./SheetRow.svelte" - const { selectedCellId, bounds, visibleRows, config } = - getContext("spreadsheet") + const { selectedCellId, bounds, visibleRows, config } = getContext("sheet") let ref @@ -27,8 +26,8 @@ on:click|self={() => ($selectedCellId = null)} > - {#each $visibleRows as row} - + {#each $visibleRows as row, idx} + {/each} {#if $config.allowAddRows} diff --git a/packages/frontend-core/src/components/sheet/SheetCell.svelte b/packages/frontend-core/src/components/sheet/SheetCell.svelte index 821ef30b65..4527acf900 100644 --- a/packages/frontend-core/src/components/sheet/SheetCell.svelte +++ b/packages/frontend-core/src/components/sheet/SheetCell.svelte @@ -1,4 +1,6 @@
- {#if selectedUser} -
- {selectedUser.email} + + {#if selectedUser && !selected} +
+ {selectedUser.label} wwwwwwwwwwwwwwwwwwwwaaaaaa
{/if} -
diff --git a/packages/frontend-core/src/components/sheet/SheetControls.svelte b/packages/frontend-core/src/components/sheet/SheetControls.svelte index f994a2654a..f19b14b2d1 100644 --- a/packages/frontend-core/src/components/sheet/SheetControls.svelte +++ b/packages/frontend-core/src/components/sheet/SheetControls.svelte @@ -2,7 +2,7 @@ import { getContext } from "svelte" import { ActionButton } from "@budibase/bbui" - const { rows } = getContext("spreadsheet") + const { rows } = getContext("sheet") $: rowCount = $rows.length diff --git a/packages/frontend-core/src/components/sheet/SheetRow.svelte b/packages/frontend-core/src/components/sheet/SheetRow.svelte index 920cd48c0c..3447fc5587 100644 --- a/packages/frontend-core/src/components/sheet/SheetRow.svelte +++ b/packages/frontend-core/src/components/sheet/SheetRow.svelte @@ -4,6 +4,7 @@ import { getCellRenderer } from "./renderers" export let row + export let idx const { selectedCellId, @@ -13,7 +14,7 @@ visibleColumns, hoveredRowId, selectedCellMap, - } = getContext("spreadsheet") + } = getContext("sheet") $: rowSelected = !!$selectedRows[row._id] $: rowHovered = $hoveredRowId === row._id @@ -30,6 +31,7 @@ ($hoveredRowId = null)}> - {#each $visibleRows as row} + {#each $visibleRows as row, idx} {@const rowSelected = !!$selectedRows[row._id]} {@const rowHovered = $hoveredRowId === row._id}
($hoveredRowId = row._id)}> @@ -123,8 +124,10 @@ ($selectedCellId = cellIdx)} width={$stickyColumn.width} reorderTarget={$reorder.targetColumn === $stickyColumn.name} diff --git a/packages/frontend-core/src/components/sheet/UserAvatars.svelte b/packages/frontend-core/src/components/sheet/UserAvatars.svelte new file mode 100644 index 0000000000..8d8f80c9b7 --- /dev/null +++ b/packages/frontend-core/src/components/sheet/UserAvatars.svelte @@ -0,0 +1,21 @@ + + +
+ {#each $users as user} + + {/each} +
+ + diff --git a/packages/frontend-core/src/components/sheet/cells/OptionsCell.svelte b/packages/frontend-core/src/components/sheet/cells/OptionsCell.svelte index f3112a1252..64456fe0f2 100644 --- a/packages/frontend-core/src/components/sheet/cells/OptionsCell.svelte +++ b/packages/frontend-core/src/components/sheet/cells/OptionsCell.svelte @@ -138,8 +138,9 @@ user-select: none; } .arrow { + border-right: 2px solid var(--spectrum-global-color-blue-400); position: absolute; - right: 2px; + right: 0; top: 2px; bottom: 2px; padding: 0 6px 0 16px; diff --git a/packages/frontend-core/src/components/sheet/stores/users.js b/packages/frontend-core/src/components/sheet/stores/users.js index d79b74899d..a52ef2b00d 100644 --- a/packages/frontend-core/src/components/sheet/stores/users.js +++ b/packages/frontend-core/src/components/sheet/stores/users.js @@ -4,6 +4,52 @@ export const createUserStores = () => { const users = writable([]) const userId = writable(null) + // Enrich users with unique colours + const enrichedUsers = derived([users, userId], ([$users, $userId]) => { + return ( + $users + .slice() + // Place current user first + .sort((a, b) => { + if (a.id === $userId) { + return -1 + } else if (b.id === $userId) { + return 1 + } else { + return 0 + } + }) + // Enrich users with colors + .map((user, idx) => { + // Generate random colour hue + let hue = 1 + for (let i = 0; i < user.email.length && i < 5; i++) { + hue *= user.email.charCodeAt(i) + } + hue = hue % 360 + const color = + idx === 0 + ? "var(--spectrum-global-color-blue-400)" + : `hsl(${hue}, 50%, 40%)` + + // Generate friendly label + let label = user.email + if (user.firstName) { + label = user.firstName + if (user.lastName) { + label += ` ${user.lastName}` + } + } + + return { + ...user, + color, + label, + } + }) + ) + }) + const updateUser = user => { const $users = get(users) const index = $users.findIndex(x => x.id === user.id) @@ -17,22 +63,35 @@ export const createUserStores = () => { } } - const selectedCellMap = derived([users, userId], ([$users, $userId]) => { - let map = {} - $users.forEach(user => { - if (user.selectedCellId && user.id !== $userId) { - map[user.selectedCellId] = user - } + const removeUser = user => { + users.update(state => { + return state.filter(x => x.id !== user.id) }) - console.log(map) - return map - }) + } + + // Generate a lookup map of cell ID to the user that has it selected, to make + // lookups inside sheet cells extremely fast + const selectedCellMap = derived( + [enrichedUsers, userId], + ([$enrichedUsers, $userId]) => { + let map = {} + $enrichedUsers.forEach(user => { + if (user.selectedCellId && user.id !== $userId) { + map[user.selectedCellId] = user + } + }) + return map + } + ) return { users: { - ...users, + ...enrichedUsers, + set: users.set, + update: users.update, actions: { updateUser, + removeUser, }, }, selectedCellMap, diff --git a/packages/frontend-core/src/components/sheet/utils.js b/packages/frontend-core/src/components/sheet/utils.js index 1a959b3289..996465a6ca 100644 --- a/packages/frontend-core/src/components/sheet/utils.js +++ b/packages/frontend-core/src/components/sheet/utils.js @@ -1,8 +1,8 @@ -export const getColor = idx => { +export const getColor = (idx, opacity = 0.3) => { if (idx == null || idx === -1) { return null } - return `hsla(${((idx + 1) * 222) % 360}, 90%, 75%, 0.3)` + return `hsla(${((idx + 1) * 222) % 360}, 90%, 75%, ${opacity})` } export const getIconForField = field => { diff --git a/packages/frontend-core/src/components/sheet/websocket.js b/packages/frontend-core/src/components/sheet/websocket.js index 7df00bd6d9..fad716d667 100644 --- a/packages/frontend-core/src/components/sheet/websocket.js +++ b/packages/frontend-core/src/components/sheet/websocket.js @@ -25,12 +25,11 @@ export const createWebsocket = context => { if (!socket.connected) { return } - console.log("Idenifying dataspace", tableId) + console.log("Identifying dataspace", tableId) // Identify which dataspace we are editing socket.emit("select-dataspace", tableId, response => { // handle initial connection info - console.log("response", response) users.set(response.users) userId.set(response.id) }) @@ -42,17 +41,22 @@ export const createWebsocket = context => { }) socket.on("row-update", data => { - console.log("row-update:", data.id) + console.log("row-update", data.id) if (data.id) { rows.actions.refreshRow(data.id) } }) socket.on("user-update", user => { - console.log("user-update", user) + console.log("user-update", user.id) users.actions.updateUser(user) }) + socket.on("user-disconnect", user => { + console.log("user-disconnect", user.id) + users.actions.removeUser(user) + }) + socket.on("connect_error", err => { console.log("Failed to connect to websocket:", err.message) }) diff --git a/packages/server/src/websockets/dataspace.ts b/packages/server/src/websockets/dataspace.ts index 7c4ff38c90..ca245f7c06 100644 --- a/packages/server/src/websockets/dataspace.ts +++ b/packages/server/src/websockets/dataspace.ts @@ -15,31 +15,40 @@ export default class DataspaceSocket extends Socket { // Socket state let currentRoom: string - // Initial identification of conneted dataspace + // Initial identification of connected dataspace socket.on("select-dataspace", async (tableId, callback) => { + // Leave current room if (currentRoom) { + socket.to(currentRoom).emit("user-disconnect", socket.data.user) socket.leave(currentRoom) } - socket.join(tableId) - currentRoom = tableId - const sockets = await this.io.in(tableId).fetchSockets() - socket.broadcast.emit("user-update", socket.data.user) + // Join new room + currentRoom = tableId + socket.join(currentRoom) + socket.to(currentRoom).emit("user-update", socket.data.user) + + // Reply with all users in current room + const sockets = await this.io.in(currentRoom).fetchSockets() callback({ users: sockets.map(socket => socket.data.user), id: user.id, }) }) + // Handle users selecting a new cell socket.on("select-cell", cellId => { - console.log("cell update for " + user.id + " to " + cellId) socket.data.user.selectedCellId = cellId - socket.broadcast.emit("user-update", socket.data.user) + if (currentRoom) { + socket.to(currentRoom).emit("user-update", socket.data.user) + } }) // Disconnection cleanup - socket.on("disconnect", reason => { - console.log(`Disconnecting ${user.email} because of ${reason}`) + socket.on("disconnect", () => { + if (currentRoom) { + socket.to(currentRoom).emit("user-disconnect", socket.data.user) + } }) }) } From d15b1748ef9e4394cdfb0b4b12d3efb1f0ab62ba Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 6 Mar 2023 11:30:38 +0000 Subject: [PATCH 075/235] Update layout and remove logging --- .../frontend-core/src/components/sheet/RowCount.svelte | 9 +++++++++ packages/frontend-core/src/components/sheet/Sheet.svelte | 1 + .../frontend-core/src/components/sheet/SheetCell.svelte | 3 +-- packages/frontend-core/src/components/sheet/websocket.js | 9 --------- 4 files changed, 11 insertions(+), 11 deletions(-) create mode 100644 packages/frontend-core/src/components/sheet/RowCount.svelte diff --git a/packages/frontend-core/src/components/sheet/RowCount.svelte b/packages/frontend-core/src/components/sheet/RowCount.svelte new file mode 100644 index 0000000000..383e50a525 --- /dev/null +++ b/packages/frontend-core/src/components/sheet/RowCount.svelte @@ -0,0 +1,9 @@ + + +
+ {$rows.length} row{$rows.length === 1 ? "" : "s"} +
diff --git a/packages/frontend-core/src/components/sheet/Sheet.svelte b/packages/frontend-core/src/components/sheet/Sheet.svelte index 3df2d27287..0e5a4f5d1a 100644 --- a/packages/frontend-core/src/components/sheet/Sheet.svelte +++ b/packages/frontend-core/src/components/sheet/Sheet.svelte @@ -19,6 +19,7 @@ import ScrollOverlay from "./ScrollOverlay.svelte" import StickyColumn from "./StickyColumn.svelte" import UserAvatars from "./UserAvatars.svelte" + import RowCount from "./RowCount.svelte" export let API export let tableId diff --git a/packages/frontend-core/src/components/sheet/SheetCell.svelte b/packages/frontend-core/src/components/sheet/SheetCell.svelte index 4527acf900..a4140b6434 100644 --- a/packages/frontend-core/src/components/sheet/SheetCell.svelte +++ b/packages/frontend-core/src/components/sheet/SheetCell.svelte @@ -19,7 +19,6 @@ const getStyle = (width, selectedUser) => { let style = `flex: 0 0 ${width}px;` if (selectedUser) { - console.log(selectedUser) style += `--user-color:${selectedUser.color};` } return style @@ -47,7 +46,7 @@ {#if selectedUser && !selected}
- {selectedUser.label} wwwwwwwwwwwwwwwwwwwwaaaaaa + {selectedUser.label}
{/if}
diff --git a/packages/frontend-core/src/components/sheet/websocket.js b/packages/frontend-core/src/components/sheet/websocket.js index fad716d667..60d0df05ae 100644 --- a/packages/frontend-core/src/components/sheet/websocket.js +++ b/packages/frontend-core/src/components/sheet/websocket.js @@ -25,8 +25,6 @@ export const createWebsocket = context => { if (!socket.connected) { return } - console.log("Identifying dataspace", tableId) - // Identify which dataspace we are editing socket.emit("select-dataspace", tableId, response => { // handle initial connection info @@ -39,24 +37,17 @@ export const createWebsocket = context => { socket.on("connect", () => { connectToDataspace(get(tableId)) }) - socket.on("row-update", data => { - console.log("row-update", data.id) if (data.id) { rows.actions.refreshRow(data.id) } }) - socket.on("user-update", user => { - console.log("user-update", user.id) users.actions.updateUser(user) }) - socket.on("user-disconnect", user => { - console.log("user-disconnect", user.id) users.actions.removeUser(user) }) - socket.on("connect_error", err => { console.log("Failed to connect to websocket:", err.message) }) From 36e8664605a54e65661c79d20bf6f76750e149f5 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 6 Mar 2023 12:29:18 +0000 Subject: [PATCH 076/235] WIP column popovers for dataspace sheets --- .../src/components/sheet/HeaderRow.svelte | 24 +----- .../src/components/sheet/NewRow.svelte | 2 +- .../src/components/sheet/Sheet.svelte | 3 +- .../src/components/sheet/SheetRow.svelte | 2 +- .../src/components/sheet/StickyColumn.svelte | 2 +- .../components/sheet/cells/HeaderCell.svelte | 78 +++++++++++++++++++ .../sheet/{ => cells}/SheetCell.svelte | 22 ------ 7 files changed, 85 insertions(+), 48 deletions(-) create mode 100644 packages/frontend-core/src/components/sheet/cells/HeaderCell.svelte rename packages/frontend-core/src/components/sheet/{ => cells}/SheetCell.svelte (87%) diff --git a/packages/frontend-core/src/components/sheet/HeaderRow.svelte b/packages/frontend-core/src/components/sheet/HeaderRow.svelte index 553ee6430a..54129a9b63 100644 --- a/packages/frontend-core/src/components/sheet/HeaderRow.svelte +++ b/packages/frontend-core/src/components/sheet/HeaderRow.svelte @@ -1,34 +1,16 @@
{#each $visibleColumns as column} - reorder.actions.startReordering(column.name, e)} - width={column.width} - left={column.left} - > - - - {column.name} - - + {/each}
diff --git a/packages/frontend-core/src/components/sheet/NewRow.svelte b/packages/frontend-core/src/components/sheet/NewRow.svelte index 7d01f6010c..90af4cc958 100644 --- a/packages/frontend-core/src/components/sheet/NewRow.svelte +++ b/packages/frontend-core/src/components/sheet/NewRow.svelte @@ -1,5 +1,5 @@ + +
+ reorder.actions.startReordering(column.name, e)} + on:click={openPopover} + width={column.width} + left={column.left} + > +
+ +
+ {column.name} asdasdasd asdasdas asdasdasd +
+
+ +
+
+
+
+ +asdsad asdasd asdasd asasa + + diff --git a/packages/frontend-core/src/components/sheet/SheetCell.svelte b/packages/frontend-core/src/components/sheet/cells/SheetCell.svelte similarity index 87% rename from packages/frontend-core/src/components/sheet/SheetCell.svelte rename to packages/frontend-core/src/components/sheet/cells/SheetCell.svelte index a4140b6434..e719087adb 100644 --- a/packages/frontend-core/src/components/sheet/SheetCell.svelte +++ b/packages/frontend-core/src/components/sheet/cells/SheetCell.svelte @@ -1,7 +1,4 @@ @@ -89,8 +80,7 @@ class="resize-slider sticky" class:visible={columnIdx === "sticky"} on:mousedown={e => startResizing("sticky", e)} - style="--left:{40 + - $stickyColumn.width}px; --content-height:{contentHeight}px;" + style="--left:{40 + $stickyColumn.width}px;" >
@@ -100,7 +90,7 @@ class="resize-slider" class:visible={columnIdx === col.idx} on:mousedown={e => startResizing(col.idx, e)} - style={getStyle(col, offset, scrollLeft, contentHeight)} + style={getStyle(col, offset, scrollLeft)} >
@@ -114,7 +104,7 @@ height: var(--cell-height); left: var(--left); opacity: 0; - padding: 0 16px; + padding: 0 8px; transform: translateX(-50%); user-select: none; } @@ -122,7 +112,6 @@ .resize-slider.visible { cursor: col-resize; opacity: 1; - height: min(var(--content-height), 100%); } .resize-slider.sticky { z-index: 2; diff --git a/packages/frontend-core/src/components/sheet/StickyColumn.svelte b/packages/frontend-core/src/components/sheet/StickyColumn.svelte index 084ec02284..3782bc3463 100644 --- a/packages/frontend-core/src/components/sheet/StickyColumn.svelte +++ b/packages/frontend-core/src/components/sheet/StickyColumn.svelte @@ -5,6 +5,7 @@ import SheetCell from "./cells/SheetCell.svelte" import { getCellRenderer } from "./renderers" import SheetScrollWrapper from "./SheetScrollWrapper.svelte" + import HeaderCell from "./cells/HeaderCell.svelte" const { rows, @@ -67,6 +68,7 @@ {#if $stickyColumn} - - - - {$stickyColumn.name} - - + {/if}
diff --git a/packages/frontend-core/src/components/sheet/cells/HeaderCell.svelte b/packages/frontend-core/src/components/sheet/cells/HeaderCell.svelte index 4bf4e1a461..793f7979cf 100644 --- a/packages/frontend-core/src/components/sheet/cells/HeaderCell.svelte +++ b/packages/frontend-core/src/components/sheet/cells/HeaderCell.svelte @@ -1,56 +1,92 @@ -
+
reorder.actions.startReordering(column.name, e)} - on:click={openPopover} + on:mousedown={startReordering} + on:mouseup={stopReordering} + on:click={onClick} width={column.width} left={column.left} > -
- -
- {column.name} asdasdasd asdasdas asdasdasd -
-
- -
+ +
+ {column.name} +
+
+
-asdsad asdasd asdasd asasa + + Edit column + Sort ascending + Sort descending + Move left + Move right + Delete + + diff --git a/packages/frontend-core/src/components/sheet/cells/SheetCell.svelte b/packages/frontend-core/src/components/sheet/cells/SheetCell.svelte index e719087adb..8c09891a9d 100644 --- a/packages/frontend-core/src/components/sheet/cells/SheetCell.svelte +++ b/packages/frontend-core/src/components/sheet/cells/SheetCell.svelte @@ -10,6 +10,7 @@ export let center = false export let selectedUser = null export let rowIdx + export let foo $: style = getStyle(width, selectedUser) @@ -24,6 +25,7 @@
@@ -113,6 +116,9 @@ justify-content: center; align-items: center; } + .cell.foo { + background: var(--spectrum-global-color-gray-100); + } /* Other user email */ .user { diff --git a/packages/frontend-core/src/components/sheet/stores/reorder.js b/packages/frontend-core/src/components/sheet/stores/reorder.js index 76b82ba79a..13a89c736d 100644 --- a/packages/frontend-core/src/components/sheet/stores/reorder.js +++ b/packages/frontend-core/src/components/sheet/stores/reorder.js @@ -1,4 +1,4 @@ -import { get, writable } from "svelte/store" +import { get, writable, derived } from "svelte/store" export const createReorderStores = context => { const { columns, rand, scroll, bounds, stickyColumn } = context @@ -7,8 +7,11 @@ export const createReorderStores = context => { targetColumn: null, breakpoints: [], initialMouseX: null, + scrollLeft: 0, + sheetLeft: 0, } const reorder = writable(reorderInitialState) + const isReordering = derived(reorder, $reorder => !!$reorder.sourceColumn) // Callback when dragging on a colum header and starting reordering const startReordering = (column, e) => { @@ -115,5 +118,6 @@ export const createReorderStores = context => { stopReordering, }, }, + isReordering, } } diff --git a/packages/server/src/websockets/websocket.ts b/packages/server/src/websockets/websocket.ts index c58419748c..d3c1a7a8b3 100644 --- a/packages/server/src/websockets/websocket.ts +++ b/packages/server/src/websockets/websocket.ts @@ -4,6 +4,7 @@ import Koa from "koa" import Cookies from "cookies" import { userAgent } from "koa-useragent" import { auth } from "@budibase/backend-core" +import currentApp from "../middleware/currentapp" export default class Socket { io: Server @@ -25,6 +26,7 @@ export default class Socket { const middlewares = [ userAgent, authenticate, + // currentApp, ...(additionalMiddlewares || []), ] From b5a72438e127e0d573288160b8652b5e686b9d21 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 6 Mar 2023 15:15:00 +0000 Subject: [PATCH 078/235] Tidy reordering stuff --- .../src/components/sheet/ResizeOverlay.svelte | 42 ++++++++++--------- .../src/components/sheet/StickyColumn.svelte | 2 +- .../components/sheet/cells/HeaderCell.svelte | 11 +++-- .../components/sheet/cells/SheetCell.svelte | 3 -- .../src/components/sheet/stores/reorder.js | 2 - .../src/components/sheet/stores/resize.js | 0 6 files changed, 30 insertions(+), 30 deletions(-) create mode 100644 packages/frontend-core/src/components/sheet/stores/resize.js diff --git a/packages/frontend-core/src/components/sheet/ResizeOverlay.svelte b/packages/frontend-core/src/components/sheet/ResizeOverlay.svelte index 85d69a6f97..3a3a0c462c 100644 --- a/packages/frontend-core/src/components/sheet/ResizeOverlay.svelte +++ b/packages/frontend-core/src/components/sheet/ResizeOverlay.svelte @@ -1,7 +1,7 @@ -{#if $stickyColumn} -
startResizing("sticky", e)} - style="--left:{40 + $stickyColumn.width}px;" - > -
-
+{#if !$isReordering} + {#if $stickyColumn} +
startResizing("sticky", e)} + style="--left:{40 + $stickyColumn.width}px;" + > +
+
+ {/if} + {#each $visibleColumns as col} +
startResizing(col.idx, e)} + style={getStyle(col, offset, scrollLeft)} + > +
+
+ {/each} {/if} -{#each $visibleColumns as col} -
startResizing(col.idx, e)} - style={getStyle(col, offset, scrollLeft)} - > -
-
-{/each} diff --git a/packages/frontend-core/src/components/sheet/Sheet.svelte b/packages/frontend-core/src/components/sheet/Sheet.svelte index 520a479ad5..cd0b2316c0 100644 --- a/packages/frontend-core/src/components/sheet/Sheet.svelte +++ b/packages/frontend-core/src/components/sheet/Sheet.svelte @@ -12,6 +12,7 @@ export { createUserStores } from "./stores/users" import { createWebsocket } from "./websocket" import { createUserStores } from "./stores/users" + import { createResizeStores } from "./stores/resize" import DeleteButton from "./DeleteButton.svelte" import SheetBody from "./SheetBody.svelte" import ResizeOverlay from "./ResizeOverlay.svelte" @@ -50,6 +51,7 @@ } context = { ...context, ...createRowsStore(context) } context = { ...context, ...createColumnsStores(context) } + context = { ...context, ...createResizeStores(context) } context = { ...context, ...createBoundsStores(context) } context = { ...context, ...createScrollStores(context) } context = { ...context, ...createViewportStores(context) } @@ -57,6 +59,9 @@ context = { ...context, ...createInterfaceStores(context) } context = { ...context, ...createUserStores(context) } + // Reference some stores for local use + const isResizing = context.isResizing + // Keep config store up to date $: config.set({ tableId, @@ -72,7 +77,12 @@ onMount(() => createWebsocket(context)) -
+
@@ -116,6 +126,9 @@ .sheet :global(*) { box-sizing: border-box; } + .sheet.is-resizing :global(*) { + cursor: col-resize !important; + } .sheet-data { flex: 1 1 auto; diff --git a/packages/frontend-core/src/components/sheet/cells/HeaderCell.svelte b/packages/frontend-core/src/components/sheet/cells/HeaderCell.svelte index 6566c86bc4..770aa91d34 100644 --- a/packages/frontend-core/src/components/sheet/cells/HeaderCell.svelte +++ b/packages/frontend-core/src/components/sheet/cells/HeaderCell.svelte @@ -7,7 +7,7 @@ export let column export let orderable = true - const { reorder, isReordering, rand } = getContext("sheet") + const { reorder, isReordering, isResizing, rand } = getContext("sheet") let timeout let anchor @@ -39,7 +39,7 @@ class:open style="flex: 0 0 {column.width}px;" bind:this={anchor} - class:disabled={$isReordering} + class:disabled={$isReordering || $isResizing} > { width: same ? existingWidth : defaultWidth, left: 40, schema: primaryDisplay[1], + idx: "sticky", }) }) diff --git a/packages/frontend-core/src/components/sheet/stores/resize.js b/packages/frontend-core/src/components/sheet/stores/resize.js index e69de29bb2..6ffb51b886 100644 --- a/packages/frontend-core/src/components/sheet/stores/resize.js +++ b/packages/frontend-core/src/components/sheet/stores/resize.js @@ -0,0 +1,86 @@ +import { writable, get, derived } from "svelte/store" + +export const createResizeStores = context => { + const { columns, stickyColumn } = context + const initialState = { + initialMouseX: null, + initialWidth: null, + columnIdx: null, + width: 0, + left: 0, + } + const resize = writable(initialState) + const isResizing = derived(resize, $resize => $resize.columnIdx != null) + const MinColumnWidth = 100 + + // Starts resizing a certain column + const startResizing = (column, e) => { + // Prevent propagation to stop reordering triggering + e.stopPropagation() + + resize.set({ + width: column.width, + left: column.left, + initialWidth: column.width, + initialMouseX: e.clientX, + columnIdx: column.idx, + }) + + // Add mouse event listeners to handle resizing + document.addEventListener("mousemove", onResizeMouseMove) + document.addEventListener("mouseup", stopResizing) + } + + // Handler for moving the mouse to resize columns + const onResizeMouseMove = e => { + const { initialMouseX, initialWidth, width, columnIdx } = get(resize) + const dx = e.clientX - initialMouseX + const newWidth = Math.round(Math.max(MinColumnWidth, initialWidth + dx)) + + // Ignore small changes + if (Math.abs(width - newWidth) < 5) { + return + } + + // Update column state + if (columnIdx === "sticky") { + stickyColumn.update(state => ({ + ...state, + width: newWidth, + })) + } else { + columns.update(state => { + state[columnIdx].width = newWidth + let offset = state[columnIdx].left + newWidth + for (let i = columnIdx + 1; i < state.length; i++) { + state[i].left = offset + offset += state[i].width + } + return [...state] + }) + } + + // Update state + resize.update(state => ({ + ...state, + width: newWidth, + })) + } + + // Stop resizing any columns + const stopResizing = () => { + resize.set(initialState) + document.removeEventListener("mousemove", onResizeMouseMove) + document.removeEventListener("mouseup", stopResizing) + } + + return { + resize: { + ...resize, + actions: { + startResizing, + }, + }, + isResizing, + } +} From 9579c9c0d2690362d1574c8cc53365d429ea51f2 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 7 Mar 2023 08:15:27 +0000 Subject: [PATCH 080/235] Add column sorting and reordering via popover --- .../src/components/sheet/Sheet.svelte | 6 +- .../components/sheet/cells/HeaderCell.svelte | 98 +++++++++++++------ .../src/components/sheet/stores/columns.js | 31 ++++-- .../src/components/sheet/stores/reorder.js | 44 ++++++--- .../src/components/sheet/stores/resize.js | 5 - .../src/components/sheet/stores/rows.js | 11 ++- .../src/components/sheet/utils.js | 2 +- 7 files changed, 136 insertions(+), 61 deletions(-) diff --git a/packages/frontend-core/src/components/sheet/Sheet.svelte b/packages/frontend-core/src/components/sheet/Sheet.svelte index cd0b2316c0..553bafd2ad 100644 --- a/packages/frontend-core/src/components/sheet/Sheet.svelte +++ b/packages/frontend-core/src/components/sheet/Sheet.svelte @@ -60,7 +60,7 @@ context = { ...context, ...createUserStores(context) } // Reference some stores for local use - const isResizing = context.isResizing + const { isResizing, isReordering } = context // Keep config store up to date $: config.set({ @@ -80,6 +80,7 @@
@@ -129,6 +130,9 @@ .sheet.is-resizing :global(*) { cursor: col-resize !important; } + .sheet.is-reordering :global(*) { + cursor: grabbing !important; + } .sheet-data { flex: 1 1 auto; diff --git a/packages/frontend-core/src/components/sheet/cells/HeaderCell.svelte b/packages/frontend-core/src/components/sheet/cells/HeaderCell.svelte index 770aa91d34..2780d73a9c 100644 --- a/packages/frontend-core/src/components/sheet/cells/HeaderCell.svelte +++ b/packages/frontend-core/src/components/sheet/cells/HeaderCell.svelte @@ -7,30 +7,51 @@ export let column export let orderable = true - const { reorder, isReordering, isResizing, rand } = getContext("sheet") + const { reorder, isReordering, isResizing, rand, sort, columns } = + getContext("sheet") - let timeout let anchor let open = false - let isClick = true + let timeout + + $: sortedBy = column.name === $sort.column + $: canMoveLeft = orderable && column.idx > 0 + $: canMoveRight = orderable && column.idx < $columns.length - 1 const startReordering = e => { - isClick = true timeout = setTimeout(() => { - isClick = false reorder.actions.startReordering(column.name, e) - }, 250) + }, 200) } const stopReordering = () => { clearTimeout(timeout) } - const onClick = () => { - if (isClick) { - stopReordering() - open = true - } + const sortAscending = () => { + sort.set({ + column: column.name, + order: "ascending", + }) + open = false + } + + const sortDescending = () => { + sort.set({ + column: column.name, + order: "descending", + }) + open = false + } + + const moveLeft = () => { + reorder.actions.moveColumnLeft(column.name) + open = false + } + + const moveRight = () => { + reorder.actions.moveColumnRight(column.name) + open = false } @@ -40,26 +61,34 @@ style="flex: 0 0 {column.width}px;" bind:this={anchor} class:disabled={$isReordering || $isResizing} + class:sorted={sortedBy} >
{column.name}
-
- +
(open = true)} + > +
@@ -67,19 +96,25 @@ Edit column - Sort ascending - Sort descending - {#if orderable} - Move left - Move right - {/if} + + Sort ascending + + + Sort descending + + + Move left + + + Move right + Delete @@ -88,17 +123,15 @@ .header-cell { display: flex; } - .header-cell:not(.disabled):hover :global(.cell), - .header-cell:not(.disabled).open :global(.cell) { - cursor: pointer; - background: var(--spectrum-global-color-gray-200); - } + .header-cell :global(.cell) { background: var(--background); padding: 0 var(--cell-padding); gap: calc(2 * var(--cell-spacing)); border-bottom: none; } + .header-cell.sorted :global(.cell) { + } .name { flex: 1 1 auto; @@ -109,10 +142,13 @@ } .more { - display: none; + padding: 4px; + margin: 0 -4px; } - .header-cell:not(.disabled):hover .more, - .header-cell:not(.disabled).open .more { - display: block; + .more:hover { + cursor: pointer; + } + .more:hover :global(.spectrum-Icon) { + color: var(--spectrum-global-color-gray-800) !important; } diff --git a/packages/frontend-core/src/components/sheet/stores/columns.js b/packages/frontend-core/src/components/sheet/stores/columns.js index f500f7f2f5..b648f7dd32 100644 --- a/packages/frontend-core/src/components/sheet/stores/columns.js +++ b/packages/frontend-core/src/components/sheet/stores/columns.js @@ -1,4 +1,4 @@ -import { get, writable } from "svelte/store" +import { derived, get, writable } from "svelte/store" export const createColumnsStores = context => { const { schema } = context @@ -6,6 +6,21 @@ export const createColumnsStores = context => { const columns = writable([]) const stickyColumn = writable(null) + // Derive an enriched version of columns with left offsets and indexes + // automatically calculated + const enrichedColumns = derived(columns, $columns => { + let offset = 0 + return $columns.map((column, idx) => { + const enriched = { + ...column, + idx, + left: offset, + } + offset += column.width + return enriched + }) + }) + // Merge new schema fields with existing schema in order to preserve widths schema.subscribe($schema => { const currentColumns = get(columns) @@ -23,19 +38,14 @@ export const createColumnsStores = context => { }) // Update columns, removing extraneous columns and adding missing ones - let offset = 0 columns.set( - fields.map((field, idx) => { + fields.map(field => { const existing = currentColumns.find(x => x.name === field) - const newCol = { - idx, + return { name: field, width: existing?.width || defaultWidth, - left: offset, schema: $schema[field], } - offset += newCol.width - return newCol }) ) }) @@ -60,7 +70,10 @@ export const createColumnsStores = context => { }) return { - columns, + columns: { + ...columns, + subscribe: enrichedColumns.subscribe, + }, stickyColumn, } } diff --git a/packages/frontend-core/src/components/sheet/stores/reorder.js b/packages/frontend-core/src/components/sheet/stores/reorder.js index b0978de599..d547e14f69 100644 --- a/packages/frontend-core/src/components/sheet/stores/reorder.js +++ b/packages/frontend-core/src/components/sheet/stores/reorder.js @@ -77,27 +77,19 @@ export const createReorderStores = context => { // Callback when stopping reordering columns const stopReordering = () => { // Swap position of columns - const $columns = get(columns) let { sourceColumn, targetColumn } = get(reorder) + const $columns = get(columns) let sourceIdx = $columns.findIndex(x => x.name === sourceColumn) let targetIdx = $columns.findIndex(x => x.name === targetColumn) targetIdx++ + console.log(sourceIdx, targetIdx) columns.update(state => { const removed = state.splice(sourceIdx, 1) if (--targetIdx < sourceIdx) { targetIdx++ } state.splice(targetIdx, 0, removed[0]) - let offset = 0 - return state.map((col, idx) => { - const newCol = { - ...col, - idx, - left: offset, - } - offset += col.width - return newCol - }) + return state.slice() }) // Reset state @@ -108,12 +100,42 @@ export const createReorderStores = context => { document.removeEventListener("mouseup", stopReordering) } + const moveColumnLeft = 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() + }) + } + + const moveColumnRight = column => { + const $columns = get(columns) + const sourceIdx = $columns.findIndex(x => x.name === column) + if (sourceIdx === $columns.length - 1) { + return + } + columns.update(state => { + let tmp = state[sourceIdx] + state[sourceIdx] = state[sourceIdx + 1] + state[sourceIdx + 1] = tmp + return state.slice() + }) + } + return { reorder: { ...reorder, actions: { startReordering, stopReordering, + moveColumnLeft, + moveColumnRight, }, }, isReordering, diff --git a/packages/frontend-core/src/components/sheet/stores/resize.js b/packages/frontend-core/src/components/sheet/stores/resize.js index 6ffb51b886..0ca16dac13 100644 --- a/packages/frontend-core/src/components/sheet/stores/resize.js +++ b/packages/frontend-core/src/components/sheet/stores/resize.js @@ -51,11 +51,6 @@ export const createResizeStores = context => { } else { columns.update(state => { state[columnIdx].width = newWidth - let offset = state[columnIdx].left + newWidth - for (let i = columnIdx + 1; i < state.length; i++) { - state[i].left = offset - offset += state[i].width - } return [...state] }) } diff --git a/packages/frontend-core/src/components/sheet/stores/rows.js b/packages/frontend-core/src/components/sheet/stores/rows.js index 0b7bd01368..e5e8235c36 100644 --- a/packages/frontend-core/src/components/sheet/stores/rows.js +++ b/packages/frontend-core/src/components/sheet/stores/rows.js @@ -7,6 +7,10 @@ export const createRowsStore = context => { const { config, API } = context const tableId = derived(config, $config => $config.tableId) const filter = derived(config, $config => $config.filter) + const sort = writable({ + column: null, + order: "ascending", + }) // Flag for whether this is the first time loading our fetch let loaded = false @@ -20,7 +24,7 @@ export const createRowsStore = context => { // Local stores for managing fetching data const query = derived(filter, $filter => buildLuceneQuery($filter)) - const fetch = derived([tableId, query], ([$tableId, $query]) => { + const fetch = derived([tableId, query, sort], ([$tableId, $query, $sort]) => { if (!$tableId) { return null } @@ -35,8 +39,8 @@ export const createRowsStore = context => { tableId: $tableId, }, options: { - sortColumn: null, - sortOrder: null, + sortColumn: $sort.column, + sortOrder: $sort.order, query: $query, limit: 100, paginate: true, @@ -241,5 +245,6 @@ export const createRowsStore = context => { }, }, schema, + sort, } } diff --git a/packages/frontend-core/src/components/sheet/utils.js b/packages/frontend-core/src/components/sheet/utils.js index 996465a6ca..21f8d4a6e6 100644 --- a/packages/frontend-core/src/components/sheet/utils.js +++ b/packages/frontend-core/src/components/sheet/utils.js @@ -8,7 +8,7 @@ export const getColor = (idx, opacity = 0.3) => { export const getIconForField = field => { const type = field.schema.type if (type === "options") { - return "ChevronDown" + return "Dropdown" } else if (type === "datetime") { return "Date" } From f516011182ea7a626f880a60d964224e0b206c9c Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 7 Mar 2023 08:23:50 +0000 Subject: [PATCH 081/235] Handle context menu events in header cells --- .../components/sheet/cells/HeaderCell.svelte | 26 +++++++++++++------ .../components/sheet/cells/SheetCell.svelte | 1 + 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/packages/frontend-core/src/components/sheet/cells/HeaderCell.svelte b/packages/frontend-core/src/components/sheet/cells/HeaderCell.svelte index 2780d73a9c..5b113b4d48 100644 --- a/packages/frontend-core/src/components/sheet/cells/HeaderCell.svelte +++ b/packages/frontend-core/src/components/sheet/cells/HeaderCell.svelte @@ -18,14 +18,23 @@ $: canMoveLeft = orderable && column.idx > 0 $: canMoveRight = orderable && column.idx < $columns.length - 1 - const startReordering = e => { - timeout = setTimeout(() => { - reorder.actions.startReordering(column.name, e) - }, 200) + const onMouseDown = e => { + if (e.button === 0 && orderable) { + timeout = setTimeout(() => { + reorder.actions.startReordering(column.name, e) + }, 200) + } } - const stopReordering = () => { - clearTimeout(timeout) + const onMouseUp = e => { + if (e.button === 0 && orderable) { + clearTimeout(timeout) + } + } + + const onContextMenu = e => { + e.preventDefault() + open = true } const sortAscending = () => { @@ -66,8 +75,9 @@ diff --git a/packages/frontend-core/src/components/sheet/cells/SheetCell.svelte b/packages/frontend-core/src/components/sheet/cells/SheetCell.svelte index 39282c821b..7764022f48 100644 --- a/packages/frontend-core/src/components/sheet/cells/SheetCell.svelte +++ b/packages/frontend-core/src/components/sheet/cells/SheetCell.svelte @@ -39,6 +39,7 @@ on:mousedown on:mouseup on:click + on:contextmenu {style} data-row={rowIdx} > From ca92d520b3eb484f6963715b216b3c57f5d5d78c Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 7 Mar 2023 11:40:32 +0000 Subject: [PATCH 082/235] Fully integrates sheets with datasection and remove lots of old stuff --- .../backend/DataTable/DataTable.svelte | 222 ++---------------- .../buttons/CreateColumnButton.svelte | 2 + .../buttons/ExistingRelationshipButton.svelte | 16 +- .../sheet/SheetCreateColumnButton.svelte | 18 ++ .../buttons/sheet/SheetCreateRowButton.svelte | 17 ++ .../sheet/SheetCreateViewButton.svelte | 10 + .../buttons/sheet/SheetExportButton.svelte | 21 ++ .../buttons/sheet/SheetFilterButton.svelte | 20 ++ .../buttons/sheet/SheetImportButton.svelte | 12 + .../sheet/SheetManageAccessButton.svelte | 8 + .../sheet/SheetRelationshipButton.svelte | 13 + .../modals/sheet/SheetEditColumnModal.svelte | 29 +++ .../src/components/sheet/Sheet.svelte | 12 +- .../components/sheet/cells/HeaderCell.svelte | 10 +- .../src/components/sheet/events.js | 29 +++ .../src/components/sheet/stores/rows.js | 53 ++++- packages/frontend-core/src/fetch/DataFetch.js | 21 ++ 17 files changed, 289 insertions(+), 224 deletions(-) create mode 100644 packages/builder/src/components/backend/DataTable/buttons/sheet/SheetCreateColumnButton.svelte create mode 100644 packages/builder/src/components/backend/DataTable/buttons/sheet/SheetCreateRowButton.svelte create mode 100644 packages/builder/src/components/backend/DataTable/buttons/sheet/SheetCreateViewButton.svelte create mode 100644 packages/builder/src/components/backend/DataTable/buttons/sheet/SheetExportButton.svelte create mode 100644 packages/builder/src/components/backend/DataTable/buttons/sheet/SheetFilterButton.svelte create mode 100644 packages/builder/src/components/backend/DataTable/buttons/sheet/SheetImportButton.svelte create mode 100644 packages/builder/src/components/backend/DataTable/buttons/sheet/SheetManageAccessButton.svelte create mode 100644 packages/builder/src/components/backend/DataTable/buttons/sheet/SheetRelationshipButton.svelte create mode 100644 packages/builder/src/components/backend/DataTable/modals/sheet/SheetEditColumnModal.svelte create mode 100644 packages/frontend-core/src/components/sheet/events.js diff --git a/packages/builder/src/components/backend/DataTable/DataTable.svelte b/packages/builder/src/components/backend/DataTable/DataTable.svelte index c7a7c74f99..6fd6c91b95 100644 --- a/packages/builder/src/components/backend/DataTable/DataTable.svelte +++ b/packages/builder/src/components/backend/DataTable/DataTable.svelte @@ -1,226 +1,52 @@
- + - + {#if !isUsersTable} - + {/if} {#if isInternal} - + {/if} - + {#if isUsersTable} {/if} {#if !isInternal} - + {/if} - - - {#key id} - - {/key} + {#if !isUsersTable} + + {/if} + + +
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/frontend-core/src/components/sheet/Sheet.svelte b/packages/frontend-core/src/components/sheet/Sheet.svelte index 49f3abd18d..629496b159 100644 --- a/packages/frontend-core/src/components/sheet/Sheet.svelte +++ b/packages/frontend-core/src/components/sheet/Sheet.svelte @@ -26,6 +26,8 @@ export let tableId export let allowAddRows = true export let allowSelectRows = true + export let allowAddColumns = true + export let allowEditColumns = true // Sheet constants const cellHeight = 36 @@ -36,6 +38,8 @@ tableId, allowAddRows, allowSelectRows, + allowAddColumns, + allowEditColumns, }) // Build up spreadsheet context @@ -65,6 +69,8 @@ tableId, allowAddRows, allowSelectRows, + allowAddColumns, + allowEditColumns, }) // Set context for children to consume diff --git a/packages/frontend-core/src/components/sheet/cells/HeaderCell.svelte b/packages/frontend-core/src/components/sheet/cells/HeaderCell.svelte index 108f17e0ef..064b513d20 100644 --- a/packages/frontend-core/src/components/sheet/cells/HeaderCell.svelte +++ b/packages/frontend-core/src/components/sheet/cells/HeaderCell.svelte @@ -7,8 +7,16 @@ export let column export let orderable = true - const { reorder, isReordering, isResizing, rand, sort, columns, dispatch } = - getContext("sheet") + const { + reorder, + isReordering, + isResizing, + rand, + sort, + columns, + dispatch, + config, + } = getContext("sheet") let anchor let open = false @@ -117,7 +125,9 @@ animate={false} > - Edit column + {#if $config.allowEditColumns} + Edit column + {/if} Sort ascending From a25af10c0e76530dfcca12eae1d6039d0a8c7962 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 7 Mar 2023 11:54:41 +0000 Subject: [PATCH 085/235] Count context menu clicks when considering the click outside handler --- packages/bbui/src/Actions/click_outside.js | 1 + .../frontend-core/src/components/sheet/cells/HeaderCell.svelte | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/bbui/src/Actions/click_outside.js b/packages/bbui/src/Actions/click_outside.js index bdcbaa5d88..43904e0af2 100644 --- a/packages/bbui/src/Actions/click_outside.js +++ b/packages/bbui/src/Actions/click_outside.js @@ -29,6 +29,7 @@ const handleClick = event => { }) } document.documentElement.addEventListener("click", handleClick, true) +document.documentElement.addEventListener("contextmenu", handleClick, true) /** * Adds or updates a click handler diff --git a/packages/frontend-core/src/components/sheet/cells/HeaderCell.svelte b/packages/frontend-core/src/components/sheet/cells/HeaderCell.svelte index 064b513d20..38498b4698 100644 --- a/packages/frontend-core/src/components/sheet/cells/HeaderCell.svelte +++ b/packages/frontend-core/src/components/sheet/cells/HeaderCell.svelte @@ -47,7 +47,7 @@ const onContextMenu = e => { e.preventDefault() - open = true + open = !open } const sortAscending = () => { From df757ce09b6e2c36b6d1a1d6b7c076283258e67b Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 7 Mar 2023 11:57:28 +0000 Subject: [PATCH 086/235] Prevent adding rows to users table and remove log --- .../builder/src/components/backend/DataTable/DataTable.svelte | 2 +- packages/frontend-core/src/components/sheet/stores/reorder.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/builder/src/components/backend/DataTable/DataTable.svelte b/packages/builder/src/components/backend/DataTable/DataTable.svelte index 6fd6c91b95..9e04763b7d 100644 --- a/packages/builder/src/components/backend/DataTable/DataTable.svelte +++ b/packages/builder/src/components/backend/DataTable/DataTable.svelte @@ -21,7 +21,7 @@
- + {#if !isUsersTable} diff --git a/packages/frontend-core/src/components/sheet/stores/reorder.js b/packages/frontend-core/src/components/sheet/stores/reorder.js index d547e14f69..c090a58c91 100644 --- a/packages/frontend-core/src/components/sheet/stores/reorder.js +++ b/packages/frontend-core/src/components/sheet/stores/reorder.js @@ -82,7 +82,6 @@ export const createReorderStores = context => { let sourceIdx = $columns.findIndex(x => x.name === sourceColumn) let targetIdx = $columns.findIndex(x => x.name === targetColumn) targetIdx++ - console.log(sourceIdx, targetIdx) columns.update(state => { const removed = state.splice(sourceIdx, 1) if (--targetIdx < sourceIdx) { From 0e9fc297fb2c617ea2637f0d4947fd65314c2f5e Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 7 Mar 2023 12:05:00 +0000 Subject: [PATCH 087/235] Expose loading state of sheet and improve column highlighting logic --- .../buttons/sheet/SheetCreateColumnButton.svelte | 6 ++++-- .../buttons/sheet/SheetCreateRowButton.svelte | 6 +++--- .../src/components/sheet/stores/reorder.js | 2 +- .../frontend-core/src/components/sheet/stores/rows.js | 11 +++++------ 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/builder/src/components/backend/DataTable/buttons/sheet/SheetCreateColumnButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/sheet/SheetCreateColumnButton.svelte index 5d0bd5140a..fe11290008 100644 --- a/packages/builder/src/components/backend/DataTable/buttons/sheet/SheetCreateColumnButton.svelte +++ b/packages/builder/src/components/backend/DataTable/buttons/sheet/SheetCreateColumnButton.svelte @@ -2,11 +2,13 @@ import CreateColumnButton from "../CreateColumnButton.svelte" import { getContext, onMount } from "svelte" - const { rows, columns, subscribe, filter } = getContext("sheet") + const { rows, columns, subscribe, filter, loaded } = getContext("sheet") let createColumnModal - $: highlighted = !$filter.length && (!$rows.length || !$columns.length) + $: hasCols = !$loaded || $columns.length + $: hasRows = !$loaded || $filter.length || $rows.length + $: highlighted = !hasRows || !hasCols onMount(() => subscribe("add-column", createColumnModal.show)) diff --git a/packages/builder/src/components/backend/DataTable/buttons/sheet/SheetCreateRowButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/sheet/SheetCreateRowButton.svelte index d2903b9526..c59f45392c 100644 --- a/packages/builder/src/components/backend/DataTable/buttons/sheet/SheetCreateRowButton.svelte +++ b/packages/builder/src/components/backend/DataTable/buttons/sheet/SheetCreateRowButton.svelte @@ -3,10 +3,10 @@ import CreateEditRow from "../../modals/CreateEditRow.svelte" import { getContext } from "svelte" - const { rows, columns, filter } = getContext("sheet") + const { rows, columns, filter, loaded } = getContext("sheet") - $: hasCols = !!$columns.length - $: hasRows = $rows.length || $filter.length + $: hasCols = !$loaded || $columns.length + $: hasRows = !$loaded || $filter.length || $rows.length { - const { columns, rand, scroll, bounds, stickyColumn } = context + const { columns, scroll, bounds, stickyColumn } = context const reorderInitialState = { sourceColumn: null, targetColumn: null, diff --git a/packages/frontend-core/src/components/sheet/stores/rows.js b/packages/frontend-core/src/components/sheet/stores/rows.js index 187c094eb2..3a81cc235a 100644 --- a/packages/frontend-core/src/components/sheet/stores/rows.js +++ b/packages/frontend-core/src/components/sheet/stores/rows.js @@ -10,14 +10,12 @@ export const createRowsStore = context => { const schema = writable({}) const table = writable(null) const filter = writable([]) + const loaded = writable(false) const sort = writable({ column: null, order: null, }) - // Flag for whether this is the first time loading our fetch - let loaded = false - // Local cache of row IDs to speed up checking if a row exists let rowCacheMap = {} @@ -37,7 +35,7 @@ export const createRowsStore = context => { return null } // Wipe state and fully hydrate next time our fetch returns data - loaded = false + loaded.set(false) // Create fetch and load initial data return fetchData({ @@ -63,9 +61,9 @@ export const createRowsStore = context => { } $fetch.subscribe($$fetch => { if ($$fetch.loaded) { - if (!loaded) { + if (!get(loaded)) { // Hydrate initial data - loaded = true + loaded.set(true) rowCacheMap = {} rows.set([]) } @@ -273,5 +271,6 @@ export const createRowsStore = context => { schema, sort, filter, + loaded, } } From 7f231aecdf2cefedcfd9d4325cffc20b07722efb Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 7 Mar 2023 12:10:50 +0000 Subject: [PATCH 088/235] Small style updates --- packages/frontend-core/src/components/sheet/HeaderRow.svelte | 2 +- packages/frontend-core/src/components/sheet/UserAvatars.svelte | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/frontend-core/src/components/sheet/HeaderRow.svelte b/packages/frontend-core/src/components/sheet/HeaderRow.svelte index 6b800fdf7d..b69265dd41 100644 --- a/packages/frontend-core/src/components/sheet/HeaderRow.svelte +++ b/packages/frontend-core/src/components/sheet/HeaderRow.svelte @@ -40,7 +40,7 @@ background: var(--spectrum-global-color-gray-100); display: grid; place-items: center; - width: 40px; + width: 46px; border-left: var(--cell-border); } .new-column:hover { diff --git a/packages/frontend-core/src/components/sheet/UserAvatars.svelte b/packages/frontend-core/src/components/sheet/UserAvatars.svelte index 8d8f80c9b7..36aee32855 100644 --- a/packages/frontend-core/src/components/sheet/UserAvatars.svelte +++ b/packages/frontend-core/src/components/sheet/UserAvatars.svelte @@ -15,7 +15,6 @@ .users { display: flex; flex-direction: row; - padding: 0 10px; gap: 8px; } From ef5481376430b775dc49beb91e6ef2c624022f16 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 7 Mar 2023 14:14:16 +0000 Subject: [PATCH 089/235] Update delete button and allow horizontal scrolling --- .../src/components/sheet/DeleteButton.svelte | 19 +++++++------------ .../src/components/sheet/Sheet.svelte | 5 ++++- .../sheet/SheetScrollWrapper.svelte | 5 ++++- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/packages/frontend-core/src/components/sheet/DeleteButton.svelte b/packages/frontend-core/src/components/sheet/DeleteButton.svelte index b27a750a68..3d698a9f9d 100644 --- a/packages/frontend-core/src/components/sheet/DeleteButton.svelte +++ b/packages/frontend-core/src/components/sheet/DeleteButton.svelte @@ -1,5 +1,5 @@ @@ -33,7 +40,7 @@ + import { + clickOutside, + Menu, + MenuItem, + Modal, + ModalContent, + notifications, + } from "@budibase/bbui" + import { getContext } from "svelte" + + const { selectedCellRow, menu, rows, columns, selectedCellId } = + getContext("sheet") + + let modal + + $: style = makeStyle($menu) + + const makeStyle = menu => { + return `left:${menu.left}px; top:${menu.top}px;` + } + + const deleteRow = () => { + rows.actions.deleteRows([$selectedCellRow]) + menu.actions.close() + notifications.success("Deleted 1 row") + } + + const duplicate = async () => { + let clone = { ...$selectedCellRow } + delete clone._id + delete clone._rev + delete clone.__idx + const newRow = await rows.actions.addRow(clone, $selectedCellRow.__idx + 1) + if (newRow) { + $selectedCellId = `${newRow._id}-${$columns[0].name}` + menu.actions.close() + } + } + + +{#if $menu.visible} + +{/if} + + + + Are you sure you want to delete this row? + + + + diff --git a/packages/frontend-core/src/components/sheet/Sheet.svelte b/packages/frontend-core/src/components/sheet/Sheet.svelte index 8b3e60edd2..333a0a0d66 100644 --- a/packages/frontend-core/src/components/sheet/Sheet.svelte +++ b/packages/frontend-core/src/components/sheet/Sheet.svelte @@ -14,11 +14,13 @@ import { createWebsocket } from "./websocket" import { createUserStores } from "./stores/users" import { createResizeStores } from "./stores/resize" + import { createMenuStores } from "./stores/menu" import DeleteButton from "./DeleteButton.svelte" import SheetBody from "./SheetBody.svelte" import ResizeOverlay from "./ResizeOverlay.svelte" import HeaderRow from "./HeaderRow.svelte" import ScrollOverlay from "./ScrollOverlay.svelte" + import MenuOverlay from "./MenuOverlay.svelte" import StickyColumn from "./StickyColumn.svelte" import UserAvatars from "./UserAvatars.svelte" @@ -60,6 +62,7 @@ context = { ...context, ...createReorderStores(context) } context = { ...context, ...createInterfaceStores(context) } context = { ...context, ...createUserStores(context) } + context = { ...context, ...createMenuStores(context) } // Reference some stores for local use const { isResizing, isReordering } = context @@ -107,6 +110,7 @@
+
@@ -122,7 +126,7 @@ /* Variables */ --cell-background: var(--spectrum-global-color-gray-50); - --cell-background-hover: var(--spectrum-global-color-gray-75); + --cell-background-hover: var(--spectrum-global-color-gray-100); --cell-padding: 10px; --cell-spacing: 4px; --cell-font-size: 14px; diff --git a/packages/frontend-core/src/components/sheet/SheetBody.svelte b/packages/frontend-core/src/components/sheet/SheetBody.svelte index 586aaafb2f..7b37337ab9 100644 --- a/packages/frontend-core/src/components/sheet/SheetBody.svelte +++ b/packages/frontend-core/src/components/sheet/SheetBody.svelte @@ -4,27 +4,23 @@ import NewRow from "./NewRow.svelte" import SheetRow from "./SheetRow.svelte" - const { selectedCellId, bounds, visibleRows, config } = getContext("sheet") + const { bounds, visibleRows, config } = getContext("sheet") - let ref + let body onMount(() => { // Observe and record the height of the body const observer = new ResizeObserver(() => { - bounds.set(ref.getBoundingClientRect()) + bounds.set(body.getBoundingClientRect()) }) - observer.observe(ref) + observer.observe(body) return () => { observer.disconnect() } }) -
($selectedCellId = null)} -> +
{#each $visibleRows as row, idx} diff --git a/packages/frontend-core/src/components/sheet/SheetRow.svelte b/packages/frontend-core/src/components/sheet/SheetRow.svelte index 2238469a30..4f28982d21 100644 --- a/packages/frontend-core/src/components/sheet/SheetRow.svelte +++ b/packages/frontend-core/src/components/sheet/SheetRow.svelte @@ -14,10 +14,13 @@ visibleColumns, hoveredRowId, selectedCellMap, + selectedCellRow, + menu, } = getContext("sheet") $: rowSelected = !!$selectedRows[row._id] $: rowHovered = $hoveredRowId === row._id + $: containsSelectedCell = $selectedCellRow?._id === row._id
($hoveredRowId = null)} > {#each $visibleColumns as column (column.name)} - {@const cellIdx = `${row._id}-${column.name}`} + {@const cellId = `${row._id}-${column.name}`} ($selectedCellId = cellIdx)} + on:click={() => ($selectedCellId = cellId)} + on:contextmenu={e => menu.actions.open(cellId, e)} width={column.width} > rows.actions.updateRow(row._id, column, val)} readonly={column.schema.autocolumn} /> diff --git a/packages/frontend-core/src/components/sheet/SheetScrollWrapper.svelte b/packages/frontend-core/src/components/sheet/SheetScrollWrapper.svelte index d5c1fad67d..893be80542 100644 --- a/packages/frontend-core/src/components/sheet/SheetScrollWrapper.svelte +++ b/packages/frontend-core/src/components/sheet/SheetScrollWrapper.svelte @@ -12,6 +12,7 @@ hoveredRowId, maxScrollTop, maxScrollLeft, + selectedCellId, } = getContext("sheet") export let scrollVertically = true @@ -71,7 +72,11 @@ }) -
+
($selectedCellId = null)} +>
diff --git a/packages/frontend-core/src/components/sheet/StickyColumn.svelte b/packages/frontend-core/src/components/sheet/StickyColumn.svelte index 090f9c6c98..9dd2230483 100644 --- a/packages/frontend-core/src/components/sheet/StickyColumn.svelte +++ b/packages/frontend-core/src/components/sheet/StickyColumn.svelte @@ -1,7 +1,6 @@ diff --git a/packages/frontend-core/src/components/sheet/Sheet.svelte b/packages/frontend-core/src/components/sheet/Sheet.svelte index 333a0a0d66..6c3bcfa321 100644 --- a/packages/frontend-core/src/components/sheet/Sheet.svelte +++ b/packages/frontend-core/src/components/sheet/Sheet.svelte @@ -9,7 +9,7 @@ import { createColumnsStores } from "./stores/columns" import { createScrollStores } from "./stores/scroll" import { createBoundsStores } from "./stores/bounds" - import { createInterfaceStores } from "./stores/interface" + import { createUIStores } from "./stores/ui" export { createUserStores } from "./stores/users" import { createWebsocket } from "./websocket" import { createUserStores } from "./stores/users" @@ -23,6 +23,8 @@ import MenuOverlay from "./MenuOverlay.svelte" import StickyColumn from "./StickyColumn.svelte" import UserAvatars from "./UserAvatars.svelte" + import KeyboardManager from "./KeyboardManager.svelte" + import { clickOutside } from "@budibase/bbui" export let API export let tableId @@ -60,12 +62,12 @@ context = { ...context, ...createScrollStores(context) } context = { ...context, ...createViewportStores(context) } context = { ...context, ...createReorderStores(context) } - context = { ...context, ...createInterfaceStores(context) } + context = { ...context, ...createUIStores(context) } context = { ...context, ...createUserStores(context) } context = { ...context, ...createMenuStores(context) } // Reference some stores for local use - const { isResizing, isReordering } = context + const { isResizing, isReordering, ui } = context // Keep config store up to date $: config.set({ @@ -88,10 +90,11 @@
@@ -112,6 +115,7 @@
+
diff --git a/packages/frontend-core/src/components/sheet/Sheet.svelte b/packages/frontend-core/src/components/sheet/Sheet.svelte index 655035287d..681c5d2fe2 100644 --- a/packages/frontend-core/src/components/sheet/Sheet.svelte +++ b/packages/frontend-core/src/components/sheet/Sheet.svelte @@ -25,6 +25,7 @@ import UserAvatars from "./UserAvatars.svelte" import KeyboardManager from "./KeyboardManager.svelte" import { clickOutside } from "@budibase/bbui" + import NewRow from "./NewRow.svelte" export let API export let tableId @@ -159,17 +160,17 @@ display: flex; flex-direction: row; justify-items: flex-start; - align-items: flex-start; + align-items: stretch; overflow: hidden; height: 0; position: relative; + background: var(--spectrum-global-color-gray-75); } .sheet-main { flex: 1 1 auto; overflow: hidden; display: flex; flex-direction: column; - align-self: stretch; } /* Controls */ diff --git a/packages/frontend-core/src/components/sheet/SheetBody.svelte b/packages/frontend-core/src/components/sheet/SheetBody.svelte index 7b37337ab9..6990a68395 100644 --- a/packages/frontend-core/src/components/sheet/SheetBody.svelte +++ b/packages/frontend-core/src/components/sheet/SheetBody.svelte @@ -1,10 +1,9 @@
0} >
@@ -88,7 +81,11 @@ {@const rowSelected = !!$selectedRows[row._id]} {@const rowHovered = $hoveredRowId === row._id} {@const containsSelectedRow = $selectedCellRow?._id === row._id} -
($hoveredRowId = row._id)}> +
($hoveredRowId = row._id)} + on:mouseleave={() => ($hoveredRowId = null)} + > {/each} - - {#if $config.allowAddRows} -
($hoveredRowId = "new")} - > - - - - {#if $stickyColumn} - - {/if} -
- {/if}
diff --git a/packages/frontend-core/src/components/sheet/Sheet.svelte b/packages/frontend-core/src/components/sheet/Sheet.svelte index 681c5d2fe2..e3e857252e 100644 --- a/packages/frontend-core/src/components/sheet/Sheet.svelte +++ b/packages/frontend-core/src/components/sheet/Sheet.svelte @@ -25,7 +25,8 @@ import UserAvatars from "./UserAvatars.svelte" import KeyboardManager from "./KeyboardManager.svelte" import { clickOutside } from "@budibase/bbui" - import NewRow from "./NewRow.svelte" + import AddRowButton from "./AddRowButton.svelte" + import SheetControls from "./SheetControls.svelte" export let API export let tableId @@ -104,6 +105,7 @@
+
@@ -120,6 +122,9 @@ + {#if $config.allowAddRows} + + {/if}
{/if} diff --git a/packages/frontend-core/src/components/sheet/SheetControls.svelte b/packages/frontend-core/src/components/sheet/SheetControls.svelte index f19b14b2d1..ac6b747510 100644 --- a/packages/frontend-core/src/components/sheet/SheetControls.svelte +++ b/packages/frontend-core/src/components/sheet/SheetControls.svelte @@ -3,43 +3,7 @@ import { ActionButton } from "@budibase/bbui" const { rows } = getContext("sheet") - - $: rowCount = $rows.length -
-
- Filter - Group - Sort - Hide fields -
-
-
- {rowCount} row{rowCount === 1 ? "" : "s"} -
-
- - +Sort +Hide fields From 9231ce88c6df4a670a79a414f2fca45d0001352c Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 10 Mar 2023 16:23:56 +0000 Subject: [PATCH 102/235] Optimise sheet data loading and add sort button --- .../src/components/sheet/Sheet.svelte | 6 +- .../src/components/sheet/SheetControls.svelte | 3 +- .../sheet/controls/SortButton.svelte | 52 +++++++++++ .../src/components/sheet/stores/rows.js | 91 +++++++++++-------- packages/frontend-core/src/fetch/DataFetch.js | 47 +++++----- .../frontend-core/src/fetch/QueryFetch.js | 2 +- 6 files changed, 137 insertions(+), 64 deletions(-) create mode 100644 packages/frontend-core/src/components/sheet/controls/SortButton.svelte diff --git a/packages/frontend-core/src/components/sheet/Sheet.svelte b/packages/frontend-core/src/components/sheet/Sheet.svelte index e3e857252e..ceb158b529 100644 --- a/packages/frontend-core/src/components/sheet/Sheet.svelte +++ b/packages/frontend-core/src/components/sheet/Sheet.svelte @@ -86,10 +86,6 @@ // Expose ability to retrieve context externally to allow sheet control export const getContext = () => context - // Local flag for if the sheet has ever had data - let initialised = false - loaded.subscribe(state => (initialised = initialised || state)) - // Initialise websocket for multi-user onMount(() => createWebsocket(context)) @@ -112,7 +108,7 @@
- {#if initialised} + {#if $loaded}
diff --git a/packages/frontend-core/src/components/sheet/SheetControls.svelte b/packages/frontend-core/src/components/sheet/SheetControls.svelte index ac6b747510..28f1ec6ddb 100644 --- a/packages/frontend-core/src/components/sheet/SheetControls.svelte +++ b/packages/frontend-core/src/components/sheet/SheetControls.svelte @@ -1,9 +1,10 @@ -Sort + Hide fields diff --git a/packages/frontend-core/src/components/sheet/controls/SortButton.svelte b/packages/frontend-core/src/components/sheet/controls/SortButton.svelte new file mode 100644 index 0000000000..1e28af66cb --- /dev/null +++ b/packages/frontend-core/src/components/sheet/controls/SortButton.svelte @@ -0,0 +1,52 @@ + + +
+ (open = !open)} + selected={!!$sort.order} + > + Sort + +
+ + +
+ +
+
+ + diff --git a/packages/frontend-core/src/components/sheet/stores/rows.js b/packages/frontend-core/src/components/sheet/stores/rows.js index b7ce3b7ad2..c9219ffdc1 100644 --- a/packages/frontend-core/src/components/sheet/stores/rows.js +++ b/packages/frontend-core/src/components/sheet/stores/rows.js @@ -1,5 +1,4 @@ import { writable, derived, get } from "svelte/store" -import { LuceneUtils } from "../../../index" import { fetchData } from "../../../fetch/fetchData" import { notifications } from "@budibase/bbui" @@ -11,20 +10,25 @@ export const createRowsStore = context => { const table = writable(null) const filter = writable([]) const loaded = writable(false) + const fetch = writable(null) const sort = writable({ column: null, order: null, }) + + // Enrich rows with an index property const enrichedRows = derived(rows, $rows => { return $rows.map((row, idx) => ({ ...row, __idx: idx, })) }) + + // Generate a lookup map to quick find a row by ID const rowLookupMap = derived(enrichedRows, $rows => { let map = {} - for (let i = 0; i < $rows.length; i++) { - map[$rows[i]._id] = i + for (let row of $rows) { + map[row._id] = row.__idx } return map }) @@ -33,69 +37,84 @@ export const createRowsStore = context => { let rowCacheMap = {} // Reset everything when table ID changes - tableId.subscribe(() => { - filter.set([]) + let unsubscribe = null + tableId.subscribe($tableId => { + // Unsub from previous fetch if one exists + unsubscribe?.() + fetch.set(null) + + // Reset state sort.set({ column: null, order: null, }) - }) + filter.set([]) - // Local stores for managing fetching data - const query = derived(filter, $filter => - LuceneUtils.buildLuceneQuery($filter) - ) - const fetch = derived([tableId, query, sort], ([$tableId, $query, $sort]) => { - if (!$tableId) { - return null - } - // Wipe state and fully hydrate next time our fetch returns data - loaded.set(false) - - // Create fetch and load initial data - return fetchData({ + // Create new fetch model + const newFetch = fetchData({ API, datasource: { type: "table", tableId: $tableId, }, options: { - sortColumn: $sort.column, - sortOrder: $sort.order, - query: $query, + filter: [], + sortColumn: null, + sortOrder: null, limit: 100, paginate: true, }, }) - }) - // Observe each data fetch and extract some data - fetch.subscribe($fetch => { - if (!$fetch) { - return - } - $fetch.subscribe($$fetch => { - if ($$fetch.loaded) { - if (!get(loaded)) { + // Subscribe to changes of this fetch model + unsubscribe = newFetch.subscribe($fetch => { + if ($fetch.loaded && !$fetch.loading) { + if ($fetch.pageNumber === 0) { // Hydrate initial data - loaded.set(true) rowCacheMap = {} rows.set([]) + + // Update sorting from fetch if required + const $sort = get(sort) + if (!$sort.column) { + sort.set({ + column: $fetch.sortColumn, + order: $fetch.sortOrder, + }) + } } // Update schema and enrich primary display into schema - let newSchema = $$fetch.schema - const primaryDisplay = $$fetch.definition?.primaryDisplay + let newSchema = $fetch.schema + const primaryDisplay = $fetch.definition?.primaryDisplay if (primaryDisplay && newSchema[primaryDisplay]) { newSchema[primaryDisplay].primaryDisplay = true } schema.set(newSchema) - table.set($$fetch.definition) + table.set($fetch.definition) // Process new rows - handleNewRows($$fetch.rows) + handleNewRows($fetch.rows) + + // Notify that we're loaded + loaded.set(true) } }) + + fetch.set(newFetch) + }) + + // Update fetch when filter or sort config changes + filter.subscribe($filter => { + get(fetch)?.update({ + filter: $filter, + }) + }) + sort.subscribe($sort => { + get(fetch)?.update({ + sortOrder: $sort.order, + sortColumn: $sort.column, + }) }) // Adds a new empty row diff --git a/packages/frontend-core/src/fetch/DataFetch.js b/packages/frontend-core/src/fetch/DataFetch.js index 4c47f13172..049a840128 100644 --- a/packages/frontend-core/src/fetch/DataFetch.js +++ b/packages/frontend-core/src/fetch/DataFetch.js @@ -21,11 +21,11 @@ export default class DataFetch { this.API = null // Feature flags - this.featureStore = writable({ + this.features = { supportsSearch: false, supportsSort: false, supportsPagination: false, - }) + } // Config this.options = { @@ -81,17 +81,14 @@ export default class DataFetch { this.prevPage = this.prevPage.bind(this) // Derive certain properties to return - this.derivedStore = derived( - [this.store, this.featureStore], - ([$store, $featureStore]) => { - return { - ...$store, - ...$featureStore, - hasNextPage: this.hasNextPage($store), - hasPrevPage: this.hasPrevPage($store), - } + this.derivedStore = derived(this.store, $store => { + return { + ...$store, + ...this.features, + hasNextPage: this.hasNextPage($store), + hasPrevPage: this.hasPrevPage($store), } - ) + }) // Mark as loaded if we have no datasource if (!this.options.datasource) { @@ -120,11 +117,11 @@ export default class DataFetch { // Fetch datasource definition and determine feature flags const definition = await this.getDefinition(datasource) const features = this.determineFeatureFlags(definition) - this.featureStore.set({ + this.features = { supportsSearch: !!features?.supportsSearch, supportsSort: !!features?.supportsSort, supportsPagination: paginate && !!features?.supportsPagination, - }) + } // Fetch and enrich schema let schema = this.getSchema(datasource, definition) @@ -138,11 +135,17 @@ export default class DataFetch { this.options.sortOrder = "ascending" } - // If no sort column, use the first field in the schema + // If no sort column, use the primary display and fallback to first column if (!this.options.sortColumn) { - this.options.sortColumn = Object.keys(schema)[0] + let newSortColumn + if (definition?.primaryDisplay && schema[definition.primaryDisplay]) { + newSortColumn = definition.primaryDisplay + } else { + newSortColumn = Object.keys(schema)[0] + } + this.options.sortColumn = newSortColumn } - const { sortColumn } = this.options + const { sortOrder, sortColumn } = this.options // Determine what sort type to use let sortType = "string" @@ -167,6 +170,8 @@ export default class DataFetch { loading: true, cursors: [], cursor: null, + sortOrder, + sortColumn, })) // Actually fetch data @@ -189,23 +194,23 @@ export default class DataFetch { async getPage() { const { sortColumn, sortOrder, sortType, limit } = this.options const { query } = get(this.store) - const features = get(this.featureStore) // Get the actual data + console.log("===== FETCH =====") let { rows, info, hasNextPage, cursor, error } = await this.getData() // If we don't support searching, do a client search - if (!features.supportsSearch) { + if (!this.features.supportsSearch) { rows = runLuceneQuery(rows, query) } // If we don't support sorting, do a client-side sort - if (!features.supportsSort) { + if (!this.features.supportsSort) { rows = luceneSort(rows, sortColumn, sortOrder, sortType) } // If we don't support pagination, do a client-side limit - if (!features.supportsPagination) { + if (!this.features.supportsPagination) { rows = luceneLimit(rows, limit) } diff --git a/packages/frontend-core/src/fetch/QueryFetch.js b/packages/frontend-core/src/fetch/QueryFetch.js index 65f0a4f130..456abaec79 100644 --- a/packages/frontend-core/src/fetch/QueryFetch.js +++ b/packages/frontend-core/src/fetch/QueryFetch.js @@ -31,7 +31,7 @@ export default class QueryFetch extends DataFetch { async getData() { const { datasource, limit, paginate } = this.options - const { supportsPagination } = get(this.featureStore) + const { supportsPagination } = this.features const { cursor, definition } = get(this.store) const type = definition?.fields?.pagination?.type From c573955998be482503db6f774a2c27ee9a6de909 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 10 Mar 2023 16:29:33 +0000 Subject: [PATCH 103/235] Update sorting and remove logs --- .../src/components/sheet/SheetControls.svelte | 5 +---- .../src/components/sheet/cells/HeaderCell.svelte | 8 ++------ .../src/components/sheet/controls/SortButton.svelte | 5 ++--- packages/frontend-core/src/fetch/DataFetch.js | 1 - 4 files changed, 5 insertions(+), 14 deletions(-) diff --git a/packages/frontend-core/src/components/sheet/SheetControls.svelte b/packages/frontend-core/src/components/sheet/SheetControls.svelte index 28f1ec6ddb..2da051df06 100644 --- a/packages/frontend-core/src/components/sheet/SheetControls.svelte +++ b/packages/frontend-core/src/components/sheet/SheetControls.svelte @@ -1,10 +1,7 @@ - Hide fields + diff --git a/packages/frontend-core/src/components/sheet/cells/HeaderCell.svelte b/packages/frontend-core/src/components/sheet/cells/HeaderCell.svelte index e20fb5b9ce..4fb81a6458 100644 --- a/packages/frontend-core/src/components/sheet/cells/HeaderCell.svelte +++ b/packages/frontend-core/src/components/sheet/cells/HeaderCell.svelte @@ -128,12 +128,8 @@ {#if $config.allowEditColumns} Edit column {/if} - - Sort ascending - - - Sort descending - + Sort A-Z + Sort Z-A Move left diff --git a/packages/frontend-core/src/components/sheet/controls/SortButton.svelte b/packages/frontend-core/src/components/sheet/controls/SortButton.svelte index 1e28af66cb..42e09380c9 100644 --- a/packages/frontend-core/src/components/sheet/controls/SortButton.svelte +++ b/packages/frontend-core/src/components/sheet/controls/SortButton.svelte @@ -4,15 +4,14 @@ const { sort, columns, stickyColumn } = getContext("sheet") const orderOptions = [ - { label: "Ascending", value: "ascending" }, - { label: "Descending", value: "descending" }, + { label: "A-Z", value: "ascending" }, + { label: "Z-A", value: "descending" }, ] let open = false let anchor $: columnOptions = getColumnOptions($stickyColumn, $columns) - $: console.log($sort) const getColumnOptions = (stickyColumn, columns) => { let options = [] diff --git a/packages/frontend-core/src/fetch/DataFetch.js b/packages/frontend-core/src/fetch/DataFetch.js index 049a840128..f7afa3e911 100644 --- a/packages/frontend-core/src/fetch/DataFetch.js +++ b/packages/frontend-core/src/fetch/DataFetch.js @@ -196,7 +196,6 @@ export default class DataFetch { const { query } = get(this.store) // Get the actual data - console.log("===== FETCH =====") let { rows, info, hasNextPage, cursor, error } = await this.getData() // If we don't support searching, do a client search From 57c82c4a5dee999e9ba3df535493b3f06b51f1ae Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Sat, 11 Mar 2023 14:10:45 +0000 Subject: [PATCH 104/235] Add sheet button to control column visibilty, improve sorting, improve disabled states --- packages/builder/yarn.lock | 51 +---------- .../src/components/sheet/AddRowButton.svelte | 10 ++- .../src/components/sheet/HeaderRow.svelte | 8 +- .../src/components/sheet/NewRow.svelte | 4 +- .../src/components/sheet/ResizeOverlay.svelte | 10 +-- .../src/components/sheet/SheetBody.svelte | 4 +- .../src/components/sheet/SheetControls.svelte | 4 +- .../src/components/sheet/SheetRow.svelte | 4 +- .../sheet/SheetScrollWrapper.svelte | 16 ++-- .../src/components/sheet/StickyColumn.svelte | 6 +- .../components/sheet/cells/HeaderCell.svelte | 8 +- .../sheet/controls/HideColumnsButton.svelte | 88 +++++++++++++++++++ .../sheet/controls/SortButton.svelte | 31 ++++++- .../src/components/sheet/stores/columns.js | 14 ++- .../src/components/sheet/stores/resize.js | 13 ++- .../src/components/sheet/stores/scroll.js | 2 +- .../src/components/sheet/stores/viewport.js | 48 +++++----- 17 files changed, 206 insertions(+), 115 deletions(-) create mode 100644 packages/frontend-core/src/components/sheet/controls/HideColumnsButton.svelte diff --git a/packages/builder/yarn.lock b/packages/builder/yarn.lock index 56bfd6ee8c..7035ca1765 100644 --- a/packages/builder/yarn.lock +++ b/packages/builder/yarn.lock @@ -1467,11 +1467,6 @@ dependencies: "@sinonjs/commons" "^1.7.0" -"@socket.io/component-emitter@~3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" - integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg== - "@spectrum-css/accordion@^3.0.24": version "3.0.30" resolved "https://registry.yarnpkg.com/@spectrum-css/accordion/-/accordion-3.0.30.tgz#0893a6db28bab984bf5adaf7e1ba194e741db615" @@ -2586,7 +2581,7 @@ dayjs@^1.10.4, dayjs@^1.11.2: resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.7.tgz#4b296922642f70999544d1144a2c25730fce63e2" integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ== -debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: +debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -2779,22 +2774,6 @@ end-of-stream@^1.1.0: dependencies: once "^1.4.0" -engine.io-client@~6.4.0: - version "6.4.0" - resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.4.0.tgz#88cd3082609ca86d7d3c12f0e746d12db4f47c91" - integrity sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g== - dependencies: - "@socket.io/component-emitter" "~3.1.0" - debug "~4.3.1" - engine.io-parser "~5.0.3" - ws "~8.11.0" - xmlhttprequest-ssl "~2.0.0" - -engine.io-parser@~5.0.3: - version "5.0.6" - resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.6.tgz#7811244af173e157295dec9b2718dfe42a64ef45" - integrity sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw== - enquirer@^2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" @@ -5917,24 +5896,6 @@ snapdragon@^0.8.1: source-map-resolve "^0.5.0" use "^3.1.0" -socket.io-client@^4.6.1: - version "4.6.1" - resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.6.1.tgz#80d97d5eb0feca448a0fb6d69a7b222d3d547eab" - integrity sha512-5UswCV6hpaRsNg5kkEHVcbBIXEYoVbMQaHJBXJCyEQ+CiFPV1NIOY0XOFWG4XR4GZcB8Kn6AsRs/9cy9TbqVMQ== - dependencies: - "@socket.io/component-emitter" "~3.1.0" - debug "~4.3.2" - engine.io-client "~6.4.0" - socket.io-parser "~4.2.1" - -socket.io-parser@~4.2.1: - version "4.2.2" - resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.2.tgz#1dd384019e25b7a3d374877f492ab34f2ad0d206" - integrity sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw== - dependencies: - "@socket.io/component-emitter" "~3.1.0" - debug "~4.3.1" - source-map-js@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" @@ -6752,11 +6713,6 @@ ws@^7.4.6: resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== -ws@~8.11.0: - version "8.11.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" - integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== - xml-name-validator@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" @@ -6767,11 +6723,6 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== -xmlhttprequest-ssl@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67" - integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A== - y18n@^4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" diff --git a/packages/frontend-core/src/components/sheet/AddRowButton.svelte b/packages/frontend-core/src/components/sheet/AddRowButton.svelte index 3c7c023d89..25a721e8f1 100644 --- a/packages/frontend-core/src/components/sheet/AddRowButton.svelte +++ b/packages/frontend-core/src/components/sheet/AddRowButton.svelte @@ -2,12 +2,14 @@ import { Icon } from "@budibase/bbui" import { getContext } from "svelte" - const { dispatch } = getContext("sheet") + const { dispatch, columns } = getContext("sheet") -
dispatch("add-row")}> - -
+{#if $columns.length} +
dispatch("add-row")}> + +
+{/if} diff --git a/packages/frontend-core/src/components/sheet/controls/SortButton.svelte b/packages/frontend-core/src/components/sheet/controls/SortButton.svelte index 42e09380c9..2e44dcdc35 100644 --- a/packages/frontend-core/src/components/sheet/controls/SortButton.svelte +++ b/packages/frontend-core/src/components/sheet/controls/SortButton.svelte @@ -2,7 +2,7 @@ import { getContext } from "svelte" import { ActionButton, Popover, Select } from "@budibase/bbui" - const { sort, columns, stickyColumn } = getContext("sheet") + const { sort, visibleColumns, stickyColumn } = getContext("sheet") const orderOptions = [ { label: "A-Z", value: "ascending" }, { label: "Z-A", value: "descending" }, @@ -11,7 +11,8 @@ let open = false let anchor - $: columnOptions = getColumnOptions($stickyColumn, $columns) + $: columnOptions = getColumnOptions($stickyColumn, $visibleColumns) + $: checkValidSortColumn($sort.column, $stickyColumn, $visibleColumns) const getColumnOptions = (stickyColumn, columns) => { let options = [] @@ -20,6 +21,29 @@ } return [...options, ...columns.map(col => col.name)] } + + // Ensure we never have a sort column selected that is not visible + const checkValidSortColumn = (sortColumn, stickyColumn, visibleColumns) => { + if (!sortColumn) { + return + } + if ( + sortColumn !== stickyColumn?.name && + !visibleColumns.some(col => col.name === sortColumn) + ) { + if (stickyColumn) { + sort.update(state => ({ + ...state, + column: stickyColumn.name, + })) + } else { + sort.update(state => ({ + ...state, + column: visibleColumns[0]?.name, + })) + } + } + }
@@ -28,7 +52,8 @@ quiet size="M" on:click={() => (open = !open)} - selected={!!$sort.order} + selected={!!$sort.column} + disabled={!$visibleColumns.length} > Sort diff --git a/packages/frontend-core/src/components/sheet/stores/columns.js b/packages/frontend-core/src/components/sheet/stores/columns.js index b648f7dd32..a878c51c69 100644 --- a/packages/frontend-core/src/components/sheet/stores/columns.js +++ b/packages/frontend-core/src/components/sheet/stores/columns.js @@ -10,17 +10,23 @@ export const createColumnsStores = context => { // automatically calculated const enrichedColumns = derived(columns, $columns => { let offset = 0 - return $columns.map((column, idx) => { + return $columns.map(column => { const enriched = { ...column, - idx, left: offset, } - offset += column.width + if (column.visible) { + offset += column.width + } return enriched }) }) + // Derived list of columns which have not been explicitly hidden + const visibleColumns = derived(enrichedColumns, $columns => { + return $columns.filter(col => col.visible) + }) + // Merge new schema fields with existing schema in order to preserve widths schema.subscribe($schema => { const currentColumns = get(columns) @@ -45,6 +51,7 @@ export const createColumnsStores = context => { name: field, width: existing?.width || defaultWidth, schema: $schema[field], + visible: existing?.visible ?? true, } }) ) @@ -75,5 +82,6 @@ export const createColumnsStores = context => { subscribe: enrichedColumns.subscribe, }, stickyColumn, + visibleColumns, } } diff --git a/packages/frontend-core/src/components/sheet/stores/resize.js b/packages/frontend-core/src/components/sheet/stores/resize.js index 0ca16dac13..d67a112c40 100644 --- a/packages/frontend-core/src/components/sheet/stores/resize.js +++ b/packages/frontend-core/src/components/sheet/stores/resize.js @@ -5,12 +5,13 @@ export const createResizeStores = context => { const initialState = { initialMouseX: null, initialWidth: null, + column: null, columnIdx: null, width: 0, left: 0, } const resize = writable(initialState) - const isResizing = derived(resize, $resize => $resize.columnIdx != null) + const isResizing = derived(resize, $resize => $resize.column != null) const MinColumnWidth = 100 // Starts resizing a certain column @@ -18,12 +19,20 @@ export const createResizeStores = context => { // Prevent propagation to stop reordering triggering e.stopPropagation() + // Find and cache index + let columnIdx = get(columns).findIndex(col => col.name === column.name) + if (columnIdx === -1) { + columnIdx = "sticky" + } + + // Set initial store state resize.set({ width: column.width, left: column.left, initialWidth: column.width, initialMouseX: e.clientX, - columnIdx: column.idx, + column: column.name, + columnIdx, }) // Add mouse event listeners to handle resizing diff --git a/packages/frontend-core/src/components/sheet/stores/scroll.js b/packages/frontend-core/src/components/sheet/stores/scroll.js index 5d5dbfe1cc..e708fbd3d6 100644 --- a/packages/frontend-core/src/components/sheet/stores/scroll.js +++ b/packages/frontend-core/src/components/sheet/stores/scroll.js @@ -17,7 +17,7 @@ export const createScrollStores = context => { const width = derived(bounds, $bounds => $bounds.width, 0) const contentHeight = derived( rows, - $rows => ($rows.length + 1) * cellHeight + padding, + $rows => $rows.length * cellHeight + padding, 0 ) const maxScrollTop = derived( diff --git a/packages/frontend-core/src/components/sheet/stores/viewport.js b/packages/frontend-core/src/components/sheet/stores/viewport.js index 63880ada06..f47e8e0ee0 100644 --- a/packages/frontend-core/src/components/sheet/stores/viewport.js +++ b/packages/frontend-core/src/components/sheet/stores/viewport.js @@ -1,7 +1,7 @@ import { derived, get } from "svelte/store" export const createViewportStores = context => { - const { cellHeight, columns, rows, scroll, bounds } = context + const { cellHeight, visibleColumns, rows, scroll, bounds } = context const scrollTop = derived(scroll, $scroll => $scroll.top, 0) const scrollLeft = derived(scroll, $scroll => $scroll.left, 0) @@ -15,53 +15,59 @@ export const createViewportStores = context => { const firstRowIdx = derived(scrollTop, $scrollTop => { return Math.floor($scrollTop / cellHeight) }) - const visibleRowCount = derived(height, $height => { + const renderedRowCount = derived(height, $height => { return Math.ceil($height / cellHeight) }) - const visibleRows = derived( - [rows, firstRowIdx, visibleRowCount], + const renderedRows = derived( + [rows, firstRowIdx, renderedRowCount], ([$rows, $firstRowIdx, $visibleRowCount]) => { return $rows.slice($firstRowIdx, $firstRowIdx + $visibleRowCount) } ) // Derive visible columns - const visibleColumns = derived( - [columns, scrollLeft, width], - ([$columns, $scrollLeft, $width]) => { - if (!$columns.length) { + const renderedColumns = derived( + [visibleColumns, scrollLeft, width], + ([$visibleColumns, $scrollLeft, $width]) => { + if (!$visibleColumns.length) { return [] } let startColIdx = 0 - let rightEdge = $columns[0].width - while (rightEdge < $scrollLeft && startColIdx < $columns.length - 1) { + let rightEdge = $visibleColumns[0].width + while ( + rightEdge < $scrollLeft && + startColIdx < $visibleColumns.length - 1 + ) { startColIdx++ - rightEdge += $columns[startColIdx].width + rightEdge += $visibleColumns[startColIdx].width } let endColIdx = startColIdx + 1 let leftEdge = rightEdge - while (leftEdge < $width + $scrollLeft && endColIdx < $columns.length) { - leftEdge += $columns[endColIdx].width + while ( + leftEdge < $width + $scrollLeft && + endColIdx < $visibleColumns.length + ) { + leftEdge += $visibleColumns[endColIdx].width endColIdx++ } - const nextVisibleColumns = $columns.slice(startColIdx, endColIdx) + const nextRenderedColumns = $visibleColumns.slice(startColIdx, endColIdx) // Cautiously shrink the number of rendered columns. // This is to avoid rapidly shrinking and growing the visible column count // which results in remounting cells - const currentCount = get(visibleColumns).length - if (currentCount === nextVisibleColumns.length + 1) { - return $columns.slice(startColIdx, endColIdx + 1) + const currentCount = get(renderedColumns).length + if (currentCount === nextRenderedColumns.length + 1) { + return $visibleColumns.slice(startColIdx, endColIdx + 1) } else { - return nextVisibleColumns + return nextRenderedColumns } }, [] ) // Fetch next page when approaching end of data - visibleRows.subscribe($visibleRows => { - const lastVisible = $visibleRows[$visibleRows.length - 1] + renderedRows.subscribe($renderedRows => { + const lastVisible = $renderedRows[$renderedRows.length - 1] const $rows = get(rows) const lastRow = $rows[$rows.length - 1] if (lastVisible && lastRow && lastVisible._id === lastRow._id) { @@ -69,5 +75,5 @@ export const createViewportStores = context => { } }) - return { visibleRows, visibleColumns } + return { renderedRows, renderedColumns } } From c1128618fbdd0e59a640747d2d6e00f972b54175 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Sat, 11 Mar 2023 18:28:58 +0000 Subject: [PATCH 105/235] Fix bug with select placeholders and fix sorting loops causing endless refreshes --- packages/bbui/src/Form/Core/Select.svelte | 5 ++- .../src/components/sheet/StickyColumn.svelte | 6 ++-- .../sheet/controls/SortButton.svelte | 34 ++++++++++++++++--- .../src/components/sheet/stores/rows.js | 30 +++++----------- .../src/components/sheet/stores/scroll.js | 8 ++--- 5 files changed, 50 insertions(+), 33 deletions(-) diff --git a/packages/bbui/src/Form/Core/Select.svelte b/packages/bbui/src/Form/Core/Select.svelte index af45c1d9ff..6e50cfd1f4 100644 --- a/packages/bbui/src/Form/Core/Select.svelte +++ b/packages/bbui/src/Form/Core/Select.svelte @@ -44,7 +44,10 @@ const getFieldText = (value, options, placeholder) => { // Always use placeholder if no value if (value == null || value === "") { - return placeholder !== false ? "Choose an option" : "" + if (placeholder === false) { + return "" + } + return placeholder || "Choose an option" } return getFieldAttribute(getOptionLabel, value, options) diff --git a/packages/frontend-core/src/components/sheet/StickyColumn.svelte b/packages/frontend-core/src/components/sheet/StickyColumn.svelte index c53d587d5b..3416173b53 100644 --- a/packages/frontend-core/src/components/sheet/StickyColumn.svelte +++ b/packages/frontend-core/src/components/sheet/StickyColumn.svelte @@ -59,14 +59,16 @@ class:scrolled={scrollLeft > 0} >
- {#if $config.allowSelectRows} - + {/if} diff --git a/packages/frontend-core/src/components/sheet/controls/SortButton.svelte b/packages/frontend-core/src/components/sheet/controls/SortButton.svelte index 2e44dcdc35..e84bd45087 100644 --- a/packages/frontend-core/src/components/sheet/controls/SortButton.svelte +++ b/packages/frontend-core/src/components/sheet/controls/SortButton.svelte @@ -22,6 +22,20 @@ return [...options, ...columns.map(col => col.name)] } + const updateSortColumn = e => { + sort.update(state => ({ + ...state, + column: e.detail, + })) + } + + const updateSortOrder = e => { + sort.update(state => ({ + ...state, + order: e.detail, + })) + } + // Ensure we never have a sort column selected that is not visible const checkValidSortColumn = (sortColumn, stickyColumn, visibleColumns) => { if (!sortColumn) { @@ -52,8 +66,8 @@ quiet size="M" on:click={() => (open = !open)} - selected={!!$sort.column} - disabled={!$visibleColumns.length} + selected={open || $sort.column} + disabled={!columnOptions.length} > Sort @@ -61,8 +75,20 @@
- +
diff --git a/packages/frontend-core/src/components/sheet/stores/rows.js b/packages/frontend-core/src/components/sheet/stores/rows.js index c9219ffdc1..14d056efdb 100644 --- a/packages/frontend-core/src/components/sheet/stores/rows.js +++ b/packages/frontend-core/src/components/sheet/stores/rows.js @@ -11,10 +11,11 @@ export const createRowsStore = context => { const filter = writable([]) const loaded = writable(false) const fetch = writable(null) - const sort = writable({ + const initialSortState = { column: null, - order: null, - }) + order: "ascending", + } + const sort = writable(initialSortState) // Enrich rows with an index property const enrichedRows = derived(rows, $rows => { @@ -44,10 +45,7 @@ export const createRowsStore = context => { fetch.set(null) // Reset state - sort.set({ - column: null, - order: null, - }) + sort.set(initialSortState) filter.set([]) // Create new fetch model @@ -59,8 +57,8 @@ export const createRowsStore = context => { }, options: { filter: [], - sortColumn: null, - sortOrder: null, + sortColumn: initialSortState.column, + sortOrder: initialSortState.order, limit: 100, paginate: true, }, @@ -73,15 +71,6 @@ export const createRowsStore = context => { // Hydrate initial data rowCacheMap = {} rows.set([]) - - // Update sorting from fetch if required - const $sort = get(sort) - if (!$sort.column) { - sort.set({ - column: $fetch.sortColumn, - order: $fetch.sortOrder, - }) - } } // Update schema and enrich primary display into schema @@ -180,10 +169,7 @@ export const createRowsStore = context => { // Refreshes all data const refreshData = () => { filter.set([]) - sort.set({ - column: null, - order: null, - }) + sort.set(initialSortState) } // Updates a value of a row diff --git a/packages/frontend-core/src/components/sheet/stores/scroll.js b/packages/frontend-core/src/components/sheet/stores/scroll.js index e708fbd3d6..599f16770b 100644 --- a/packages/frontend-core/src/components/sheet/stores/scroll.js +++ b/packages/frontend-core/src/components/sheet/stores/scroll.js @@ -1,7 +1,7 @@ import { derived, get, writable } from "svelte/store" export const createScrollStores = context => { - const { rows, columns, stickyColumn, bounds, cellHeight } = context + const { rows, visibleColumns, stickyColumn, bounds, cellHeight } = context const padding = 180 const scroll = writable({ left: 0, @@ -28,10 +28,10 @@ export const createScrollStores = context => { // Derive horizontal limits const contentWidth = derived( - [columns, stickyColumn], - ([$columns, $stickyColumn]) => { + [visibleColumns, stickyColumn], + ([$visibleColumns, $stickyColumn]) => { let width = 40 + padding + ($stickyColumn?.width || 0) - $columns.forEach(col => { + $visibleColumns.forEach(col => { width += col.width }) return width From 38a3ef0c3400fcb0c5f5a2182159e9629d358268 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Sat, 11 Mar 2023 18:54:38 +0000 Subject: [PATCH 106/235] Update filter button to look consistent and add double click to resize columns to default width --- .../buttons/TableFilterButton.svelte | 2 +- .../src/components/sheet/ResizeOverlay.svelte | 2 ++ .../src/components/sheet/stores/columns.js | 7 ++++--- .../src/components/sheet/stores/resize.js | 21 ++++++++++++++++++- .../src/components/sheet/stores/rows.js | 3 +-- 5 files changed, 28 insertions(+), 7 deletions(-) diff --git a/packages/builder/src/components/backend/DataTable/buttons/TableFilterButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/TableFilterButton.svelte index 505f7f947e..5af8d1bac8 100644 --- a/packages/builder/src/components/backend/DataTable/buttons/TableFilterButton.svelte +++ b/packages/builder/src/components/backend/DataTable/buttons/TableFilterButton.svelte @@ -21,7 +21,7 @@ quiet {disabled} on:click={modal.show} - active={tempValue?.length > 0} + selected={tempValue?.length > 0} > Filter diff --git a/packages/frontend-core/src/components/sheet/ResizeOverlay.svelte b/packages/frontend-core/src/components/sheet/ResizeOverlay.svelte index d46468929a..f3d308bded 100644 --- a/packages/frontend-core/src/components/sheet/ResizeOverlay.svelte +++ b/packages/frontend-core/src/components/sheet/ResizeOverlay.svelte @@ -27,6 +27,7 @@ class="resize-slider sticky" class:visible={column === $stickyColumn.name} on:mousedown={e => resize.actions.startResizing($stickyColumn, e)} + on:dblclick={() => resize.actions.resetSize($stickyColumn)} style="left:{40 + $stickyColumn.width}px;" >
@@ -37,6 +38,7 @@ class="resize-slider" class:visible={column === column.name} on:mousedown={e => resize.actions.startResizing(column, e)} + on:dblclick={() => resize.actions.resetSize(column)} style={getStyle(column, offset, scrollLeft)} >
diff --git a/packages/frontend-core/src/components/sheet/stores/columns.js b/packages/frontend-core/src/components/sheet/stores/columns.js index a878c51c69..95d1d618fe 100644 --- a/packages/frontend-core/src/components/sheet/stores/columns.js +++ b/packages/frontend-core/src/components/sheet/stores/columns.js @@ -1,8 +1,9 @@ import { derived, get, writable } from "svelte/store" +export const DefaultColumnWidth = 200 + export const createColumnsStores = context => { const { schema } = context - const defaultWidth = 200 const columns = writable([]) const stickyColumn = writable(null) @@ -49,7 +50,7 @@ export const createColumnsStores = context => { const existing = currentColumns.find(x => x.name === field) return { name: field, - width: existing?.width || defaultWidth, + width: existing?.width || DefaultColumnWidth, schema: $schema[field], visible: existing?.visible ?? true, } @@ -69,7 +70,7 @@ export const createColumnsStores = context => { const same = primaryDisplay[0] === get(stickyColumn)?.name stickyColumn.set({ name: primaryDisplay[0], - width: same ? existingWidth : defaultWidth, + width: same ? existingWidth : DefaultColumnWidth, left: 40, schema: primaryDisplay[1], idx: "sticky", diff --git a/packages/frontend-core/src/components/sheet/stores/resize.js b/packages/frontend-core/src/components/sheet/stores/resize.js index d67a112c40..0ef87c96b7 100644 --- a/packages/frontend-core/src/components/sheet/stores/resize.js +++ b/packages/frontend-core/src/components/sheet/stores/resize.js @@ -1,4 +1,7 @@ import { writable, get, derived } from "svelte/store" +import { DefaultColumnWidth } from "./columns" + +export const MinColumnWidth = 100 export const createResizeStores = context => { const { columns, stickyColumn } = context @@ -12,7 +15,6 @@ export const createResizeStores = context => { } const resize = writable(initialState) const isResizing = derived(resize, $resize => $resize.column != null) - const MinColumnWidth = 100 // Starts resizing a certain column const startResizing = (column, e) => { @@ -78,11 +80,28 @@ export const createResizeStores = context => { document.removeEventListener("mouseup", stopResizing) } + // Resets a column size back to default + const resetSize = 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] + }) + } + } + return { resize: { ...resize, actions: { startResizing, + resetSize, }, }, isResizing, diff --git a/packages/frontend-core/src/components/sheet/stores/rows.js b/packages/frontend-core/src/components/sheet/stores/rows.js index 14d056efdb..883dde78ec 100644 --- a/packages/frontend-core/src/components/sheet/stores/rows.js +++ b/packages/frontend-core/src/components/sheet/stores/rows.js @@ -168,8 +168,7 @@ export const createRowsStore = context => { // Refreshes all data const refreshData = () => { - filter.set([]) - sort.set(initialSortState) + get(fetch)?.getInitialData() } // Updates a value of a row From d7666272e0cfc9c4b58eb555bee34160a513050e Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Sat, 11 Mar 2023 19:20:38 +0000 Subject: [PATCH 107/235] Ensure all derived stores have default values --- .../src/components/sheet/stores/columns.js | 40 +++--- .../src/components/sheet/stores/reorder.js | 6 +- .../src/components/sheet/stores/resize.js | 2 +- .../src/components/sheet/stores/rows.js | 34 +++-- .../src/components/sheet/stores/scroll.js | 21 +++- .../src/components/sheet/stores/ui.js | 3 +- .../src/components/sheet/stores/users.js | 119 +++++++++--------- .../src/components/sheet/stores/viewport.js | 37 +++--- packages/frontend-core/src/fetch/DataFetch.js | 1 + 9 files changed, 153 insertions(+), 110 deletions(-) diff --git a/packages/frontend-core/src/components/sheet/stores/columns.js b/packages/frontend-core/src/components/sheet/stores/columns.js index 95d1d618fe..ec169cb683 100644 --- a/packages/frontend-core/src/components/sheet/stores/columns.js +++ b/packages/frontend-core/src/components/sheet/stores/columns.js @@ -9,24 +9,32 @@ export const createColumnsStores = context => { // Derive an enriched version of columns with left offsets and indexes // automatically calculated - const enrichedColumns = derived(columns, $columns => { - let offset = 0 - return $columns.map(column => { - const enriched = { - ...column, - left: offset, - } - if (column.visible) { - offset += column.width - } - return enriched - }) - }) + const enrichedColumns = derived( + columns, + $columns => { + let offset = 0 + return $columns.map(column => { + const enriched = { + ...column, + left: offset, + } + if (column.visible) { + offset += column.width + } + return enriched + }) + }, + [] + ) // Derived list of columns which have not been explicitly hidden - const visibleColumns = derived(enrichedColumns, $columns => { - return $columns.filter(col => col.visible) - }) + const visibleColumns = derived( + enrichedColumns, + $columns => { + return $columns.filter(col => col.visible) + }, + [] + ) // Merge new schema fields with existing schema in order to preserve widths schema.subscribe($schema => { diff --git a/packages/frontend-core/src/components/sheet/stores/reorder.js b/packages/frontend-core/src/components/sheet/stores/reorder.js index 699ff0baff..243ab68e3c 100644 --- a/packages/frontend-core/src/components/sheet/stores/reorder.js +++ b/packages/frontend-core/src/components/sheet/stores/reorder.js @@ -11,7 +11,11 @@ export const createReorderStores = context => { sheetLeft: 0, } const reorder = writable(reorderInitialState) - const isReordering = derived(reorder, $reorder => !!$reorder.sourceColumn) + const isReordering = derived( + reorder, + $reorder => !!$reorder.sourceColumn, + false + ) // Callback when dragging on a colum header and starting reordering const startReordering = (column, e) => { diff --git a/packages/frontend-core/src/components/sheet/stores/resize.js b/packages/frontend-core/src/components/sheet/stores/resize.js index 0ef87c96b7..56a63f7bb3 100644 --- a/packages/frontend-core/src/components/sheet/stores/resize.js +++ b/packages/frontend-core/src/components/sheet/stores/resize.js @@ -14,7 +14,7 @@ export const createResizeStores = context => { left: 0, } const resize = writable(initialState) - const isResizing = derived(resize, $resize => $resize.column != null) + const isResizing = derived(resize, $resize => $resize.column != null, false) // Starts resizing a certain column const startResizing = (column, e) => { diff --git a/packages/frontend-core/src/components/sheet/stores/rows.js b/packages/frontend-core/src/components/sheet/stores/rows.js index 883dde78ec..958fd1e569 100644 --- a/packages/frontend-core/src/components/sheet/stores/rows.js +++ b/packages/frontend-core/src/components/sheet/stores/rows.js @@ -18,21 +18,29 @@ export const createRowsStore = context => { const sort = writable(initialSortState) // Enrich rows with an index property - const enrichedRows = derived(rows, $rows => { - return $rows.map((row, idx) => ({ - ...row, - __idx: idx, - })) - }) + const enrichedRows = derived( + rows, + $rows => { + return $rows.map((row, idx) => ({ + ...row, + __idx: idx, + })) + }, + [] + ) // Generate a lookup map to quick find a row by ID - const rowLookupMap = derived(enrichedRows, $rows => { - let map = {} - for (let row of $rows) { - map[row._id] = row.__idx - } - return map - }) + const rowLookupMap = derived( + enrichedRows, + $rows => { + let map = {} + for (let row of $rows) { + map[row._id] = row.__idx + } + return map + }, + {} + ) // Local cache of row IDs to speed up checking if a row exists let rowCacheMap = {} diff --git a/packages/frontend-core/src/components/sheet/stores/scroll.js b/packages/frontend-core/src/components/sheet/stores/scroll.js index 599f16770b..fa88ac46b4 100644 --- a/packages/frontend-core/src/components/sheet/stores/scroll.js +++ b/packages/frontend-core/src/components/sheet/stores/scroll.js @@ -9,8 +9,8 @@ export const createScrollStores = context => { }) // Memoize store primitives - const scrollTop = derived(scroll, $scroll => $scroll.top) - const scrollLeft = derived(scroll, $scroll => $scroll.left) + const scrollTop = derived(scroll, $scroll => $scroll.top, 0) + const scrollLeft = derived(scroll, $scroll => $scroll.left, 0) // Derive vertical limits const height = derived(bounds, $bounds => $bounds.height, 0) @@ -80,6 +80,23 @@ export const createScrollStores = context => { } }) + // Fetch next page when fewer than 50 scrollable rows remaining + const scrollableRows = derived( + [scrollTop, maxScrollTop], + ([$scrollTop, $maxScrollTop]) => { + if (!$maxScrollTop) { + return 100 + } + return ($maxScrollTop - $scrollTop) / cellHeight + }, + 100 + ) + scrollableRows.subscribe(count => { + if (count < 25) { + rows.actions.loadNextPage() + } + }) + return { scroll, contentHeight, diff --git a/packages/frontend-core/src/components/sheet/stores/ui.js b/packages/frontend-core/src/components/sheet/stores/ui.js index 1a90dd75ad..895fe60d12 100644 --- a/packages/frontend-core/src/components/sheet/stores/ui.js +++ b/packages/frontend-core/src/components/sheet/stores/ui.js @@ -14,7 +14,8 @@ export const createUIStores = context => { const rowId = $selectedCellId?.split("-")[0] const index = $rowLookupMap[rowId] return $rows[index] - } + }, + null ) // Ensure we clear invalid rows from state if they disappear diff --git a/packages/frontend-core/src/components/sheet/stores/users.js b/packages/frontend-core/src/components/sheet/stores/users.js index a52ef2b00d..c9a2e13453 100644 --- a/packages/frontend-core/src/components/sheet/stores/users.js +++ b/packages/frontend-core/src/components/sheet/stores/users.js @@ -5,50 +5,70 @@ export const createUserStores = () => { const userId = writable(null) // Enrich users with unique colours - const enrichedUsers = derived([users, userId], ([$users, $userId]) => { - return ( - $users - .slice() - // Place current user first - .sort((a, b) => { - if (a.id === $userId) { - return -1 - } else if (b.id === $userId) { - return 1 - } else { - return 0 - } - }) - // Enrich users with colors - .map((user, idx) => { - // Generate random colour hue - let hue = 1 - for (let i = 0; i < user.email.length && i < 5; i++) { - hue *= user.email.charCodeAt(i) - } - hue = hue % 360 - const color = - idx === 0 - ? "var(--spectrum-global-color-blue-400)" - : `hsl(${hue}, 50%, 40%)` - - // Generate friendly label - let label = user.email - if (user.firstName) { - label = user.firstName - if (user.lastName) { - label += ` ${user.lastName}` + const enrichedUsers = derived( + [users, userId], + ([$users, $userId]) => { + return ( + $users + .slice() + // Place current user first + .sort((a, b) => { + if (a.id === $userId) { + return -1 + } else if (b.id === $userId) { + return 1 + } else { + return 0 } - } + }) + // Enrich users with colors + .map((user, idx) => { + // Generate random colour hue + let hue = 1 + for (let i = 0; i < user.email.length && i < 5; i++) { + hue *= user.email.charCodeAt(i) + } + hue = hue % 360 + const color = + idx === 0 + ? "var(--spectrum-global-color-blue-400)" + : `hsl(${hue}, 50%, 40%)` - return { - ...user, - color, - label, - } - }) - ) - }) + // Generate friendly label + let label = user.email + if (user.firstName) { + label = user.firstName + if (user.lastName) { + label += ` ${user.lastName}` + } + } + + return { + ...user, + color, + label, + } + }) + ) + }, + [] + ) + + // Generate a lookup map of cell ID to the user that has it selected, to make + // lookups inside sheet cells extremely fast + const selectedCellMap = derived( + [enrichedUsers, userId], + ([$enrichedUsers, $userId]) => { + let map = {} + $enrichedUsers.forEach(user => { + if (user.selectedCellId && user.id !== $userId) { + map[user.selectedCellId] = user + } + }) + return map + }, + {} + ) const updateUser = user => { const $users = get(users) @@ -69,21 +89,6 @@ export const createUserStores = () => { }) } - // Generate a lookup map of cell ID to the user that has it selected, to make - // lookups inside sheet cells extremely fast - const selectedCellMap = derived( - [enrichedUsers, userId], - ([$enrichedUsers, $userId]) => { - let map = {} - $enrichedUsers.forEach(user => { - if (user.selectedCellId && user.id !== $userId) { - map[user.selectedCellId] = user - } - }) - return map - } - ) - return { users: { ...enrichedUsers, diff --git a/packages/frontend-core/src/components/sheet/stores/viewport.js b/packages/frontend-core/src/components/sheet/stores/viewport.js index f47e8e0ee0..79a075dee9 100644 --- a/packages/frontend-core/src/components/sheet/stores/viewport.js +++ b/packages/frontend-core/src/components/sheet/stores/viewport.js @@ -6,23 +6,32 @@ export const createViewportStores = context => { const scrollLeft = derived(scroll, $scroll => $scroll.left, 0) // Derive height and width as primitives to avoid wasted computation - const width = derived(bounds, $bounds => $bounds.width) - const height = derived(bounds, $bounds => $bounds.height) + const width = derived(bounds, $bounds => $bounds.width, 0) + const height = derived(bounds, $bounds => $bounds.height, 0) // Derive visible rows // Split into multiple stores containing primitives to optimise invalidation // as mich as possible - const firstRowIdx = derived(scrollTop, $scrollTop => { - return Math.floor($scrollTop / cellHeight) - }) - const renderedRowCount = derived(height, $height => { - return Math.ceil($height / cellHeight) - }) + const firstRowIdx = derived( + scrollTop, + $scrollTop => { + return Math.floor($scrollTop / cellHeight) + }, + 0 + ) + const renderedRowCount = derived( + height, + $height => { + return Math.ceil($height / cellHeight) + }, + 0 + ) const renderedRows = derived( [rows, firstRowIdx, renderedRowCount], ([$rows, $firstRowIdx, $visibleRowCount]) => { return $rows.slice($firstRowIdx, $firstRowIdx + $visibleRowCount) - } + }, + [] ) // Derive visible columns @@ -65,15 +74,5 @@ export const createViewportStores = context => { [] ) - // Fetch next page when approaching end of data - renderedRows.subscribe($renderedRows => { - const lastVisible = $renderedRows[$renderedRows.length - 1] - const $rows = get(rows) - const lastRow = $rows[$rows.length - 1] - if (lastVisible && lastRow && lastVisible._id === lastRow._id) { - rows.actions.loadNextPage() - } - }) - return { renderedRows, renderedColumns } } diff --git a/packages/frontend-core/src/fetch/DataFetch.js b/packages/frontend-core/src/fetch/DataFetch.js index f7afa3e911..6bf453a6dd 100644 --- a/packages/frontend-core/src/fetch/DataFetch.js +++ b/packages/frontend-core/src/fetch/DataFetch.js @@ -419,6 +419,7 @@ export default class DataFetch { if (state.loading || !this.options.paginate || !state.hasNextPage) { return } + console.log("NEXT PAGE") // Fetch next page const nextCursor = state.cursors[state.pageNumber + 1] From e76c541627a42a3d99a7ee5719bcb3f063ae6593 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Sun, 12 Mar 2023 16:04:17 +0000 Subject: [PATCH 108/235] Reset scrolling when datasource changes and fix wasted pagination calls --- .../src/components/sheet/Sheet.svelte | 10 +- .../src/components/sheet/stores/max-scroll.js | 86 +++++++++++++++ .../src/components/sheet/stores/pagination.js | 26 +++++ .../src/components/sheet/stores/rows.js | 29 +++-- .../src/components/sheet/stores/scroll.js | 100 +----------------- .../src/components/sheet/stores/viewport.js | 15 +-- 6 files changed, 151 insertions(+), 115 deletions(-) create mode 100644 packages/frontend-core/src/components/sheet/stores/max-scroll.js create mode 100644 packages/frontend-core/src/components/sheet/stores/pagination.js diff --git a/packages/frontend-core/src/components/sheet/Sheet.svelte b/packages/frontend-core/src/components/sheet/Sheet.svelte index ceb158b529..ec717ba824 100644 --- a/packages/frontend-core/src/components/sheet/Sheet.svelte +++ b/packages/frontend-core/src/components/sheet/Sheet.svelte @@ -15,6 +15,8 @@ import { createUserStores } from "./stores/users" import { createResizeStores } from "./stores/resize" import { createMenuStores } from "./stores/menu" + import { createMaxScrollStores } from "./stores/max-scroll" + import { createPaginationStores } from "./stores/pagination" import DeleteButton from "./DeleteButton.svelte" import SheetBody from "./SheetBody.svelte" import ResizeOverlay from "./ResizeOverlay.svelte" @@ -57,16 +59,18 @@ config, } context = { ...context, ...createEventManagers() } - context = { ...context, ...createRowsStore(context) } - context = { ...context, ...createColumnsStores(context) } - context = { ...context, ...createResizeStores(context) } context = { ...context, ...createBoundsStores(context) } context = { ...context, ...createScrollStores(context) } + context = { ...context, ...createRowsStore(context) } + context = { ...context, ...createColumnsStores(context) } + context = { ...context, ...createMaxScrollStores(context) } + context = { ...context, ...createResizeStores(context) } context = { ...context, ...createViewportStores(context) } context = { ...context, ...createReorderStores(context) } context = { ...context, ...createUIStores(context) } context = { ...context, ...createUserStores(context) } context = { ...context, ...createMenuStores(context) } + context = { ...context, ...createPaginationStores(context) } // Reference some stores for local use const { isResizing, isReordering, ui, loaded } = context diff --git a/packages/frontend-core/src/components/sheet/stores/max-scroll.js b/packages/frontend-core/src/components/sheet/stores/max-scroll.js new file mode 100644 index 0000000000..8c368e0d8e --- /dev/null +++ b/packages/frontend-core/src/components/sheet/stores/max-scroll.js @@ -0,0 +1,86 @@ +import { derived, get } from "svelte/store" + +export const createMaxScrollStores = context => { + const { rows, visibleColumns, stickyColumn, bounds, cellHeight, scroll } = + context + const padding = 180 + + // Memoize store primitives + const scrollTop = derived(scroll, $scroll => $scroll.top, 0) + const scrollLeft = derived(scroll, $scroll => $scroll.left, 0) + + // Derive vertical limits + const height = derived(bounds, $bounds => $bounds.height, 0) + const width = derived(bounds, $bounds => $bounds.width, 0) + const contentHeight = derived( + rows, + $rows => $rows.length * cellHeight + padding, + 0 + ) + const maxScrollTop = derived( + [height, contentHeight], + ([$height, $contentHeight]) => Math.max($contentHeight - $height, 0), + 0 + ) + + // Derive horizontal limits + const contentWidth = derived( + [visibleColumns, stickyColumn], + ([$visibleColumns, $stickyColumn]) => { + let width = 40 + padding + ($stickyColumn?.width || 0) + $visibleColumns.forEach(col => { + width += col.width + }) + return width + }, + 0 + ) + const screenWidth = derived( + [width, stickyColumn], + ([$width, $stickyColumn]) => $width + 40 + ($stickyColumn?.width || 0), + 0 + ) + const maxScrollLeft = derived( + [contentWidth, screenWidth], + ([$contentWidth, $screenWidth]) => { + return Math.max($contentWidth - $screenWidth, 0) + }, + 0 + ) + + // Ensure scroll state never goes invalid, which can happen when changing + // rows or tables + const overscrollTop = derived( + [scrollTop, maxScrollTop], + ([$scrollTop, $maxScrollTop]) => $scrollTop > $maxScrollTop, + false + ) + const overscrollLeft = derived( + [scrollLeft, maxScrollLeft], + ([$scrollLeft, $maxScrollLeft]) => $scrollLeft > $maxScrollLeft, + false + ) + overscrollTop.subscribe(overscroll => { + if (overscroll) { + scroll.update(state => ({ + ...state, + top: get(maxScrollTop), + })) + } + }) + overscrollLeft.subscribe(overscroll => { + if (overscroll) { + scroll.update(state => ({ + ...state, + left: get(maxScrollLeft), + })) + } + }) + + return { + contentHeight, + contentWidth, + maxScrollTop, + maxScrollLeft, + } +} diff --git a/packages/frontend-core/src/components/sheet/stores/pagination.js b/packages/frontend-core/src/components/sheet/stores/pagination.js new file mode 100644 index 0000000000..24bd10f19e --- /dev/null +++ b/packages/frontend-core/src/components/sheet/stores/pagination.js @@ -0,0 +1,26 @@ +import { derived } from "svelte/store" + +export const createPaginationStores = context => { + const { scrolledRowCount, rows, visualRowCapacity } = context + + // Derive how many rows we have in total + const rowCount = derived(rows, $rows => $rows.length, 0) + + // Derive how many rows we have available to scroll + const remainingRows = derived( + [scrolledRowCount, rowCount, visualRowCapacity], + ([$scrolledRowCount, $rowCount, $visualRowCapacity]) => { + return Math.max(0, $rowCount - $scrolledRowCount - $visualRowCapacity) + }, + 100 + ) + + // Fetch next page when fewer than 25 remaining rows to scroll + remainingRows.subscribe(remaining => { + if (remaining < 25) { + rows.actions.loadNextPage() + } + }) + + return null +} diff --git a/packages/frontend-core/src/components/sheet/stores/rows.js b/packages/frontend-core/src/components/sheet/stores/rows.js index 958fd1e569..6c6fc7e439 100644 --- a/packages/frontend-core/src/components/sheet/stores/rows.js +++ b/packages/frontend-core/src/components/sheet/stores/rows.js @@ -3,13 +3,14 @@ import { fetchData } from "../../../fetch/fetchData" import { notifications } from "@budibase/bbui" export const createRowsStore = context => { - const { config, API } = context + const { config, API, scroll } = context const tableId = derived(config, $config => $config.tableId) const rows = writable([]) const schema = writable({}) const table = writable(null) const filter = writable([]) const loaded = writable(false) + const instanceLoaded = writable(false) const fetch = writable(null) const initialSortState = { column: null, @@ -51,6 +52,7 @@ export const createRowsStore = context => { // Unsub from previous fetch if one exists unsubscribe?.() fetch.set(null) + instanceLoaded.set(false) // Reset state sort.set(initialSortState) @@ -75,10 +77,16 @@ export const createRowsStore = context => { // Subscribe to changes of this fetch model unsubscribe = newFetch.subscribe($fetch => { if ($fetch.loaded && !$fetch.loading) { - if ($fetch.pageNumber === 0) { - // Hydrate initial data - rowCacheMap = {} - rows.set([]) + const resetRows = $fetch.pageNumber === 0 + + // Reset scroll state when data changes + if (!get(instanceLoaded)) { + // Reset both top and left for a new table ID + instanceLoaded.set(true) + scroll.set({ top: 0, left: 0 }) + } else if (resetRows) { + // Only reset top scroll position when resetting rows + scroll.update(state => ({ ...state, top: 0 })) } // Update schema and enrich primary display into schema @@ -91,7 +99,7 @@ export const createRowsStore = context => { table.set($fetch.definition) // Process new rows - handleNewRows($fetch.rows) + handleNewRows($fetch.rows, resetRows) // Notify that we're loaded loaded.set(true) @@ -222,7 +230,10 @@ export const createRowsStore = context => { // Local handler to process new rows inside the fetch, and append any new // rows to state that we haven't encountered before - const handleNewRows = newRows => { + const handleNewRows = (newRows, resetRows) => { + if (resetRows) { + rowCacheMap = {} + } let rowsToAppend = [] let newRow for (let i = 0; i < newRows.length; i++) { @@ -232,7 +243,9 @@ export const createRowsStore = context => { rowsToAppend.push(newRow) } } - if (rowsToAppend.length) { + if (resetRows) { + rows.set(rowsToAppend) + } else if (rowsToAppend.length) { rows.update(state => [...state, ...rowsToAppend]) } } diff --git a/packages/frontend-core/src/components/sheet/stores/scroll.js b/packages/frontend-core/src/components/sheet/stores/scroll.js index fa88ac46b4..e0f33c7d87 100644 --- a/packages/frontend-core/src/components/sheet/stores/scroll.js +++ b/packages/frontend-core/src/components/sheet/stores/scroll.js @@ -1,107 +1,11 @@ -import { derived, get, writable } from "svelte/store" +import { writable } from "svelte/store" -export const createScrollStores = context => { - const { rows, visibleColumns, stickyColumn, bounds, cellHeight } = context - const padding = 180 +export const createScrollStores = () => { const scroll = writable({ left: 0, top: 0, }) - - // Memoize store primitives - const scrollTop = derived(scroll, $scroll => $scroll.top, 0) - const scrollLeft = derived(scroll, $scroll => $scroll.left, 0) - - // Derive vertical limits - const height = derived(bounds, $bounds => $bounds.height, 0) - const width = derived(bounds, $bounds => $bounds.width, 0) - const contentHeight = derived( - rows, - $rows => $rows.length * cellHeight + padding, - 0 - ) - const maxScrollTop = derived( - [height, contentHeight], - ([$height, $contentHeight]) => Math.max($contentHeight - $height, 0), - 0 - ) - - // Derive horizontal limits - const contentWidth = derived( - [visibleColumns, stickyColumn], - ([$visibleColumns, $stickyColumn]) => { - let width = 40 + padding + ($stickyColumn?.width || 0) - $visibleColumns.forEach(col => { - width += col.width - }) - return width - }, - 0 - ) - const screenWidth = derived( - [width, stickyColumn], - ([$width, $stickyColumn]) => $width + 40 + ($stickyColumn?.width || 0), - 0 - ) - const maxScrollLeft = derived( - [contentWidth, screenWidth], - ([$contentWidth, $screenWidth]) => { - return Math.max($contentWidth - $screenWidth, 0) - }, - 0 - ) - - // Ensure scroll state never goes invalid, which can happen when changing - // rows or tables - const overscrollTop = derived( - [scrollTop, maxScrollTop], - ([$scrollTop, $maxScrollTop]) => $scrollTop > $maxScrollTop, - false - ) - const overscrollLeft = derived( - [scrollLeft, maxScrollLeft], - ([$scrollLeft, $maxScrollLeft]) => $scrollLeft > $maxScrollLeft, - false - ) - overscrollTop.subscribe(overscroll => { - if (overscroll) { - scroll.update(state => ({ - ...state, - top: get(maxScrollTop), - })) - } - }) - overscrollLeft.subscribe(overscroll => { - if (overscroll) { - scroll.update(state => ({ - ...state, - left: get(maxScrollLeft), - })) - } - }) - - // Fetch next page when fewer than 50 scrollable rows remaining - const scrollableRows = derived( - [scrollTop, maxScrollTop], - ([$scrollTop, $maxScrollTop]) => { - if (!$maxScrollTop) { - return 100 - } - return ($maxScrollTop - $scrollTop) / cellHeight - }, - 100 - ) - scrollableRows.subscribe(count => { - if (count < 25) { - rows.actions.loadNextPage() - } - }) - return { scroll, - contentHeight, - contentWidth, - maxScrollTop, - maxScrollLeft, } } diff --git a/packages/frontend-core/src/components/sheet/stores/viewport.js b/packages/frontend-core/src/components/sheet/stores/viewport.js index 79a075dee9..27341a7cfe 100644 --- a/packages/frontend-core/src/components/sheet/stores/viewport.js +++ b/packages/frontend-core/src/components/sheet/stores/viewport.js @@ -12,14 +12,14 @@ export const createViewportStores = context => { // Derive visible rows // Split into multiple stores containing primitives to optimise invalidation // as mich as possible - const firstRowIdx = derived( + const scrolledRowCount = derived( scrollTop, $scrollTop => { return Math.floor($scrollTop / cellHeight) }, 0 ) - const renderedRowCount = derived( + const visualRowCapacity = derived( height, $height => { return Math.ceil($height / cellHeight) @@ -27,9 +27,12 @@ export const createViewportStores = context => { 0 ) const renderedRows = derived( - [rows, firstRowIdx, renderedRowCount], - ([$rows, $firstRowIdx, $visibleRowCount]) => { - return $rows.slice($firstRowIdx, $firstRowIdx + $visibleRowCount) + [rows, scrolledRowCount, visualRowCapacity], + ([$rows, $scrolledRowCount, $visualRowCapacity]) => { + return $rows.slice( + $scrolledRowCount, + $scrolledRowCount + $visualRowCapacity + ) }, [] ) @@ -74,5 +77,5 @@ export const createViewportStores = context => { [] ) - return { renderedRows, renderedColumns } + return { scrolledRowCount, visualRowCapacity, renderedRows, renderedColumns } } From 8a8152168de34747ae69a79710790bfd4ba6d20b Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Sun, 12 Mar 2023 16:25:39 +0000 Subject: [PATCH 109/235] Improve performance by removing searches through the full row array --- .../src/components/sheet/stores/rows.js | 17 +++++++++-------- .../src/components/sheet/stores/ui.js | 9 +++++---- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/frontend-core/src/components/sheet/stores/rows.js b/packages/frontend-core/src/components/sheet/stores/rows.js index 6c6fc7e439..91a3ad5f4f 100644 --- a/packages/frontend-core/src/components/sheet/stores/rows.js +++ b/packages/frontend-core/src/components/sheet/stores/rows.js @@ -32,11 +32,11 @@ export const createRowsStore = context => { // Generate a lookup map to quick find a row by ID const rowLookupMap = derived( - enrichedRows, + rows, $rows => { let map = {} - for (let row of $rows) { - map[row._id] = row.__idx + for (let i = 0; i < $rows.length; i++) { + map[$rows[i]._id] = i } return map }, @@ -259,11 +259,6 @@ export const createRowsStore = context => { rows.update(state => { return state.filter(row => !deletedIds.includes(row._id)) }) - - // If we ended up with no rows, try getting the next page - if (!get(rows).length) { - loadNextPage() - } } // Loads the next page of data if available @@ -276,6 +271,11 @@ export const createRowsStore = context => { return await get(fetch)?.refreshDefinition() } + // Checks if we have a row with a certain ID + const hasRow = id => { + return get(rowLookupMap)[id] != null + } + return { rows: { ...rows, @@ -284,6 +284,7 @@ export const createRowsStore = context => { addRow, updateRow, deleteRows, + hasRow, loadNextPage, refreshRow, refreshData, diff --git a/packages/frontend-core/src/components/sheet/stores/ui.js b/packages/frontend-core/src/components/sheet/stores/ui.js index 895fe60d12..fb1bf213ec 100644 --- a/packages/frontend-core/src/components/sheet/stores/ui.js +++ b/packages/frontend-core/src/components/sheet/stores/ui.js @@ -19,19 +19,20 @@ export const createUIStores = context => { ) // Ensure we clear invalid rows from state if they disappear - rows.subscribe($rows => { + rows.subscribe(() => { const $selectedCellId = get(selectedCellId) const $selectedRows = get(selectedRows) const $hoveredRowId = get(hoveredRowId) + const hasRow = rows.actions.hasRow // Check selected cell const selectedRowId = $selectedCellId?.split("-")[0] - if (selectedRowId && !$rows.find(row => row._id === selectedRowId)) { + if (selectedRowId && !hasRow(selectedRowId)) { selectedCellId.set(null) } // Check hovered row - if ($hoveredRowId && !$rows.find(row => row._id === $hoveredRowId)) { + if ($hoveredRowId && !hasRow($hoveredRowId)) { hoveredRowId.set(null) } @@ -40,7 +41,7 @@ export const createUIStores = context => { let selectedRowsNeedsUpdate = false const selectedIds = Object.keys($selectedRows) for (let i = 0; i < selectedIds.length; i++) { - if (!$rows.find(row => row._id === selectedIds[i])) { + if (!hasRow(selectedIds[i])) { delete newSelectedRows[selectedIds[i]] selectedRowsNeedsUpdate = true } From d4a2bcae4faa44e5e7b09bb4ad81481f1046be24 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 13 Mar 2023 18:45:28 +0000 Subject: [PATCH 110/235] Add advanced key handling for spreadsheets and improve blur and focus UX --- .../src/components/sheet/AddRowButton.svelte | 9 ++- .../src/components/sheet/DeleteButton.svelte | 4 +- .../src/components/sheet/HeaderRow.svelte | 9 ++- .../components/sheet/KeyboardManager.svelte | 25 ++++++- .../src/components/sheet/MenuOverlay.svelte | 18 +---- .../src/components/sheet/Sheet.svelte | 5 +- .../src/components/sheet/SheetRow.svelte | 19 ++--- .../src/components/sheet/StickyColumn.svelte | 21 ++---- .../components/sheet/cells/DataCell.svelte | 49 ++++++++++++ .../components/sheet/cells/HeaderCell.svelte | 4 +- .../sheet/cells/MultiSelectCell.svelte | 4 +- .../components/sheet/cells/NumberCell.svelte | 4 +- .../components/sheet/cells/OptionsCell.svelte | 74 +++++++++++++------ .../components/sheet/cells/TextCell.svelte | 35 ++++++++- .../src/components/sheet/stores/reorder.js | 3 +- .../src/components/sheet/stores/resize.js | 3 +- .../src/components/sheet/stores/ui.js | 7 ++ 17 files changed, 211 insertions(+), 82 deletions(-) create mode 100644 packages/frontend-core/src/components/sheet/cells/DataCell.svelte diff --git a/packages/frontend-core/src/components/sheet/AddRowButton.svelte b/packages/frontend-core/src/components/sheet/AddRowButton.svelte index 25a721e8f1..aa3dd407b1 100644 --- a/packages/frontend-core/src/components/sheet/AddRowButton.svelte +++ b/packages/frontend-core/src/components/sheet/AddRowButton.svelte @@ -2,11 +2,16 @@ import { Icon } from "@budibase/bbui" import { getContext } from "svelte" - const { dispatch, columns } = getContext("sheet") + const { dispatch, columns, ui } = getContext("sheet") + + const addRow = () => { + ui.actions.blur() + dispatch("add-row") + } {#if $columns.length} -
dispatch("add-row")}> +
{/if} diff --git a/packages/frontend-core/src/components/sheet/DeleteButton.svelte b/packages/frontend-core/src/components/sheet/DeleteButton.svelte index 2be379e21b..a5146b0ecd 100644 --- a/packages/frontend-core/src/components/sheet/DeleteButton.svelte +++ b/packages/frontend-core/src/components/sheet/DeleteButton.svelte @@ -31,8 +31,8 @@ {#if selectedRowCount} -
- +
+ Delete {selectedRowCount} row{selectedRowCount === 1 ? "" : "s"}
diff --git a/packages/frontend-core/src/components/sheet/HeaderRow.svelte b/packages/frontend-core/src/components/sheet/HeaderRow.svelte index ad33e9945c..bcd1393f73 100644 --- a/packages/frontend-core/src/components/sheet/HeaderRow.svelte +++ b/packages/frontend-core/src/components/sheet/HeaderRow.svelte @@ -4,7 +4,12 @@ import HeaderCell from "./cells/HeaderCell.svelte" import { Icon } from "@budibase/bbui" - const { renderedColumns, dispatch, config } = getContext("sheet") + const { renderedColumns, dispatch, config, ui } = getContext("sheet") + + const addColumn = () => { + ui.actions.blur() + dispatch("add-column") + }
@@ -16,7 +21,7 @@
{#if $config.allowAddColumns} -
dispatch("add-column")}> +
{/if} diff --git a/packages/frontend-core/src/components/sheet/KeyboardManager.svelte b/packages/frontend-core/src/components/sheet/KeyboardManager.svelte index 5254928004..bb3e9410e4 100644 --- a/packages/frontend-core/src/components/sheet/KeyboardManager.svelte +++ b/packages/frontend-core/src/components/sheet/KeyboardManager.svelte @@ -1,10 +1,26 @@ + + selectedCellId.set(cellId)} + on:contextmenu={e => menu.actions.open(cellId, e)} + width={column.width} +> + rows.actions.updateRow(row._id, column.name, val)} + readonly={column.schema.autocolumn} + /> + \ No newline at end of file diff --git a/packages/frontend-core/src/components/sheet/cells/HeaderCell.svelte b/packages/frontend-core/src/components/sheet/cells/HeaderCell.svelte index 6957f391df..155a287df2 100644 --- a/packages/frontend-core/src/components/sheet/cells/HeaderCell.svelte +++ b/packages/frontend-core/src/components/sheet/cells/HeaderCell.svelte @@ -17,6 +17,7 @@ renderedColumns, dispatch, config, + ui } = getContext("sheet") let anchor @@ -48,6 +49,7 @@ const onContextMenu = e => { e.preventDefault() + ui.actions.blur() open = !open } @@ -81,7 +83,7 @@
import OptionsCell from "./OptionsCell.svelte" + + export let api - + diff --git a/packages/frontend-core/src/components/sheet/cells/NumberCell.svelte b/packages/frontend-core/src/components/sheet/cells/NumberCell.svelte index 2e0ef27c39..ac5f8d8cb7 100644 --- a/packages/frontend-core/src/components/sheet/cells/NumberCell.svelte +++ b/packages/frontend-core/src/components/sheet/cells/NumberCell.svelte @@ -1,5 +1,7 @@ - + diff --git a/packages/frontend-core/src/components/sheet/cells/OptionsCell.svelte b/packages/frontend-core/src/components/sheet/cells/OptionsCell.svelte index e8117312dd..1c96eab83c 100644 --- a/packages/frontend-core/src/components/sheet/cells/OptionsCell.svelte +++ b/packages/frontend-core/src/components/sheet/cells/OptionsCell.svelte @@ -1,6 +1,7 @@ -
-
+
+
{#each values as val} {@const color = getOptionColor(val)} {#if color} @@ -74,11 +97,15 @@
{/if} - {#if open} + {#if isOpen}
e.stopPropagation()}> - {#each values as val} + {#each values as val, idx} {@const color = getOptionColor(val)} -
toggleOption(val)}> +
toggleOption(val)} + class:focused={focusedOptionIdx === idx} + >
{val}
@@ -88,8 +115,12 @@ />
{/each} - {#each unselectedOptions as option} -
toggleOption(option)}> + {#each unselectedOptions as option, idx} +
toggleOption(option)} + class:focused={focusedOptionIdx === values.length + idx} + >
{option}
@@ -177,7 +208,8 @@ .option:first-child { flex: 0 0 calc(var(--cell-height) - 1px); } - .option:hover { + .option:hover, + .option.focused { background-color: var(--spectrum-global-color-gray-200); } diff --git a/packages/frontend-core/src/components/sheet/cells/TextCell.svelte b/packages/frontend-core/src/components/sheet/cells/TextCell.svelte index 40d15818f8..ed26dffceb 100644 --- a/packages/frontend-core/src/components/sheet/cells/TextCell.svelte +++ b/packages/frontend-core/src/components/sheet/cells/TextCell.svelte @@ -1,19 +1,52 @@ {#if editable} - + (focused = true)} + on:blur={() => (focused = false)} + {type} + value={value || ""} + on:change={handleChange} + /> {:else}
{value || ""} diff --git a/packages/frontend-core/src/components/sheet/stores/reorder.js b/packages/frontend-core/src/components/sheet/stores/reorder.js index 243ab68e3c..2200707682 100644 --- a/packages/frontend-core/src/components/sheet/stores/reorder.js +++ b/packages/frontend-core/src/components/sheet/stores/reorder.js @@ -1,7 +1,7 @@ import { get, writable, derived } from "svelte/store" export const createReorderStores = context => { - const { columns, scroll, bounds, stickyColumn } = context + const { columns, scroll, bounds, stickyColumn, ui } = context const reorderInitialState = { sourceColumn: null, targetColumn: null, @@ -23,6 +23,7 @@ export const createReorderStores = context => { 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 => ({ diff --git a/packages/frontend-core/src/components/sheet/stores/resize.js b/packages/frontend-core/src/components/sheet/stores/resize.js index 56a63f7bb3..50d544a1ee 100644 --- a/packages/frontend-core/src/components/sheet/stores/resize.js +++ b/packages/frontend-core/src/components/sheet/stores/resize.js @@ -4,7 +4,7 @@ import { DefaultColumnWidth } from "./columns" export const MinColumnWidth = 100 export const createResizeStores = context => { - const { columns, stickyColumn } = context + const { columns, stickyColumn, ui } = context const initialState = { initialMouseX: null, initialWidth: null, @@ -20,6 +20,7 @@ export const createResizeStores = context => { const startResizing = (column, e) => { // Prevent propagation to stop reordering triggering e.stopPropagation() + ui.actions.blur() // Find and cache index let columnIdx = get(columns).findIndex(col => col.name === column.name) diff --git a/packages/frontend-core/src/components/sheet/stores/ui.js b/packages/frontend-core/src/components/sheet/stores/ui.js index fb1bf213ec..cf6f6ee4c3 100644 --- a/packages/frontend-core/src/components/sheet/stores/ui.js +++ b/packages/frontend-core/src/components/sheet/stores/ui.js @@ -72,6 +72,13 @@ export const createUIStores = context => { hoveredRowId.set(null) } + // Remove selected cell API when no selected cell is present + selectedCellId.subscribe(cell => { + if (!cell && get(selectedCellAPI)) { + selectedCellAPI.set(null) + } + }) + return { selectedCellId, selectedRows, From f2cf17455758f43d55dc56da65d4c766e36e95b0 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 13 Mar 2023 19:45:03 +0000 Subject: [PATCH 111/235] Ensure the selected cell is always visible --- .../src/components/sheet/Sheet.svelte | 2 +- .../src/components/sheet/SheetRow.svelte | 2 +- .../src/components/sheet/StickyColumn.svelte | 2 +- .../src/components/sheet/stores/max-scroll.js | 78 ++++++++++++++++++- .../src/components/sheet/stores/ui.js | 7 ++ 5 files changed, 86 insertions(+), 5 deletions(-) diff --git a/packages/frontend-core/src/components/sheet/Sheet.svelte b/packages/frontend-core/src/components/sheet/Sheet.svelte index 40d7e578d6..e68771b54a 100644 --- a/packages/frontend-core/src/components/sheet/Sheet.svelte +++ b/packages/frontend-core/src/components/sheet/Sheet.svelte @@ -63,10 +63,10 @@ context = { ...context, ...createScrollStores(context) } context = { ...context, ...createRowsStore(context) } context = { ...context, ...createColumnsStores(context) } - context = { ...context, ...createMaxScrollStores(context) } context = { ...context, ...createUIStores(context) } context = { ...context, ...createResizeStores(context) } context = { ...context, ...createViewportStores(context) } + context = { ...context, ...createMaxScrollStores(context) } context = { ...context, ...createReorderStores(context) } context = { ...context, ...createUserStores(context) } context = { ...context, ...createMenuStores(context) } diff --git a/packages/frontend-core/src/components/sheet/SheetRow.svelte b/packages/frontend-core/src/components/sheet/SheetRow.svelte index 3a71c64d01..aa0b7752d9 100644 --- a/packages/frontend-core/src/components/sheet/SheetRow.svelte +++ b/packages/frontend-core/src/components/sheet/SheetRow.svelte @@ -27,7 +27,7 @@
($hoveredRowId = row._id)} + on:mouseenter={() => ($hoveredRowId = row._id)} on:mouseleave={() => ($hoveredRowId = null)} > {#each $renderedColumns as column (column.name)} diff --git a/packages/frontend-core/src/components/sheet/StickyColumn.svelte b/packages/frontend-core/src/components/sheet/StickyColumn.svelte index 3a6d6ed432..4686a50cba 100644 --- a/packages/frontend-core/src/components/sheet/StickyColumn.svelte +++ b/packages/frontend-core/src/components/sheet/StickyColumn.svelte @@ -85,7 +85,7 @@ {@const containsSelectedRow = $selectedCellRow?._id === row._id}
($hoveredRowId = row._id)} + on:mouseenter={() => ($hoveredRowId = row._id)} on:mouseleave={() => ($hoveredRowId = null)} > { - const { rows, visibleColumns, stickyColumn, bounds, cellHeight, scroll } = - context + const { + rows, + visibleColumns, + stickyColumn, + bounds, + cellHeight, + scroll, + selectedCellRow, + scrolledRowCount, + visualRowCapacity, + selectedCellId, + } = context const padding = 180 // Memoize store primitives @@ -77,6 +87,70 @@ export const createMaxScrollStores = context => { } }) + // Ensure the selected cell is visible + selectedCellRow.subscribe(row => { + if (!row) { + return + } + const $scroll = get(scroll) + const $bounds = get(bounds) + const scrollBarOffset = 16 + + // Ensure row is not below bottom of screen + const rowYPos = row.__idx * cellHeight + const bottomCutoff = + $scroll.top + $bounds.height - cellHeight - scrollBarOffset + let delta = rowYPos - bottomCutoff + if (delta > 0) { + scroll.update(state => ({ + ...state, + top: state.top + delta, + })) + } + + // Ensure row is not above top of screen + else { + delta = $scroll.top - rowYPos + if (delta > 0) { + scroll.update(state => ({ + ...state, + top: Math.max(0, state.top - delta), + })) + } + } + + // Check horizontal position of columns next + const $selectedCellId = get(selectedCellId) + const $visibleColumns = get(visibleColumns) + const columnName = $selectedCellId?.split("-")[1] + const column = $visibleColumns.find(col => col.name === columnName) + if (!column) { + return + } + + // Ensure column is not cutoff on left edge + delta = $scroll.left - column.left + if (delta > 0) { + scroll.update(state => ({ + ...state, + left: state.left - delta, + })) + } + + // Ensure column is not cutoff on right edge + else { + const rightEdge = column.left + column.width + const rightBound = $bounds.width + $scroll.left + delta = rightEdge - rightBound + if (delta > 0) { + scroll.update(state => ({ + ...state, + left: state.left + delta + scrollBarOffset, + })) + } + } + }) + return { contentHeight, contentWidth, diff --git a/packages/frontend-core/src/components/sheet/stores/ui.js b/packages/frontend-core/src/components/sheet/stores/ui.js index cf6f6ee4c3..427a6be694 100644 --- a/packages/frontend-core/src/components/sheet/stores/ui.js +++ b/packages/frontend-core/src/components/sheet/stores/ui.js @@ -79,6 +79,13 @@ export const createUIStores = context => { } }) + // Remove hovered row when a cell is selected + selectedCellId.subscribe(cell => { + if (cell && get(hoveredRowId)) { + hoveredRowId.set(null) + } + }) + return { selectedCellId, selectedRows, From aefdfabe3985d9f22dfcced92c8c9ca0d5a8ae08 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 14 Mar 2023 08:52:32 +0000 Subject: [PATCH 112/235] Add icons for all data types --- .../src/components/sheet/SheetRow.svelte | 4 ---- .../src/components/sheet/StickyColumn.svelte | 1 - .../src/components/sheet/utils.js | 22 ++++++++++++++----- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/packages/frontend-core/src/components/sheet/SheetRow.svelte b/packages/frontend-core/src/components/sheet/SheetRow.svelte index aa0b7752d9..7c3f708f42 100644 --- a/packages/frontend-core/src/components/sheet/SheetRow.svelte +++ b/packages/frontend-core/src/components/sheet/SheetRow.svelte @@ -1,8 +1,6 @@ + +{#if isOpen} +