@budibase/frontend-core 2.6.19-alpha.4 → 2.6.19-alpha.41
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/api/app.js +6 -0
- package/src/api/automations.js +2 -2
- package/src/api/datasources.js +11 -0
- package/src/api/tables.js +4 -2
- package/src/components/UserAvatar.svelte +58 -0
- package/src/components/grid/cells/BooleanCell.svelte +3 -0
- package/src/components/grid/cells/DataCell.svelte +2 -0
- package/src/components/grid/cells/GridCell.svelte +36 -16
- package/src/components/grid/cells/GutterCell.svelte +2 -10
- package/src/components/grid/cells/HeaderCell.svelte +5 -1
- package/src/components/grid/cells/TextCell.svelte +1 -1
- package/src/components/grid/layout/Grid.svelte +22 -3
- package/src/components/grid/layout/GridBody.svelte +5 -1
- package/src/components/grid/layout/GridRow.svelte +3 -2
- package/src/components/grid/layout/HeaderRow.svelte +1 -1
- package/src/components/grid/layout/KeyboardShortcut.svelte +1 -1
- package/src/components/grid/layout/NewRow.svelte +3 -3
- package/src/components/grid/layout/StickyColumn.svelte +2 -1
- package/src/components/grid/layout/UserAvatars.svelte +14 -4
- package/src/components/grid/lib/websocket.js +33 -32
- package/src/components/grid/overlays/KeyboardManager.svelte +3 -4
- package/src/components/grid/stores/columns.js +18 -8
- package/src/components/grid/stores/reorder.js +77 -9
- package/src/components/grid/stores/rows.js +22 -12
- package/src/components/grid/stores/ui.js +20 -1
- package/src/components/grid/stores/users.js +18 -63
- package/src/components/index.js +1 -0
- package/src/constants.js +1 -0
- package/src/utils/index.js +1 -0
- package/src/utils/websocket.js +41 -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.41",
|
|
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.41",
|
|
10
|
+
"@budibase/shared-core": "2.6.19-alpha.41",
|
|
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": "b45483f4b33be53615d8e5d1e3df8915f06bf0dd"
|
|
17
17
|
}
|
package/src/api/app.js
CHANGED
package/src/api/automations.js
CHANGED
|
@@ -4,10 +4,10 @@ export const buildAutomationEndpoints = API => ({
|
|
|
4
4
|
* @param automationId the ID of the automation to trigger
|
|
5
5
|
* @param fields the fields to trigger the automation with
|
|
6
6
|
*/
|
|
7
|
-
triggerAutomation: async ({ automationId, fields }) => {
|
|
7
|
+
triggerAutomation: async ({ automationId, fields, timeout }) => {
|
|
8
8
|
return await API.post({
|
|
9
9
|
url: `/api/automations/${automationId}/trigger`,
|
|
10
|
-
body: { fields },
|
|
10
|
+
body: { fields, timeout },
|
|
11
11
|
})
|
|
12
12
|
},
|
|
13
13
|
|
package/src/api/datasources.js
CHANGED
|
@@ -58,4 +58,15 @@ export const buildDatasourceEndpoints = API => ({
|
|
|
58
58
|
url: `/api/datasources/${datasourceId}/${datasourceRev}`,
|
|
59
59
|
})
|
|
60
60
|
},
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Validate a datasource configuration
|
|
64
|
+
* @param datasource the datasource configuration to validate
|
|
65
|
+
*/
|
|
66
|
+
validateDatasource: async datasource => {
|
|
67
|
+
return await API.post({
|
|
68
|
+
url: `/api/datasources/verify`,
|
|
69
|
+
body: { datasource },
|
|
70
|
+
})
|
|
71
|
+
},
|
|
61
72
|
})
|
package/src/api/tables.js
CHANGED
|
@@ -62,13 +62,15 @@ export const buildTableEndpoints = API => ({
|
|
|
62
62
|
/**
|
|
63
63
|
* Imports data into an existing table
|
|
64
64
|
* @param tableId the table ID to import to
|
|
65
|
-
* @param
|
|
65
|
+
* @param rows the data import object
|
|
66
|
+
* @param identifierFields column names to be used as keys for overwriting existing rows
|
|
66
67
|
*/
|
|
67
|
-
importTableData: async ({ tableId, rows }) => {
|
|
68
|
+
importTableData: async ({ tableId, rows, identifierFields }) => {
|
|
68
69
|
return await API.post({
|
|
69
70
|
url: `/api/tables/${tableId}/import`,
|
|
70
71
|
body: {
|
|
71
72
|
rows,
|
|
73
|
+
identifierFields,
|
|
72
74
|
},
|
|
73
75
|
})
|
|
74
76
|
},
|
|
@@ -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>
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
export let selected
|
|
12
12
|
export let rowFocused
|
|
13
13
|
export let rowIdx
|
|
14
|
+
export let topRow = false
|
|
14
15
|
export let focused
|
|
15
16
|
export let selectedUser
|
|
16
17
|
export let column
|
|
@@ -68,6 +69,7 @@
|
|
|
68
69
|
{highlighted}
|
|
69
70
|
{selected}
|
|
70
71
|
{rowIdx}
|
|
72
|
+
{topRow}
|
|
71
73
|
{focused}
|
|
72
74
|
{selectedUser}
|
|
73
75
|
{readonly}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
export let selectedUser = null
|
|
7
7
|
export let error = null
|
|
8
8
|
export let rowIdx
|
|
9
|
+
export let topRow = false
|
|
9
10
|
export let defaultHeight = false
|
|
10
11
|
export let center = false
|
|
11
12
|
export let readonly = false
|
|
@@ -15,7 +16,7 @@
|
|
|
15
16
|
const getStyle = (width, selectedUser) => {
|
|
16
17
|
let style = `flex: 0 0 ${width}px;`
|
|
17
18
|
if (selectedUser) {
|
|
18
|
-
style += `--
|
|
19
|
+
style += `--user-color:${selectedUser.color};`
|
|
19
20
|
}
|
|
20
21
|
return style
|
|
21
22
|
}
|
|
@@ -31,13 +32,14 @@
|
|
|
31
32
|
class:readonly
|
|
32
33
|
class:default-height={defaultHeight}
|
|
33
34
|
class:selected-other={selectedUser != null}
|
|
35
|
+
class:alt={rowIdx % 2 === 1}
|
|
36
|
+
class:top={topRow}
|
|
34
37
|
on:focus
|
|
35
38
|
on:mousedown
|
|
36
39
|
on:mouseup
|
|
37
40
|
on:click
|
|
38
41
|
on:contextmenu
|
|
39
42
|
{style}
|
|
40
|
-
data-row={rowIdx}
|
|
41
43
|
>
|
|
42
44
|
{#if error}
|
|
43
45
|
<div class="label">
|
|
@@ -70,6 +72,9 @@
|
|
|
70
72
|
width: 0;
|
|
71
73
|
--cell-color: transparent;
|
|
72
74
|
}
|
|
75
|
+
.cell.alt {
|
|
76
|
+
--cell-background: var(--cell-background-alt);
|
|
77
|
+
}
|
|
73
78
|
.cell.default-height {
|
|
74
79
|
height: var(--default-row-height);
|
|
75
80
|
}
|
|
@@ -94,14 +99,15 @@
|
|
|
94
99
|
}
|
|
95
100
|
|
|
96
101
|
/* Cell border for cells with labels */
|
|
97
|
-
.cell.error:after
|
|
98
|
-
.cell.selected-other:not(.focused):after {
|
|
102
|
+
.cell.error:after {
|
|
99
103
|
border-radius: 0 2px 2px 2px;
|
|
100
104
|
}
|
|
101
|
-
.cell
|
|
102
|
-
.cell[data-row="0"].selected-other:not(.focused):after {
|
|
105
|
+
.cell.top.error:after {
|
|
103
106
|
border-radius: 2px 2px 2px 0;
|
|
104
107
|
}
|
|
108
|
+
.cell.selected-other:not(.focused):after {
|
|
109
|
+
border-radius: 2px;
|
|
110
|
+
}
|
|
105
111
|
|
|
106
112
|
/* Cell z-index */
|
|
107
113
|
.cell.error,
|
|
@@ -111,21 +117,30 @@
|
|
|
111
117
|
.cell.focused {
|
|
112
118
|
z-index: 2;
|
|
113
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
|
+
}
|
|
114
134
|
.cell.focused {
|
|
115
135
|
--cell-color: var(--spectrum-global-color-blue-400);
|
|
116
136
|
}
|
|
117
137
|
.cell.error {
|
|
118
138
|
--cell-color: var(--spectrum-global-color-red-500);
|
|
119
139
|
}
|
|
120
|
-
.cell.readonly {
|
|
140
|
+
.cell.focused.readonly {
|
|
121
141
|
--cell-color: var(--spectrum-global-color-gray-600);
|
|
122
142
|
}
|
|
123
|
-
|
|
124
|
-
user-select: none;
|
|
125
|
-
}
|
|
126
|
-
.cell:hover {
|
|
127
|
-
cursor: default;
|
|
128
|
-
}
|
|
143
|
+
|
|
129
144
|
.cell.highlighted:not(.focused),
|
|
130
145
|
.cell.focused.readonly {
|
|
131
146
|
--cell-background: var(--cell-background-hover);
|
|
@@ -141,7 +156,7 @@
|
|
|
141
156
|
left: 0;
|
|
142
157
|
padding: 1px 4px 3px 4px;
|
|
143
158
|
margin: 0 0 -2px 0;
|
|
144
|
-
background: var(--
|
|
159
|
+
background: var(--cell-color);
|
|
145
160
|
border-radius: 2px;
|
|
146
161
|
display: block;
|
|
147
162
|
color: white;
|
|
@@ -152,14 +167,19 @@
|
|
|
152
167
|
overflow: hidden;
|
|
153
168
|
user-select: none;
|
|
154
169
|
}
|
|
155
|
-
.cell
|
|
170
|
+
.cell.top .label {
|
|
156
171
|
bottom: auto;
|
|
157
172
|
top: 100%;
|
|
158
|
-
border-radius: 0 2px 2px 2px;
|
|
159
173
|
padding: 2px 4px 2px 4px;
|
|
160
174
|
margin: -2px 0 0 0;
|
|
161
175
|
}
|
|
162
176
|
.error .label {
|
|
163
177
|
background: var(--spectrum-global-color-red-500);
|
|
164
178
|
}
|
|
179
|
+
.selected-other:not(.error) .label {
|
|
180
|
+
display: none;
|
|
181
|
+
}
|
|
182
|
+
.selected-other:not(.error):hover .label {
|
|
183
|
+
display: block;
|
|
184
|
+
}
|
|
165
185
|
</style>
|
|
@@ -21,16 +21,7 @@
|
|
|
21
21
|
svelteDispatch("select")
|
|
22
22
|
const id = row?._id
|
|
23
23
|
if (id) {
|
|
24
|
-
selectedRows.
|
|
25
|
-
let newState = {
|
|
26
|
-
...state,
|
|
27
|
-
[id]: !state[id],
|
|
28
|
-
}
|
|
29
|
-
if (!newState[id]) {
|
|
30
|
-
delete newState[id]
|
|
31
|
-
}
|
|
32
|
-
return newState
|
|
33
|
-
})
|
|
24
|
+
selectedRows.actions.toggleRow(id)
|
|
34
25
|
}
|
|
35
26
|
}
|
|
36
27
|
|
|
@@ -47,6 +38,7 @@
|
|
|
47
38
|
highlighted={rowFocused || rowHovered}
|
|
48
39
|
selected={rowSelected}
|
|
49
40
|
{defaultHeight}
|
|
41
|
+
rowIdx={row?.__idx}
|
|
50
42
|
>
|
|
51
43
|
<div class="gutter">
|
|
52
44
|
{#if $$slots.default}
|
|
@@ -196,7 +196,11 @@
|
|
|
196
196
|
<MenuItem disabled={!canMoveRight} icon="ChevronRight" on:click={moveRight}>
|
|
197
197
|
Move right
|
|
198
198
|
</MenuItem>
|
|
199
|
-
<MenuItem
|
|
199
|
+
<MenuItem
|
|
200
|
+
disabled={idx === "sticky"}
|
|
201
|
+
icon="VisibilityOff"
|
|
202
|
+
on:click={hideColumn}>Hide column</MenuItem
|
|
203
|
+
>
|
|
200
204
|
</Menu>
|
|
201
205
|
</Popover>
|
|
202
206
|
|
|
@@ -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,
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
|
|
34
35
|
export let API = null
|
|
35
36
|
export let tableId = null
|
|
37
|
+
export let tableType = null
|
|
36
38
|
export let schemaOverrides = null
|
|
37
39
|
export let allowAddRows = true
|
|
38
40
|
export let allowAddColumns = true
|
|
@@ -40,6 +42,9 @@
|
|
|
40
42
|
export let allowExpandRows = true
|
|
41
43
|
export let allowEditRows = true
|
|
42
44
|
export let allowDeleteRows = true
|
|
45
|
+
export let stripeRows = false
|
|
46
|
+
export let collaboration = true
|
|
47
|
+
export let showAvatars = true
|
|
43
48
|
|
|
44
49
|
// Unique identifier for DOM nodes inside this instance
|
|
45
50
|
const rand = Math.random()
|
|
@@ -54,6 +59,7 @@
|
|
|
54
59
|
allowExpandRows,
|
|
55
60
|
allowEditRows,
|
|
56
61
|
allowDeleteRows,
|
|
62
|
+
stripeRows,
|
|
57
63
|
})
|
|
58
64
|
|
|
59
65
|
// Build up context
|
|
@@ -62,6 +68,7 @@
|
|
|
62
68
|
rand,
|
|
63
69
|
config,
|
|
64
70
|
tableId: tableIdStore,
|
|
71
|
+
tableType,
|
|
65
72
|
schemaOverrides: schemaOverridesStore,
|
|
66
73
|
}
|
|
67
74
|
context = { ...context, ...createEventManagers() }
|
|
@@ -88,6 +95,7 @@
|
|
|
88
95
|
allowExpandRows,
|
|
89
96
|
allowEditRows,
|
|
90
97
|
allowDeleteRows,
|
|
98
|
+
stripeRows,
|
|
91
99
|
})
|
|
92
100
|
|
|
93
101
|
// Set context for children to consume
|
|
@@ -97,7 +105,11 @@
|
|
|
97
105
|
export const getContext = () => context
|
|
98
106
|
|
|
99
107
|
// Initialise websocket for multi-user
|
|
100
|
-
|
|
108
|
+
onMount(() => {
|
|
109
|
+
if (collaboration) {
|
|
110
|
+
return createGridWebsocket(context)
|
|
111
|
+
}
|
|
112
|
+
})
|
|
101
113
|
</script>
|
|
102
114
|
|
|
103
115
|
<div
|
|
@@ -105,6 +117,7 @@
|
|
|
105
117
|
id="grid-{rand}"
|
|
106
118
|
class:is-resizing={$isResizing}
|
|
107
119
|
class:is-reordering={$isReordering}
|
|
120
|
+
class:stripe={$config.stripeRows}
|
|
108
121
|
style="--row-height:{$rowHeight}px; --default-row-height:{DefaultRowHeight}px; --gutter-width:{GutterWidth}px; --max-cell-render-height:{MaxCellRenderHeight}px; --max-cell-render-width-overflow:{MaxCellRenderWidthOverflow}px; --content-lines:{$contentLines};"
|
|
109
122
|
>
|
|
110
123
|
<div class="controls">
|
|
@@ -118,7 +131,9 @@
|
|
|
118
131
|
<RowHeightButton />
|
|
119
132
|
</div>
|
|
120
133
|
<div class="controls-right">
|
|
121
|
-
|
|
134
|
+
{#if showAvatars}
|
|
135
|
+
<UserAvatars />
|
|
136
|
+
{/if}
|
|
122
137
|
</div>
|
|
123
138
|
</div>
|
|
124
139
|
{#if $loaded}
|
|
@@ -167,6 +182,7 @@
|
|
|
167
182
|
/* Variables */
|
|
168
183
|
--cell-background: var(--spectrum-global-color-gray-50);
|
|
169
184
|
--cell-background-hover: var(--spectrum-global-color-gray-100);
|
|
185
|
+
--cell-background-alt: var(--cell-background);
|
|
170
186
|
--cell-padding: 8px;
|
|
171
187
|
--cell-spacing: 4px;
|
|
172
188
|
--cell-border: 1px solid var(--spectrum-global-color-gray-200);
|
|
@@ -183,6 +199,9 @@
|
|
|
183
199
|
.grid.is-reordering :global(*) {
|
|
184
200
|
cursor: grabbing !important;
|
|
185
201
|
}
|
|
202
|
+
.grid.stripe {
|
|
203
|
+
--cell-background-alt: var(--spectrum-global-color-gray-75);
|
|
204
|
+
}
|
|
186
205
|
|
|
187
206
|
.grid-data-outer,
|
|
188
207
|
.grid-data-inner {
|
|
@@ -36,7 +36,11 @@
|
|
|
36
36
|
<div bind:this={body} class="grid-body">
|
|
37
37
|
<GridScrollWrapper scrollHorizontally scrollVertically wheelInteractive>
|
|
38
38
|
{#each $renderedRows as row, idx}
|
|
39
|
-
<GridRow
|
|
39
|
+
<GridRow
|
|
40
|
+
{row}
|
|
41
|
+
top={idx === 0}
|
|
42
|
+
invertY={idx >= $rowVerticalInversionIndex}
|
|
43
|
+
/>
|
|
40
44
|
{/each}
|
|
41
45
|
{#if $config.allowAddRows && $renderedColumns.length}
|
|
42
46
|
<div
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import DataCell from "../cells/DataCell.svelte"
|
|
4
4
|
|
|
5
5
|
export let row
|
|
6
|
-
export let
|
|
6
|
+
export let top = false
|
|
7
7
|
export let invertY = false
|
|
8
8
|
|
|
9
9
|
const {
|
|
@@ -41,7 +41,8 @@
|
|
|
41
41
|
invertX={columnIdx >= $columnHorizontalInversionIndex}
|
|
42
42
|
highlighted={rowHovered || rowFocused || reorderSource === column.name}
|
|
43
43
|
selected={rowSelected}
|
|
44
|
-
rowIdx={
|
|
44
|
+
rowIdx={row.__idx}
|
|
45
|
+
topRow={top}
|
|
45
46
|
focused={$focusedCellId === cellId}
|
|
46
47
|
selectedUser={$selectedCellMap[cellId]}
|
|
47
48
|
width={column.width}
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
padding: 2px 6px;
|
|
39
39
|
font-size: 12px;
|
|
40
40
|
font-weight: 600;
|
|
41
|
-
background-color: var(--spectrum-global-color-gray-
|
|
41
|
+
background-color: var(--spectrum-global-color-gray-300);
|
|
42
42
|
color: var(--spectrum-global-color-gray-700);
|
|
43
43
|
border-radius: 4px;
|
|
44
44
|
text-align: center;
|
|
@@ -167,7 +167,7 @@
|
|
|
167
167
|
focused={$focusedCellId === cellId}
|
|
168
168
|
width={$stickyColumn.width}
|
|
169
169
|
{updateValue}
|
|
170
|
-
|
|
170
|
+
topRow={offset === 0}
|
|
171
171
|
{invertY}
|
|
172
172
|
>
|
|
173
173
|
{#if $stickyColumn?.schema?.autocolumn}
|
|
@@ -193,7 +193,7 @@
|
|
|
193
193
|
row={newRow}
|
|
194
194
|
focused={$focusedCellId === cellId}
|
|
195
195
|
width={column.width}
|
|
196
|
-
|
|
196
|
+
topRow={offset === 0}
|
|
197
197
|
invertX={columnIdx >= $columnHorizontalInversionIndex}
|
|
198
198
|
{invertY}
|
|
199
199
|
>
|
|
@@ -219,7 +219,7 @@
|
|
|
219
219
|
<Button size="M" secondary newStyles on:click={clear}>
|
|
220
220
|
<div class="button-with-keys">
|
|
221
221
|
Cancel
|
|
222
|
-
<KeyboardShortcut
|
|
222
|
+
<KeyboardShortcut keybind="Esc" />
|
|
223
223
|
</div>
|
|
224
224
|
</Button>
|
|
225
225
|
</div>
|
|
@@ -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,54 +1,55 @@
|
|
|
1
1
|
import { get } from "svelte/store"
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
+
import { SocketEvent, GridSocketEvent } from "@budibase/shared-core"
|
|
4
|
+
|
|
5
|
+
export const createGridWebsocket = context => {
|
|
6
|
+
const { rows, tableId, users, focusedCellId, table } = context
|
|
7
|
+
const socket = createWebsocket("/socket/grid")
|
|
22
8
|
|
|
23
9
|
const connectToTable = tableId => {
|
|
24
10
|
if (!socket.connected) {
|
|
25
11
|
return
|
|
26
12
|
}
|
|
27
13
|
// Identify which table we are editing
|
|
28
|
-
socket.emit(
|
|
14
|
+
socket.emit(GridSocketEvent.SelectTable, tableId, response => {
|
|
29
15
|
// handle initial connection info
|
|
30
16
|
users.set(response.users)
|
|
31
|
-
userId.set(response.id)
|
|
32
17
|
})
|
|
33
18
|
}
|
|
34
19
|
|
|
35
|
-
//
|
|
20
|
+
// Built-in events
|
|
36
21
|
socket.on("connect", () => {
|
|
37
22
|
connectToTable(get(tableId))
|
|
38
23
|
})
|
|
39
|
-
socket.on("
|
|
40
|
-
|
|
41
|
-
rows.actions.refreshRow(data.id)
|
|
42
|
-
}
|
|
24
|
+
socket.on("connect_error", err => {
|
|
25
|
+
console.log("Failed to connect to grid websocket:", err.message)
|
|
43
26
|
})
|
|
44
|
-
|
|
27
|
+
|
|
28
|
+
// User events
|
|
29
|
+
socket.on(SocketEvent.UserUpdate, user => {
|
|
45
30
|
users.actions.updateUser(user)
|
|
46
31
|
})
|
|
47
|
-
socket.on(
|
|
32
|
+
socket.on(SocketEvent.UserDisconnect, user => {
|
|
48
33
|
users.actions.removeUser(user)
|
|
49
34
|
})
|
|
50
|
-
|
|
51
|
-
|
|
35
|
+
|
|
36
|
+
// Row events
|
|
37
|
+
socket.on(GridSocketEvent.RowChange, async data => {
|
|
38
|
+
if (data.id) {
|
|
39
|
+
rows.actions.replaceRow(data.id, data.row)
|
|
40
|
+
} else if (data.row.id) {
|
|
41
|
+
// Handle users table edge cased
|
|
42
|
+
await rows.actions.refreshRow(data.row.id)
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// Table events
|
|
47
|
+
socket.on(GridSocketEvent.TableChange, data => {
|
|
48
|
+
// Only update table if one exists. If the table was deleted then we don't
|
|
49
|
+
// want to know - let the builder navigate away
|
|
50
|
+
if (data.table) {
|
|
51
|
+
table.set(data.table)
|
|
52
|
+
}
|
|
52
53
|
})
|
|
53
54
|
|
|
54
55
|
// Change websocket connection when table changes
|
|
@@ -56,7 +57,7 @@ export const createWebsocket = context => {
|
|
|
56
57
|
|
|
57
58
|
// Notify selected cell changes
|
|
58
59
|
focusedCellId.subscribe($focusedCellId => {
|
|
59
|
-
socket.emit(
|
|
60
|
+
socket.emit(GridSocketEvent.SelectCell, $focusedCellId)
|
|
60
61
|
})
|
|
61
62
|
|
|
62
63
|
return () => socket?.disconnect()
|
|
@@ -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()
|
|
@@ -224,10 +226,7 @@
|
|
|
224
226
|
if (!id || id === NewRowID) {
|
|
225
227
|
return
|
|
226
228
|
}
|
|
227
|
-
selectedRows.
|
|
228
|
-
state[id] = !state[id]
|
|
229
|
-
return state
|
|
230
|
-
})
|
|
229
|
+
selectedRows.actions.toggleRow(id)
|
|
231
230
|
}
|
|
232
231
|
|
|
233
232
|
onMount(() => {
|
|
@@ -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,
|
|
@@ -4,9 +4,10 @@ const reorderInitialState = {
|
|
|
4
4
|
sourceColumn: null,
|
|
5
5
|
targetColumn: null,
|
|
6
6
|
breakpoints: [],
|
|
7
|
-
initialMouseX: null,
|
|
8
|
-
scrollLeft: 0,
|
|
9
7
|
gridLeft: 0,
|
|
8
|
+
width: 0,
|
|
9
|
+
latestX: 0,
|
|
10
|
+
increment: 0,
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export const createStores = () => {
|
|
@@ -23,14 +24,24 @@ export const createStores = () => {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export const deriveStores = context => {
|
|
26
|
-
const {
|
|
27
|
-
|
|
27
|
+
const {
|
|
28
|
+
reorder,
|
|
29
|
+
columns,
|
|
30
|
+
visibleColumns,
|
|
31
|
+
scroll,
|
|
32
|
+
bounds,
|
|
33
|
+
stickyColumn,
|
|
34
|
+
ui,
|
|
35
|
+
maxScrollLeft,
|
|
36
|
+
} = context
|
|
37
|
+
|
|
38
|
+
let autoScrollInterval
|
|
39
|
+
let isAutoScrolling
|
|
28
40
|
|
|
29
41
|
// Callback when dragging on a colum header and starting reordering
|
|
30
42
|
const startReordering = (column, e) => {
|
|
31
43
|
const $visibleColumns = get(visibleColumns)
|
|
32
44
|
const $bounds = get(bounds)
|
|
33
|
-
const $scroll = get(scroll)
|
|
34
45
|
const $stickyColumn = get(stickyColumn)
|
|
35
46
|
ui.actions.blur()
|
|
36
47
|
|
|
@@ -51,9 +62,8 @@ export const deriveStores = context => {
|
|
|
51
62
|
sourceColumn: column,
|
|
52
63
|
targetColumn: null,
|
|
53
64
|
breakpoints,
|
|
54
|
-
initialMouseX: e.clientX,
|
|
55
|
-
scrollLeft: $scroll.left,
|
|
56
65
|
gridLeft: $bounds.left,
|
|
66
|
+
width: $bounds.width,
|
|
57
67
|
})
|
|
58
68
|
|
|
59
69
|
// Add listeners to handle mouse movement
|
|
@@ -66,12 +76,44 @@ export const deriveStores = context => {
|
|
|
66
76
|
|
|
67
77
|
// Callback when moving the mouse when reordering columns
|
|
68
78
|
const onReorderMouseMove = e => {
|
|
79
|
+
// Immediately handle the current position
|
|
80
|
+
const x = e.clientX
|
|
81
|
+
reorder.update(state => ({
|
|
82
|
+
...state,
|
|
83
|
+
latestX: x,
|
|
84
|
+
}))
|
|
85
|
+
considerReorderPosition()
|
|
86
|
+
|
|
87
|
+
// Check if we need to start auto-scrolling
|
|
88
|
+
const $reorder = get(reorder)
|
|
89
|
+
const proximityCutoff = 140
|
|
90
|
+
const speedFactor = 8
|
|
91
|
+
const rightProximity = Math.max(0, $reorder.gridLeft + $reorder.width - x)
|
|
92
|
+
const leftProximity = Math.max(0, x - $reorder.gridLeft)
|
|
93
|
+
if (rightProximity < proximityCutoff) {
|
|
94
|
+
const weight = proximityCutoff - rightProximity
|
|
95
|
+
const increment = (weight / proximityCutoff) * speedFactor
|
|
96
|
+
reorder.update(state => ({ ...state, increment }))
|
|
97
|
+
startAutoScroll()
|
|
98
|
+
} else if (leftProximity < proximityCutoff) {
|
|
99
|
+
const weight = -1 * (proximityCutoff - leftProximity)
|
|
100
|
+
const increment = (weight / proximityCutoff) * speedFactor
|
|
101
|
+
reorder.update(state => ({ ...state, increment }))
|
|
102
|
+
startAutoScroll()
|
|
103
|
+
} else {
|
|
104
|
+
stopAutoScroll()
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Actual logic to consider the current position and determine the new order
|
|
109
|
+
const considerReorderPosition = () => {
|
|
69
110
|
const $reorder = get(reorder)
|
|
111
|
+
const $scroll = get(scroll)
|
|
70
112
|
|
|
71
113
|
// Compute the closest breakpoint to the current position
|
|
72
114
|
let targetColumn
|
|
73
115
|
let minDistance = Number.MAX_SAFE_INTEGER
|
|
74
|
-
const mouseX =
|
|
116
|
+
const mouseX = $reorder.latestX - $reorder.gridLeft + $scroll.left
|
|
75
117
|
$reorder.breakpoints.forEach(point => {
|
|
76
118
|
const distance = Math.abs(point.x - mouseX)
|
|
77
119
|
if (distance < minDistance) {
|
|
@@ -79,7 +121,6 @@ export const deriveStores = context => {
|
|
|
79
121
|
targetColumn = point.column
|
|
80
122
|
}
|
|
81
123
|
})
|
|
82
|
-
|
|
83
124
|
if (targetColumn !== $reorder.targetColumn) {
|
|
84
125
|
reorder.update(state => ({
|
|
85
126
|
...state,
|
|
@@ -88,8 +129,35 @@ export const deriveStores = context => {
|
|
|
88
129
|
}
|
|
89
130
|
}
|
|
90
131
|
|
|
132
|
+
// Commences auto-scrolling in a certain direction, triggered when the mouse
|
|
133
|
+
// approaches the edges of the grid
|
|
134
|
+
const startAutoScroll = () => {
|
|
135
|
+
if (isAutoScrolling) {
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
isAutoScrolling = true
|
|
139
|
+
autoScrollInterval = setInterval(() => {
|
|
140
|
+
const $maxLeft = get(maxScrollLeft)
|
|
141
|
+
const { increment } = get(reorder)
|
|
142
|
+
scroll.update(state => ({
|
|
143
|
+
...state,
|
|
144
|
+
left: Math.max(0, Math.min($maxLeft, state.left + increment)),
|
|
145
|
+
}))
|
|
146
|
+
considerReorderPosition()
|
|
147
|
+
}, 10)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Stops auto scrolling
|
|
151
|
+
const stopAutoScroll = () => {
|
|
152
|
+
isAutoScrolling = false
|
|
153
|
+
clearInterval(autoScrollInterval)
|
|
154
|
+
}
|
|
155
|
+
|
|
91
156
|
// Callback when stopping reordering columns
|
|
92
157
|
const stopReordering = async () => {
|
|
158
|
+
// Ensure auto-scrolling is stopped
|
|
159
|
+
stopAutoScroll()
|
|
160
|
+
|
|
93
161
|
// Swap position of columns
|
|
94
162
|
let { sourceColumn, targetColumn } = get(reorder)
|
|
95
163
|
moveColumn(sourceColumn, targetColumn)
|
|
@@ -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
|
},
|
|
@@ -25,14 +25,33 @@ export const createStores = () => {
|
|
|
25
25
|
null
|
|
26
26
|
)
|
|
27
27
|
|
|
28
|
+
// Toggles whether a certain row ID is selected or not
|
|
29
|
+
const toggleSelectedRow = id => {
|
|
30
|
+
selectedRows.update(state => {
|
|
31
|
+
let newState = {
|
|
32
|
+
...state,
|
|
33
|
+
[id]: !state[id],
|
|
34
|
+
}
|
|
35
|
+
if (!newState[id]) {
|
|
36
|
+
delete newState[id]
|
|
37
|
+
}
|
|
38
|
+
return newState
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
28
42
|
return {
|
|
29
43
|
focusedCellId,
|
|
30
44
|
focusedCellAPI,
|
|
31
45
|
focusedRowId,
|
|
32
46
|
previousFocusedRowId,
|
|
33
|
-
selectedRows,
|
|
34
47
|
hoveredRowId,
|
|
35
48
|
rowHeight,
|
|
49
|
+
selectedRows: {
|
|
50
|
+
...selectedRows,
|
|
51
|
+
actions: {
|
|
52
|
+
toggleRow: toggleSelectedRow,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
36
55
|
}
|
|
37
56
|
}
|
|
38
57
|
|
|
@@ -1,104 +1,59 @@
|
|
|
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
|
})
|
|
96
51
|
}
|
|
97
52
|
}
|
|
98
53
|
|
|
99
|
-
const removeUser =
|
|
54
|
+
const removeUser = sessionId => {
|
|
100
55
|
users.update(state => {
|
|
101
|
-
return state.filter(x => x.
|
|
56
|
+
return state.filter(x => x.sessionId !== 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/constants.js
CHANGED
package/src/utils/index.js
CHANGED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { io } from "socket.io-client"
|
|
2
|
+
import { SocketEvent, SocketSessionTTL } from "@budibase/shared-core"
|
|
3
|
+
|
|
4
|
+
export const createWebsocket = (path, heartbeat = true) => {
|
|
5
|
+
if (!path) {
|
|
6
|
+
throw "A websocket path must be provided"
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Determine connection info
|
|
10
|
+
const tls = location.protocol === "https:"
|
|
11
|
+
const proto = tls ? "wss:" : "ws:"
|
|
12
|
+
const host = location.hostname
|
|
13
|
+
const port = location.port || (tls ? 443 : 80)
|
|
14
|
+
const socket = io(`${proto}//${host}:${port}`, {
|
|
15
|
+
path,
|
|
16
|
+
// Cap reconnection attempts to 3 (total of 15 seconds before giving up)
|
|
17
|
+
reconnectionAttempts: 3,
|
|
18
|
+
// Delay reconnection attempt by 5 seconds
|
|
19
|
+
reconnectionDelay: 5000,
|
|
20
|
+
reconnectionDelayMax: 5000,
|
|
21
|
+
// Timeout after 4 seconds so we never stack requests
|
|
22
|
+
timeout: 4000,
|
|
23
|
+
// Disable polling and rely on websocket only, as HTTP transport
|
|
24
|
+
// will only work with sticky sessions which we don't have
|
|
25
|
+
transports: ["websocket"],
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
// Set up a heartbeat that's half of the session TTL
|
|
29
|
+
let interval
|
|
30
|
+
if (heartbeat) {
|
|
31
|
+
interval = setInterval(() => {
|
|
32
|
+
socket.emit(SocketEvent.Heartbeat)
|
|
33
|
+
}, SocketSessionTTL * 500)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
socket.on("disconnect", () => {
|
|
37
|
+
clearInterval(interval)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
return socket
|
|
41
|
+
}
|
|
@@ -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>
|