@budibase/frontend-core 2.26.3 → 2.27.3

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,18 +1,18 @@
1
1
  {
2
2
  "name": "@budibase/frontend-core",
3
- "version": "2.26.3",
3
+ "version": "2.27.3",
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.26.3",
10
- "@budibase/shared-core": "2.26.3",
11
- "@budibase/types": "2.26.3",
9
+ "@budibase/bbui": "2.27.3",
10
+ "@budibase/shared-core": "2.27.3",
11
+ "@budibase/types": "2.27.3",
12
12
  "dayjs": "^1.10.8",
13
13
  "lodash": "4.17.21",
14
14
  "shortid": "2.2.15",
15
15
  "socket.io-client": "^4.6.1"
16
16
  },
17
- "gitHead": "4be0c2e7f1698fe76235b87109a8c2e4a56c5470"
17
+ "gitHead": "7edd7b2ee3284719cd17f7004a17f4ed5d818c65"
18
18
  }
@@ -0,0 +1,59 @@
1
+ <script>
2
+ import { Modal, ModalContent, Body, CoreSignature } from "@budibase/bbui"
3
+
4
+ export let onConfirm = () => {}
5
+ export let value
6
+ export let title
7
+ export let darkMode
8
+
9
+ export const show = () => {
10
+ edited = false
11
+ modal.show()
12
+ }
13
+
14
+ let modal
15
+ let canvas
16
+ let edited = false
17
+ </script>
18
+
19
+ <Modal bind:this={modal}>
20
+ <ModalContent
21
+ showConfirmButton
22
+ showCancelButton={false}
23
+ showCloseIcon={false}
24
+ custom
25
+ disabled={!edited}
26
+ showDivider={false}
27
+ onConfirm={() => {
28
+ onConfirm(canvas)
29
+ }}
30
+ >
31
+ <div slot="header">
32
+ <Body>{title}</Body>
33
+ </div>
34
+ <div class="signature-wrap modal">
35
+ <CoreSignature
36
+ {darkMode}
37
+ {value}
38
+ saveIcon={false}
39
+ bind:this={canvas}
40
+ on:update={() => {
41
+ edited = true
42
+ }}
43
+ />
44
+ </div>
45
+ </ModalContent>
46
+ </Modal>
47
+
48
+ <style>
49
+ .signature-wrap {
50
+ display: flex;
51
+ flex-direction: column;
52
+ justify-content: flex-start;
53
+ align-items: stretch;
54
+ background-color: var(--spectrum-global-color-gray-50);
55
+ color: var(--spectrum-alias-text-color);
56
+ box-sizing: border-box;
57
+ position: relative;
58
+ }
59
+ </style>
@@ -8,11 +8,10 @@
8
8
  export let onChange
9
9
  export let readonly = false
10
10
  export let api
11
- export let invertX = false
12
11
  export let schema
13
12
  export let maximum
14
13
 
15
- const { API, notifications } = getContext("grid")
14
+ const { API, notifications, props } = getContext("grid")
16
15
  const imageExtensions = ["png", "tiff", "gif", "raw", "jpg", "jpeg"]
17
16
 
18
17
  let isOpen = false
@@ -92,13 +91,7 @@
92
91
  </div>
93
92
 
94
93
  {#if isOpen}
95
- <GridPopover
96
- open={isOpen}
97
- {anchor}
98
- {invertX}
99
- maxHeight={null}
100
- on:close={close}
101
- >
94
+ <GridPopover open={isOpen} {anchor} maxHeight={null} on:close={close}>
102
95
  <div class="dropzone">
103
96
  <Dropzone
104
97
  {value}
@@ -106,7 +99,7 @@
106
99
  on:change={e => onChange(e.detail)}
107
100
  maximum={maximum || schema.constraints?.length?.maximum}
108
101
  {processFiles}
109
- {handleFileTooLarge}
102
+ handleFileTooLarge={$props.isCloud ? handleFileTooLarge : null}
110
103
  />
111
104
  </div>
112
105
  </GridPopover>
@@ -18,8 +18,6 @@
18
18
  export let row
19
19
  export let cellId
20
20
  export let updateValue = rows.actions.updateValue
21
- export let invertX = false
22
- export let invertY = false
23
21
  export let contentLines = 1
24
22
  export let hidden = false
25
23
 
@@ -93,8 +91,6 @@
93
91
  onChange={cellAPI.setValue}
94
92
  {focused}
95
93
  {readonly}
96
- {invertY}
97
- {invertX}
98
94
  {contentLines}
99
95
  />
100
96
  <slot />
@@ -10,7 +10,6 @@
10
10
  export let focused = false
11
11
  export let readonly = false
12
12
  export let api
13
- export let invertX = false
14
13
 
15
14
  let isOpen
16
15
  let anchor
@@ -111,7 +110,7 @@
111
110
  </div>
112
111
 
113
112
  {#if isOpen}
114
- <GridPopover {anchor} {invertX} maxHeight={null} on:close={close}>
113
+ <GridPopover {anchor} maxHeight={null} on:close={close}>
115
114
  <CoreDatePickerPopoverContents
116
115
  value={parsedValue}
117
116
  useKeyboardShortcuts={false}
@@ -23,7 +23,6 @@
23
23
  subscribe,
24
24
  config,
25
25
  ui,
26
- columns,
27
26
  definition,
28
27
  datasource,
29
28
  schema,
@@ -158,17 +157,13 @@
158
157
  }
159
158
 
160
159
  const makeDisplayColumn = () => {
161
- columns.actions.changePrimaryDisplay(column.name)
160
+ datasource.actions.changePrimaryDisplay(column.name)
162
161
  open = false
163
162
  }
164
163
 
165
164
  const hideColumn = () => {
166
- columns.update(state => {
167
- const index = state.findIndex(col => col.name === column.name)
168
- state[index].visible = false
169
- return state.slice()
170
- })
171
- columns.actions.saveChanges()
165
+ datasource.actions.addSchemaMutation(column.name, { visible: false })
166
+ datasource.actions.saveSchemaMutations()
172
167
  open = false
173
168
  }
174
169
 
@@ -8,7 +8,6 @@
8
8
  export let onChange
9
9
  export let readonly = false
10
10
  export let api
11
- export let invertX = false
12
11
 
13
12
  let textarea
14
13
  let isOpen = false
@@ -67,7 +66,7 @@
67
66
  </div>
68
67
 
69
68
  {#if isOpen}
70
- <GridPopover {anchor} {invertX} on:close={close}>
69
+ <GridPopover {anchor} on:close={close}>
71
70
  <textarea
72
71
  bind:this={textarea}
73
72
  value={value || ""}
@@ -11,7 +11,6 @@
11
11
  export let multi = false
12
12
  export let readonly = false
13
13
  export let api
14
- export let invertX
15
14
  export let contentLines = 1
16
15
 
17
16
  let isOpen = false
@@ -120,7 +119,7 @@
120
119
  </div>
121
120
 
122
121
  {#if isOpen}
123
- <GridPopover {anchor} {invertX} on:close={close}>
122
+ <GridPopover {anchor} on:close={close}>
124
123
  <div class="options">
125
124
  {#each options as option, idx}
126
125
  {@const color = optionColors[option] || getOptionColor(option)}
@@ -13,7 +13,6 @@
13
13
  export let focused
14
14
  export let schema
15
15
  export let onChange
16
- export let invertX = false
17
16
  export let contentLines = 1
18
17
  export let searchFunction = API.searchTable
19
18
  export let primaryDisplay
@@ -275,7 +274,7 @@
275
274
  <!-- svelte-ignore a11y-no-static-element-interactions -->
276
275
  <!-- svelte-ignore a11y-click-events-have-key-events -->
277
276
  {#if isOpen}
278
- <GridPopover open={isOpen} {anchor} {invertX} on:close={close}>
277
+ <GridPopover open={isOpen} {anchor} on:close={close}>
279
278
  <div class="dropdown" on:wheel|stopPropagation>
280
279
  <div class="search">
281
280
  <Input
@@ -0,0 +1,162 @@
1
+ <script>
2
+ import { onMount, getContext } from "svelte"
3
+ import { SignatureModal } from "@budibase/frontend-core/src/components"
4
+ import { CoreSignature, ActionButton } from "@budibase/bbui"
5
+ import GridPopover from "../overlays/GridPopover.svelte"
6
+
7
+ export let schema
8
+ export let value
9
+ export let focused = false
10
+ export let onChange
11
+ export let readonly = false
12
+ export let api
13
+
14
+ const { API, notifications, props } = getContext("grid")
15
+
16
+ let isOpen = false
17
+ let modal
18
+ let anchor
19
+
20
+ $: editable = focused && !readonly
21
+ $: {
22
+ if (!focused) {
23
+ close()
24
+ }
25
+ }
26
+
27
+ const onKeyDown = () => {
28
+ return false
29
+ }
30
+
31
+ const open = () => {
32
+ isOpen = true
33
+ }
34
+
35
+ const close = () => {
36
+ isOpen = false
37
+ }
38
+
39
+ const deleteSignature = async () => {
40
+ onChange(null)
41
+ }
42
+
43
+ const saveSignature = async sigCanvas => {
44
+ const signatureFile = sigCanvas.toFile()
45
+
46
+ let attachRequest = new FormData()
47
+ attachRequest.append("file", signatureFile)
48
+
49
+ try {
50
+ const uploadReq = await API.uploadBuilderAttachment(attachRequest)
51
+ const [signatureAttachment] = uploadReq
52
+ onChange(signatureAttachment)
53
+ } catch (error) {
54
+ $notifications.error(error.message || "Failed to save signature")
55
+ return []
56
+ }
57
+ }
58
+
59
+ onMount(() => {
60
+ api = {
61
+ focus: () => open(),
62
+ blur: () => close(),
63
+ isActive: () => isOpen,
64
+ onKeyDown,
65
+ }
66
+ })
67
+ </script>
68
+
69
+ <!-- svelte-ignore a11y-no-static-element-interactions -->
70
+ <!-- svelte-ignore a11y-click-events-have-key-events -->
71
+ <div
72
+ class="signature-cell"
73
+ class:light={!$props?.darkMode}
74
+ class:editable
75
+ bind:this={anchor}
76
+ on:click={editable ? open : null}
77
+ >
78
+ {#if value?.url}
79
+ <!-- svelte-ignore a11y-missing-attribute -->
80
+ <img src={value?.url} />
81
+ {/if}
82
+ </div>
83
+
84
+ <SignatureModal
85
+ onConfirm={saveSignature}
86
+ title={schema?.name}
87
+ {value}
88
+ darkMode={$props.darkMode}
89
+ bind:this={modal}
90
+ />
91
+
92
+ {#if isOpen}
93
+ <GridPopover open={isOpen} {anchor} maxHeight={null} on:close={close}>
94
+ <div class="signature" class:empty={!value}>
95
+ {#if value?.key}
96
+ <div class="signature-wrap">
97
+ <CoreSignature
98
+ darkMode={$props.darkMode}
99
+ editable={false}
100
+ {value}
101
+ on:change={saveSignature}
102
+ on:clear={deleteSignature}
103
+ />
104
+ </div>
105
+ {:else}
106
+ <div class="add-signature">
107
+ <ActionButton
108
+ fullWidth
109
+ on:click={() => {
110
+ modal.show()
111
+ }}
112
+ >
113
+ Add signature
114
+ </ActionButton>
115
+ </div>
116
+ {/if}
117
+ </div>
118
+ </GridPopover>
119
+ {/if}
120
+
121
+ <style>
122
+ .signature {
123
+ min-width: 320px;
124
+ padding: var(--cell-padding);
125
+ background: var(--grid-background-alt);
126
+ border: var(--cell-border);
127
+ }
128
+ .signature.empty {
129
+ width: 100%;
130
+ min-width: unset;
131
+ }
132
+ .signature-cell.light img {
133
+ -webkit-filter: invert(100%);
134
+ filter: invert(100%);
135
+ }
136
+ .signature-cell {
137
+ flex: 1 1 auto;
138
+ display: flex;
139
+ flex-direction: row;
140
+ align-items: stretch;
141
+ max-width: 320px;
142
+ padding-left: var(--cell-padding);
143
+ padding-right: var(--cell-padding);
144
+ flex-wrap: nowrap;
145
+ align-self: stretch;
146
+ overflow: hidden;
147
+ user-select: none;
148
+ }
149
+ .signature-cell.editable:hover {
150
+ cursor: pointer;
151
+ }
152
+ .signature-wrap {
153
+ display: flex;
154
+ flex-direction: column;
155
+ justify-content: flex-start;
156
+ align-items: stretch;
157
+ background-color: var(--spectrum-global-color-gray-50);
158
+ color: var(--spectrum-alias-text-color);
159
+ box-sizing: border-box;
160
+ position: relative;
161
+ }
162
+ </style>
@@ -3,7 +3,7 @@
3
3
  import { ActionButton, Popover, Toggle, Icon } from "@budibase/bbui"
4
4
  import { getColumnIcon } from "../lib/utils"
5
5
 
6
- const { columns, stickyColumn, dispatch } = getContext("grid")
6
+ const { columns, datasource, stickyColumn, dispatch } = getContext("grid")
7
7
 
8
8
  let open = false
9
9
  let anchor
@@ -11,36 +11,20 @@
11
11
  $: anyHidden = $columns.some(col => !col.visible)
12
12
  $: text = getText($columns)
13
13
 
14
- const toggleVisibility = async (column, visible) => {
15
- columns.update(state => {
16
- const index = state.findIndex(col => col.name === column.name)
17
- state[index].visible = visible
18
- return state.slice()
19
- })
20
- await columns.actions.saveChanges()
14
+ const toggleColumn = async (column, visible) => {
15
+ datasource.actions.addSchemaMutation(column.name, { visible })
16
+ await datasource.actions.saveSchemaMutations()
21
17
  dispatch(visible ? "show-column" : "hide-column")
22
18
  }
23
19
 
24
- const showAll = async () => {
25
- columns.update(state => {
26
- return state.map(col => ({
27
- ...col,
28
- visible: true,
29
- }))
20
+ const toggleAll = async visible => {
21
+ let mutations = {}
22
+ $columns.forEach(column => {
23
+ mutations[column.name] = { visible }
30
24
  })
31
- await columns.actions.saveChanges()
32
- dispatch("show-column")
33
- }
34
-
35
- const hideAll = async () => {
36
- columns.update(state => {
37
- return state.map(col => ({
38
- ...col,
39
- visible: false,
40
- }))
41
- })
42
- await columns.actions.saveChanges()
43
- dispatch("hide-column")
25
+ datasource.actions.addSchemaMutations(mutations)
26
+ await datasource.actions.saveSchemaMutations()
27
+ dispatch(visible ? "show-column" : "hide-column")
44
28
  }
45
29
 
46
30
  const getText = columns => {
@@ -80,14 +64,14 @@
80
64
  <Toggle
81
65
  size="S"
82
66
  value={column.visible}
83
- on:change={e => toggleVisibility(column, e.detail)}
67
+ on:change={e => toggleColumn(column, e.detail)}
84
68
  disabled={column.primaryDisplay}
85
69
  />
86
70
  {/each}
87
71
  </div>
88
72
  <div class="buttons">
89
- <ActionButton on:click={showAll}>Show all</ActionButton>
90
- <ActionButton on:click={hideAll}>Hide all</ActionButton>
73
+ <ActionButton on:click={() => toggleAll(true)}>Show all</ActionButton>
74
+ <ActionButton on:click={() => toggleAll(false)}>Hide all</ActionButton>
91
75
  </div>
92
76
  </div>
93
77
  </Popover>
@@ -1,6 +1,6 @@
1
1
  <script>
2
2
  import { setContext, onMount } from "svelte"
3
- import { writable } from "svelte/store"
3
+ import { writable, derived } from "svelte/store"
4
4
  import { fade } from "svelte/transition"
5
5
  import { clickOutside, ProgressCircle } from "@budibase/bbui"
6
6
  import { createEventManagers } from "../lib/events"
@@ -54,6 +54,8 @@
54
54
  export let notifySuccess = null
55
55
  export let notifyError = null
56
56
  export let buttons = null
57
+ export let darkMode
58
+ export let isCloud = null
57
59
 
58
60
  // Unique identifier for DOM nodes inside this instance
59
61
  const gridID = `grid-${Math.random().toString().slice(2)}`
@@ -108,9 +110,16 @@
108
110
  notifySuccess,
109
111
  notifyError,
110
112
  buttons,
113
+ darkMode,
114
+ isCloud,
111
115
  })
112
- $: minHeight =
113
- Padding + SmallRowHeight + $rowHeight + (showControls ? ControlsHeight : 0)
116
+
117
+ // Derive min height and make available in context
118
+ const minHeight = derived(rowHeight, $height => {
119
+ const heightForControls = showControls ? ControlsHeight : 0
120
+ return Padding + SmallRowHeight + $height + heightForControls
121
+ })
122
+ context = { ...context, minHeight }
114
123
 
115
124
  // Set context for children to consume
116
125
  setContext("grid", context)
@@ -136,7 +145,7 @@
136
145
  class:quiet
137
146
  on:mouseenter={() => gridFocused.set(true)}
138
147
  on:mouseleave={() => gridFocused.set(false)}
139
- style="--row-height:{$rowHeight}px; --default-row-height:{DefaultRowHeight}px; --gutter-width:{GutterWidth}px; --max-cell-render-overflow:{MaxCellRenderOverflow}px; --content-lines:{$contentLines}; --min-height:{minHeight}px; --controls-height:{ControlsHeight}px;"
148
+ style="--row-height:{$rowHeight}px; --default-row-height:{DefaultRowHeight}px; --gutter-width:{GutterWidth}px; --max-cell-render-overflow:{MaxCellRenderOverflow}px; --content-lines:{$contentLines}; --min-height:{$minHeight}px; --controls-height:{ControlsHeight}px;"
140
149
  >
141
150
  {#if showControls}
142
151
  <div class="controls">
@@ -9,7 +9,6 @@
9
9
  bounds,
10
10
  renderedRows,
11
11
  visibleColumns,
12
- rowVerticalInversionIndex,
13
12
  hoveredRowId,
14
13
  dispatch,
15
14
  isDragging,
@@ -41,11 +40,7 @@
41
40
  <div bind:this={body} class="grid-body">
42
41
  <GridScrollWrapper scrollHorizontally scrollVertically attachHandlers>
43
42
  {#each $renderedRows as row, idx}
44
- <GridRow
45
- {row}
46
- top={idx === 0}
47
- invertY={idx >= $rowVerticalInversionIndex}
48
- />
43
+ <GridRow {row} top={idx === 0} />
49
44
  {/each}
50
45
  {#if $config.canAddRows}
51
46
  <div
@@ -5,7 +5,6 @@
5
5
 
6
6
  export let row
7
7
  export let top = false
8
- export let invertY = false
9
8
 
10
9
  const {
11
10
  focusedCellId,
@@ -15,7 +14,6 @@
15
14
  hoveredRowId,
16
15
  selectedCellMap,
17
16
  focusedRow,
18
- columnHorizontalInversionIndex,
19
17
  contentLines,
20
18
  isDragging,
21
19
  dispatch,
@@ -38,15 +36,13 @@
38
36
  on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
39
37
  on:click={() => dispatch("rowclick", rows.actions.cleanRow(row))}
40
38
  >
41
- {#each $visibleColumns as column, columnIdx}
39
+ {#each $visibleColumns as column}
42
40
  {@const cellId = getCellID(row._id, column.name)}
43
41
  <DataCell
44
42
  {cellId}
45
43
  {column}
46
44
  {row}
47
- {invertY}
48
45
  {rowFocused}
49
- invertX={columnIdx >= $columnHorizontalInversionIndex}
50
46
  highlighted={rowHovered || rowFocused || reorderSource === column.name}
51
47
  selected={rowSelected}
52
48
  rowIdx={row.__idx}
@@ -24,8 +24,6 @@
24
24
  rowHeight,
25
25
  hasNextPage,
26
26
  maxScrollTop,
27
- rowVerticalInversionIndex,
28
- columnHorizontalInversionIndex,
29
27
  selectedRows,
30
28
  loaded,
31
29
  refreshing,
@@ -43,17 +41,9 @@
43
41
  $: firstColumn = $stickyColumn || $visibleColumns[0]
44
42
  $: width = GutterWidth + ($stickyColumn?.width || 0)
45
43
  $: $datasource, (visible = false)
46
- $: invertY = shouldInvertY(offset, $rowVerticalInversionIndex, $renderedRows)
47
44
  $: selectedRowCount = Object.values($selectedRows).length
48
45
  $: hasNoRows = !$rows.length
49
46
 
50
- const shouldInvertY = (offset, inversionIndex, rows) => {
51
- if (offset === 0) {
52
- return false
53
- }
54
- return rows.length >= inversionIndex
55
- }
56
-
57
47
  const addRow = async () => {
58
48
  // Blur the active cell and tick to let final value updates propagate
59
49
  isAdding = true
@@ -205,7 +195,6 @@
205
195
  width={$stickyColumn.width}
206
196
  {updateValue}
207
197
  topRow={offset === 0}
208
- {invertY}
209
198
  >
210
199
  {#if $stickyColumn?.schema?.autocolumn}
211
200
  <div class="readonly-overlay">Can't edit auto column</div>
@@ -219,7 +208,7 @@
219
208
  <div class="normal-columns" transition:fade|local={{ duration: 130 }}>
220
209
  <GridScrollWrapper scrollHorizontally attachHandlers>
221
210
  <div class="row">
222
- {#each $visibleColumns as column, columnIdx}
211
+ {#each $visibleColumns as column}
223
212
  {@const cellId = `new-${column.name}`}
224
213
  <DataCell
225
214
  {cellId}
@@ -230,8 +219,6 @@
230
219
  focused={$focusedCellId === cellId}
231
220
  width={column.width}
232
221
  topRow={offset === 0}
233
- invertX={columnIdx >= $columnHorizontalInversionIndex}
234
- {invertY}
235
222
  hidden={!$columnRenderMap[column.name]}
236
223
  >
237
224
  {#if column?.schema?.autocolumn}
@@ -13,6 +13,7 @@ import JSONCell from "../cells/JSONCell.svelte"
13
13
  import AttachmentCell from "../cells/AttachmentCell.svelte"
14
14
  import AttachmentSingleCell from "../cells/AttachmentSingleCell.svelte"
15
15
  import BBReferenceCell from "../cells/BBReferenceCell.svelte"
16
+ import SignatureCell from "../cells/SignatureCell.svelte"
16
17
  import BBReferenceSingleCell from "../cells/BBReferenceSingleCell.svelte"
17
18
 
18
19
  const TypeComponentMap = {
@@ -20,6 +21,7 @@ const TypeComponentMap = {
20
21
  [FieldType.OPTIONS]: OptionsCell,
21
22
  [FieldType.DATETIME]: DateCell,
22
23
  [FieldType.BARCODEQR]: TextCell,
24
+ [FieldType.SIGNATURE_SINGLE]: SignatureCell,
23
25
  [FieldType.LONGFORM]: LongFormCell,
24
26
  [FieldType.ARRAY]: MultiSelectCell,
25
27
  [FieldType.NUMBER]: NumberCell,
@@ -1,5 +1,4 @@
1
1
  import { derived, get, writable } from "svelte/store"
2
- import { cloneDeep } from "lodash/fp"
3
2
  import { GutterWidth, DefaultColumnWidth } from "../lib/constants"
4
3
 
5
4
  export const createStores = () => {
@@ -75,72 +74,23 @@ export const deriveStores = context => {
75
74
  }
76
75
 
77
76
  export const createActions = context => {
78
- const { columns, stickyColumn, datasource, definition, schema } = context
79
-
80
- // Updates the datasources primary display column
81
- const changePrimaryDisplay = async column => {
82
- return await datasource.actions.saveDefinition({
83
- ...get(definition),
84
- primaryDisplay: column,
85
- })
86
- }
77
+ const { columns, datasource, schema } = context
87
78
 
88
79
  // Updates the width of all columns
89
80
  const changeAllColumnWidths = async width => {
90
- columns.update(state => {
91
- return state.map(col => ({
92
- ...col,
93
- width,
94
- }))
95
- })
96
- if (get(stickyColumn)) {
97
- stickyColumn.update(state => ({
98
- ...state,
99
- width,
100
- }))
101
- }
102
- await saveChanges()
103
- }
104
-
105
- // Persists column changes by saving metadata against datasource schema
106
- const saveChanges = async () => {
107
- const $columns = get(columns)
108
- const $definition = get(definition)
109
- const $stickyColumn = get(stickyColumn)
110
- let newSchema = cloneDeep(get(schema)) || {}
111
-
112
- // Build new updated datasource schema
113
- Object.keys(newSchema).forEach(column => {
114
- // Respect order specified by columns
115
- const index = $columns.findIndex(x => x.name === column)
116
- if (index !== -1) {
117
- newSchema[column].order = index
118
- } else {
119
- delete newSchema[column].order
120
- }
121
-
122
- // Copy over metadata
123
- if (column === $stickyColumn?.name) {
124
- newSchema[column].visible = true
125
- newSchema[column].width = $stickyColumn.width || DefaultColumnWidth
126
- } else {
127
- newSchema[column].visible = $columns[index]?.visible ?? true
128
- newSchema[column].width = $columns[index]?.width || DefaultColumnWidth
129
- }
130
- })
131
-
132
- await datasource.actions.saveDefinition({
133
- ...$definition,
134
- schema: newSchema,
81
+ const $schema = get(schema)
82
+ let mutations = {}
83
+ Object.keys($schema).forEach(field => {
84
+ mutations[field] = { width }
135
85
  })
86
+ datasource.actions.addSchemaMutations(mutations)
87
+ await datasource.actions.saveSchemaMutations()
136
88
  }
137
89
 
138
90
  return {
139
91
  columns: {
140
92
  ...columns,
141
93
  actions: {
142
- saveChanges,
143
- changePrimaryDisplay,
144
94
  changeAllColumnWidths,
145
95
  },
146
96
  },
@@ -4,15 +4,23 @@ import { memo } from "../../../utils"
4
4
 
5
5
  export const createStores = () => {
6
6
  const definition = memo(null)
7
+ const schemaMutations = memo({})
7
8
 
8
9
  return {
9
10
  definition,
11
+ schemaMutations,
10
12
  }
11
13
  }
12
14
 
13
15
  export const deriveStores = context => {
14
- const { API, definition, schemaOverrides, columnWhitelist, datasource } =
15
- context
16
+ const {
17
+ API,
18
+ definition,
19
+ schemaOverrides,
20
+ columnWhitelist,
21
+ datasource,
22
+ schemaMutations,
23
+ } = context
16
24
 
17
25
  const schema = derived(definition, $definition => {
18
26
  let schema = getDatasourceSchema({
@@ -35,42 +43,26 @@ export const deriveStores = context => {
35
43
  return schema
36
44
  })
37
45
 
46
+ // Derives the total enriched schema, made up of the saved schema and any
47
+ // prop and user overrides
38
48
  const enrichedSchema = derived(
39
- [schema, schemaOverrides, columnWhitelist],
40
- ([$schema, $schemaOverrides, $columnWhitelist]) => {
49
+ [schema, schemaOverrides, schemaMutations, columnWhitelist],
50
+ ([$schema, $schemaOverrides, $schemaMutations, $columnWhitelist]) => {
41
51
  if (!$schema) {
42
52
  return null
43
53
  }
44
- let enrichedSchema = { ...$schema }
45
-
46
- // Apply schema overrides
47
- Object.keys($schemaOverrides || {}).forEach(field => {
48
- if (enrichedSchema[field]) {
49
- enrichedSchema[field] = {
50
- ...enrichedSchema[field],
51
- ...$schemaOverrides[field],
52
- }
54
+ let enrichedSchema = {}
55
+ Object.keys($schema).forEach(field => {
56
+ // Apply whitelist if provided
57
+ if ($columnWhitelist?.length && !$columnWhitelist.includes(field)) {
58
+ return
59
+ }
60
+ enrichedSchema[field] = {
61
+ ...$schema[field],
62
+ ...$schemaOverrides?.[field],
63
+ ...$schemaMutations[field],
53
64
  }
54
65
  })
55
-
56
- // Apply whitelist if specified
57
- if ($columnWhitelist?.length) {
58
- const sortedColumns = {}
59
-
60
- $columnWhitelist.forEach((columnKey, idx) => {
61
- const enrichedColumn = enrichedSchema[columnKey]
62
- if (enrichedColumn) {
63
- sortedColumns[columnKey] = {
64
- ...enrichedColumn,
65
- order: idx,
66
- visible: true,
67
- }
68
- }
69
- })
70
-
71
- return sortedColumns
72
- }
73
-
74
66
  return enrichedSchema
75
67
  }
76
68
  )
@@ -100,6 +92,8 @@ export const createActions = context => {
100
92
  table,
101
93
  viewV2,
102
94
  nonPlus,
95
+ schemaMutations,
96
+ schema,
103
97
  } = context
104
98
 
105
99
  // Gets the appropriate API for the configured datasource type
@@ -136,11 +130,81 @@ export const createActions = context => {
136
130
  // Update server
137
131
  if (get(config).canSaveSchema) {
138
132
  await getAPI()?.actions.saveDefinition(newDefinition)
133
+
134
+ // Broadcast change so external state can be updated, as this change
135
+ // will not be received by the builder websocket because we caused it
136
+ // ourselves
137
+ dispatch("updatedatasource", newDefinition)
138
+ }
139
+ }
140
+
141
+ // Updates the datasources primary display column
142
+ const changePrimaryDisplay = async column => {
143
+ return await saveDefinition({
144
+ ...get(definition),
145
+ primaryDisplay: column,
146
+ })
147
+ }
148
+
149
+ // Adds a schema mutation for a single field
150
+ const addSchemaMutation = (field, mutation) => {
151
+ if (!field || !mutation) {
152
+ return
153
+ }
154
+ schemaMutations.update($schemaMutations => {
155
+ return {
156
+ ...$schemaMutations,
157
+ [field]: {
158
+ ...$schemaMutations[field],
159
+ ...mutation,
160
+ },
161
+ }
162
+ })
163
+ }
164
+
165
+ // Adds schema mutations for multiple fields at once
166
+ const addSchemaMutations = mutations => {
167
+ const fields = Object.keys(mutations || {})
168
+ if (!fields.length) {
169
+ return
170
+ }
171
+ schemaMutations.update($schemaMutations => {
172
+ let newSchemaMutations = { ...$schemaMutations }
173
+ fields.forEach(field => {
174
+ newSchemaMutations[field] = {
175
+ ...newSchemaMutations[field],
176
+ ...mutations[field],
177
+ }
178
+ })
179
+ return newSchemaMutations
180
+ })
181
+ }
182
+
183
+ // Saves schema changes to the server, if possible
184
+ const saveSchemaMutations = async () => {
185
+ // If we can't save schema changes then we just want to keep this in memory
186
+ if (!get(config).canSaveSchema) {
187
+ return
139
188
  }
189
+ const $definition = get(definition)
190
+ const $schemaMutations = get(schemaMutations)
191
+ const $schema = get(schema)
192
+ let newSchema = {}
140
193
 
141
- // Broadcast change to external state can be updated, as this change
142
- // will not be received by the builder websocket because we caused it ourselves
143
- dispatch("updatedatasource", newDefinition)
194
+ // Build new updated datasource schema
195
+ Object.keys($schema).forEach(column => {
196
+ newSchema[column] = {
197
+ ...$schema[column],
198
+ ...$schemaMutations[column],
199
+ }
200
+ })
201
+
202
+ // Save the changes, then reset our local mutations
203
+ await saveDefinition({
204
+ ...$definition,
205
+ schema: newSchema,
206
+ })
207
+ schemaMutations.set({})
144
208
  }
145
209
 
146
210
  // Adds a row to the datasource
@@ -185,6 +249,10 @@ export const createActions = context => {
185
249
  getRow,
186
250
  isDatasourceValid,
187
251
  canUseColumn,
252
+ changePrimaryDisplay,
253
+ addSchemaMutation,
254
+ addSchemaMutations,
255
+ saveSchemaMutations,
188
256
  },
189
257
  },
190
258
  }
@@ -34,6 +34,7 @@ export const createActions = context => {
34
34
  stickyColumn,
35
35
  maxScrollLeft,
36
36
  width,
37
+ datasource,
37
38
  } = context
38
39
 
39
40
  let autoScrollInterval
@@ -173,20 +174,17 @@ export const createActions = context => {
173
174
  document.removeEventListener("touchend", stopReordering)
174
175
  document.removeEventListener("touchcancel", stopReordering)
175
176
 
176
- // Ensure there's actually a change
177
- let { sourceColumn, targetColumn } = get(reorder)
177
+ // Ensure there's actually a change before saving
178
+ const { sourceColumn, targetColumn } = get(reorder)
179
+ reorder.set(reorderInitialState)
178
180
  if (sourceColumn !== targetColumn) {
179
- moveColumn(sourceColumn, targetColumn)
180
- await columns.actions.saveChanges()
181
+ await moveColumn(sourceColumn, targetColumn)
181
182
  }
182
-
183
- // Reset state
184
- reorder.set(reorderInitialState)
185
183
  }
186
184
 
187
185
  // Moves a column after another columns.
188
186
  // An undefined target column will move the source to index 0.
189
- const moveColumn = (sourceColumn, targetColumn) => {
187
+ const moveColumn = async (sourceColumn, targetColumn) => {
190
188
  let $columns = get(columns)
191
189
  let sourceIdx = $columns.findIndex(x => x.name === sourceColumn)
192
190
  let targetIdx = $columns.findIndex(x => x.name === targetColumn)
@@ -198,14 +196,21 @@ export const createActions = context => {
198
196
  }
199
197
  return state.toSpliced(targetIdx, 0, removed[0])
200
198
  })
199
+
200
+ // Extract new orders as schema mutations
201
+ let mutations = {}
202
+ get(columns).forEach((column, idx) => {
203
+ mutations[column.name] = { order: idx }
204
+ })
205
+ datasource.actions.addSchemaMutations(mutations)
206
+ await datasource.actions.saveSchemaMutations()
201
207
  }
202
208
 
203
209
  // Moves a column one place left (as appears visually)
204
210
  const moveColumnLeft = async column => {
205
211
  const $visibleColumns = get(visibleColumns)
206
212
  const sourceIdx = $visibleColumns.findIndex(x => x.name === column)
207
- moveColumn(column, $visibleColumns[sourceIdx - 2]?.name)
208
- await columns.actions.saveChanges()
213
+ await moveColumn(column, $visibleColumns[sourceIdx - 2]?.name)
209
214
  }
210
215
 
211
216
  // Moves a column one place right (as appears visually)
@@ -215,8 +220,7 @@ export const createActions = context => {
215
220
  if (sourceIdx === $visibleColumns.length - 1) {
216
221
  return
217
222
  }
218
- moveColumn(column, $visibleColumns[sourceIdx + 1]?.name)
219
- await columns.actions.saveChanges()
223
+ await moveColumn(column, $visibleColumns[sourceIdx + 1]?.name)
220
224
  }
221
225
 
222
226
  return {
@@ -6,7 +6,6 @@ const initialState = {
6
6
  initialMouseX: null,
7
7
  initialWidth: null,
8
8
  column: null,
9
- columnIdx: null,
10
9
  width: 0,
11
10
  left: 0,
12
11
  }
@@ -21,7 +20,7 @@ export const createStores = () => {
21
20
  }
22
21
 
23
22
  export const createActions = context => {
24
- const { resize, columns, stickyColumn, ui } = context
23
+ const { resize, ui, datasource } = context
25
24
 
26
25
  // Starts resizing a certain column
27
26
  const startResizing = (column, e) => {
@@ -32,12 +31,6 @@ export const createActions = context => {
32
31
  e.preventDefault()
33
32
  ui.actions.blur()
34
33
 
35
- // Find and cache index
36
- let columnIdx = get(columns).findIndex(col => col.name === column.name)
37
- if (columnIdx === -1) {
38
- columnIdx = "sticky"
39
- }
40
-
41
34
  // Set initial store state
42
35
  resize.set({
43
36
  width: column.width,
@@ -45,7 +38,6 @@ export const createActions = context => {
45
38
  initialWidth: column.width,
46
39
  initialMouseX: x,
47
40
  column: column.name,
48
- columnIdx,
49
41
  })
50
42
 
51
43
  // Add mouse event listeners to handle resizing
@@ -58,7 +50,7 @@ export const createActions = context => {
58
50
 
59
51
  // Handler for moving the mouse to resize columns
60
52
  const onResizeMouseMove = e => {
61
- const { initialMouseX, initialWidth, width, columnIdx } = get(resize)
53
+ const { initialMouseX, initialWidth, width, column } = get(resize)
62
54
  const { x } = parseEventLocation(e)
63
55
  const dx = x - initialMouseX
64
56
  const newWidth = Math.round(Math.max(MinColumnWidth, initialWidth + dx))
@@ -69,17 +61,7 @@ export const createActions = context => {
69
61
  }
70
62
 
71
63
  // Update column state
72
- if (columnIdx === "sticky") {
73
- stickyColumn.update(state => ({
74
- ...state,
75
- width: newWidth,
76
- }))
77
- } else {
78
- columns.update(state => {
79
- state[columnIdx].width = newWidth
80
- return [...state]
81
- })
82
- }
64
+ datasource.actions.addSchemaMutation(column, { width })
83
65
 
84
66
  // Update state
85
67
  resize.update(state => ({
@@ -101,26 +83,16 @@ export const createActions = context => {
101
83
 
102
84
  // Persist width if it changed
103
85
  if ($resize.width !== $resize.initialWidth) {
104
- await columns.actions.saveChanges()
86
+ await datasource.actions.saveSchemaMutations()
105
87
  }
106
88
  }
107
89
 
108
90
  // Resets a column size back to default
109
91
  const resetSize = async column => {
110
- const $stickyColumn = get(stickyColumn)
111
- if (column.name === $stickyColumn?.name) {
112
- stickyColumn.update(state => ({
113
- ...state,
114
- width: DefaultColumnWidth,
115
- }))
116
- } else {
117
- columns.update(state => {
118
- const columnIdx = state.findIndex(x => x.name === column.name)
119
- state[columnIdx].width = DefaultColumnWidth
120
- return [...state]
121
- })
122
- }
123
- await columns.actions.saveChanges()
92
+ datasource.actions.addSchemaMutation(column.name, {
93
+ width: DefaultColumnWidth,
94
+ })
95
+ await datasource.actions.saveSchemaMutations()
124
96
  }
125
97
 
126
98
  return {
@@ -1,9 +1,5 @@
1
1
  import { derived } from "svelte/store"
2
- import {
3
- MaxCellRenderOverflow,
4
- MinColumnWidth,
5
- ScrollBarSize,
6
- } from "../lib/constants"
2
+ import { MinColumnWidth } from "../lib/constants"
7
3
 
8
4
  export const deriveStores = context => {
9
5
  const {
@@ -85,51 +81,10 @@ export const deriveStores = context => {
85
81
  }
86
82
  )
87
83
 
88
- // Determine the row index at which we should start vertically inverting cell
89
- // dropdowns
90
- const rowVerticalInversionIndex = derived(
91
- [height, rowHeight, scrollTop],
92
- ([$height, $rowHeight, $scrollTop]) => {
93
- const offset = $scrollTop % $rowHeight
94
-
95
- // Compute the last row index with space to render popovers below it
96
- const minBottom =
97
- $height - ScrollBarSize * 3 - MaxCellRenderOverflow + offset
98
- const lastIdx = Math.floor(minBottom / $rowHeight)
99
-
100
- // Compute the first row index with space to render popovers above it
101
- const minTop = MaxCellRenderOverflow + offset
102
- const firstIdx = Math.ceil(minTop / $rowHeight)
103
-
104
- // Use the greater of the two indices so that we prefer content below,
105
- // unless there is room to render the entire popover above
106
- return Math.max(lastIdx, firstIdx)
107
- }
108
- )
109
-
110
- // Determine the column index at which we should start horizontally inverting
111
- // cell dropdowns
112
- const columnHorizontalInversionIndex = derived(
113
- [visibleColumns, scrollLeft, width],
114
- ([$visibleColumns, $scrollLeft, $width]) => {
115
- const cutoff = $width + $scrollLeft - ScrollBarSize * 3
116
- let inversionIdx = $visibleColumns.length
117
- for (let i = $visibleColumns.length - 1; i >= 0; i--, inversionIdx--) {
118
- const rightEdge = $visibleColumns[i].left + $visibleColumns[i].width
119
- if (rightEdge + MaxCellRenderOverflow <= cutoff) {
120
- break
121
- }
122
- }
123
- return inversionIdx
124
- }
125
- )
126
-
127
84
  return {
128
85
  scrolledRowCount,
129
86
  visualRowCapacity,
130
87
  renderedRows,
131
88
  columnRenderMap,
132
- rowVerticalInversionIndex,
133
- columnHorizontalInversionIndex,
134
89
  }
135
90
  }
@@ -1,5 +1,6 @@
1
1
  export { default as SplitPage } from "./SplitPage.svelte"
2
2
  export { default as TestimonialPage } from "./TestimonialPage.svelte"
3
+ export { default as SignatureModal } from "./SignatureModal.svelte"
3
4
  export { default as Testimonial } from "./Testimonial.svelte"
4
5
  export { default as UserAvatar } from "./UserAvatar.svelte"
5
6
  export { default as UserAvatars } from "./UserAvatars.svelte"
package/src/constants.js CHANGED
@@ -121,6 +121,7 @@ export const TypeIconMap = {
121
121
  [FieldType.OPTIONS]: "Dropdown",
122
122
  [FieldType.DATETIME]: "Calendar",
123
123
  [FieldType.BARCODEQR]: "Camera",
124
+ [FieldType.SIGNATURE_SINGLE]: "AnnotatePen",
124
125
  [FieldType.LONGFORM]: "TextAlignLeft",
125
126
  [FieldType.ARRAY]: "Duplicate",
126
127
  [FieldType.NUMBER]: "123",