@budibase/frontend-core 2.6.19-alpha.26 → 2.6.19-alpha.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/components/UserAvatar.svelte +58 -0
- package/src/components/grid/cells/GridCell.svelte +29 -14
- package/src/components/grid/cells/TextCell.svelte +1 -1
- package/src/components/grid/layout/Grid.svelte +12 -3
- package/src/components/grid/layout/UserAvatars.svelte +14 -4
- package/src/components/grid/lib/websocket.js +28 -28
- package/src/components/grid/overlays/KeyboardManager.svelte +2 -0
- package/src/components/grid/stores/columns.js +18 -8
- package/src/components/grid/stores/rows.js +22 -12
- package/src/components/grid/stores/users.js +17 -62
- package/src/components/index.js +1 -0
- package/src/utils/index.js +1 -0
- package/src/utils/websocket.js +26 -0
- package/src/components/grid/layout/Avatar.svelte +0 -24
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@budibase/frontend-core",
|
|
3
|
-
"version": "2.6.19-alpha.
|
|
3
|
+
"version": "2.6.19-alpha.28",
|
|
4
4
|
"description": "Budibase frontend core libraries used in builder and client",
|
|
5
5
|
"author": "Budibase",
|
|
6
6
|
"license": "MPL-2.0",
|
|
7
7
|
"svelte": "src/index.js",
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@budibase/bbui": "2.6.19-alpha.
|
|
10
|
-
"@budibase/shared-core": "2.6.19-alpha.
|
|
9
|
+
"@budibase/bbui": "2.6.19-alpha.28",
|
|
10
|
+
"@budibase/shared-core": "2.6.19-alpha.28",
|
|
11
11
|
"dayjs": "^1.11.7",
|
|
12
12
|
"lodash": "^4.17.21",
|
|
13
13
|
"socket.io-client": "^4.6.1",
|
|
14
14
|
"svelte": "^3.46.2"
|
|
15
15
|
},
|
|
16
|
-
"gitHead": "
|
|
16
|
+
"gitHead": "11c9bcc5f5b0a6c900474ab3f90371767132bd35"
|
|
17
17
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { Avatar, Tooltip } from "@budibase/bbui"
|
|
3
|
+
import { helpers } from "@budibase/shared-core"
|
|
4
|
+
|
|
5
|
+
export let user
|
|
6
|
+
export let size
|
|
7
|
+
export let tooltipDirection = "top"
|
|
8
|
+
export let showTooltip = true
|
|
9
|
+
|
|
10
|
+
$: tooltipStyle = getTooltipStyle(tooltipDirection)
|
|
11
|
+
|
|
12
|
+
const getTooltipStyle = direction => {
|
|
13
|
+
if (!direction) {
|
|
14
|
+
return ""
|
|
15
|
+
}
|
|
16
|
+
if (direction === "top") {
|
|
17
|
+
return "transform: translateX(-50%) translateY(-100%);"
|
|
18
|
+
} else if (direction === "bottom") {
|
|
19
|
+
return "transform: translateX(-50%) translateY(100%);"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
{#if user}
|
|
25
|
+
<div class="user-avatar">
|
|
26
|
+
<Avatar
|
|
27
|
+
{size}
|
|
28
|
+
initials={helpers.getUserInitials(user)}
|
|
29
|
+
color={helpers.getUserColor(user)}
|
|
30
|
+
/>
|
|
31
|
+
{#if showTooltip}
|
|
32
|
+
<div class="tooltip" style={tooltipStyle}>
|
|
33
|
+
<Tooltip
|
|
34
|
+
direction={tooltipDirection}
|
|
35
|
+
textWrapping
|
|
36
|
+
text={user.email}
|
|
37
|
+
size="S"
|
|
38
|
+
/>
|
|
39
|
+
</div>
|
|
40
|
+
{/if}
|
|
41
|
+
</div>
|
|
42
|
+
{/if}
|
|
43
|
+
|
|
44
|
+
<style>
|
|
45
|
+
.user-avatar {
|
|
46
|
+
position: relative;
|
|
47
|
+
}
|
|
48
|
+
.tooltip {
|
|
49
|
+
display: none;
|
|
50
|
+
position: absolute;
|
|
51
|
+
top: 0;
|
|
52
|
+
left: 50%;
|
|
53
|
+
white-space: nowrap;
|
|
54
|
+
}
|
|
55
|
+
.user-avatar:hover .tooltip {
|
|
56
|
+
display: block;
|
|
57
|
+
}
|
|
58
|
+
</style>
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
const getStyle = (width, selectedUser) => {
|
|
17
17
|
let style = `flex: 0 0 ${width}px;`
|
|
18
18
|
if (selectedUser) {
|
|
19
|
-
style += `--
|
|
19
|
+
style += `--user-color:${selectedUser.color};`
|
|
20
20
|
}
|
|
21
21
|
return style
|
|
22
22
|
}
|
|
@@ -99,14 +99,15 @@
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
/* Cell border for cells with labels */
|
|
102
|
-
.cell.error:after
|
|
103
|
-
.cell.selected-other:not(.focused):after {
|
|
102
|
+
.cell.error:after {
|
|
104
103
|
border-radius: 0 2px 2px 2px;
|
|
105
104
|
}
|
|
106
|
-
.cell.top.error:after
|
|
107
|
-
.cell.top.selected-other:not(.focused):after {
|
|
105
|
+
.cell.top.error:after {
|
|
108
106
|
border-radius: 2px 2px 2px 0;
|
|
109
107
|
}
|
|
108
|
+
.cell.selected-other:not(.focused):after {
|
|
109
|
+
border-radius: 2px;
|
|
110
|
+
}
|
|
110
111
|
|
|
111
112
|
/* Cell z-index */
|
|
112
113
|
.cell.error,
|
|
@@ -116,21 +117,30 @@
|
|
|
116
117
|
.cell.focused {
|
|
117
118
|
z-index: 2;
|
|
118
119
|
}
|
|
120
|
+
.cell.selected-other:hover {
|
|
121
|
+
z-index: 2;
|
|
122
|
+
}
|
|
123
|
+
.cell:not(.focused) {
|
|
124
|
+
user-select: none;
|
|
125
|
+
}
|
|
126
|
+
.cell:hover {
|
|
127
|
+
cursor: default;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* Cell color overrides */
|
|
131
|
+
.cell.selected-other {
|
|
132
|
+
--cell-color: var(--user-color);
|
|
133
|
+
}
|
|
119
134
|
.cell.focused {
|
|
120
135
|
--cell-color: var(--spectrum-global-color-blue-400);
|
|
121
136
|
}
|
|
122
137
|
.cell.error {
|
|
123
138
|
--cell-color: var(--spectrum-global-color-red-500);
|
|
124
139
|
}
|
|
125
|
-
.cell.readonly {
|
|
140
|
+
.cell.focused.readonly {
|
|
126
141
|
--cell-color: var(--spectrum-global-color-gray-600);
|
|
127
142
|
}
|
|
128
|
-
|
|
129
|
-
user-select: none;
|
|
130
|
-
}
|
|
131
|
-
.cell:hover {
|
|
132
|
-
cursor: default;
|
|
133
|
-
}
|
|
143
|
+
|
|
134
144
|
.cell.highlighted:not(.focused),
|
|
135
145
|
.cell.focused.readonly {
|
|
136
146
|
--cell-background: var(--cell-background-hover);
|
|
@@ -146,7 +156,7 @@
|
|
|
146
156
|
left: 0;
|
|
147
157
|
padding: 1px 4px 3px 4px;
|
|
148
158
|
margin: 0 0 -2px 0;
|
|
149
|
-
background: var(--
|
|
159
|
+
background: var(--cell-color);
|
|
150
160
|
border-radius: 2px;
|
|
151
161
|
display: block;
|
|
152
162
|
color: white;
|
|
@@ -160,11 +170,16 @@
|
|
|
160
170
|
.cell.top .label {
|
|
161
171
|
bottom: auto;
|
|
162
172
|
top: 100%;
|
|
163
|
-
border-radius: 0 2px 2px 2px;
|
|
164
173
|
padding: 2px 4px 2px 4px;
|
|
165
174
|
margin: -2px 0 0 0;
|
|
166
175
|
}
|
|
167
176
|
.error .label {
|
|
168
177
|
background: var(--spectrum-global-color-red-500);
|
|
169
178
|
}
|
|
179
|
+
.selected-other:not(.error) .label {
|
|
180
|
+
display: none;
|
|
181
|
+
}
|
|
182
|
+
.selected-other:not(.error):hover .label {
|
|
183
|
+
display: block;
|
|
184
|
+
}
|
|
170
185
|
</style>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
import { setContext } from "svelte"
|
|
2
|
+
import { setContext, onMount } from "svelte"
|
|
3
3
|
import { writable } from "svelte/store"
|
|
4
4
|
import { fade } from "svelte/transition"
|
|
5
5
|
import { clickOutside, ProgressCircle } from "@budibase/bbui"
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
import RowHeightButton from "../controls/RowHeightButton.svelte"
|
|
25
25
|
import ColumnWidthButton from "../controls/ColumnWidthButton.svelte"
|
|
26
26
|
import NewRow from "./NewRow.svelte"
|
|
27
|
+
import { createGridWebsocket } from "../lib/websocket"
|
|
27
28
|
import {
|
|
28
29
|
MaxCellRenderHeight,
|
|
29
30
|
MaxCellRenderWidthOverflow,
|
|
@@ -42,6 +43,8 @@
|
|
|
42
43
|
export let allowEditRows = true
|
|
43
44
|
export let allowDeleteRows = true
|
|
44
45
|
export let stripeRows = false
|
|
46
|
+
export let collaboration = true
|
|
47
|
+
export let showAvatars = true
|
|
45
48
|
|
|
46
49
|
// Unique identifier for DOM nodes inside this instance
|
|
47
50
|
const rand = Math.random()
|
|
@@ -102,7 +105,11 @@
|
|
|
102
105
|
export const getContext = () => context
|
|
103
106
|
|
|
104
107
|
// Initialise websocket for multi-user
|
|
105
|
-
|
|
108
|
+
onMount(() => {
|
|
109
|
+
if (collaboration) {
|
|
110
|
+
return createGridWebsocket(context)
|
|
111
|
+
}
|
|
112
|
+
})
|
|
106
113
|
</script>
|
|
107
114
|
|
|
108
115
|
<div
|
|
@@ -124,7 +131,9 @@
|
|
|
124
131
|
<RowHeightButton />
|
|
125
132
|
</div>
|
|
126
133
|
<div class="controls-right">
|
|
127
|
-
|
|
134
|
+
{#if showAvatars}
|
|
135
|
+
<UserAvatars />
|
|
136
|
+
{/if}
|
|
128
137
|
</div>
|
|
129
138
|
</div>
|
|
130
139
|
{#if $loaded}
|
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import { getContext } from "svelte"
|
|
3
|
-
import
|
|
3
|
+
import UserAvatar from "../../UserAvatar.svelte"
|
|
4
4
|
|
|
5
5
|
const { users } = getContext("grid")
|
|
6
|
+
|
|
7
|
+
$: uniqueUsers = unique($users)
|
|
8
|
+
|
|
9
|
+
const unique = users => {
|
|
10
|
+
let uniqueUsers = {}
|
|
11
|
+
users?.forEach(user => {
|
|
12
|
+
uniqueUsers[user.email] = user
|
|
13
|
+
})
|
|
14
|
+
return Object.values(uniqueUsers)
|
|
15
|
+
}
|
|
6
16
|
</script>
|
|
7
17
|
|
|
8
18
|
<div class="users">
|
|
9
|
-
{#each
|
|
10
|
-
<
|
|
19
|
+
{#each uniqueUsers as user}
|
|
20
|
+
<UserAvatar {user} />
|
|
11
21
|
{/each}
|
|
12
22
|
</div>
|
|
13
23
|
|
|
@@ -15,6 +25,6 @@
|
|
|
15
25
|
.users {
|
|
16
26
|
display: flex;
|
|
17
27
|
flex-direction: row;
|
|
18
|
-
gap:
|
|
28
|
+
gap: 4px;
|
|
19
29
|
}
|
|
20
30
|
</style>
|
|
@@ -1,24 +1,9 @@
|
|
|
1
1
|
import { get } from "svelte/store"
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
export const
|
|
5
|
-
const { rows, tableId, users,
|
|
6
|
-
|
|
7
|
-
// Determine connection info
|
|
8
|
-
const tls = location.protocol === "https:"
|
|
9
|
-
const proto = tls ? "wss:" : "ws:"
|
|
10
|
-
const host = location.hostname
|
|
11
|
-
const port = location.port || (tls ? 443 : 80)
|
|
12
|
-
const socket = io(`${proto}//${host}:${port}`, {
|
|
13
|
-
path: "/socket/grid",
|
|
14
|
-
// Cap reconnection attempts to 3 (total of 15 seconds before giving up)
|
|
15
|
-
reconnectionAttempts: 3,
|
|
16
|
-
// Delay reconnection attempt by 5 seconds
|
|
17
|
-
reconnectionDelay: 5000,
|
|
18
|
-
reconnectionDelayMax: 5000,
|
|
19
|
-
// Timeout after 4 seconds so we never stack requests
|
|
20
|
-
timeout: 4000,
|
|
21
|
-
})
|
|
2
|
+
import { createWebsocket } from "../../../utils"
|
|
3
|
+
|
|
4
|
+
export const createGridWebsocket = context => {
|
|
5
|
+
const { rows, tableId, users, focusedCellId, table } = context
|
|
6
|
+
const socket = createWebsocket("/socket/grid")
|
|
22
7
|
|
|
23
8
|
const connectToTable = tableId => {
|
|
24
9
|
if (!socket.connected) {
|
|
@@ -28,27 +13,42 @@ export const createWebsocket = context => {
|
|
|
28
13
|
socket.emit("select-table", tableId, response => {
|
|
29
14
|
// handle initial connection info
|
|
30
15
|
users.set(response.users)
|
|
31
|
-
userId.set(response.id)
|
|
32
16
|
})
|
|
33
17
|
}
|
|
34
18
|
|
|
35
|
-
//
|
|
19
|
+
// Connection events
|
|
36
20
|
socket.on("connect", () => {
|
|
37
21
|
connectToTable(get(tableId))
|
|
38
22
|
})
|
|
39
|
-
socket.on("
|
|
40
|
-
|
|
41
|
-
rows.actions.refreshRow(data.id)
|
|
42
|
-
}
|
|
23
|
+
socket.on("connect_error", err => {
|
|
24
|
+
console.log("Failed to connect to grid websocket:", err.message)
|
|
43
25
|
})
|
|
26
|
+
|
|
27
|
+
// User events
|
|
44
28
|
socket.on("user-update", user => {
|
|
45
29
|
users.actions.updateUser(user)
|
|
46
30
|
})
|
|
47
31
|
socket.on("user-disconnect", user => {
|
|
48
32
|
users.actions.removeUser(user)
|
|
49
33
|
})
|
|
50
|
-
|
|
51
|
-
|
|
34
|
+
|
|
35
|
+
// Row events
|
|
36
|
+
socket.on("row-change", async data => {
|
|
37
|
+
if (data.id) {
|
|
38
|
+
rows.actions.replaceRow(data.id, data.row)
|
|
39
|
+
} else if (data.row.id) {
|
|
40
|
+
// Handle users table edge case
|
|
41
|
+
await rows.actions.refreshRow(data.row.id)
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
// Table events
|
|
46
|
+
socket.on("table-change", data => {
|
|
47
|
+
// Only update table if one exists. If the table was deleted then we don't
|
|
48
|
+
// want to know - let the builder navigate away
|
|
49
|
+
if (data.table) {
|
|
50
|
+
table.set(data.table)
|
|
51
|
+
}
|
|
52
52
|
})
|
|
53
53
|
|
|
54
54
|
// Change websocket connection when table changes
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
dispatch,
|
|
15
15
|
selectedRows,
|
|
16
16
|
config,
|
|
17
|
+
menu,
|
|
17
18
|
} = getContext("grid")
|
|
18
19
|
|
|
19
20
|
const ignoredOriginSelectors = [
|
|
@@ -61,6 +62,7 @@
|
|
|
61
62
|
} else {
|
|
62
63
|
$focusedCellId = null
|
|
63
64
|
}
|
|
65
|
+
menu.actions.close()
|
|
64
66
|
return
|
|
65
67
|
} else if (e.key === "Tab") {
|
|
66
68
|
e.preventDefault()
|
|
@@ -46,7 +46,7 @@ export const createStores = () => {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
export const deriveStores = context => {
|
|
49
|
-
const { table, columns, stickyColumn, API
|
|
49
|
+
const { table, columns, stickyColumn, API } = context
|
|
50
50
|
|
|
51
51
|
// Updates the tables primary display column
|
|
52
52
|
const changePrimaryDisplay = async column => {
|
|
@@ -90,10 +90,6 @@ export const deriveStores = context => {
|
|
|
90
90
|
// Update local state
|
|
91
91
|
table.set(newTable)
|
|
92
92
|
|
|
93
|
-
// Broadcast event so that we can keep sync with external state
|
|
94
|
-
// (e.g. data section which maintains a list of table definitions)
|
|
95
|
-
dispatch("updatetable", newTable)
|
|
96
|
-
|
|
97
93
|
// Update server
|
|
98
94
|
await API.saveTable(newTable)
|
|
99
95
|
}
|
|
@@ -116,10 +112,24 @@ export const initialise = context => {
|
|
|
116
112
|
const schema = derived(
|
|
117
113
|
[table, schemaOverrides],
|
|
118
114
|
([$table, $schemaOverrides]) => {
|
|
119
|
-
|
|
120
|
-
if (!newSchema) {
|
|
115
|
+
if (!$table?.schema) {
|
|
121
116
|
return null
|
|
122
117
|
}
|
|
118
|
+
let newSchema = { ...$table?.schema }
|
|
119
|
+
|
|
120
|
+
// Edge case to temporarily allow deletion of duplicated user
|
|
121
|
+
// fields that were saved with the "disabled" flag set.
|
|
122
|
+
// By overriding the saved schema we ensure only overrides can
|
|
123
|
+
// set the disabled flag.
|
|
124
|
+
// TODO: remove in future
|
|
125
|
+
Object.keys(newSchema).forEach(field => {
|
|
126
|
+
newSchema[field] = {
|
|
127
|
+
...newSchema[field],
|
|
128
|
+
disabled: false,
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
// Apply schema overrides
|
|
123
133
|
Object.keys($schemaOverrides || {}).forEach(field => {
|
|
124
134
|
if (newSchema[field]) {
|
|
125
135
|
newSchema[field] = {
|
|
@@ -160,7 +170,7 @@ export const initialise = context => {
|
|
|
160
170
|
fields
|
|
161
171
|
.map(field => ({
|
|
162
172
|
name: field,
|
|
163
|
-
label: $schema[field].
|
|
173
|
+
label: $schema[field].displayName || field,
|
|
164
174
|
schema: $schema[field],
|
|
165
175
|
width: $schema[field].width || DefaultColumnWidth,
|
|
166
176
|
visible: $schema[field].visible ?? true,
|
|
@@ -268,27 +268,25 @@ export const deriveStores = context => {
|
|
|
268
268
|
return res?.rows?.[0]
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
-
//
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
const newRow = await fetchRow(id)
|
|
275
|
-
|
|
271
|
+
// Replaces a row in state with the newly defined row, handling updates,
|
|
272
|
+
// addition and deletion
|
|
273
|
+
const replaceRow = (id, row) => {
|
|
276
274
|
// Get index of row to check if it exists
|
|
277
275
|
const $rows = get(rows)
|
|
278
276
|
const $rowLookupMap = get(rowLookupMap)
|
|
279
277
|
const index = $rowLookupMap[id]
|
|
280
278
|
|
|
281
279
|
// Process as either an update, addition or deletion
|
|
282
|
-
if (
|
|
280
|
+
if (row) {
|
|
283
281
|
if (index != null) {
|
|
284
282
|
// An existing row was updated
|
|
285
283
|
rows.update(state => {
|
|
286
|
-
state[index] = { ...
|
|
284
|
+
state[index] = { ...row }
|
|
287
285
|
return state
|
|
288
286
|
})
|
|
289
287
|
} else {
|
|
290
288
|
// A new row was created
|
|
291
|
-
handleNewRows([
|
|
289
|
+
handleNewRows([row])
|
|
292
290
|
}
|
|
293
291
|
} else if (index != null) {
|
|
294
292
|
// A row was removed
|
|
@@ -296,6 +294,12 @@ export const deriveStores = context => {
|
|
|
296
294
|
}
|
|
297
295
|
}
|
|
298
296
|
|
|
297
|
+
// Refreshes a specific row
|
|
298
|
+
const refreshRow = async id => {
|
|
299
|
+
const row = await fetchRow(id)
|
|
300
|
+
replaceRow(id, row)
|
|
301
|
+
}
|
|
302
|
+
|
|
299
303
|
// Refreshes all data
|
|
300
304
|
const refreshData = () => {
|
|
301
305
|
get(fetch)?.getInitialData()
|
|
@@ -341,10 +345,15 @@ export const deriveStores = context => {
|
|
|
341
345
|
const saved = await API.saveRow({ ...row, ...get(rowChangeCache)[rowId] })
|
|
342
346
|
|
|
343
347
|
// Update state after a successful change
|
|
344
|
-
|
|
345
|
-
state
|
|
346
|
-
|
|
347
|
-
|
|
348
|
+
if (saved?._id) {
|
|
349
|
+
rows.update(state => {
|
|
350
|
+
state[index] = saved
|
|
351
|
+
return state.slice()
|
|
352
|
+
})
|
|
353
|
+
} else if (saved?.id) {
|
|
354
|
+
// Handle users table edge case
|
|
355
|
+
await refreshRow(saved.id)
|
|
356
|
+
}
|
|
348
357
|
rowChangeCache.update(state => {
|
|
349
358
|
delete state[rowId]
|
|
350
359
|
return state
|
|
@@ -455,6 +464,7 @@ export const deriveStores = context => {
|
|
|
455
464
|
hasRow,
|
|
456
465
|
loadNextPage,
|
|
457
466
|
refreshRow,
|
|
467
|
+
replaceRow,
|
|
458
468
|
refreshData,
|
|
459
469
|
refreshTableDefinition,
|
|
460
470
|
},
|
|
@@ -1,95 +1,50 @@
|
|
|
1
1
|
import { writable, get, derived } from "svelte/store"
|
|
2
|
+
import { helpers } from "@budibase/shared-core"
|
|
2
3
|
|
|
3
4
|
export const createStores = () => {
|
|
4
5
|
const users = writable([])
|
|
5
|
-
const userId = writable(null)
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
// Place current user first
|
|
15
|
-
.sort((a, b) => {
|
|
16
|
-
if (a.id === $userId) {
|
|
17
|
-
return -1
|
|
18
|
-
} else if (b.id === $userId) {
|
|
19
|
-
return 1
|
|
20
|
-
} else {
|
|
21
|
-
return 0
|
|
22
|
-
}
|
|
23
|
-
})
|
|
24
|
-
// Enrich users with colors
|
|
25
|
-
.map((user, idx) => {
|
|
26
|
-
// Generate random colour hue
|
|
27
|
-
let hue = 1
|
|
28
|
-
for (let i = 0; i < user.email.length && i < 5; i++) {
|
|
29
|
-
hue *= user.email.charCodeAt(i + 1)
|
|
30
|
-
hue /= 17
|
|
31
|
-
}
|
|
32
|
-
hue = hue % 360
|
|
33
|
-
const color =
|
|
34
|
-
idx === 0
|
|
35
|
-
? "var(--spectrum-global-color-blue-400)"
|
|
36
|
-
: `hsl(${hue}, 50%, 40%)`
|
|
37
|
-
|
|
38
|
-
// Generate friendly label
|
|
39
|
-
let label = user.email
|
|
40
|
-
if (user.firstName) {
|
|
41
|
-
label = user.firstName
|
|
42
|
-
if (user.lastName) {
|
|
43
|
-
label += ` ${user.lastName}`
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return {
|
|
48
|
-
...user,
|
|
49
|
-
color,
|
|
50
|
-
label,
|
|
51
|
-
}
|
|
52
|
-
})
|
|
53
|
-
)
|
|
54
|
-
},
|
|
55
|
-
[]
|
|
56
|
-
)
|
|
7
|
+
const enrichedUsers = derived(users, $users => {
|
|
8
|
+
return $users.map(user => ({
|
|
9
|
+
...user,
|
|
10
|
+
color: helpers.getUserColor(user),
|
|
11
|
+
label: helpers.getUserLabel(user),
|
|
12
|
+
}))
|
|
13
|
+
})
|
|
57
14
|
|
|
58
15
|
return {
|
|
59
16
|
users: {
|
|
60
17
|
...users,
|
|
61
18
|
subscribe: enrichedUsers.subscribe,
|
|
62
19
|
},
|
|
63
|
-
userId,
|
|
64
20
|
}
|
|
65
21
|
}
|
|
66
22
|
|
|
67
23
|
export const deriveStores = context => {
|
|
68
|
-
const { users,
|
|
24
|
+
const { users, focusedCellId } = context
|
|
69
25
|
|
|
70
26
|
// Generate a lookup map of cell ID to the user that has it selected, to make
|
|
71
27
|
// lookups inside cells extremely fast
|
|
72
28
|
const selectedCellMap = derived(
|
|
73
|
-
[users,
|
|
74
|
-
([$
|
|
29
|
+
[users, focusedCellId],
|
|
30
|
+
([$users, $focusedCellId]) => {
|
|
75
31
|
let map = {}
|
|
76
|
-
$
|
|
77
|
-
if (user.focusedCellId && user.
|
|
32
|
+
$users.forEach(user => {
|
|
33
|
+
if (user.focusedCellId && user.focusedCellId !== $focusedCellId) {
|
|
78
34
|
map[user.focusedCellId] = user
|
|
79
35
|
}
|
|
80
36
|
})
|
|
81
37
|
return map
|
|
82
|
-
}
|
|
83
|
-
{}
|
|
38
|
+
}
|
|
84
39
|
)
|
|
85
40
|
|
|
86
41
|
const updateUser = user => {
|
|
87
42
|
const $users = get(users)
|
|
88
|
-
|
|
89
|
-
if (index === -1) {
|
|
43
|
+
if (!$users.some(x => x.sessionId === user.sessionId)) {
|
|
90
44
|
users.set([...$users, user])
|
|
91
45
|
} else {
|
|
92
46
|
users.update(state => {
|
|
47
|
+
const index = state.findIndex(x => x.sessionId === user.sessionId)
|
|
93
48
|
state[index] = user
|
|
94
49
|
return state.slice()
|
|
95
50
|
})
|
|
@@ -98,7 +53,7 @@ export const deriveStores = context => {
|
|
|
98
53
|
|
|
99
54
|
const removeUser = user => {
|
|
100
55
|
users.update(state => {
|
|
101
|
-
return state.filter(x => x.
|
|
56
|
+
return state.filter(x => x.sessionId !== user.sessionId)
|
|
102
57
|
})
|
|
103
58
|
}
|
|
104
59
|
|
package/src/components/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { default as SplitPage } from "./SplitPage.svelte"
|
|
2
2
|
export { default as TestimonialPage } from "./TestimonialPage.svelte"
|
|
3
3
|
export { default as Testimonial } from "./Testimonial.svelte"
|
|
4
|
+
export { default as UserAvatar } from "./UserAvatar.svelte"
|
|
4
5
|
export { Grid } from "./grid"
|
package/src/utils/index.js
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { io } from "socket.io-client"
|
|
2
|
+
|
|
3
|
+
export const createWebsocket = path => {
|
|
4
|
+
if (!path) {
|
|
5
|
+
throw "A websocket path must be provided"
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Determine connection info
|
|
9
|
+
const tls = location.protocol === "https:"
|
|
10
|
+
const proto = tls ? "wss:" : "ws:"
|
|
11
|
+
const host = location.hostname
|
|
12
|
+
const port = location.port || (tls ? 443 : 80)
|
|
13
|
+
return io(`${proto}//${host}:${port}`, {
|
|
14
|
+
path,
|
|
15
|
+
// Cap reconnection attempts to 3 (total of 15 seconds before giving up)
|
|
16
|
+
reconnectionAttempts: 3,
|
|
17
|
+
// Delay reconnection attempt by 5 seconds
|
|
18
|
+
reconnectionDelay: 5000,
|
|
19
|
+
reconnectionDelayMax: 5000,
|
|
20
|
+
// Timeout after 4 seconds so we never stack requests
|
|
21
|
+
timeout: 4000,
|
|
22
|
+
// Disable polling and rely on websocket only, as HTTP transport
|
|
23
|
+
// will only work with sticky sessions which we don't have
|
|
24
|
+
transports: ["websocket"]
|
|
25
|
+
})
|
|
26
|
+
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
<script>
|
|
2
|
-
export let user
|
|
3
|
-
</script>
|
|
4
|
-
|
|
5
|
-
<div class="user" style="background:{user.color};" title={user.email}>
|
|
6
|
-
{user.email[0]}
|
|
7
|
-
</div>
|
|
8
|
-
|
|
9
|
-
<style>
|
|
10
|
-
div {
|
|
11
|
-
width: 24px;
|
|
12
|
-
height: 24px;
|
|
13
|
-
display: grid;
|
|
14
|
-
place-items: center;
|
|
15
|
-
color: white;
|
|
16
|
-
border-radius: 50%;
|
|
17
|
-
font-size: 12px;
|
|
18
|
-
font-weight: 700;
|
|
19
|
-
text-transform: uppercase;
|
|
20
|
-
}
|
|
21
|
-
div:hover {
|
|
22
|
-
cursor: pointer;
|
|
23
|
-
}
|
|
24
|
-
</style>
|