@budibase/frontend-core 2.6.19-alpha.24 → 2.6.19-alpha.27

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 CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@budibase/frontend-core",
3
- "version": "2.6.19-alpha.24",
3
+ "version": "2.6.19-alpha.27",
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.24",
10
- "@budibase/shared-core": "2.6.19-alpha.24",
9
+ "@budibase/bbui": "2.6.19-alpha.27",
10
+ "@budibase/shared-core": "2.6.19-alpha.27",
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": "6b0a2aae2469836c64fb6807ec65f52dc4968e28"
16
+ "gitHead": "84334ce650f8782d49ca9c7b5a7186e5232ad91a"
17
17
  }
@@ -0,0 +1,58 @@
1
+ <script>
2
+ import { Avatar, Tooltip } from "@budibase/bbui"
3
+ import { helpers } from "@budibase/shared-core"
4
+
5
+ export let user
6
+ export let size
7
+ export let tooltipDirection = "top"
8
+ export let showTooltip = true
9
+
10
+ $: tooltipStyle = getTooltipStyle(tooltipDirection)
11
+
12
+ const getTooltipStyle = direction => {
13
+ if (!direction) {
14
+ return ""
15
+ }
16
+ if (direction === "top") {
17
+ return "transform: translateX(-50%) translateY(-100%);"
18
+ } else if (direction === "bottom") {
19
+ return "transform: translateX(-50%) translateY(100%);"
20
+ }
21
+ }
22
+ </script>
23
+
24
+ {#if user}
25
+ <div class="user-avatar">
26
+ <Avatar
27
+ {size}
28
+ initials={helpers.getUserInitials(user)}
29
+ color={helpers.getUserColor(user)}
30
+ />
31
+ {#if showTooltip}
32
+ <div class="tooltip" style={tooltipStyle}>
33
+ <Tooltip
34
+ direction={tooltipDirection}
35
+ textWrapping
36
+ text={user.email}
37
+ size="S"
38
+ />
39
+ </div>
40
+ {/if}
41
+ </div>
42
+ {/if}
43
+
44
+ <style>
45
+ .user-avatar {
46
+ position: relative;
47
+ }
48
+ .tooltip {
49
+ display: none;
50
+ position: absolute;
51
+ top: 0;
52
+ left: 50%;
53
+ white-space: nowrap;
54
+ }
55
+ .user-avatar:hover .tooltip {
56
+ display: block;
57
+ }
58
+ </style>
@@ -16,7 +16,7 @@
16
16
  const getStyle = (width, selectedUser) => {
17
17
  let style = `flex: 0 0 ${width}px;`
18
18
  if (selectedUser) {
19
- style += `--cell-color:${selectedUser.color};`
19
+ style += `--user-color:${selectedUser.color};`
20
20
  }
21
21
  return style
22
22
  }
@@ -99,14 +99,15 @@
99
99
  }
100
100
 
101
101
  /* Cell border for cells with labels */
102
- .cell.error:after,
103
- .cell.selected-other:not(.focused):after {
102
+ .cell.error:after {
104
103
  border-radius: 0 2px 2px 2px;
105
104
  }
106
- .cell.top.error:after,
107
- .cell.top.selected-other:not(.focused):after {
105
+ .cell.top.error:after {
108
106
  border-radius: 2px 2px 2px 0;
109
107
  }
108
+ .cell.selected-other:not(.focused):after {
109
+ border-radius: 2px;
110
+ }
110
111
 
111
112
  /* Cell z-index */
112
113
  .cell.error,
@@ -116,21 +117,30 @@
116
117
  .cell.focused {
117
118
  z-index: 2;
118
119
  }
120
+ .cell.selected-other:hover {
121
+ z-index: 2;
122
+ }
123
+ .cell:not(.focused) {
124
+ user-select: none;
125
+ }
126
+ .cell:hover {
127
+ cursor: default;
128
+ }
129
+
130
+ /* Cell color overrides */
131
+ .cell.selected-other {
132
+ --cell-color: var(--user-color);
133
+ }
119
134
  .cell.focused {
120
135
  --cell-color: var(--spectrum-global-color-blue-400);
121
136
  }
122
137
  .cell.error {
123
138
  --cell-color: var(--spectrum-global-color-red-500);
124
139
  }
125
- .cell.readonly {
140
+ .cell.focused.readonly {
126
141
  --cell-color: var(--spectrum-global-color-gray-600);
127
142
  }
128
- .cell:not(.focused) {
129
- user-select: none;
130
- }
131
- .cell:hover {
132
- cursor: default;
133
- }
143
+
134
144
  .cell.highlighted:not(.focused),
135
145
  .cell.focused.readonly {
136
146
  --cell-background: var(--cell-background-hover);
@@ -146,7 +156,7 @@
146
156
  left: 0;
147
157
  padding: 1px 4px 3px 4px;
148
158
  margin: 0 0 -2px 0;
149
- background: var(--user-color);
159
+ background: var(--cell-color);
150
160
  border-radius: 2px;
151
161
  display: block;
152
162
  color: white;
@@ -160,11 +170,16 @@
160
170
  .cell.top .label {
161
171
  bottom: auto;
162
172
  top: 100%;
163
- border-radius: 0 2px 2px 2px;
164
173
  padding: 2px 4px 2px 4px;
165
174
  margin: -2px 0 0 0;
166
175
  }
167
176
  .error .label {
168
177
  background: var(--spectrum-global-color-red-500);
169
178
  }
179
+ .selected-other:not(.error) .label {
180
+ display: none;
181
+ }
182
+ .selected-other:not(.error):hover .label {
183
+ display: block;
184
+ }
170
185
  </style>
@@ -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,
@@ -42,6 +43,8 @@
42
43
  export let allowEditRows = true
43
44
  export let allowDeleteRows = true
44
45
  export let stripeRows = false
46
+ export let collaboration = true
47
+ export let showAvatars = true
45
48
 
46
49
  // Unique identifier for DOM nodes inside this instance
47
50
  const rand = Math.random()
@@ -102,7 +105,11 @@
102
105
  export const getContext = () => context
103
106
 
104
107
  // Initialise websocket for multi-user
105
- // onMount(() => createWebsocket(context))
108
+ onMount(() => {
109
+ if (collaboration) {
110
+ return createGridWebsocket(context)
111
+ }
112
+ })
106
113
  </script>
107
114
 
108
115
  <div
@@ -124,7 +131,9 @@
124
131
  <RowHeightButton />
125
132
  </div>
126
133
  <div class="controls-right">
127
- <UserAvatars />
134
+ {#if showAvatars}
135
+ <UserAvatars />
136
+ {/if}
128
137
  </div>
129
138
  </div>
130
139
  {#if $loaded}
@@ -1,13 +1,23 @@
1
1
  <script>
2
2
  import { getContext } from "svelte"
3
- import 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,24 +1,9 @@
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
+
4
+ export const createGridWebsocket = context => {
5
+ const { rows, tableId, users, focusedCellId, table } = context
6
+ const socket = createWebsocket("/socket/grid")
22
7
 
23
8
  const connectToTable = tableId => {
24
9
  if (!socket.connected) {
@@ -28,27 +13,42 @@ export const createWebsocket = context => {
28
13
  socket.emit("select-table", tableId, response => {
29
14
  // handle initial connection info
30
15
  users.set(response.users)
31
- userId.set(response.id)
32
16
  })
33
17
  }
34
18
 
35
- // Event handlers
19
+ // Connection events
36
20
  socket.on("connect", () => {
37
21
  connectToTable(get(tableId))
38
22
  })
39
- socket.on("row-update", data => {
40
- if (data.id) {
41
- rows.actions.refreshRow(data.id)
42
- }
23
+ socket.on("connect_error", err => {
24
+ console.log("Failed to connect to grid websocket:", err.message)
43
25
  })
26
+
27
+ // User events
44
28
  socket.on("user-update", user => {
45
29
  users.actions.updateUser(user)
46
30
  })
47
31
  socket.on("user-disconnect", user => {
48
32
  users.actions.removeUser(user)
49
33
  })
50
- socket.on("connect_error", err => {
51
- console.log("Failed to connect to grid websocket:", err.message)
34
+
35
+ // Row events
36
+ socket.on("row-change", async data => {
37
+ if (data.id) {
38
+ rows.actions.replaceRow(data.id, data.row)
39
+ } else if (data.row.id) {
40
+ // Handle users table edge case
41
+ await rows.actions.refreshRow(data.row.id)
42
+ }
43
+ })
44
+
45
+ // Table events
46
+ socket.on("table-change", data => {
47
+ // Only update table if one exists. If the table was deleted then we don't
48
+ // want to know - let the builder navigate away
49
+ if (data.table) {
50
+ table.set(data.table)
51
+ }
52
52
  })
53
53
 
54
54
  // Change websocket connection when table changes
@@ -14,6 +14,7 @@
14
14
  dispatch,
15
15
  selectedRows,
16
16
  config,
17
+ menu,
17
18
  } = getContext("grid")
18
19
 
19
20
  const ignoredOriginSelectors = [
@@ -61,6 +62,7 @@
61
62
  } else {
62
63
  $focusedCellId = null
63
64
  }
65
+ menu.actions.close()
64
66
  return
65
67
  } else if (e.key === "Tab") {
66
68
  e.preventDefault()
@@ -46,7 +46,7 @@ export const createStores = () => {
46
46
  }
47
47
 
48
48
  export const deriveStores = context => {
49
- const { table, columns, stickyColumn, API, 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,
@@ -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
  },
@@ -1,95 +1,50 @@
1
1
  import { writable, get, derived } from "svelte/store"
2
+ import { helpers } from "@budibase/shared-core"
2
3
 
3
4
  export const createStores = () => {
4
5
  const users = writable([])
5
- const userId = writable(null)
6
6
 
7
- // 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
  })
@@ -98,7 +53,7 @@ export const deriveStores = context => {
98
53
 
99
54
  const removeUser = user => {
100
55
  users.update(state => {
101
- return state.filter(x => x.id !== user.id)
56
+ return state.filter(x => x.sessionId !== user.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"
@@ -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,23 @@
1
+ import { io } from "socket.io-client"
2
+
3
+ export const createWebsocket = path => {
4
+ if (!path) {
5
+ throw "A websocket path must be provided"
6
+ }
7
+
8
+ // Determine connection info
9
+ const tls = location.protocol === "https:"
10
+ const proto = tls ? "wss:" : "ws:"
11
+ const host = location.hostname
12
+ const port = location.port || (tls ? 443 : 80)
13
+ return io(`${proto}//${host}:${port}`, {
14
+ path,
15
+ // Cap reconnection attempts to 3 (total of 15 seconds before giving up)
16
+ reconnectionAttempts: 3,
17
+ // Delay reconnection attempt by 5 seconds
18
+ reconnectionDelay: 5000,
19
+ reconnectionDelayMax: 5000,
20
+ // Timeout after 4 seconds so we never stack requests
21
+ timeout: 4000,
22
+ })
23
+ }
@@ -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>