@budibase/frontend-core 2.6.22 → 2.6.24-alpha.0

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 (42) 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/groups.js +17 -0
  6. package/src/api/index.js +10 -0
  7. package/src/api/tables.js +4 -2
  8. package/src/components/Testimonial.svelte +67 -0
  9. package/src/components/TestimonialPage.svelte +2 -61
  10. package/src/components/UserAvatar.svelte +58 -0
  11. package/src/components/grid/cells/BooleanCell.svelte +3 -0
  12. package/src/components/grid/cells/DataCell.svelte +3 -0
  13. package/src/components/grid/cells/DateCell.svelte +46 -4
  14. package/src/components/grid/cells/GridCell.svelte +36 -16
  15. package/src/components/grid/cells/GutterCell.svelte +2 -10
  16. package/src/components/grid/cells/HeaderCell.svelte +5 -1
  17. package/src/components/grid/layout/Grid.svelte +22 -5
  18. package/src/components/grid/layout/GridBody.svelte +5 -1
  19. package/src/components/grid/layout/GridRow.svelte +3 -2
  20. package/src/components/grid/layout/HeaderRow.svelte +1 -1
  21. package/src/components/grid/layout/KeyboardShortcut.svelte +1 -1
  22. package/src/components/grid/layout/NewRow.svelte +3 -3
  23. package/src/components/grid/layout/StickyColumn.svelte +2 -1
  24. package/src/components/grid/layout/UserAvatars.svelte +14 -4
  25. package/src/components/grid/lib/constants.js +1 -1
  26. package/src/components/grid/lib/websocket.js +40 -36
  27. package/src/components/grid/overlays/KeyboardManager.svelte +3 -4
  28. package/src/components/grid/overlays/MenuOverlay.svelte +34 -4
  29. package/src/components/grid/stores/columns.js +2 -2
  30. package/src/components/grid/stores/reorder.js +77 -9
  31. package/src/components/grid/stores/rows.js +27 -19
  32. package/src/components/grid/stores/ui.js +20 -1
  33. package/src/components/grid/stores/users.js +18 -63
  34. package/src/components/index.js +2 -0
  35. package/src/constants.js +1 -0
  36. package/src/fetch/DataFetch.js +23 -1
  37. package/src/fetch/GroupUserFetch.js +51 -0
  38. package/src/fetch/fetchData.js +2 -0
  39. package/src/utils/index.js +1 -0
  40. package/src/utils/websocket.js +60 -0
  41. package/src/components/grid/controls/BetaButton.svelte +0 -46
  42. package/src/components/grid/layout/Avatar.svelte +0 -24
@@ -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"
@@ -7,7 +7,6 @@
7
7
  import { createAPIClient } from "../../../api"
8
8
  import { attachStores } from "../stores"
9
9
  import BulkDeleteHandler from "../controls/BulkDeleteHandler.svelte"
10
- import BetaButton from "../controls/BetaButton.svelte"
11
10
  import GridBody from "./GridBody.svelte"
12
11
  import ResizeOverlay from "../overlays/ResizeOverlay.svelte"
13
12
  import ReorderOverlay from "../overlays/ReorderOverlay.svelte"
@@ -24,6 +23,7 @@
24
23
  import RowHeightButton from "../controls/RowHeightButton.svelte"
25
24
  import ColumnWidthButton from "../controls/ColumnWidthButton.svelte"
26
25
  import NewRow from "./NewRow.svelte"
26
+ import { createGridWebsocket } from "../lib/websocket"
27
27
  import {
28
28
  MaxCellRenderHeight,
29
29
  MaxCellRenderWidthOverflow,
@@ -33,6 +33,7 @@
33
33
 
34
34
  export let API = null
35
35
  export let tableId = null
36
+ export let tableType = null
36
37
  export let schemaOverrides = null
37
38
  export let allowAddRows = true
38
39
  export let allowAddColumns = true
@@ -40,6 +41,9 @@
40
41
  export let allowExpandRows = true
41
42
  export let allowEditRows = true
42
43
  export let allowDeleteRows = true
44
+ export let stripeRows = false
45
+ export let collaboration = true
46
+ export let showAvatars = true
43
47
 
44
48
  // Unique identifier for DOM nodes inside this instance
45
49
  const rand = Math.random()
@@ -54,6 +58,7 @@
54
58
  allowExpandRows,
55
59
  allowEditRows,
56
60
  allowDeleteRows,
61
+ stripeRows,
57
62
  })
58
63
 
59
64
  // Build up context
@@ -62,6 +67,7 @@
62
67
  rand,
63
68
  config,
64
69
  tableId: tableIdStore,
70
+ tableType,
65
71
  schemaOverrides: schemaOverridesStore,
66
72
  }
67
73
  context = { ...context, ...createEventManagers() }
@@ -88,6 +94,7 @@
88
94
  allowExpandRows,
89
95
  allowEditRows,
90
96
  allowDeleteRows,
97
+ stripeRows,
91
98
  })
92
99
 
93
100
  // Set context for children to consume
@@ -97,7 +104,11 @@
97
104
  export const getContext = () => context
98
105
 
99
106
  // Initialise websocket for multi-user
100
- // onMount(() => createWebsocket(context))
107
+ onMount(() => {
108
+ if (collaboration) {
109
+ return createGridWebsocket(context)
110
+ }
111
+ })
101
112
  </script>
102
113
 
103
114
  <div
@@ -105,6 +116,7 @@
105
116
  id="grid-{rand}"
106
117
  class:is-resizing={$isResizing}
107
118
  class:is-reordering={$isReordering}
119
+ class:stripe={$config.stripeRows}
108
120
  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
121
  >
110
122
  <div class="controls">
@@ -118,7 +130,9 @@
118
130
  <RowHeightButton />
119
131
  </div>
120
132
  <div class="controls-right">
121
- <UserAvatars />
133
+ {#if showAvatars}
134
+ <UserAvatars />
135
+ {/if}
122
136
  </div>
123
137
  </div>
124
138
  {#if $loaded}
@@ -129,7 +143,6 @@
129
143
  <HeaderRow />
130
144
  <GridBody />
131
145
  </div>
132
- <BetaButton />
133
146
  {#if allowAddRows}
134
147
  <NewRow />
135
148
  {/if}
@@ -167,6 +180,7 @@
167
180
  /* Variables */
168
181
  --cell-background: var(--spectrum-global-color-gray-50);
169
182
  --cell-background-hover: var(--spectrum-global-color-gray-100);
183
+ --cell-background-alt: var(--cell-background);
170
184
  --cell-padding: 8px;
171
185
  --cell-spacing: 4px;
172
186
  --cell-border: 1px solid var(--spectrum-global-color-gray-200);
@@ -183,6 +197,9 @@
183
197
  .grid.is-reordering :global(*) {
184
198
  cursor: grabbing !important;
185
199
  }
200
+ .grid.stripe {
201
+ --cell-background-alt: var(--spectrum-global-color-gray-75);
202
+ }
186
203
 
187
204
  .grid-data-outer,
188
205
  .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,4 +1,4 @@
1
- export const Padding = 128
1
+ export const Padding = 256
2
2
  export const MaxCellRenderHeight = 252
3
3
  export const MaxCellRenderWidthOverflow = 200
4
4
  export const ScrollBarSize = 8
@@ -1,54 +1,58 @@
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 => {
29
- // handle initial connection info
30
- users.set(response.users)
31
- userId.set(response.id)
32
- })
14
+ socket.emit(
15
+ GridSocketEvent.SelectTable,
16
+ { tableId },
17
+ ({ users: gridUsers }) => {
18
+ users.set(gridUsers)
19
+ }
20
+ )
33
21
  }
34
22
 
35
- // Event handlers
23
+ // Built-in events
36
24
  socket.on("connect", () => {
37
25
  connectToTable(get(tableId))
38
26
  })
39
- socket.on("row-update", data => {
40
- if (data.id) {
41
- rows.actions.refreshRow(data.id)
42
- }
27
+ socket.on("connect_error", err => {
28
+ console.log("Failed to connect to grid websocket:", err.message)
43
29
  })
44
- socket.on("user-update", user => {
30
+
31
+ // User events
32
+ socket.onOther(SocketEvent.UserUpdate, ({ user }) => {
45
33
  users.actions.updateUser(user)
46
34
  })
47
- socket.on("user-disconnect", user => {
48
- users.actions.removeUser(user)
35
+ socket.onOther(SocketEvent.UserDisconnect, ({ sessionId }) => {
36
+ users.actions.removeUser(sessionId)
49
37
  })
50
- socket.on("connect_error", err => {
51
- console.log("Failed to connect to grid websocket:", err.message)
38
+
39
+ // Row events
40
+ socket.onOther(GridSocketEvent.RowChange, async ({ id, row }) => {
41
+ if (id) {
42
+ rows.actions.replaceRow(id, row)
43
+ } else if (row.id) {
44
+ // Handle users table edge cased
45
+ await rows.actions.refreshRow(row.id)
46
+ }
47
+ })
48
+
49
+ // Table events
50
+ socket.onOther(GridSocketEvent.TableChange, ({ table: newTable }) => {
51
+ // Only update table if one exists. If the table was deleted then we don't
52
+ // want to know - let the builder navigate away
53
+ if (newTable) {
54
+ table.set(newTable)
55
+ }
52
56
  })
53
57
 
54
58
  // Change websocket connection when table changes
@@ -56,7 +60,7 @@ export const createWebsocket = context => {
56
60
 
57
61
  // Notify selected cell changes
58
62
  focusedCellId.subscribe($focusedCellId => {
59
- socket.emit("select-cell", $focusedCellId)
63
+ socket.emit(GridSocketEvent.SelectCell, { cellId: $focusedCellId })
60
64
  })
61
65
 
62
66
  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(() => {
@@ -1,6 +1,13 @@
1
1
  <script>
2
- import { clickOutside, Menu, MenuItem, notifications } from "@budibase/bbui"
2
+ import {
3
+ clickOutside,
4
+ Menu,
5
+ MenuItem,
6
+ Helpers,
7
+ notifications,
8
+ } from "@budibase/bbui"
3
9
  import { getContext } from "svelte"
10
+ import { NewRowID } from "../lib/constants"
4
11
 
5
12
  const {
6
13
  focusedRow,
@@ -14,9 +21,11 @@
14
21
  clipboard,
15
22
  dispatch,
16
23
  focusedCellAPI,
24
+ focusedRowId,
17
25
  } = getContext("grid")
18
26
 
19
27
  $: style = makeStyle($menu)
28
+ $: isNewRow = $focusedRowId === NewRowID
20
29
 
21
30
  const makeStyle = menu => {
22
31
  return `left:${menu.left}px; top:${menu.top}px;`
@@ -36,6 +45,11 @@
36
45
  $focusedCellId = `${newRow._id}-${column}`
37
46
  }
38
47
  }
48
+
49
+ const copyToClipboard = async value => {
50
+ await Helpers.copyToClipboard(value)
51
+ notifications.success("Copied to clipboard")
52
+ }
39
53
  </script>
40
54
 
41
55
  {#if $menu.visible}
@@ -58,22 +72,38 @@
58
72
  </MenuItem>
59
73
  <MenuItem
60
74
  icon="Maximize"
61
- disabled={!$config.allowEditRows}
75
+ disabled={isNewRow || !$config.allowEditRows}
62
76
  on:click={() => dispatch("edit-row", $focusedRow)}
63
77
  on:click={menu.actions.close}
64
78
  >
65
79
  Edit row in modal
66
80
  </MenuItem>
81
+ <MenuItem
82
+ icon="Copy"
83
+ disabled={isNewRow || !$focusedRow?._id}
84
+ on:click={() => copyToClipboard($focusedRow?._id)}
85
+ on:click={menu.actions.close}
86
+ >
87
+ Copy row _id
88
+ </MenuItem>
89
+ <MenuItem
90
+ icon="Copy"
91
+ disabled={isNewRow || !$focusedRow?._rev}
92
+ on:click={() => copyToClipboard($focusedRow?._rev)}
93
+ on:click={menu.actions.close}
94
+ >
95
+ Copy row _rev
96
+ </MenuItem>
67
97
  <MenuItem
68
98
  icon="Duplicate"
69
- disabled={!$config.allowAddRows}
99
+ disabled={isNewRow || !$config.allowAddRows}
70
100
  on:click={duplicate}
71
101
  >
72
102
  Duplicate row
73
103
  </MenuItem>
74
104
  <MenuItem
75
105
  icon="Delete"
76
- disabled={!$config.allowDeleteRows}
106
+ disabled={isNewRow || !$config.allowDeleteRows}
77
107
  on:click={deleteRow}
78
108
  >
79
109
  Delete row
@@ -90,8 +90,8 @@ 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)
93
+ // Broadcast change to external state can be updated, as this change
94
+ // will not be received by the builder websocket because we caused it ourselves
95
95
  dispatch("updatetable", newTable)
96
96
 
97
97
  // Update server
@@ -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)