@budibase/frontend-core 2.29.21 → 2.29.23

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 (51) hide show
  1. package/package.json +5 -5
  2. package/src/components/FilterBuilder.svelte +1 -5
  3. package/src/components/grid/cells/DataCell.svelte +63 -12
  4. package/src/components/grid/cells/GridCell.svelte +36 -16
  5. package/src/components/grid/cells/GutterCell.svelte +15 -8
  6. package/src/components/grid/cells/HeaderCell.svelte +3 -3
  7. package/src/components/grid/cells/RelationshipCell.svelte +16 -8
  8. package/src/components/grid/controls/BulkDeleteHandler.svelte +98 -13
  9. package/src/components/grid/controls/BulkDuplicationHandler.svelte +79 -0
  10. package/src/components/grid/controls/ClipboardHandler.svelte +67 -0
  11. package/src/components/grid/controls/ColumnsSettingButton.svelte +5 -10
  12. package/src/components/grid/controls/SizeButton.svelte +6 -13
  13. package/src/components/grid/controls/SortButton.svelte +8 -22
  14. package/src/components/grid/layout/ButtonColumn.svelte +12 -6
  15. package/src/components/grid/layout/Grid.svelte +11 -7
  16. package/src/components/grid/layout/GridBody.svelte +2 -2
  17. package/src/components/grid/layout/GridRow.svelte +12 -6
  18. package/src/components/grid/layout/GridScrollWrapper.svelte +9 -7
  19. package/src/components/grid/layout/HeaderRow.svelte +2 -2
  20. package/src/components/grid/layout/NewColumnButton.svelte +11 -5
  21. package/src/components/grid/layout/NewRow.svelte +16 -12
  22. package/src/components/grid/layout/StickyColumn.svelte +24 -14
  23. package/src/components/grid/lib/utils.js +4 -4
  24. package/src/components/grid/overlays/KeyboardManager.svelte +144 -95
  25. package/src/components/grid/overlays/MenuOverlay.svelte +114 -63
  26. package/src/components/grid/overlays/ReorderOverlay.svelte +14 -18
  27. package/src/components/grid/overlays/ResizeOverlay.svelte +8 -21
  28. package/src/components/grid/stores/clipboard.js +215 -18
  29. package/src/components/grid/stores/columns.js +78 -97
  30. package/src/components/grid/stores/conditions.js +157 -0
  31. package/src/components/grid/stores/config.js +2 -2
  32. package/src/components/grid/stores/datasource.js +4 -14
  33. package/src/components/grid/stores/datasources/nonPlus.js +2 -4
  34. package/src/components/grid/stores/datasources/table.js +6 -5
  35. package/src/components/grid/stores/datasources/viewV2.js +7 -9
  36. package/src/components/grid/stores/index.js +5 -3
  37. package/src/components/grid/stores/menu.js +40 -6
  38. package/src/components/grid/stores/pagination.js +9 -3
  39. package/src/components/grid/stores/reorder.js +67 -42
  40. package/src/components/grid/stores/resize.js +1 -1
  41. package/src/components/grid/stores/rows.js +220 -85
  42. package/src/components/grid/stores/scroll.js +31 -28
  43. package/src/components/grid/stores/ui.js +295 -70
  44. package/src/components/grid/stores/users.js +2 -2
  45. package/src/components/grid/stores/validation.js +43 -16
  46. package/src/components/grid/stores/viewport.js +30 -24
  47. package/src/components/index.js +1 -0
  48. package/src/constants.js +3 -0
  49. package/src/themes/midnight.css +18 -17
  50. package/src/themes/nord.css +2 -1
  51. package/src/utils/utils.js +2 -0
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@budibase/frontend-core",
3
- "version": "2.29.21",
3
+ "version": "2.29.23",
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.29.21",
10
- "@budibase/shared-core": "2.29.21",
11
- "@budibase/types": "2.29.21",
9
+ "@budibase/bbui": "2.29.23",
10
+ "@budibase/shared-core": "2.29.23",
11
+ "@budibase/types": "2.29.23",
12
12
  "dayjs": "^1.10.8",
13
13
  "lodash": "4.17.21",
14
14
  "shortid": "2.2.15",
15
15
  "socket.io-client": "^4.7.5"
16
16
  },
17
- "gitHead": "6678715072f390d56bd823f449ebd377fb7e0229"
17
+ "gitHead": "7e4c562339380dedd324b8a0cf23ef36baebe676"
18
18
  }
@@ -36,11 +36,7 @@
36
36
  ) &&
37
37
  !schemaFields.some(field => field.name === "_id")
38
38
  ) {
39
- schemaFields = [
40
- ...schemaFields,
41
- { name: "_id", type: "string" },
42
- { name: "_rev", type: "string" },
43
- ]
39
+ schemaFields = [...schemaFields, { name: "_id", type: "string" }]
44
40
  }
45
41
  }
46
42
 
@@ -4,12 +4,21 @@
4
4
  import { getCellRenderer } from "../lib/renderers"
5
5
  import { derived, writable } from "svelte/store"
6
6
 
7
- const { rows, focusedCellId, focusedCellAPI, menu, config, validation } =
8
- getContext("grid")
7
+ const {
8
+ rows,
9
+ columns,
10
+ focusedCellId,
11
+ focusedCellAPI,
12
+ menu,
13
+ config,
14
+ validation,
15
+ selectedCells,
16
+ selectedCellCount,
17
+ } = getContext("grid")
9
18
 
10
19
  export let highlighted
11
- export let selected
12
20
  export let rowFocused
21
+ export let rowSelected
13
22
  export let rowIdx
14
23
  export let topRow = false
15
24
  export let focused
@@ -20,29 +29,32 @@
20
29
  export let updateValue = rows.actions.updateValue
21
30
  export let contentLines = 1
22
31
  export let hidden = false
32
+ export let isSelectingCells = false
33
+ export let cellSelected = false
23
34
 
24
35
  const emptyError = writable(null)
25
36
 
26
37
  let api
27
38
 
28
- // Get the error for this cell if the row is focused
39
+ // Get the error for this cell if the cell is focused or selected
29
40
  $: error = getErrorStore(rowFocused, cellId)
30
41
 
31
42
  // Determine if the cell is editable
32
43
  $: readonly =
33
- column.schema.autocolumn ||
34
- column.schema.disabled ||
35
- column.schema.type === "formula" ||
36
- (!$config.canEditRows && !row._isNewRow) ||
37
- column.schema.readonly
44
+ columns.actions.isReadonly(column) ||
45
+ (!$config.canEditRows && !row._isNewRow)
38
46
 
39
- // Register this cell API if the row is focused
47
+ // Register this cell API if this cell is focused
40
48
  $: {
41
49
  if (focused) {
42
50
  focusedCellAPI.set(cellAPI)
43
51
  }
44
52
  }
45
53
 
54
+ // Callbacks for cell selection
55
+ $: updateSelectionCallback = isSelectingCells ? updateSelection : null
56
+ $: stopSelectionCallback = isSelectingCells ? stopSelection : null
57
+
46
58
  const getErrorStore = (selected, cellId) => {
47
59
  if (!selected) {
48
60
  return emptyError
@@ -68,21 +80,60 @@
68
80
  })
69
81
  },
70
82
  }
83
+
84
+ const startSelection = e => {
85
+ if (e.button !== 0 || e.shiftKey) {
86
+ return
87
+ }
88
+ selectedCells.actions.startSelecting(cellId)
89
+ }
90
+
91
+ const updateSelection = e => {
92
+ if (e.buttons !== 1) {
93
+ selectedCells.actions.stopSelecting()
94
+ return
95
+ }
96
+ selectedCells.actions.updateTarget(cellId)
97
+ }
98
+
99
+ const stopSelection = () => {
100
+ selectedCells.actions.stopSelecting()
101
+ }
102
+
103
+ const handleClick = e => {
104
+ if (e.shiftKey && $focusedCellId) {
105
+ // If we have a focused cell, select the range from that cell to here
106
+ selectedCells.actions.selectRange($focusedCellId, cellId)
107
+ } else if (e.shiftKey && $selectedCellCount) {
108
+ // If we already have a selected range of cell, update it
109
+ selectedCells.actions.updateTarget(cellId)
110
+ } else {
111
+ // Otherwise just select this cell
112
+ focusedCellId.set(cellId)
113
+ }
114
+ }
71
115
  </script>
72
116
 
73
117
  <GridCell
74
118
  {highlighted}
75
- {selected}
76
119
  {rowIdx}
77
120
  {topRow}
78
121
  {focused}
79
122
  {selectedUser}
80
123
  {readonly}
81
124
  {hidden}
125
+ selected={rowSelected || cellSelected}
82
126
  error={$error}
83
- on:click={() => focusedCellId.set(cellId)}
84
127
  on:contextmenu={e => menu.actions.open(cellId, e)}
128
+ on:mousedown={startSelection}
129
+ on:mouseenter={updateSelectionCallback}
130
+ on:mouseup={stopSelectionCallback}
131
+ on:click={handleClick}
85
132
  width={column.width}
133
+ metadata={{
134
+ ...row.__metadata?.row,
135
+ ...row.__metadata?.cell[column.name],
136
+ }}
86
137
  >
87
138
  <svelte:component
88
139
  this={getCellRenderer(column)}
@@ -11,13 +11,20 @@
11
11
  export let center = false
12
12
  export let readonly = false
13
13
  export let hidden = false
14
+ export let metadata = null
14
15
 
15
- $: style = getStyle(width, selectedUser)
16
+ $: style = getStyle(width, selectedUser, metadata)
16
17
 
17
- const getStyle = (width, selectedUser) => {
18
+ const getStyle = (width, selectedUser, metadata) => {
18
19
  let style = width === "auto" ? "width: auto;" : `flex: 0 0 ${width}px;`
19
20
  if (selectedUser) {
20
- style += `--user-color:${selectedUser.color};`
21
+ style += `--user-color :${selectedUser.color};`
22
+ }
23
+ if (metadata?.backgroundColor) {
24
+ style += `--cell-background: ${metadata.backgroundColor};`
25
+ }
26
+ if (metadata?.textColor) {
27
+ style += `--cell-font-color: ${metadata.textColor};`
21
28
  }
22
29
  return style
23
30
  }
@@ -43,9 +50,10 @@
43
50
  on:mouseup
44
51
  on:click
45
52
  on:contextmenu
46
- on:touchstart
53
+ on:touchstart|passive
47
54
  on:touchend
48
55
  on:touchcancel
56
+ on:mouseenter
49
57
  {style}
50
58
  >
51
59
  {#if error}
@@ -71,7 +79,7 @@
71
79
  flex-direction: row;
72
80
  justify-content: flex-start;
73
81
  align-items: flex-start;
74
- color: var(--spectrum-global-color-gray-800);
82
+ color: var(--cell-font-color);
75
83
  font-size: var(--cell-font-size);
76
84
  gap: var(--cell-spacing);
77
85
  background: var(--cell-background);
@@ -93,9 +101,9 @@
93
101
  }
94
102
 
95
103
  /* Cell border */
96
- .cell.focused:after,
97
- .cell.error:after,
98
- .cell.selected-other:not(.focused):after {
104
+ .cell.focused::after,
105
+ .cell.error::after,
106
+ .cell.selected-other:not(.focused)::after {
99
107
  content: " ";
100
108
  position: absolute;
101
109
  top: 0;
@@ -108,14 +116,30 @@
108
116
  box-sizing: border-box;
109
117
  }
110
118
 
119
+ /* Cell background overlay */
120
+ .cell.selected::before {
121
+ content: " ";
122
+ position: absolute;
123
+ top: 0;
124
+ left: 0;
125
+ pointer-events: none;
126
+ box-sizing: border-box;
127
+ height: calc(100% + 1px);
128
+ width: calc(100% + 1px);
129
+ opacity: 0.16;
130
+ background: var(--spectrum-global-color-blue-400);
131
+ z-index: 2;
132
+ pointer-events: none;
133
+ }
134
+
111
135
  /* Cell border for cells with labels */
112
- .cell.error:after {
136
+ .cell.error::after {
113
137
  border-radius: 0 2px 2px 2px;
114
138
  }
115
- .cell.top.error:after {
139
+ .cell.top.error::after {
116
140
  border-radius: 2px 2px 2px 0;
117
141
  }
118
- .cell.selected-other:not(.focused):after {
142
+ .cell.selected-other:not(.focused)::after {
119
143
  border-radius: 2px;
120
144
  }
121
145
 
@@ -150,14 +174,10 @@
150
174
  .cell.focused.readonly {
151
175
  --cell-color: var(--spectrum-global-color-gray-600);
152
176
  }
153
-
154
- .cell.highlighted:not(.focused),
177
+ .cell.highlighted:not(.focused):not(.selected),
155
178
  .cell.focused.readonly {
156
179
  --cell-background: var(--cell-background-hover);
157
180
  }
158
- .cell.selected:not(.focused) {
159
- --cell-background: var(--spectrum-global-color-blue-100);
160
- }
161
181
 
162
182
  /* Label for additional text */
163
183
  .label {
@@ -16,14 +16,22 @@
16
16
  const { config, dispatch, selectedRows } = getContext("grid")
17
17
  const svelteDispatch = createEventDispatcher()
18
18
 
19
- $: selectionEnabled = $config.canSelectRows || $config.canDeleteRows
20
-
21
19
  const select = e => {
22
20
  e.stopPropagation()
23
21
  svelteDispatch("select")
24
22
  const id = row?._id
25
23
  if (id) {
26
- selectedRows.actions.toggleRow(id)
24
+ // Bulk select with shift
25
+ if (e.shiftKey) {
26
+ // Prevent default if already selected, to prevent checkbox clearing
27
+ if (rowSelected) {
28
+ e.preventDefault()
29
+ } else {
30
+ selectedRows.actions.bulkSelectRows(id)
31
+ }
32
+ } else {
33
+ selectedRows.actions.toggleRow(id)
34
+ }
27
35
  }
28
36
  }
29
37
 
@@ -46,6 +54,7 @@
46
54
  selected={rowSelected}
47
55
  {defaultHeight}
48
56
  rowIdx={row?.__idx}
57
+ metadata={row?.__metadata?.row}
49
58
  >
50
59
  <div class="gutter">
51
60
  {#if $$slots.default}
@@ -54,16 +63,14 @@
54
63
  <div
55
64
  on:click={select}
56
65
  class="checkbox"
57
- class:visible={selectionEnabled &&
58
- (disableNumber || rowSelected || rowHovered || rowFocused)}
66
+ class:visible={disableNumber || rowSelected || rowHovered || rowFocused}
59
67
  >
60
68
  <Checkbox value={rowSelected} {disabled} />
61
69
  </div>
62
70
  {#if !disableNumber}
63
71
  <div
64
72
  class="number"
65
- class:visible={!selectionEnabled ||
66
- !(rowSelected || rowHovered || rowFocused)}
73
+ class:visible={!(rowSelected || rowHovered || rowFocused)}
67
74
  >
68
75
  {row.__idx + 1}
69
76
  </div>
@@ -109,7 +116,7 @@
109
116
  margin: 3px 0 0 0;
110
117
  }
111
118
  .number {
112
- color: var(--spectrum-global-color-gray-500);
119
+ color: val(--cell-font-color, var(--spectrum-global-color-gray-500));
113
120
  }
114
121
  .checkbox.visible,
115
122
  .number.visible {
@@ -18,7 +18,7 @@
18
18
  isReordering,
19
19
  isResizing,
20
20
  sort,
21
- visibleColumns,
21
+ scrollableColumns,
22
22
  dispatch,
23
23
  subscribe,
24
24
  config,
@@ -51,7 +51,7 @@
51
51
 
52
52
  $: sortedBy = column.name === $sort.column
53
53
  $: canMoveLeft = orderable && idx > 0
54
- $: canMoveRight = orderable && idx < $visibleColumns.length - 1
54
+ $: canMoveRight = orderable && idx < $scrollableColumns.length - 1
55
55
  $: sortingLabels = getSortingLabels(column.schema?.type)
56
56
  $: searchable = isColumnSearchable(column)
57
57
  $: resetSearchValue(column.name)
@@ -270,7 +270,7 @@
270
270
  on:touchcancel={onMouseUp}
271
271
  on:contextmenu={onContextMenu}
272
272
  width={column.width}
273
- left={column.left}
273
+ left={column.__left}
274
274
  defaultHeight
275
275
  center
276
276
  >
@@ -7,7 +7,7 @@
7
7
 
8
8
  const { API, cache } = getContext("grid")
9
9
 
10
- export let value
10
+ export let value = []
11
11
  export let api
12
12
  export let readonly
13
13
  export let focused
@@ -30,9 +30,10 @@
30
30
  let container
31
31
  let anchor
32
32
 
33
+ $: fieldValue = parseValue(value)
33
34
  $: oneRowOnly = schema?.relationshipType === "one-to-many"
34
35
  $: editable = focused && !readonly
35
- $: lookupMap = buildLookupMap(value, isOpen)
36
+ $: lookupMap = buildLookupMap(fieldValue, isOpen)
36
37
  $: debouncedSearch(searchString)
37
38
  $: {
38
39
  if (!focused && isOpen) {
@@ -40,6 +41,13 @@
40
41
  }
41
42
  }
42
43
 
44
+ const parseValue = value => {
45
+ if (Array.isArray(value) && value.every(x => x?._id)) {
46
+ return value
47
+ }
48
+ return []
49
+ }
50
+
43
51
  // Builds a lookup map to quickly check which rows are selected
44
52
  const buildLookupMap = (value, isOpen) => {
45
53
  let map = {}
@@ -177,13 +185,13 @@
177
185
 
178
186
  // Toggles whether a row is included in the relationship or not
179
187
  const toggleRow = async row => {
180
- if (value?.some(x => x._id === row._id)) {
188
+ if (fieldValue?.some(x => x._id === row._id)) {
181
189
  // If the row is already included, remove it and update the candidate
182
190
  // row to be the same position if possible
183
191
  if (oneRowOnly) {
184
192
  await onChange([])
185
193
  } else {
186
- const newValue = value.filter(x => x._id !== row._id)
194
+ const newValue = fieldValue.filter(x => x._id !== row._id)
187
195
  if (!newValue.length) {
188
196
  candidateIndex = null
189
197
  } else {
@@ -196,7 +204,7 @@
196
204
  if (oneRowOnly) {
197
205
  await onChange([row])
198
206
  } else {
199
- await onChange(sortRows([...(value || []), row]))
207
+ await onChange(sortRows([...(fieldValue || []), row]))
200
208
  }
201
209
  candidateIndex = null
202
210
  }
@@ -238,7 +246,7 @@
238
246
  class:wrap={editable || contentLines > 1}
239
247
  on:wheel={e => (focused ? e.stopPropagation() : null)}
240
248
  >
241
- {#each value || [] as relationship}
249
+ {#each fieldValue || [] as relationship}
242
250
  {#if relationship[primaryDisplay] || relationship.primaryDisplay}
243
251
  <div class="badge">
244
252
  <span>
@@ -263,9 +271,9 @@
263
271
  </div>
264
272
  {/if}
265
273
  </div>
266
- {#if !hideCounter && value?.length}
274
+ {#if !hideCounter && fieldValue?.length}
267
275
  <div class="count">
268
- {value?.length || 0}
276
+ {fieldValue?.length || 0}
269
277
  </div>
270
278
  {/if}
271
279
  </div>
@@ -1,35 +1,120 @@
1
1
  <script>
2
- import { Modal, ModalContent } from "@budibase/bbui"
2
+ import { Modal, ModalContent, ProgressBar } from "@budibase/bbui"
3
3
  import { getContext, onMount } from "svelte"
4
+ import { parseCellID } from "../lib/utils"
5
+ import { sleep } from "../../../utils/utils"
4
6
 
5
- const { selectedRows, rows, subscribe, notifications } = getContext("grid")
7
+ const {
8
+ selectedRows,
9
+ rows,
10
+ subscribe,
11
+ notifications,
12
+ menu,
13
+ selectedCellCount,
14
+ selectedRowCount,
15
+ selectedCells,
16
+ rowLookupMap,
17
+ config,
18
+ } = getContext("grid")
19
+ const duration = 260
6
20
 
7
- let modal
21
+ let rowsModal
22
+ let cellsModal
23
+ let processing = false
24
+ let progressPercentage = 0
25
+ let promptQuantity = 0
8
26
 
9
- $: selectedRowCount = Object.values($selectedRows).length
10
- $: rowsToDelete = Object.entries($selectedRows)
11
- .map(entry => $rows.find(x => x._id === entry[0]))
27
+ $: rowsToDelete = Object.keys($selectedRows)
28
+ .map(rowId => $rowLookupMap[rowId])
12
29
  .filter(x => x != null)
13
30
 
14
- // Deletion callback when confirmed
15
- const performDeletion = async () => {
31
+ const handleBulkDeleteRequest = () => {
32
+ progressPercentage = 0
33
+ menu.actions.close()
34
+ if ($selectedRowCount && $config.canDeleteRows) {
35
+ if ($selectedRowCount === 1) {
36
+ bulkDeleteRows()
37
+ } else {
38
+ promptQuantity = $selectedRowCount
39
+ rowsModal?.show()
40
+ }
41
+ } else if ($selectedCellCount && $config.canEditRows) {
42
+ promptQuantity = $selectedCellCount
43
+ cellsModal?.show()
44
+ }
45
+ }
46
+
47
+ const bulkDeleteRows = async () => {
48
+ processing = true
16
49
  const count = rowsToDelete.length
17
50
  await rows.actions.deleteRows(rowsToDelete)
51
+ // This is a real bulk delete endpoint so we don't need progress.
52
+ // We just animate it uo to 100 when we're done for consistency with other
53
+ // prompts.
54
+ progressPercentage = 100
55
+ await sleep(duration)
18
56
  $notifications.success(`Deleted ${count} row${count === 1 ? "" : "s"}`)
57
+ processing = false
58
+ }
59
+
60
+ const bulkDeleteCells = async () => {
61
+ processing = true
62
+ let changeMap = {}
63
+ for (let row of $selectedCells) {
64
+ for (let cellId of row) {
65
+ const { rowId, field } = parseCellID(cellId)
66
+ if (!changeMap[rowId]) {
67
+ changeMap[rowId] = {}
68
+ }
69
+ changeMap[rowId][field] = null
70
+ }
71
+ }
72
+ await rows.actions.bulkUpdate(changeMap, progress => {
73
+ progressPercentage = progress * 100
74
+ })
75
+ await sleep(duration)
76
+ processing = false
19
77
  }
20
78
 
21
- onMount(() => subscribe("request-bulk-delete", () => modal?.show()))
79
+ onMount(() => subscribe("request-bulk-delete", handleBulkDeleteRequest))
22
80
  </script>
23
81
 
24
- <Modal bind:this={modal}>
82
+ <Modal bind:this={rowsModal}>
25
83
  <ModalContent
26
84
  title="Delete rows"
27
85
  confirmText="Continue"
28
86
  cancelText="Cancel"
29
- onConfirm={performDeletion}
87
+ onConfirm={bulkDeleteRows}
88
+ size="M"
89
+ >
90
+ Are you sure you want to delete {promptQuantity} rows?
91
+ {#if processing}
92
+ <ProgressBar
93
+ size="L"
94
+ value={progressPercentage}
95
+ {duration}
96
+ width="100%"
97
+ />
98
+ {/if}
99
+ </ModalContent>
100
+ </Modal>
101
+
102
+ <Modal bind:this={cellsModal}>
103
+ <ModalContent
104
+ title="Delete cells"
105
+ confirmText="Continue"
106
+ cancelText="Cancel"
107
+ onConfirm={bulkDeleteCells}
30
108
  size="M"
31
109
  >
32
- Are you sure you want to delete {selectedRowCount}
33
- row{selectedRowCount === 1 ? "" : "s"}?
110
+ Are you sure you want to delete {promptQuantity} cells?
111
+ {#if processing}
112
+ <ProgressBar
113
+ size="L"
114
+ value={progressPercentage}
115
+ {duration}
116
+ width="100%"
117
+ />
118
+ {/if}
34
119
  </ModalContent>
35
120
  </Modal>
@@ -0,0 +1,79 @@
1
+ <script>
2
+ import { Modal, ModalContent, ProgressBar } from "@budibase/bbui"
3
+ import { getContext, onMount } from "svelte"
4
+ import { getCellID } from "../lib/utils"
5
+ import { sleep } from "../../../utils/utils"
6
+
7
+ const {
8
+ selectedRows,
9
+ rows,
10
+ subscribe,
11
+ selectedRowCount,
12
+ visibleColumns,
13
+ selectedCells,
14
+ rowLookupMap,
15
+ } = getContext("grid")
16
+ const duration = 260
17
+
18
+ let modal
19
+ let progressPercentage = 0
20
+ let processing = false
21
+ let promptQuantity = 0
22
+
23
+ // Deletion callback when confirmed
24
+ const performDuplication = async () => {
25
+ progressPercentage = 0
26
+ processing = true
27
+
28
+ // duplicate rows
29
+ const rowsToDuplicate = Object.keys($selectedRows).map(id => {
30
+ return $rowLookupMap[id]
31
+ })
32
+ const newRows = await rows.actions.bulkDuplicate(
33
+ rowsToDuplicate,
34
+ progress => {
35
+ progressPercentage = progress * 100
36
+ }
37
+ )
38
+ await sleep(duration)
39
+
40
+ // Select new cells to highlight them
41
+ if (newRows.length) {
42
+ const firstRow = newRows[0]
43
+ const lastRow = newRows[newRows.length - 1]
44
+ const firstCol = $visibleColumns[0]
45
+ const lastCol = $visibleColumns[$visibleColumns.length - 1]
46
+ const startCellId = getCellID(firstRow._id, firstCol.name)
47
+ const endCellId = getCellID(lastRow._id, lastCol.name)
48
+ selectedCells.actions.selectRange(startCellId, endCellId)
49
+ }
50
+ processing = false
51
+ }
52
+
53
+ const handleBulkDuplicateRequest = () => {
54
+ promptQuantity = $selectedRowCount
55
+ modal?.show()
56
+ }
57
+
58
+ onMount(() => subscribe("request-bulk-duplicate", handleBulkDuplicateRequest))
59
+ </script>
60
+
61
+ <Modal bind:this={modal}>
62
+ <ModalContent
63
+ title="Duplicate rows"
64
+ confirmText="Continue"
65
+ cancelText="Cancel"
66
+ onConfirm={performDuplication}
67
+ size="M"
68
+ >
69
+ Are you sure you want to duplicate {promptQuantity} rows?
70
+ {#if processing}
71
+ <ProgressBar
72
+ size="L"
73
+ value={progressPercentage}
74
+ {duration}
75
+ width="100%"
76
+ />
77
+ {/if}
78
+ </ModalContent>
79
+ </Modal>