Fix issues with not disconnecting users when swapping datasource and improve multi-user UI
This commit is contained in:
parent
3e907af8b5
commit
4647e1bc07
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import { getContext } from "svelte"
|
||||
|
||||
const { visibleColumns, hoveredRowId, rows, selectedCellId, reorder } =
|
||||
getContext("spreadsheet")
|
||||
getContext("sheet")
|
||||
|
||||
$: rowHovered = $hoveredRowId === "new"
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
visibleColumns,
|
||||
cellHeight,
|
||||
stickyColumn,
|
||||
} = getContext("spreadsheet")
|
||||
} = getContext("sheet")
|
||||
const MinColumnWidth = 100
|
||||
|
||||
let initialMouseX = null
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
maxScrollTop,
|
||||
contentWidth,
|
||||
maxScrollLeft,
|
||||
} = getContext("spreadsheet")
|
||||
} = getContext("sheet")
|
||||
|
||||
// Bar config
|
||||
const barOffset = 4
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
hoveredRowId,
|
||||
maxScrollTop,
|
||||
maxScrollLeft,
|
||||
} = getContext("spreadsheet")
|
||||
} = getContext("sheet")
|
||||
|
||||
export let scrollVertically = true
|
||||
export let scrollHorizontally = true
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue