@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.
Files changed (32) hide show
  1. package/package.json +4 -4
  2. package/src/api/app.js +6 -0
  3. package/src/api/automations.js +2 -2
  4. package/src/api/datasources.js +11 -0
  5. package/src/api/tables.js +4 -2
  6. package/src/components/UserAvatar.svelte +58 -0
  7. package/src/components/grid/cells/BooleanCell.svelte +3 -0
  8. package/src/components/grid/cells/DataCell.svelte +2 -0
  9. package/src/components/grid/cells/GridCell.svelte +36 -16
  10. package/src/components/grid/cells/GutterCell.svelte +2 -10
  11. package/src/components/grid/cells/HeaderCell.svelte +5 -1
  12. package/src/components/grid/cells/TextCell.svelte +1 -1
  13. package/src/components/grid/layout/Grid.svelte +22 -3
  14. package/src/components/grid/layout/GridBody.svelte +5 -1
  15. package/src/components/grid/layout/GridRow.svelte +3 -2
  16. package/src/components/grid/layout/HeaderRow.svelte +1 -1
  17. package/src/components/grid/layout/KeyboardShortcut.svelte +1 -1
  18. package/src/components/grid/layout/NewRow.svelte +3 -3
  19. package/src/components/grid/layout/StickyColumn.svelte +2 -1
  20. package/src/components/grid/layout/UserAvatars.svelte +14 -4
  21. package/src/components/grid/lib/websocket.js +33 -32
  22. package/src/components/grid/overlays/KeyboardManager.svelte +3 -4
  23. package/src/components/grid/stores/columns.js +18 -8
  24. package/src/components/grid/stores/reorder.js +77 -9
  25. package/src/components/grid/stores/rows.js +22 -12
  26. package/src/components/grid/stores/ui.js +20 -1
  27. package/src/components/grid/stores/users.js +18 -63
  28. package/src/components/index.js +1 -0
  29. package/src/constants.js +1 -0
  30. package/src/utils/index.js +1 -0
  31. package/src/utils/websocket.js +41 -0
  32. 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.4",
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.4",
10
- "@budibase/shared-core": "2.6.19-alpha.4",
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": "9a9aab1169f9639b28a0feef6f63577c3cbd0251"
16
+ "gitHead": "b45483f4b33be53615d8e5d1e3df8915f06bf0dd"
17
17
  }
package/src/api/app.js CHANGED
@@ -152,4 +152,10 @@ export const buildAppEndpoints = API => ({
152
152
  url: `/api/${appId}/components/definitions`,
153
153
  })
154
154
  },
155
+
156
+ addSampleData: async appId => {
157
+ return await API.post({
158
+ url: `/api/applications/${appId}/sample`,
159
+ })
160
+ },
155
161
  })
@@ -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
 
@@ -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 data the data import object
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>
@@ -37,6 +37,9 @@
37
37
  .boolean-cell {
38
38
  padding: 2px var(--cell-padding);
39
39
  pointer-events: none;
40
+ flex: 1 1 auto;
41
+ display: flex;
42
+ justify-content: center;
40
43
  }
41
44
  .boolean-cell.editable {
42
45
  pointer-events: all;
@@ -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 += `--cell-color:${selectedUser.color};`
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[data-row="0"].error:after,
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
- .cell:not(.focused) {
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(--user-color);
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[data-row="0"] .label {
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.update(state => {
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 icon="VisibilityOff" on:click={hideColumn}>Hide column</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
 
@@ -52,7 +52,7 @@
52
52
  {:else}
53
53
  <div class="text-cell" class:number={type === "number"}>
54
54
  <div class="value">
55
- {value || ""}
55
+ {value ?? ""}
56
56
  </div>
57
57
  </div>
58
58
  {/if}
@@ -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
- // onMount(() => createWebsocket(context))
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
- <UserAvatars />
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 {row} {idx} invertY={idx >= $rowVerticalInversionIndex} />
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 idx
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={idx}
44
+ rowIdx={row.__idx}
45
+ topRow={top}
45
46
  focused={$focusedCellId === cellId}
46
47
  selectedUser={$selectedCellMap[cellId]}
47
48
  width={column.width}
@@ -61,7 +61,7 @@
61
61
  border-right: var(--cell-border);
62
62
  border-bottom: var(--cell-border);
63
63
  background: var(--spectrum-global-color-gray-100);
64
- z-index: 20;
64
+ z-index: 1;
65
65
  }
66
66
  .add:hover {
67
67
  background: var(--spectrum-global-color-gray-200);
@@ -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-200);
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
- rowIdx={0}
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
- rowIdx={0}
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 overlay keybind="Esc" />
222
+ <KeyboardShortcut keybind="Esc" />
223
223
  </div>
224
224
  </Button>
225
225
  </div>
@@ -82,7 +82,8 @@
82
82
  {rowFocused}
83
83
  selected={rowSelected}
84
84
  highlighted={rowHovered || rowFocused}
85
- rowIdx={idx}
85
+ rowIdx={row.__idx}
86
+ topRow={idx === 0}
86
87
  focused={$focusedCellId === cellId}
87
88
  selectedUser={$selectedCellMap[cellId]}
88
89
  width={$stickyColumn.width}
@@ -1,13 +1,23 @@
1
1
  <script>
2
2
  import { getContext } from "svelte"
3
- import Avatar from "./Avatar.svelte"
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 $users as user}
10
- <Avatar {user} />
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: 8px;
28
+ gap: 4px;
19
29
  }
20
30
  </style>
@@ -1,54 +1,55 @@
1
1
  import { get } from "svelte/store"
2
- import { io } from "socket.io-client"
3
-
4
- export const createWebsocket = context => {
5
- const { rows, tableId, users, userId, focusedCellId } = context
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
+ 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("select-table", tableId, response => {
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
- // Event handlers
20
+ // Built-in events
36
21
  socket.on("connect", () => {
37
22
  connectToTable(get(tableId))
38
23
  })
39
- socket.on("row-update", data => {
40
- if (data.id) {
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
- socket.on("user-update", user => {
27
+
28
+ // User events
29
+ socket.on(SocketEvent.UserUpdate, user => {
45
30
  users.actions.updateUser(user)
46
31
  })
47
- socket.on("user-disconnect", user => {
32
+ socket.on(SocketEvent.UserDisconnect, user => {
48
33
  users.actions.removeUser(user)
49
34
  })
50
- socket.on("connect_error", err => {
51
- console.log("Failed to connect to grid websocket:", err.message)
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("select-cell", $focusedCellId)
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.update(state => {
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, dispatch } = context
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
- let newSchema = $table?.schema
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].name || 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 { reorder, columns, visibleColumns, scroll, bounds, stickyColumn, ui } =
27
- context
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 = e.clientX - $reorder.gridLeft + $reorder.scrollLeft
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
- // Refreshes a specific row, handling updates, addition or deletion
272
- const refreshRow = async id => {
273
- // Fetch row from the server again
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 (newRow) {
280
+ if (row) {
283
281
  if (index != null) {
284
282
  // An existing row was updated
285
283
  rows.update(state => {
286
- state[index] = { ...newRow }
284
+ state[index] = { ...row }
287
285
  return state
288
286
  })
289
287
  } else {
290
288
  // A new row was created
291
- handleNewRows([newRow])
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
- rows.update(state => {
345
- state[index] = saved
346
- return state.slice()
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
- // Enrich users with unique colours
8
- const enrichedUsers = derived(
9
- [users, userId],
10
- ([$users, $userId]) => {
11
- return (
12
- $users
13
- .slice()
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, userId } = context
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, userId],
74
- ([$enrichedUsers, $userId]) => {
29
+ [users, focusedCellId],
30
+ ([$users, $focusedCellId]) => {
75
31
  let map = {}
76
- $enrichedUsers.forEach(user => {
77
- if (user.focusedCellId && user.id !== $userId) {
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
- const index = $users.findIndex(x => x.id === user.id)
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 = user => {
54
+ const removeUser = sessionId => {
100
55
  users.update(state => {
101
- return state.filter(x => x.id !== user.id)
56
+ return state.filter(x => x.sessionId !== sessionId)
102
57
  })
103
58
  }
104
59
 
@@ -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
@@ -70,6 +70,7 @@ export const Features = {
70
70
  ENFORCEABLE_SSO: "enforceableSSO",
71
71
  BRANDING: "branding",
72
72
  SCIM: "scim",
73
+ SYNC_AUTOMATIONS: "syncAutomations",
73
74
  }
74
75
 
75
76
  // Role IDs
@@ -3,3 +3,4 @@ export * as JSONUtils from "./json"
3
3
  export * as CookieUtils from "./cookies"
4
4
  export * as RoleUtils from "./roles"
5
5
  export * as Utils from "./utils"
6
+ export { createWebsocket } from "./websocket"
@@ -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>