From 4647e1bc07843c74cd7703a5dd9a9b0634b2d4c1 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 6 Mar 2023 11:20:47 +0000 Subject: [PATCH] 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) + } }) }) }