@budibase/frontend-core 2.21.0 → 2.21.1

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,16 +1,16 @@
1
1
  {
2
2
  "name": "@budibase/frontend-core",
3
- "version": "2.21.0",
3
+ "version": "2.21.1",
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.21.0",
10
- "@budibase/shared-core": "2.21.0",
9
+ "@budibase/bbui": "2.21.1",
10
+ "@budibase/shared-core": "2.21.1",
11
11
  "dayjs": "^1.10.8",
12
12
  "lodash": "4.17.21",
13
13
  "socket.io-client": "^4.6.1"
14
14
  },
15
- "gitHead": "9182ed52484426785be64e308deb5c4f63dcb619"
15
+ "gitHead": "8714f99f1e39db6863117b1421af8c0a15e73915"
16
16
  }
@@ -59,13 +59,13 @@
59
59
  isReadonly: () => readonly,
60
60
  getType: () => column.schema.type,
61
61
  getValue: () => row[column.name],
62
- setValue: (value, options = { save: true }) => {
62
+ setValue: (value, options = { apply: true }) => {
63
63
  validation.actions.setError(cellId, null)
64
64
  updateValue({
65
65
  rowId: row._id,
66
66
  column: column.name,
67
67
  value,
68
- save: options?.save,
68
+ apply: options?.apply,
69
69
  })
70
70
  },
71
71
  }
@@ -217,14 +217,14 @@
217
217
  const type = $focusedCellAPI.getType()
218
218
  if (type === "number" && keyCodeIsNumber(keyCode)) {
219
219
  // Update the value locally but don't save it yet
220
- $focusedCellAPI.setValue(parseInt(key), { save: false })
220
+ $focusedCellAPI.setValue(parseInt(key), { apply: false })
221
221
  $focusedCellAPI.focus()
222
222
  } else if (
223
223
  ["string", "barcodeqr", "longform"].includes(type) &&
224
224
  (keyCodeIsLetter(keyCode) || keyCodeIsNumber(keyCode))
225
225
  ) {
226
226
  // Update the value locally but don't save it yet
227
- $focusedCellAPI.setValue(key, { save: false })
227
+ $focusedCellAPI.setValue(key, { apply: false })
228
228
  $focusedCellAPI.focus()
229
229
  }
230
230
  }
@@ -327,29 +327,31 @@ export const createActions = context => {
327
327
  get(fetch)?.getInitialData()
328
328
  }
329
329
 
330
- // Patches a row with some changes
331
- const updateRow = async (rowId, changes, options = { save: true }) => {
330
+ // Checks if a changeset for a row actually mutates the row or not
331
+ const changesAreValid = (row, changes) => {
332
+ const columns = Object.keys(changes || {})
333
+ if (!row || !columns.length) {
334
+ return false
335
+ }
336
+
337
+ // Ensure there is at least 1 column that creates a difference
338
+ return columns.some(column => row[column] !== changes[column])
339
+ }
340
+
341
+ // Patches a row with some changes in local state, and returns whether a
342
+ // valid pending change was made or not
343
+ const stashRowChanges = (rowId, changes) => {
332
344
  const $rows = get(rows)
333
345
  const $rowLookupMap = get(rowLookupMap)
334
346
  const index = $rowLookupMap[rowId]
335
347
  const row = $rows[index]
336
- if (index == null || !Object.keys(changes || {}).length) {
337
- return
338
- }
339
348
 
340
- // Abandon if no changes
341
- let same = true
342
- for (let column of Object.keys(changes)) {
343
- if (row[column] !== changes[column]) {
344
- same = false
345
- break
346
- }
347
- }
348
- if (same) {
349
- return
349
+ // Check this is a valid change
350
+ if (!row || !changesAreValid(row, changes)) {
351
+ return false
350
352
  }
351
353
 
352
- // Immediately update state so that the change is reflected
354
+ // Add change to cache
353
355
  rowChangeCache.update(state => ({
354
356
  ...state,
355
357
  [rowId]: {
@@ -357,26 +359,30 @@ export const createActions = context => {
357
359
  ...changes,
358
360
  },
359
361
  }))
362
+ return true
363
+ }
360
364
 
361
- // Stop here if we don't want to persist the change
362
- if (!options?.save) {
365
+ // Saves any pending changes to a row
366
+ const applyRowChanges = async rowId => {
367
+ const $rows = get(rows)
368
+ const $rowLookupMap = get(rowLookupMap)
369
+ const index = $rowLookupMap[rowId]
370
+ const row = $rows[index]
371
+ if (row == null) {
363
372
  return
364
373
  }
365
374
 
366
375
  // Save change
367
376
  try {
368
- inProgressChanges.update(state => ({
369
- ...state,
370
- [rowId]: true,
371
- }))
377
+ // Mark as in progress
378
+ inProgressChanges.update(state => ({ ...state, [rowId]: true }))
372
379
 
373
380
  // Update row
374
- const saved = await datasource.actions.updateRow({
375
- ...cleanRow(row),
376
- ...get(rowChangeCache)[rowId],
377
- })
381
+ const changes = get(rowChangeCache)[rowId]
382
+ const newRow = { ...cleanRow(row), ...changes }
383
+ const saved = await datasource.actions.updateRow(newRow)
378
384
 
379
- // Update state after a successful change
385
+ // Update row state after a successful change
380
386
  if (saved?._id) {
381
387
  rows.update(state => {
382
388
  state[index] = saved
@@ -386,6 +392,8 @@ export const createActions = context => {
386
392
  // Handle users table edge case
387
393
  await refreshRow(saved.id)
388
394
  }
395
+
396
+ // Wipe row change cache now that we've saved the row
389
397
  rowChangeCache.update(state => {
390
398
  delete state[rowId]
391
399
  return state
@@ -393,15 +401,17 @@ export const createActions = context => {
393
401
  } catch (error) {
394
402
  handleValidationError(rowId, error)
395
403
  }
396
- inProgressChanges.update(state => ({
397
- ...state,
398
- [rowId]: false,
399
- }))
404
+
405
+ // Mark as completed
406
+ inProgressChanges.update(state => ({ ...state, [rowId]: false }))
400
407
  }
401
408
 
402
409
  // Updates a value of a row
403
- const updateValue = async ({ rowId, column, value, save = true }) => {
404
- return await updateRow(rowId, { [column]: value }, { save })
410
+ const updateValue = async ({ rowId, column, value, apply = true }) => {
411
+ const success = stashRowChanges(rowId, { [column]: value })
412
+ if (success && apply) {
413
+ await applyRowChanges(rowId)
414
+ }
405
415
  }
406
416
 
407
417
  // Deletes an array of rows
@@ -411,9 +421,7 @@ export const createActions = context => {
411
421
  }
412
422
 
413
423
  // Actually delete rows
414
- rowsToDelete.forEach(row => {
415
- delete row.__idx
416
- })
424
+ rowsToDelete.forEach(row => delete row.__idx)
417
425
  await datasource.actions.deleteRows(rowsToDelete)
418
426
 
419
427
  // Update state
@@ -433,7 +441,7 @@ export const createActions = context => {
433
441
  newRow = newRows[i]
434
442
 
435
443
  // Ensure we have a unique _id.
436
- // This means generating one for non DS+, overriting any that may already
444
+ // This means generating one for non DS+, overwriting any that may already
437
445
  // exist as we cannot allow duplicates.
438
446
  if (!$isDatasourcePlus) {
439
447
  newRow._id = Helpers.uuid()
@@ -494,7 +502,7 @@ export const createActions = context => {
494
502
  duplicateRow,
495
503
  getRow,
496
504
  updateValue,
497
- updateRow,
505
+ applyRowChanges,
498
506
  deleteRows,
499
507
  hasRow,
500
508
  loadNextPage,
@@ -508,7 +516,14 @@ export const createActions = context => {
508
516
  }
509
517
 
510
518
  export const initialise = context => {
511
- const { rowChangeCache, inProgressChanges, previousFocusedRowId } = context
519
+ const {
520
+ rowChangeCache,
521
+ inProgressChanges,
522
+ previousFocusedRowId,
523
+ previousFocusedCellId,
524
+ rows,
525
+ validation,
526
+ } = context
512
527
 
513
528
  // Wipe the row change cache when changing row
514
529
  previousFocusedRowId.subscribe(id => {
@@ -519,4 +534,15 @@ export const initialise = context => {
519
534
  })
520
535
  }
521
536
  })
537
+
538
+ // Ensure any unsaved changes are saved when changing cell
539
+ previousFocusedCellId.subscribe(async id => {
540
+ const rowId = id?.split("-")[0]
541
+ const hasErrors = validation.actions.rowHasErrors(rowId)
542
+ const hasChanges = Object.keys(get(rowChangeCache)[rowId] || {}).length > 0
543
+ const isSavingChanges = get(inProgressChanges)[rowId]
544
+ if (rowId && !hasErrors && hasChanges && !isSavingChanges) {
545
+ await rows.actions.applyRowChanges(rowId)
546
+ }
547
+ })
522
548
  }
@@ -16,6 +16,7 @@ export const createStores = context => {
16
16
  const hoveredRowId = writable(null)
17
17
  const rowHeight = writable(get(props).fixedRowHeight || DefaultRowHeight)
18
18
  const previousFocusedRowId = writable(null)
19
+ const previousFocusedCellId = writable(null)
19
20
  const gridFocused = writable(false)
20
21
  const isDragging = writable(false)
21
22
  const buttonColumnWidth = writable(0)
@@ -48,6 +49,7 @@ export const createStores = context => {
48
49
  focusedCellAPI,
49
50
  focusedRowId,
50
51
  previousFocusedRowId,
52
+ previousFocusedCellId,
51
53
  hoveredRowId,
52
54
  rowHeight,
53
55
  gridFocused,
@@ -129,6 +131,7 @@ export const initialise = context => {
129
131
  const {
130
132
  focusedRowId,
131
133
  previousFocusedRowId,
134
+ previousFocusedCellId,
132
135
  rows,
133
136
  focusedCellId,
134
137
  selectedRows,
@@ -181,6 +184,13 @@ export const initialise = context => {
181
184
  lastFocusedRowId = id
182
185
  })
183
186
 
187
+ // Remember the last focused cell ID so that we can store the previous one
188
+ let lastFocusedCellId = null
189
+ focusedCellId.subscribe(id => {
190
+ previousFocusedCellId.set(lastFocusedCellId)
191
+ lastFocusedCellId = id
192
+ })
193
+
184
194
  // Remove hovered row when a cell is selected
185
195
  focusedCellId.subscribe(cell => {
186
196
  if (cell && get(hoveredRowId)) {
@@ -1,8 +1,23 @@
1
- import { writable, get } from "svelte/store"
1
+ import { writable, get, derived } from "svelte/store"
2
2
 
3
+ // Normally we would break out actions into the explicit "createActions"
4
+ // function, but for validation all these actions are pure so can go into
5
+ // "createStores" instead to make dependency ordering simpler
3
6
  export const createStores = () => {
4
7
  const validation = writable({})
5
8
 
9
+ // Derive which rows have errors so that we can use that info later
10
+ const rowErrorMap = derived(validation, $validation => {
11
+ let map = {}
12
+ Object.entries($validation).forEach(([key, error]) => {
13
+ // Extract row ID from all errored cell IDs
14
+ if (error) {
15
+ map[key.split("-")[0]] = true
16
+ }
17
+ })
18
+ return map
19
+ })
20
+
6
21
  const setError = (cellId, error) => {
7
22
  if (!cellId) {
8
23
  return
@@ -13,11 +28,16 @@ export const createStores = () => {
13
28
  }))
14
29
  }
15
30
 
31
+ const rowHasErrors = rowId => {
32
+ return get(rowErrorMap)[rowId]
33
+ }
34
+
16
35
  return {
17
36
  validation: {
18
37
  ...validation,
19
38
  actions: {
20
39
  setError,
40
+ rowHasErrors,
21
41
  },
22
42
  },
23
43
  }