@budibase/frontend-core 2.11.39 → 2.11.41

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.11.39",
3
+ "version": "2.11.41",
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.11.39",
10
- "@budibase/shared-core": "2.11.39",
9
+ "@budibase/bbui": "2.11.41",
10
+ "@budibase/shared-core": "2.11.41",
11
11
  "dayjs": "^1.10.8",
12
12
  "lodash": "^4.17.21",
13
13
  "socket.io-client": "^4.6.1",
14
14
  "svelte": "^3.46.2"
15
15
  },
16
- "gitHead": "ecc330f4521e60e5766d7f0e87ac7030a94d8bfc"
16
+ "gitHead": "cfb21ffd5e3a748cd275a7fa91cb357e9df3973b"
17
17
  }
@@ -34,7 +34,7 @@
34
34
  column.schema.autocolumn ||
35
35
  column.schema.disabled ||
36
36
  column.schema.type === "formula" ||
37
- (!$config.canEditRows && row._id)
37
+ (!$config.canEditRows && !row._isNewRow)
38
38
 
39
39
  // Register this cell API if the row is focused
40
40
  $: {
@@ -1,6 +1,6 @@
1
1
  <script>
2
2
  import { getContext, onMount, tick } from "svelte"
3
- import { canBeDisplayColumn } from "@budibase/shared-core"
3
+ import { canBeDisplayColumn, canBeSortColumn } from "@budibase/shared-core"
4
4
  import { Icon, Popover, Menu, MenuItem, clickOutside } from "@budibase/bbui"
5
5
  import GridCell from "./GridCell.svelte"
6
6
  import { getColumnIcon } from "../lib/utils"
@@ -23,6 +23,7 @@
23
23
  columns,
24
24
  definition,
25
25
  datasource,
26
+ schema,
26
27
  } = getContext("grid")
27
28
 
28
29
  let anchor
@@ -119,16 +120,16 @@
119
120
  // Generate new name
120
121
  let newName = `${column.name} copy`
121
122
  let attempts = 2
122
- while ($definition.schema[newName]) {
123
+ while ($schema[newName]) {
123
124
  newName = `${column.name} copy ${attempts++}`
124
125
  }
125
126
 
126
127
  // Save schema with new column
127
- const existingColumnDefinition = $definition.schema[column.name]
128
+ const existingColumnDefinition = $schema[column.name]
128
129
  await datasource.actions.saveDefinition({
129
130
  ...$definition,
130
131
  schema: {
131
- ...$definition.schema,
132
+ ...$schema,
132
133
  [newName]: {
133
134
  ...existingColumnDefinition,
134
135
  name: newName,
@@ -231,14 +232,16 @@
231
232
  <MenuItem
232
233
  icon="SortOrderUp"
233
234
  on:click={sortAscending}
234
- disabled={column.name === $sort.column && $sort.order === "ascending"}
235
+ disabled={!canBeSortColumn(column.schema.type) ||
236
+ (column.name === $sort.column && $sort.order === "ascending")}
235
237
  >
236
238
  Sort {ascendingLabel}
237
239
  </MenuItem>
238
240
  <MenuItem
239
241
  icon="SortOrderDown"
240
242
  on:click={sortDescending}
241
- disabled={column.name === $sort.column && $sort.order === "descending"}
243
+ disabled={!canBeSortColumn(column.schema.type) ||
244
+ (column.name === $sort.column && $sort.order === "descending")}
242
245
  >
243
246
  Sort {descendingLabel}
244
247
  </MenuItem>
@@ -1,6 +1,7 @@
1
1
  <script>
2
2
  import { getContext } from "svelte"
3
3
  import { ActionButton, Popover, Select } from "@budibase/bbui"
4
+ import { canBeSortColumn } from "@budibase/shared-core"
4
5
 
5
6
  const { sort, columns, stickyColumn } = getContext("grid")
6
7
 
@@ -19,7 +20,7 @@
19
20
  type: stickyColumn.schema?.type,
20
21
  })
21
22
  }
22
- return [
23
+ options = [
23
24
  ...options,
24
25
  ...columns.map(col => ({
25
26
  label: col.label || col.name,
@@ -27,6 +28,7 @@
27
28
  type: col.schema?.type,
28
29
  })),
29
30
  ]
31
+ return options.filter(col => canBeSortColumn(col.type))
30
32
  }
31
33
 
32
34
  const getOrderOptions = (column, columnOptions) => {
@@ -141,7 +141,14 @@
141
141
  </div>
142
142
  </div>
143
143
  {/if}
144
- {#if $loaded}
144
+ {#if $error}
145
+ <div class="grid-error">
146
+ <div class="grid-error-title">There was a problem loading your grid</div>
147
+ <div class="grid-error-subtitle">
148
+ {$error}
149
+ </div>
150
+ </div>
151
+ {:else if $loaded}
145
152
  <div class="grid-data-outer" use:clickOutside={ui.actions.blur}>
146
153
  <div class="grid-data-inner">
147
154
  <StickyColumn>
@@ -171,13 +178,6 @@
171
178
  </div>
172
179
  </div>
173
180
  </div>
174
- {:else if $error}
175
- <div class="grid-error">
176
- <div class="grid-error-title">There was a problem loading your grid</div>
177
- <div class="grid-error-subtitle">
178
- {$error}
179
- </div>
180
- </div>
181
181
  {/if}
182
182
  {#if $loading && !$error}
183
183
  <div in:fade|local={{ duration: 130 }} class="grid-loading">
@@ -18,6 +18,7 @@
18
18
  contentLines,
19
19
  isDragging,
20
20
  dispatch,
21
+ rows,
21
22
  } = getContext("grid")
22
23
 
23
24
  $: rowSelected = !!$selectedRows[row._id]
@@ -31,7 +32,7 @@
31
32
  on:focus
32
33
  on:mouseenter={$isDragging ? null : () => ($hoveredRowId = row._id)}
33
34
  on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
34
- on:click={() => dispatch("rowclick", row)}
35
+ on:click={() => dispatch("rowclick", rows.actions.cleanRow(row))}
35
36
  >
36
37
  {#each $renderedColumns as column, columnIdx (column.name)}
37
38
  {@const cellId = `${row._id}-${column.name}`}
@@ -33,7 +33,7 @@
33
33
 
34
34
  let visible = false
35
35
  let isAdding = false
36
- let newRow = {}
36
+ let newRow
37
37
  let offset = 0
38
38
 
39
39
  $: firstColumn = $stickyColumn || $renderedColumns[0]
@@ -58,7 +58,9 @@
58
58
 
59
59
  // Create row
60
60
  const newRowIndex = offset ? undefined : 0
61
- const savedRow = await rows.actions.addRow(newRow, newRowIndex)
61
+ let rowToCreate = { ...newRow }
62
+ delete rowToCreate._isNewRow
63
+ const savedRow = await rows.actions.addRow(rowToCreate, newRowIndex)
62
64
  if (savedRow) {
63
65
  // Reset state
64
66
  clear()
@@ -109,7 +111,7 @@
109
111
  }
110
112
 
111
113
  // Update state and select initial cell
112
- newRow = {}
114
+ newRow = { _isNewRow: true }
113
115
  visible = true
114
116
  $hoveredRowId = NewRowID
115
117
  if (firstColumn) {
@@ -74,7 +74,7 @@
74
74
  class="row"
75
75
  on:mouseenter={$isDragging ? null : () => ($hoveredRowId = row._id)}
76
76
  on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
77
- on:click={() => dispatch("rowclick", row)}
77
+ on:click={() => dispatch("rowclick", rows.actions.cleanRow(row))}
78
78
  >
79
79
  <GutterCell {row} {rowFocused} {rowHovered} {rowSelected} />
80
80
  {#if $stickyColumn}
@@ -1,6 +1,6 @@
1
1
  export const getColor = (idx, opacity = 0.3) => {
2
2
  if (idx == null || idx === -1) {
3
- return null
3
+ idx = 0
4
4
  }
5
5
  return `hsla(${((idx + 1) * 222) % 360}, 90%, 75%, ${opacity})`
6
6
  }
@@ -17,6 +17,7 @@
17
17
  focusedCellAPI,
18
18
  focusedRowId,
19
19
  notifications,
20
+ isDatasourcePlus,
20
21
  } = getContext("grid")
21
22
 
22
23
  $: style = makeStyle($menu)
@@ -75,7 +76,7 @@
75
76
  </MenuItem>
76
77
  <MenuItem
77
78
  icon="Copy"
78
- disabled={isNewRow || !$focusedRow?._id}
79
+ disabled={isNewRow || !$focusedRow?._id || !$isDatasourcePlus}
79
80
  on:click={() => copyToClipboard($focusedRow?._id)}
80
81
  on:click={menu.actions.close}
81
82
  >
@@ -69,7 +69,7 @@ export const deriveStores = context => {
69
69
  }
70
70
 
71
71
  export const createActions = context => {
72
- const { columns, stickyColumn, datasource, definition } = context
72
+ const { columns, stickyColumn, datasource, definition, schema } = context
73
73
 
74
74
  // Updates the datasources primary display column
75
75
  const changePrimaryDisplay = async column => {
@@ -101,7 +101,7 @@ export const createActions = context => {
101
101
  const $columns = get(columns)
102
102
  const $definition = get(definition)
103
103
  const $stickyColumn = get(stickyColumn)
104
- const newSchema = cloneDeep($definition.schema)
104
+ let newSchema = cloneDeep(get(schema)) || {}
105
105
 
106
106
  // Build new updated datasource schema
107
107
  Object.keys(newSchema).forEach(column => {
@@ -142,26 +142,35 @@ export const createActions = context => {
142
142
  }
143
143
 
144
144
  export const initialise = context => {
145
- const { definition, columns, stickyColumn, schema } = context
145
+ const { definition, columns, stickyColumn, enrichedSchema } = context
146
146
 
147
147
  // Merge new schema fields with existing schema in order to preserve widths
148
- schema.subscribe($schema => {
149
- if (!$schema) {
148
+ enrichedSchema.subscribe($enrichedSchema => {
149
+ if (!$enrichedSchema) {
150
150
  columns.set([])
151
151
  stickyColumn.set(null)
152
152
  return
153
153
  }
154
154
  const $definition = get(definition)
155
+ const $columns = get(columns)
156
+ const $stickyColumn = get(stickyColumn)
157
+
158
+ // Generate array of all columns to easily find pre-existing columns
159
+ let allColumns = $columns || []
160
+ if ($stickyColumn) {
161
+ allColumns.push($stickyColumn)
162
+ }
155
163
 
156
164
  // Find primary display
157
165
  let primaryDisplay
158
- if ($definition.primaryDisplay && $schema[$definition.primaryDisplay]) {
159
- primaryDisplay = $definition.primaryDisplay
166
+ const candidatePD = $definition.primaryDisplay || $stickyColumn?.name
167
+ if (candidatePD && $enrichedSchema[candidatePD]) {
168
+ primaryDisplay = candidatePD
160
169
  }
161
170
 
162
171
  // Get field list
163
172
  let fields = []
164
- Object.keys($schema).forEach(field => {
173
+ Object.keys($enrichedSchema).forEach(field => {
165
174
  if (field !== primaryDisplay) {
166
175
  fields.push(field)
167
176
  }
@@ -170,14 +179,18 @@ export const initialise = context => {
170
179
  // Update columns, removing extraneous columns and adding missing ones
171
180
  columns.set(
172
181
  fields
173
- .map(field => ({
174
- name: field,
175
- label: $schema[field].displayName || field,
176
- schema: $schema[field],
177
- width: $schema[field].width || DefaultColumnWidth,
178
- visible: $schema[field].visible ?? true,
179
- order: $schema[field].order,
180
- }))
182
+ .map(field => {
183
+ const fieldSchema = $enrichedSchema[field]
184
+ const oldColumn = allColumns?.find(x => x.name === field)
185
+ return {
186
+ name: field,
187
+ label: fieldSchema.displayName || field,
188
+ schema: fieldSchema,
189
+ width: fieldSchema.width || oldColumn?.width || DefaultColumnWidth,
190
+ visible: fieldSchema.visible ?? true,
191
+ order: fieldSchema.order ?? oldColumn?.order,
192
+ }
193
+ })
181
194
  .sort((a, b) => {
182
195
  // Sort by order first
183
196
  const orderA = a.order
@@ -205,11 +218,13 @@ export const initialise = context => {
205
218
  stickyColumn.set(null)
206
219
  return
207
220
  }
221
+ const stickySchema = $enrichedSchema[primaryDisplay]
222
+ const oldStickyColumn = allColumns?.find(x => x.name === primaryDisplay)
208
223
  stickyColumn.set({
209
224
  name: primaryDisplay,
210
- label: $schema[primaryDisplay].displayName || primaryDisplay,
211
- schema: $schema[primaryDisplay],
212
- width: $schema[primaryDisplay].width || DefaultColumnWidth,
225
+ label: stickySchema.displayName || primaryDisplay,
226
+ schema: stickySchema,
227
+ width: stickySchema.width || oldStickyColumn?.width || DefaultColumnWidth,
213
228
  visible: true,
214
229
  order: 0,
215
230
  left: GutterWidth,
@@ -37,9 +37,10 @@ export const deriveStores = context => {
37
37
  [props, hasNonAutoColumn],
38
38
  ([$props, $hasNonAutoColumn]) => {
39
39
  let config = { ...$props }
40
+ const type = $props.datasource?.type
40
41
 
41
42
  // Disable some features if we're editing a view
42
- if ($props.datasource?.type === "viewV2") {
43
+ if (type === "viewV2") {
43
44
  config.canEditColumns = false
44
45
  }
45
46
 
@@ -48,6 +49,16 @@ export const deriveStores = context => {
48
49
  config.canAddRows = false
49
50
  }
50
51
 
52
+ // Disable features for non DS+
53
+ if (!["table", "viewV2"].includes(type)) {
54
+ config.canAddRows = false
55
+ config.canEditRows = false
56
+ config.canDeleteRows = false
57
+ config.canExpandRows = false
58
+ config.canSaveSchema = false
59
+ config.canEditColumns = false
60
+ }
61
+
51
62
  return config
52
63
  }
53
64
  )
@@ -1,4 +1,5 @@
1
1
  import { derived, get, writable } from "svelte/store"
2
+ import { getDatasourceDefinition } from "../../../fetch"
2
3
 
3
4
  export const createStores = () => {
4
5
  const definition = writable(null)
@@ -9,21 +10,38 @@ export const createStores = () => {
9
10
  }
10
11
 
11
12
  export const deriveStores = context => {
12
- const { definition, schemaOverrides, columnWhitelist } = context
13
+ const { definition, schemaOverrides, columnWhitelist, datasource } = context
13
14
 
14
- const schema = derived(
15
- [definition, schemaOverrides, columnWhitelist],
16
- ([$definition, $schemaOverrides, $columnWhitelist]) => {
17
- if (!$definition?.schema) {
15
+ const schema = derived(definition, $definition => {
16
+ let schema = $definition?.schema
17
+ if (!schema) {
18
+ return null
19
+ }
20
+
21
+ // Ensure schema is configured as objects.
22
+ // Certain datasources like queries use primitives.
23
+ Object.keys(schema || {}).forEach(key => {
24
+ if (typeof schema[key] !== "object") {
25
+ schema[key] = { type: schema[key] }
26
+ }
27
+ })
28
+
29
+ return schema
30
+ })
31
+
32
+ const enrichedSchema = derived(
33
+ [schema, schemaOverrides, columnWhitelist],
34
+ ([$schema, $schemaOverrides, $columnWhitelist]) => {
35
+ if (!$schema) {
18
36
  return null
19
37
  }
20
- let newSchema = { ...$definition?.schema }
38
+ let enrichedSchema = { ...$schema }
21
39
 
22
40
  // Apply schema overrides
23
41
  Object.keys($schemaOverrides || {}).forEach(field => {
24
- if (newSchema[field]) {
25
- newSchema[field] = {
26
- ...newSchema[field],
42
+ if (enrichedSchema[field]) {
43
+ enrichedSchema[field] = {
44
+ ...enrichedSchema[field],
27
45
  ...$schemaOverrides[field],
28
46
  }
29
47
  }
@@ -31,41 +49,64 @@ export const deriveStores = context => {
31
49
 
32
50
  // Apply whitelist if specified
33
51
  if ($columnWhitelist?.length) {
34
- Object.keys(newSchema).forEach(key => {
52
+ Object.keys(enrichedSchema).forEach(key => {
35
53
  if (!$columnWhitelist.includes(key)) {
36
- delete newSchema[key]
54
+ delete enrichedSchema[key]
37
55
  }
38
56
  })
39
57
  }
40
58
 
41
- return newSchema
59
+ return enrichedSchema
42
60
  }
43
61
  )
44
62
 
63
+ const isDatasourcePlus = derived(datasource, $datasource => {
64
+ return ["table", "viewV2"].includes($datasource?.type)
65
+ })
66
+
45
67
  return {
46
68
  schema,
69
+ enrichedSchema,
70
+ isDatasourcePlus,
47
71
  }
48
72
  }
49
73
 
50
74
  export const createActions = context => {
51
- const { datasource, definition, config, dispatch, table, viewV2 } = context
75
+ const {
76
+ API,
77
+ datasource,
78
+ definition,
79
+ config,
80
+ dispatch,
81
+ table,
82
+ viewV2,
83
+ nonPlus,
84
+ } = context
52
85
 
53
86
  // Gets the appropriate API for the configured datasource type
54
87
  const getAPI = () => {
55
88
  const $datasource = get(datasource)
56
- switch ($datasource?.type) {
89
+ const type = $datasource?.type
90
+ if (!type) {
91
+ return null
92
+ }
93
+ switch (type) {
57
94
  case "table":
58
95
  return table
59
96
  case "viewV2":
60
97
  return viewV2
61
98
  default:
62
- return null
99
+ return nonPlus
63
100
  }
64
101
  }
65
102
 
66
103
  // Refreshes the datasource definition
67
104
  const refreshDefinition = async () => {
68
- return await getAPI()?.actions.refreshDefinition()
105
+ const def = await getDatasourceDefinition({
106
+ API,
107
+ datasource: get(datasource),
108
+ })
109
+ definition.set(def)
69
110
  }
70
111
 
71
112
  // Saves the datasource definition
@@ -113,6 +154,11 @@ export const createActions = context => {
113
154
  return getAPI()?.actions.canUseColumn(name)
114
155
  }
115
156
 
157
+ // Gets the default number of rows for a single page
158
+ const getFeatures = () => {
159
+ return getAPI()?.actions.getFeatures()
160
+ }
161
+
116
162
  return {
117
163
  datasource: {
118
164
  ...datasource,
@@ -125,6 +171,7 @@ export const createActions = context => {
125
171
  getRow,
126
172
  isDatasourceValid,
127
173
  canUseColumn,
174
+ getFeatures,
128
175
  },
129
176
  },
130
177
  }
@@ -0,0 +1,124 @@
1
+ import { get } from "svelte/store"
2
+
3
+ export const createActions = context => {
4
+ const { columns, stickyColumn, table, viewV2 } = context
5
+
6
+ const saveDefinition = async () => {
7
+ throw "This datasource does not support updating the definition"
8
+ }
9
+
10
+ const saveRow = async () => {
11
+ throw "This datasource does not support saving rows"
12
+ }
13
+
14
+ const deleteRows = async () => {
15
+ throw "This datasource does not support deleting rows"
16
+ }
17
+
18
+ const getRow = () => {
19
+ throw "This datasource does not support fetching individual rows"
20
+ }
21
+
22
+ const isDatasourceValid = datasource => {
23
+ // There are many different types and shapes of datasource, so we only
24
+ // check that we aren't null
25
+ return (
26
+ !table.actions.isDatasourceValid(datasource) &&
27
+ !viewV2.actions.isDatasourceValid(datasource) &&
28
+ datasource?.type != null
29
+ )
30
+ }
31
+
32
+ const canUseColumn = name => {
33
+ const $columns = get(columns)
34
+ const $sticky = get(stickyColumn)
35
+ return $columns.some(col => col.name === name) || $sticky?.name === name
36
+ }
37
+
38
+ const getFeatures = () => {
39
+ // We don't support any features
40
+ return {}
41
+ }
42
+
43
+ return {
44
+ nonPlus: {
45
+ actions: {
46
+ saveDefinition,
47
+ addRow: saveRow,
48
+ updateRow: saveRow,
49
+ deleteRows,
50
+ getRow,
51
+ isDatasourceValid,
52
+ canUseColumn,
53
+ getFeatures,
54
+ },
55
+ },
56
+ }
57
+ }
58
+
59
+ // Small util to compare datasource definitions
60
+ const isSameDatasource = (a, b) => {
61
+ return JSON.stringify(a) === JSON.stringify(b)
62
+ }
63
+
64
+ export const initialise = context => {
65
+ const {
66
+ datasource,
67
+ sort,
68
+ filter,
69
+ nonPlus,
70
+ initialFilter,
71
+ initialSortColumn,
72
+ initialSortOrder,
73
+ fetch,
74
+ } = context
75
+ // Keep a list of subscriptions so that we can clear them when the datasource
76
+ // config changes
77
+ let unsubscribers = []
78
+
79
+ // Observe datasource changes and apply logic for view V2 datasources
80
+ datasource.subscribe($datasource => {
81
+ // Clear previous subscriptions
82
+ unsubscribers?.forEach(unsubscribe => unsubscribe())
83
+ unsubscribers = []
84
+ if (!nonPlus.actions.isDatasourceValid($datasource)) {
85
+ return
86
+ }
87
+
88
+ // Wipe state
89
+ filter.set(get(initialFilter))
90
+ sort.set({
91
+ column: get(initialSortColumn),
92
+ order: get(initialSortOrder) || "ascending",
93
+ })
94
+
95
+ // Update fetch when filter changes
96
+ unsubscribers.push(
97
+ filter.subscribe($filter => {
98
+ // Ensure we're updating the correct fetch
99
+ const $fetch = get(fetch)
100
+ if (!isSameDatasource($fetch?.options?.datasource, $datasource)) {
101
+ return
102
+ }
103
+ $fetch.update({
104
+ filter: $filter,
105
+ })
106
+ })
107
+ )
108
+
109
+ // Update fetch when sorting changes
110
+ unsubscribers.push(
111
+ sort.subscribe($sort => {
112
+ // Ensure we're updating the correct fetch
113
+ const $fetch = get(fetch)
114
+ if (!isSameDatasource($fetch?.options?.datasource, $datasource)) {
115
+ return
116
+ }
117
+ $fetch.update({
118
+ sortOrder: $sort.order || "ascending",
119
+ sortColumn: $sort.column,
120
+ })
121
+ })
122
+ )
123
+ })
124
+ }
@@ -1,13 +1,10 @@
1
1
  import { get } from "svelte/store"
2
+ import TableFetch from "../../../../fetch/TableFetch"
2
3
 
3
4
  const SuppressErrors = true
4
5
 
5
6
  export const createActions = context => {
6
- const { definition, API, datasource, columns, stickyColumn } = context
7
-
8
- const refreshDefinition = async () => {
9
- definition.set(await API.fetchTableDefinition(get(datasource).tableId))
10
- }
7
+ const { API, datasource, columns, stickyColumn } = context
11
8
 
12
9
  const saveDefinition = async newDefinition => {
13
10
  await API.saveTable(newDefinition)
@@ -49,10 +46,13 @@ export const createActions = context => {
49
46
  return $columns.some(col => col.name === name) || $sticky?.name === name
50
47
  }
51
48
 
49
+ const getFeatures = () => {
50
+ return new TableFetch({ API }).determineFeatureFlags()
51
+ }
52
+
52
53
  return {
53
54
  table: {
54
55
  actions: {
55
- refreshDefinition,
56
56
  saveDefinition,
57
57
  addRow: saveRow,
58
58
  updateRow: saveRow,
@@ -60,6 +60,7 @@ export const createActions = context => {
60
60
  getRow,
61
61
  isDatasourceValid,
62
62
  canUseColumn,
63
+ getFeatures,
63
64
  },
64
65
  },
65
66
  }
@@ -1,22 +1,10 @@
1
1
  import { get } from "svelte/store"
2
+ import ViewV2Fetch from "../../../../fetch/ViewV2Fetch"
2
3
 
3
4
  const SuppressErrors = true
4
5
 
5
6
  export const createActions = context => {
6
- const { definition, API, datasource, columns, stickyColumn } = context
7
-
8
- const refreshDefinition = async () => {
9
- const $datasource = get(datasource)
10
- if (!$datasource) {
11
- definition.set(null)
12
- return
13
- }
14
- const table = await API.fetchTableDefinition($datasource.tableId)
15
- const view = Object.values(table?.views || {}).find(
16
- view => view.id === $datasource.id
17
- )
18
- definition.set(view)
19
- }
7
+ const { API, datasource, columns, stickyColumn } = context
20
8
 
21
9
  const saveDefinition = async newDefinition => {
22
10
  await API.viewV2.update(newDefinition)
@@ -58,10 +46,13 @@ export const createActions = context => {
58
46
  )
59
47
  }
60
48
 
49
+ const getFeatures = () => {
50
+ return new ViewV2Fetch({ API }).determineFeatureFlags()
51
+ }
52
+
61
53
  return {
62
54
  viewV2: {
63
55
  actions: {
64
- refreshDefinition,
65
56
  saveDefinition,
66
57
  addRow: saveRow,
67
58
  updateRow: saveRow,
@@ -69,6 +60,7 @@ export const createActions = context => {
69
60
  getRow,
70
61
  isDatasourceValid,
71
62
  canUseColumn,
63
+ getFeatures,
72
64
  },
73
65
  },
74
66
  }
@@ -15,9 +15,10 @@ import * as Config from "./config"
15
15
  import * as Sort from "./sort"
16
16
  import * as Filter from "./filter"
17
17
  import * as Notifications from "./notifications"
18
- import * as Table from "./table"
19
- import * as ViewV2 from "./viewV2"
20
18
  import * as Datasource from "./datasource"
19
+ import * as Table from "./datasources/table"
20
+ import * as ViewV2 from "./datasources/viewV2"
21
+ import * as NonPlus from "./datasources/nonPlus"
21
22
 
22
23
  const DependencyOrderedStores = [
23
24
  Sort,
@@ -26,6 +27,7 @@ const DependencyOrderedStores = [
26
27
  Scroll,
27
28
  Table,
28
29
  ViewV2,
30
+ NonPlus,
29
31
  Datasource,
30
32
  Columns,
31
33
  Rows,
@@ -1,7 +1,8 @@
1
1
  import { writable, derived, get } from "svelte/store"
2
- import { fetchData } from "../../../fetch/fetchData"
2
+ import { fetchData } from "../../../fetch"
3
3
  import { NewRowID, RowPageSize } from "../lib/constants"
4
4
  import { tick } from "svelte"
5
+ import { Helpers } from "@budibase/bbui"
5
6
 
6
7
  export const createStores = () => {
7
8
  const rows = writable([])
@@ -76,11 +77,11 @@ export const createActions = context => {
76
77
  columns,
77
78
  rowChangeCache,
78
79
  inProgressChanges,
79
- previousFocusedRowId,
80
80
  hasNextPage,
81
81
  error,
82
82
  notifications,
83
83
  fetch,
84
+ isDatasourcePlus,
84
85
  } = context
85
86
  const instanceLoaded = writable(false)
86
87
 
@@ -93,12 +94,14 @@ export const createActions = context => {
93
94
  datasource.subscribe(async $datasource => {
94
95
  // Unsub from previous fetch if one exists
95
96
  unsubscribe?.()
97
+ unsubscribe = null
96
98
  fetch.set(null)
97
99
  instanceLoaded.set(false)
98
100
  loading.set(true)
99
101
 
100
102
  // Abandon if we don't have a valid datasource
101
103
  if (!datasource.actions.isDatasourceValid($datasource)) {
104
+ error.set("Datasource is invalid")
102
105
  return
103
106
  }
104
107
 
@@ -108,6 +111,10 @@ export const createActions = context => {
108
111
  const $filter = get(filter)
109
112
  const $sort = get(sort)
110
113
 
114
+ // Determine how many rows to fetch per page
115
+ const features = datasource.actions.getFeatures()
116
+ const limit = features?.supportsPagination ? RowPageSize : null
117
+
111
118
  // Create new fetch model
112
119
  const newFetch = fetchData({
113
120
  API,
@@ -116,7 +123,7 @@ export const createActions = context => {
116
123
  filter: $filter,
117
124
  sortColumn: $sort.column,
118
125
  sortOrder: $sort.order,
119
- limit: RowPageSize,
126
+ limit,
120
127
  paginate: true,
121
128
  },
122
129
  })
@@ -355,7 +362,7 @@ export const createActions = context => {
355
362
 
356
363
  // Update row
357
364
  const saved = await datasource.actions.updateRow({
358
- ...row,
365
+ ...cleanRow(row),
359
366
  ...get(rowChangeCache)[rowId],
360
367
  })
361
368
 
@@ -411,8 +418,17 @@ export const createActions = context => {
411
418
  }
412
419
  let rowsToAppend = []
413
420
  let newRow
421
+ const $isDatasourcePlus = get(isDatasourcePlus)
414
422
  for (let i = 0; i < newRows.length; i++) {
415
423
  newRow = newRows[i]
424
+
425
+ // Ensure we have a unique _id.
426
+ // This means generating one for non DS+, overriting any that may already
427
+ // exist as we cannot allow duplicates.
428
+ if (!$isDatasourcePlus) {
429
+ newRow._id = Helpers.uuid()
430
+ }
431
+
416
432
  if (!rowCacheMap[newRow._id]) {
417
433
  rowCacheMap[newRow._id] = true
418
434
  rowsToAppend.push(newRow)
@@ -449,15 +465,16 @@ export const createActions = context => {
449
465
  return get(rowLookupMap)[id] != null
450
466
  }
451
467
 
452
- // Wipe the row change cache when changing row
453
- previousFocusedRowId.subscribe(id => {
454
- if (id && !get(inProgressChanges)[id]) {
455
- rowChangeCache.update(state => {
456
- delete state[id]
457
- return state
458
- })
468
+ // Cleans a row by removing any internal grid metadata from it.
469
+ // Call this before passing a row to any sort of external flow.
470
+ const cleanRow = row => {
471
+ let clone = { ...row }
472
+ delete clone.__idx
473
+ if (!get(isDatasourcePlus)) {
474
+ delete clone._id
459
475
  }
460
- })
476
+ return clone
477
+ }
461
478
 
462
479
  return {
463
480
  rows: {
@@ -474,7 +491,22 @@ export const createActions = context => {
474
491
  refreshRow,
475
492
  replaceRow,
476
493
  refreshData,
494
+ cleanRow,
477
495
  },
478
496
  },
479
497
  }
480
498
  }
499
+
500
+ export const initialise = context => {
501
+ const { rowChangeCache, inProgressChanges, previousFocusedRowId } = context
502
+
503
+ // Wipe the row change cache when changing row
504
+ previousFocusedRowId.subscribe(id => {
505
+ if (id && !get(inProgressChanges)[id]) {
506
+ rowChangeCache.update(state => {
507
+ delete state[id]
508
+ return state
509
+ })
510
+ }
511
+ })
512
+ }
@@ -17,7 +17,7 @@ export const createStores = context => {
17
17
  }
18
18
 
19
19
  export const initialise = context => {
20
- const { sort, initialSortColumn, initialSortOrder, definition } = context
20
+ const { sort, initialSortColumn, initialSortOrder, schema } = context
21
21
 
22
22
  // Reset sort when initial sort props change
23
23
  initialSortColumn.subscribe(newSortColumn => {
@@ -28,15 +28,12 @@ export const initialise = context => {
28
28
  })
29
29
 
30
30
  // Derive if the current sort column exists in the schema
31
- const sortColumnExists = derived(
32
- [sort, definition],
33
- ([$sort, $definition]) => {
34
- if (!$sort?.column || !$definition) {
35
- return true
36
- }
37
- return $definition.schema?.[$sort.column] != null
31
+ const sortColumnExists = derived([sort, schema], ([$sort, $schema]) => {
32
+ if (!$sort?.column || !$schema) {
33
+ return true
38
34
  }
39
- )
35
+ return $schema[$sort.column] != null
36
+ })
40
37
 
41
38
  // Clear sort state if our sort column does not exist
42
39
  sortColumnExists.subscribe(exists => {
@@ -0,0 +1,145 @@
1
+ import DataFetch from "./DataFetch.js"
2
+
3
+ export default class CustomFetch extends DataFetch {
4
+ // Gets the correct Budibase type for a JS value
5
+ getType(value) {
6
+ if (value == null) {
7
+ return "string"
8
+ }
9
+ const type = typeof value
10
+ if (type === "object") {
11
+ if (Array.isArray(value)) {
12
+ // Use our custom array type to render badges
13
+ return "array"
14
+ }
15
+ // Use JSON for objects to ensure they are stringified
16
+ return "json"
17
+ } else if (!isNaN(value)) {
18
+ return "number"
19
+ } else {
20
+ return "string"
21
+ }
22
+ }
23
+
24
+ // Parses the custom data into an array format
25
+ parseCustomData(data) {
26
+ if (!data) {
27
+ return []
28
+ }
29
+
30
+ // Happy path - already an array
31
+ if (Array.isArray(data)) {
32
+ return data
33
+ }
34
+
35
+ // For strings, try JSON then fall back to attempting a CSV
36
+ if (typeof data === "string") {
37
+ try {
38
+ const js = JSON.parse(data)
39
+ return Array.isArray(js) ? js : [js]
40
+ } catch (error) {
41
+ // Ignore
42
+ }
43
+
44
+ // Try splitting by newlines first
45
+ if (data.includes("\n")) {
46
+ return data.split("\n").map(x => x.trim())
47
+ }
48
+
49
+ // Split by commas next
50
+ return data.split(",").map(x => x.trim())
51
+ }
52
+
53
+ // Other cases we just assume it's a single object and wrap it
54
+ return [data]
55
+ }
56
+
57
+ // Enriches the custom data to ensure the structure and format is usable
58
+ enrichCustomData(data) {
59
+ if (!data?.length) {
60
+ return []
61
+ }
62
+
63
+ // Filter out any invalid values
64
+ data = data.filter(x => x != null && x !== "" && !Array.isArray(x))
65
+
66
+ // Ensure all values are packed into objects
67
+ return data.map(value => {
68
+ if (typeof value === "object") {
69
+ return value
70
+ }
71
+
72
+ // Try parsing strings
73
+ if (typeof value === "string") {
74
+ const split = value.split(",").map(x => x.trim())
75
+ let obj = {}
76
+ for (let i = 0; i < split.length; i++) {
77
+ const suffix = i === 0 ? "" : ` ${i + 1}`
78
+ const key = `Value${suffix}`
79
+ obj[key] = split[i]
80
+ }
81
+ return obj
82
+ }
83
+
84
+ // For anything else, wrap in an object
85
+ return { Value: value }
86
+ })
87
+ }
88
+
89
+ // Extracts and parses the custom data from the datasource definition
90
+ getCustomData(datasource) {
91
+ return this.enrichCustomData(this.parseCustomData(datasource?.data))
92
+ }
93
+
94
+ async getDefinition(datasource) {
95
+ // Try and work out the schema from the array provided
96
+ let schema = {}
97
+ const data = this.getCustomData(datasource)
98
+ if (!data?.length) {
99
+ return { schema }
100
+ }
101
+
102
+ // Go through every object and extract all valid keys
103
+ for (let datum of data) {
104
+ for (let key of Object.keys(datum)) {
105
+ if (key === "_id") {
106
+ continue
107
+ }
108
+ if (!schema[key]) {
109
+ let type = this.getType(datum[key])
110
+ let constraints = {}
111
+
112
+ // Determine whether we should render text columns as options instead
113
+ if (type === "string") {
114
+ const uniqueValues = [...new Set(data.map(x => x[key]))]
115
+ const uniqueness = uniqueValues.length / data.length
116
+ if (uniqueness <= 0.8 && uniqueValues.length > 1) {
117
+ type = "options"
118
+ constraints.inclusion = uniqueValues
119
+ }
120
+ }
121
+
122
+ // Generate options for array columns
123
+ else if (type === "array") {
124
+ constraints.inclusion = [...new Set(data.map(x => x[key]).flat())]
125
+ }
126
+
127
+ schema[key] = {
128
+ type,
129
+ constraints,
130
+ }
131
+ }
132
+ }
133
+ }
134
+ return { schema }
135
+ }
136
+
137
+ async getData() {
138
+ const { datasource } = this.options
139
+ return {
140
+ rows: this.getCustomData(datasource),
141
+ hasNextPage: false,
142
+ cursor: null,
143
+ }
144
+ }
145
+ }
@@ -8,6 +8,7 @@ import FieldFetch from "./FieldFetch.js"
8
8
  import JSONArrayFetch from "./JSONArrayFetch.js"
9
9
  import UserFetch from "./UserFetch.js"
10
10
  import GroupUserFetch from "./GroupUserFetch.js"
11
+ import CustomFetch from "./CustomFetch.js"
11
12
 
12
13
  const DataFetchMap = {
13
14
  table: TableFetch,
@@ -17,6 +18,7 @@ const DataFetchMap = {
17
18
  link: RelationshipFetch,
18
19
  user: UserFetch,
19
20
  groupUser: GroupUserFetch,
21
+ custom: CustomFetch,
20
22
 
21
23
  // Client specific datasource types
22
24
  provider: NestedProviderFetch,
@@ -24,7 +26,18 @@ const DataFetchMap = {
24
26
  jsonarray: JSONArrayFetch,
25
27
  }
26
28
 
29
+ // Constructs a new fetch model for a certain datasource
27
30
  export const fetchData = ({ API, datasource, options }) => {
28
31
  const Fetch = DataFetchMap[datasource?.type] || TableFetch
29
32
  return new Fetch({ API, datasource, ...options })
30
33
  }
34
+
35
+ // Fetches the definition of any type of datasource
36
+ export const getDatasourceDefinition = async ({ API, datasource }) => {
37
+ const handler = DataFetchMap[datasource?.type]
38
+ if (!handler) {
39
+ return null
40
+ }
41
+ const instance = new handler({ API })
42
+ return await instance.getDefinition(datasource)
43
+ }
package/src/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { createAPIClient } from "./api"
2
- export { fetchData } from "./fetch/fetchData"
2
+ export { fetchData } from "./fetch"
3
3
  export { Utils } from "./utils"
4
4
  export * as Constants from "./constants"
5
5
  export * from "./stores"