1
0
Fork 0
mirror of synced 2024-09-10 22:46:09 +12:00

Fix issues with not disconnecting users when swapping datasource and improve multi-user UI

This commit is contained in:
Andrew Kingston 2023-03-06 11:20:47 +00:00
parent 3e907af8b5
commit 4647e1bc07
20 changed files with 249 additions and 104 deletions

View file

@ -106,8 +106,13 @@
</script>
<div class="wrapper">
<div class="buttons">
<div class="left-buttons">
<Sheet
tableId={$tables.selected?._id}
{API}
filter={filters}
on:add-column={createColumnModal.show}
>
<svelte:fragment slot="controls">
<CreateColumnButton
highlighted={!hasCols || !hasRows}
on:updatecolumns={null}
@ -124,8 +129,6 @@
{#if isInternal}
<CreateViewButton disabled={!hasCols || !hasRows} />
{/if}
</div>
<div class="right-buttons">
<ManageAccessButton resourceId={$tables.selected?._id} />
{#if isUsersTable}
<EditRolesButton />
@ -157,16 +160,8 @@
tableId={id}
/>
{/key}
</div>
</div>
<div class="sheet">
<Sheet
tableId={$tables.selected?._id}
{API}
filter={filters}
on:add-column={createColumnModal.show}
/>
</div>
</svelte:fragment>
</Sheet>
</div>
<!--<div>-->
@ -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);
}
</style>

View file

@ -0,0 +1,24 @@
<script>
export let user
</script>
<div class="user" style="background:{user.color};" title={user.email}>
{user.email[0]}
</div>
<style>
div {
width: 24px;
height: 24px;
display: grid;
place-items: center;
color: white;
border-radius: 50%;
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
}
div:hover {
cursor: pointer;
}
</style>

View file

@ -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

View file

@ -5,7 +5,7 @@
import { getIconForField } from "./utils"
import SheetScrollWrapper from "./SheetScrollWrapper.svelte"
const { visibleColumns, reorder } = getContext("spreadsheet")
const { visibleColumns, reorder } = getContext("sheet")
</script>
<div class="header">

View file

@ -3,7 +3,7 @@
import { getContext } from "svelte"
const { visibleColumns, hoveredRowId, rows, selectedCellId, reorder } =
getContext("spreadsheet")
getContext("sheet")
$: rowHovered = $hoveredRowId === "new"

View file

@ -9,7 +9,7 @@
visibleColumns,
cellHeight,
stickyColumn,
} = getContext("spreadsheet")
} = getContext("sheet")
const MinColumnWidth = 100
let initialMouseX = null

View file

@ -11,7 +11,7 @@
maxScrollTop,
contentWidth,
maxScrollLeft,
} = getContext("spreadsheet")
} = getContext("sheet")
// Bar config
const barOffset = 4

View file

@ -1,6 +1,7 @@
<script>
import { setContext, createEventDispatcher, onMount } from "svelte"
import { writable } from "svelte/store"
import { createAPIClient } from "../../api"
import { createReorderStores } from "./stores/reorder"
import { createViewportStores } from "./stores/viewport"
import { createRowsStore } from "./stores/rows"
@ -9,15 +10,15 @@
import { createBoundsStores } from "./stores/bounds"
import { createInterfaceStores } from "./stores/interface"
export { createUserStores } from "./stores/users"
import { createWebsocket } from "./websocket"
import { createUserStores } from "./stores/users"
import DeleteButton from "./DeleteButton.svelte"
import SheetBody from "./SheetBody.svelte"
import ResizeOverlay from "./ResizeOverlay.svelte"
import HeaderRow from "./HeaderRow.svelte"
import { createAPIClient } from "../../api"
import ScrollOverlay from "./ScrollOverlay.svelte"
import StickyColumn from "./StickyColumn.svelte"
import { createWebsocket } from "./websocket"
import { createUserStores } from "./stores/users"
import UserAvatars from "./UserAvatars.svelte"
export let API
export let tableId
@ -65,16 +66,21 @@
})
// Set context for children to consume
setContext("spreadsheet", context)
setContext("sheet", context)
// Initialise websocket for multi-user
onMount(() => {
return createWebsocket(context)
})
onMount(() => createWebsocket(context))
</script>
<div class="sheet" style="--cell-height:{cellHeight}px;" id="sheet-{rand}">
<!--<SheetControls />-->
<div class="controls">
<div class="controls-left">
<slot name="controls" />
</div>
<div class="controls-right">
<UserAvatars />
</div>
</div>
<div class="sheet-data">
<StickyColumn />
<div class="sheet-main">
@ -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);
}
</style>

View file

@ -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)}
>
<SheetScrollWrapper>
{#each $visibleRows as row}
<SheetRow {row} />
{#each $visibleRows as row, idx}
<SheetRow {row} {idx} />
{/each}
{#if $config.allowAddRows}
<NewRow />

View file

@ -1,4 +1,6 @@
<script>
import Avatar from "./Avatar.svelte"
export let header = false
export let label = false
export let rowSelected = false
@ -10,6 +12,18 @@
export let width = ""
export let center = false
export let selectedUser = null
export let rowIdx
$: style = getStyle(width, selectedUser)
const getStyle = (width, selectedUser) => {
let style = `flex: 0 0 ${width}px;`
if (selectedUser) {
console.log(selectedUser)
style += `--user-color:${selectedUser.color};`
}
return style
}
</script>
<div
@ -27,14 +41,15 @@
on:focus
on:click
on:mousedown
style="flex: 0 0 {width}px;"
{style}
data-row={rowIdx}
>
{#if selectedUser}
<div class="name">
{selectedUser.email}
<slot />
{#if selectedUser && !selected}
<div class="user">
{selectedUser.label} wwwwwwwwwwwwwwwwwwwwaaaaaa
</div>
{/if}
<slot />
</div>
<style>
@ -56,10 +71,11 @@
}
.cell.selected {
box-shadow: inset 0 0 0 2px var(--spectrum-global-color-blue-400);
z-index: 1;
z-index: 2;
}
.cell.selected-other {
box-shadow: inset 0 0 0 2px var(--spectrum-global-color-purple-400);
.cell.selected-other:not(.selected) {
z-index: 1;
box-shadow: inset 0 0 0 2px var(--user-color);
}
.cell:not(.selected) {
user-select: none;
@ -121,13 +137,29 @@
align-items: center;
}
.name {
/* Other user email */
.user {
position: absolute;
bottom: 100%;
background: var(--spectrum-global-color-purple-400);
padding: 1px 4px 0 4px;
border-radius: 2px;
padding: 1px 4px;
background: var(--user-color);
border-radius: 2px 2px 0 0;
display: none;
color: white;
font-size: 12px;
font-weight: 600;
max-width: 100%;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.cell[data-row="0"] .user {
bottom: auto;
top: 100%;
border-radius: 0 0 2px 2px;
padding: 0 4px 2px 4px;
}
.cell:hover .user {
display: block;
}
</style>

View file

@ -2,7 +2,7 @@
import { getContext } from "svelte"
import { ActionButton } from "@budibase/bbui"
const { rows } = getContext("spreadsheet")
const { rows } = getContext("sheet")
$: rowCount = $rows.length
</script>

View file

@ -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 @@
<SheetCell
{rowSelected}
{rowHovered}
rowIdx={idx}
selected={$selectedCellId === cellIdx}
selectedUser={$selectedCellMap[cellIdx]}
reorderSource={$reorder.sourceColumn === column.name}

View file

@ -12,7 +12,7 @@
hoveredRowId,
maxScrollTop,
maxScrollLeft,
} = getContext("spreadsheet")
} = getContext("sheet")
export let scrollVertically = true
export let scrollHorizontally = true

View file

@ -16,7 +16,8 @@
scroll,
reorder,
config,
} = getContext("spreadsheet")
selectedCellMap,
} = getContext("sheet")
$: scrollLeft = $scroll.left
$: rowCount = $rows.length
@ -96,7 +97,7 @@
<div class="content" on:mouseleave={() => ($hoveredRowId = null)}>
<SheetScrollWrapper scrollHorizontally={false}>
{#each $visibleRows as row}
{#each $visibleRows as row, idx}
{@const rowSelected = !!$selectedRows[row._id]}
{@const rowHovered = $hoveredRowId === row._id}
<div class="row" on:mouseenter={() => ($hoveredRowId = row._id)}>
@ -123,8 +124,10 @@
<SheetCell
{rowSelected}
{rowHovered}
rowIdx={idx}
sticky
selected={$selectedCellId === cellIdx}
selectedUser={$selectedCellMap[cellIdx]}
on:click={() => ($selectedCellId = cellIdx)}
width={$stickyColumn.width}
reorderTarget={$reorder.targetColumn === $stickyColumn.name}

View file

@ -0,0 +1,21 @@
<script>
import { getContext } from "svelte"
import Avatar from "./Avatar.svelte"
const { users } = getContext("sheet")
</script>
<div class="users">
{#each $users as user}
<Avatar {user} />
{/each}
</div>
<style>
.users {
display: flex;
flex-direction: row;
padding: 0 10px;
gap: 8px;
}
</style>

View file

@ -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;

View file

@ -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,

View file

@ -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 => {

View file

@ -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)
})

View file

@ -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)
}
})
})
}