@budibase/frontend-core 3.41.1 → 3.41.2

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@budibase/frontend-core",
3
- "version": "3.41.1",
3
+ "version": "3.41.2",
4
4
  "description": "Budibase frontend core libraries used in builder and client",
5
5
  "author": "Budibase",
6
6
  "license": "MPL-2.0",
@@ -23,5 +23,5 @@
23
23
  "devDependencies": {
24
24
  "vitest": "^4.1.0"
25
25
  },
26
- "gitHead": "fc94728bc509937e374c5d38f47f8858a1f405c9"
26
+ "gitHead": "556bd76bac5c556b307b1022fce8e1fe51d928d7"
27
27
  }
package/src/api/agents.ts CHANGED
@@ -109,10 +109,15 @@ export interface AgentEndpoints {
109
109
  datasourceId: string,
110
110
  authConfigId: string
111
111
  ) => Promise<FetchAgentKnowledgeSourceOptionsResponse>
112
- fetchOperationKnowledgeSourceAllEntries: (
112
+ fetchOperationKnowledgeSourceEntries: (
113
113
  agentId: string,
114
114
  operationId: string,
115
- siteId: string
115
+ siteId: string,
116
+ options?: {
117
+ driveId?: string
118
+ parentItemId?: string
119
+ parentPath?: string
120
+ }
116
121
  ) => Promise<FetchAgentKnowledgeSourceEntriesResponse>
117
122
  connectOperationSharePointSite: (
118
123
  agentId: string,
@@ -331,14 +336,24 @@ export const buildAgentEndpoints = (API: BaseAPIClient): AgentEndpoints => ({
331
336
  })
332
337
  },
333
338
 
334
- fetchOperationKnowledgeSourceAllEntries: async (
339
+ fetchOperationKnowledgeSourceEntries: async (
335
340
  agentId: string,
336
341
  operationId: string,
337
- siteId: string
342
+ siteId: string,
343
+ options
338
344
  ) => {
339
345
  const query = new URLSearchParams({ siteId })
346
+ if (options?.driveId) {
347
+ query.set("driveId", options.driveId)
348
+ }
349
+ if (options?.parentItemId) {
350
+ query.set("parentItemId", options.parentItemId)
351
+ }
352
+ if (options?.parentPath) {
353
+ query.set("parentPath", options.parentPath)
354
+ }
340
355
  return await API.get<FetchAgentKnowledgeSourceEntriesResponse>({
341
- url: `/api/agent/${agentId}/operations/${operationId}/knowledge-sources/sharepoint/entries/all?${query.toString()}`,
356
+ url: `/api/agent/${agentId}/operations/${operationId}/knowledge-sources/sharepoint/entries?${query.toString()}`,
342
357
  })
343
358
  },
344
359
 
@@ -92,7 +92,7 @@ export const buildChatAppEndpoints = (
92
92
  headers: {
93
93
  "Content-Type": "application/json",
94
94
  Accept: "application/json",
95
- [Header.APP_ID]: workspaceId,
95
+ [Header.WORKSPACE_ID]: workspaceId,
96
96
  },
97
97
  body: JSON.stringify(body),
98
98
  credentials: "same-origin",
@@ -166,7 +166,7 @@ export const buildChatAppEndpoints = (
166
166
  const url = "/api/chatapps"
167
167
  const headers = workspaceId
168
168
  ? {
169
- [Header.APP_ID]: workspaceId,
169
+ [Header.WORKSPACE_ID]: workspaceId,
170
170
  }
171
171
  : undefined
172
172
  return await API.get({
@@ -218,7 +218,7 @@ export const buildChatAppEndpoints = (
218
218
  }
219
219
 
220
220
  if (resolvedWorkspaceId) {
221
- headers[Header.APP_ID] = resolvedWorkspaceId
221
+ headers[Header.WORKSPACE_ID] = resolvedWorkspaceId
222
222
  }
223
223
 
224
224
  const response = await fetch(`/api/chatapps/${chatAppId}/conversations`, {
package/src/api/index.ts CHANGED
@@ -282,7 +282,7 @@ export const createAPIClient = (config: APIClientConfig = {}): APIClient => {
282
282
  getAppID: (): string => {
283
283
  let headers: Headers = {}
284
284
  config?.attachHeaders?.(headers)
285
- return headers?.[Header.APP_ID]
285
+ return headers?.[Header.WORKSPACE_ID]
286
286
  },
287
287
  }
288
288
 
@@ -39,12 +39,12 @@
39
39
  persistConversation?: boolean
40
40
  conversationStarters?: { prompt: string }[]
41
41
  initialPrompt?: string
42
- onchatsaved?: (_event: {
42
+ onchatsaved?: (event: {
43
43
  detail: { chatId?: string; chat: ChatConversationLike }
44
44
  }) => void
45
45
  // Fired when an escalation parks; the consumer polls the outcome and
46
46
  // injects it via appendAssistantMessage.
47
- onEscalationPending?: (_detail: { escalationId: string }) => void
47
+ onEscalationPending?: (detail: { escalationId: string }) => void
48
48
  // Live resolution per escalationId (from the poll) - drives the card state.
49
49
  escalationState?: Record<
50
50
  string,
@@ -53,8 +53,8 @@
53
53
  // Dev-only: show the inline Approve/Reject buttons on the escalation card.
54
54
  showInlineApproval?: boolean
55
55
  onResolve?: (
56
- _escalationId: string,
57
- _accepted: boolean
56
+ escalationId: string,
57
+ accepted: boolean
58
58
  ) => Promise<EscalationRespondResult | undefined>
59
59
  isAgentPreviewChat?: boolean
60
60
  readOnly?: boolean
@@ -118,7 +118,7 @@
118
118
  createAPIClient({
119
119
  attachHeaders: headers => {
120
120
  if (workspaceId) {
121
- headers[Header.APP_ID] = workspaceId
121
+ headers[Header.WORKSPACE_ID] = workspaceId
122
122
  }
123
123
  },
124
124
  })
@@ -313,7 +313,7 @@
313
313
 
314
314
  const chatInstance = new Chat<UIMessage<AgentMessageMetadata>>({
315
315
  transport: new DefaultChatTransport({
316
- headers: () => ({ [Header.APP_ID]: workspaceId }),
316
+ headers: () => ({ [Header.WORKSPACE_ID]: workspaceId }),
317
317
  prepareSendMessagesRequest: ({ messages }) => {
318
318
  const chatAppId = resolvedChatAppId || chat?.chatAppId
319
319
  const conversationId = resolvedConversationId || chat?._id || "new"
@@ -54,7 +54,8 @@
54
54
  let searchValue
55
55
  let input
56
56
 
57
- $: sortedBy = column.name === $sort.column
57
+ $: columnSort = $sort.find(sortEntry => sortEntry.column === column.name)
58
+ $: sortedBy = !!columnSort
58
59
  $: canMoveLeft = orderable && idx > 0
59
60
  $: canMoveRight = orderable && idx < $scrollableColumns.length - 1
60
61
  $: sortingLabels = getSortingLabels(column)
@@ -150,18 +151,22 @@
150
151
  }
151
152
 
152
153
  const sortAscending = () => {
153
- sort.set({
154
- column: column.name,
155
- order: SortOrder.ASCENDING,
156
- })
154
+ sort.set([
155
+ {
156
+ column: column.name,
157
+ order: SortOrder.ASCENDING,
158
+ },
159
+ ])
157
160
  open = false
158
161
  }
159
162
 
160
163
  const sortDescending = () => {
161
- sort.set({
162
- column: column.name,
163
- order: SortOrder.DESCENDING,
164
- })
164
+ sort.set([
165
+ {
166
+ column: column.name,
167
+ order: SortOrder.DESCENDING,
168
+ },
169
+ ])
165
170
  open = false
166
171
  }
167
172
 
@@ -341,7 +346,7 @@
341
346
  <Icon
342
347
  hoverable
343
348
  size="S"
344
- name={$sort.order === SortOrder.DESCENDING
349
+ name={columnSort?.order === SortOrder.DESCENDING
345
350
  ? "sort-descending"
346
351
  : "sort-ascending"}
347
352
  />
@@ -389,8 +394,7 @@
389
394
  icon="sort-ascending"
390
395
  on:click={sortAscending}
391
396
  disabled={!canBeSortColumn(column.schema) ||
392
- (column.name === $sort.column &&
393
- $sort.order === SortOrder.ASCENDING)}
397
+ columnSort?.order === SortOrder.ASCENDING}
394
398
  >
395
399
  Sort {sortingLabels.ascending}
396
400
  </MenuItem>
@@ -398,8 +402,7 @@
398
402
  icon="sort-descending"
399
403
  on:click={sortDescending}
400
404
  disabled={!canBeSortColumn(column.schema) ||
401
- (column.name === $sort.column &&
402
- $sort.order === SortOrder.DESCENDING)}
405
+ columnSort?.order === SortOrder.DESCENDING}
403
406
  >
404
407
  Sort {sortingLabels.descending}
405
408
  </MenuItem>
@@ -93,10 +93,17 @@ export const initialise = (context: StoreContext) => {
93
93
  // Wipe state
94
94
  filter.set(get(initialFilter) ?? undefined)
95
95
  inlineFilters.set([])
96
- sort.set({
97
- column: get(initialSortColumn),
98
- order: get(initialSortOrder) || SortOrder.ASCENDING,
99
- })
96
+ const initialColumn = get(initialSortColumn)
97
+ sort.set(
98
+ initialColumn
99
+ ? [
100
+ {
101
+ column: initialColumn,
102
+ order: get(initialSortOrder) || SortOrder.ASCENDING,
103
+ },
104
+ ]
105
+ : []
106
+ )
100
107
 
101
108
  // Update fetch when filter changes
102
109
  unsubscribers.push(
@@ -120,9 +127,12 @@ export const initialise = (context: StoreContext) => {
120
127
  if (!isSameDatasource($fetch?.options?.datasource, $datasource)) {
121
128
  return
122
129
  }
130
+ const sorts = $sort.map(sortEntry => ({
131
+ field: sortEntry.column,
132
+ order: sortEntry.order,
133
+ }))
123
134
  $fetch?.update({
124
- sortOrder: $sort.order || SortOrder.ASCENDING,
125
- sortColumn: $sort.column ?? undefined,
135
+ sorts,
126
136
  })
127
137
  })
128
138
  )
@@ -111,10 +111,17 @@ export const initialise = (context: StoreContext) => {
111
111
  // Wipe state
112
112
  filter.set(get(initialFilter) ?? undefined)
113
113
  inlineFilters.set([])
114
- sort.set({
115
- column: get(initialSortColumn),
116
- order: get(initialSortOrder) || SortOrder.ASCENDING,
117
- })
114
+ const initialColumn = get(initialSortColumn)
115
+ sort.set(
116
+ initialColumn
117
+ ? [
118
+ {
119
+ column: initialColumn,
120
+ order: get(initialSortOrder) || SortOrder.ASCENDING,
121
+ },
122
+ ]
123
+ : []
124
+ )
118
125
 
119
126
  // Update fetch when filter changes
120
127
  unsubscribers.push(
@@ -138,9 +145,12 @@ export const initialise = (context: StoreContext) => {
138
145
  if ($fetch?.options?.datasource?.tableId !== $datasource.tableId) {
139
146
  return
140
147
  }
148
+ const sorts = $sort.map(sortEntry => ({
149
+ field: sortEntry.column,
150
+ order: sortEntry.order,
151
+ }))
141
152
  $fetch.update({
142
- sortOrder: $sort.order || SortOrder.ASCENDING,
143
- sortColumn: $sort.column ?? undefined,
153
+ sorts,
144
154
  })
145
155
  })
146
156
  )
@@ -3,6 +3,7 @@ import {
3
3
  Row,
4
4
  SaveRowRequest,
5
5
  SortOrder,
6
+ SortField,
6
7
  UIDatasource,
7
8
  UpdateViewRequest,
8
9
  } from "@budibase/types"
@@ -12,6 +13,44 @@ import ViewV2Fetch from "../../../../fetch/ViewV2Fetch"
12
13
 
13
14
  const SuppressErrors = true
14
15
 
16
+ type GridSortEntry = {
17
+ column: string
18
+ order: SortOrder
19
+ }
20
+
21
+ const normalizeViewSort = (sort?: SortField | SortField[]) => {
22
+ if (!sort) {
23
+ return []
24
+ }
25
+ const sortEntries = Array.isArray(sort) ? sort : [sort]
26
+ return sortEntries
27
+ .filter(sortEntry => sortEntry?.field)
28
+ .map(sortEntry => ({
29
+ column: sortEntry.field,
30
+ order: sortEntry.order || SortOrder.ASCENDING,
31
+ }))
32
+ }
33
+
34
+ const toViewSort = (sorts: GridSortEntry[]) => {
35
+ const entries = sorts
36
+ .filter(sortEntry => sortEntry.column)
37
+ .map(sortEntry => ({
38
+ field: sortEntry.column,
39
+ order: sortEntry.order || SortOrder.ASCENDING,
40
+ }))
41
+ return entries.length ? entries : undefined
42
+ }
43
+
44
+ const sortArraysEqual = (a: GridSortEntry[], b: GridSortEntry[]) => {
45
+ if (a.length !== b.length) {
46
+ return false
47
+ }
48
+ return a.every(
49
+ (entry, index) =>
50
+ entry.column === b[index]?.column && entry.order === b[index]?.order
51
+ )
52
+ }
53
+
15
54
  interface ViewActions {
16
55
  viewV2: {
17
56
  actions: DatasourceViewActions
@@ -123,10 +162,17 @@ export const initialise = (context: StoreContext) => {
123
162
  // Reset state for new view
124
163
  filter.set(get(initialFilter) ?? undefined)
125
164
  inlineFilters.set([])
126
- sort.set({
127
- column: get(initialSortColumn),
128
- order: get(initialSortOrder) || SortOrder.ASCENDING,
129
- })
165
+ const initialColumn = get(initialSortColumn)
166
+ sort.set(
167
+ initialColumn
168
+ ? [
169
+ {
170
+ column: initialColumn,
171
+ order: get(initialSortOrder) || SortOrder.ASCENDING,
172
+ },
173
+ ]
174
+ : []
175
+ )
130
176
 
131
177
  // Keep sort and filter state in line with the view definition when in builder
132
178
  unsubscribers.push(
@@ -142,10 +188,7 @@ export const initialise = (context: StoreContext) => {
142
188
  }
143
189
  // Only override sorting if we don't have an initial sort column
144
190
  if (!get(initialSortColumn)) {
145
- sort.set({
146
- column: $definition.sort?.field,
147
- order: $definition.sort?.order || SortOrder.ASCENDING,
148
- })
191
+ sort.set(normalizeViewSort($definition.sort))
149
192
  }
150
193
  // Only override filter state if we don't have an initial filter
151
194
  if (!get(initialFilter)) {
@@ -154,35 +197,6 @@ export const initialise = (context: StoreContext) => {
154
197
  })
155
198
  )
156
199
 
157
- function sortHasChanged(
158
- newSort: {
159
- column: string | null | undefined
160
- order: SortOrder
161
- },
162
- existingSort?: {
163
- field: string
164
- order?: SortOrder
165
- }
166
- ) {
167
- const newColumn = newSort.column ?? null
168
- const existingColumn = existingSort?.field ?? null
169
- if (newColumn !== existingColumn) {
170
- return true
171
- }
172
-
173
- if (!newColumn) {
174
- return false
175
- }
176
-
177
- const newOrder = newSort.order ?? null
178
- const existingOrder = existingSort?.order ?? null
179
- if (newOrder !== existingOrder) {
180
- return true
181
- }
182
-
183
- return false
184
- }
185
-
186
200
  // When sorting changes, ensure view definition is kept up to date
187
201
  unsubscribers.push(
188
202
  sort.subscribe(async $sort => {
@@ -196,7 +210,8 @@ export const initialise = (context: StoreContext) => {
196
210
  }
197
211
 
198
212
  // Skip if nothing actually changed
199
- if (!sortHasChanged($sort, $view.sort)) {
213
+ const existingSorts = normalizeViewSort($view.sort)
214
+ if (sortArraysEqual($sort, existingSorts)) {
200
215
  return
201
216
  }
202
217
 
@@ -204,10 +219,7 @@ export const initialise = (context: StoreContext) => {
204
219
  if (get(config).canSaveSchema) {
205
220
  await datasource.actions.saveDefinition({
206
221
  ...$view,
207
- sort: {
208
- field: $sort.column!,
209
- order: $sort.order || SortOrder.ASCENDING,
210
- },
222
+ sort: toViewSort($sort),
211
223
  })
212
224
  }
213
225
 
@@ -217,9 +229,12 @@ export const initialise = (context: StoreContext) => {
217
229
  if ($fetch?.options?.datasource?.id !== $datasource.id) {
218
230
  return
219
231
  }
232
+ const sorts = $sort.map(sortEntry => ({
233
+ field: sortEntry.column,
234
+ order: sortEntry.order,
235
+ }))
220
236
  $fetch.update({
221
- sortOrder: $sort.order,
222
- sortColumn: $sort.column ?? undefined,
237
+ sorts,
223
238
  })
224
239
  })
225
240
  )
@@ -236,6 +236,10 @@ export const createActions = (context: StoreContext): RowActionStore => {
236
236
  await tick()
237
237
  const $allFilters = get(allFilters)
238
238
  const $sort = get(sort)
239
+ const sorts = $sort.map(sortEntry => ({
240
+ field: sortEntry.column,
241
+ order: sortEntry.order,
242
+ }))
239
243
 
240
244
  // Create new fetch model
241
245
  const newFetch = fetchData({
@@ -243,8 +247,7 @@ export const createActions = (context: StoreContext): RowActionStore => {
243
247
  datasource: $datasource,
244
248
  options: {
245
249
  filter: $allFilters,
246
- sortColumn: $sort.column,
247
- sortOrder: $sort.order,
250
+ sorts,
248
251
  limit: RowPageSize,
249
252
  paginate: true,
250
253
 
@@ -3,11 +3,13 @@ import { memo } from "../../../utils"
3
3
  import { SortOrder } from "@budibase/types"
4
4
  import { Store as StoreContext } from "."
5
5
 
6
+ export interface SortEntry {
7
+ column: string
8
+ order: SortOrder
9
+ }
10
+
6
11
  interface SortStore {
7
- sort: Writable<{
8
- column: string | null | undefined
9
- order: SortOrder
10
- }>
12
+ sort: Writable<SortEntry[]>
11
13
  }
12
14
 
13
15
  export type Store = SortStore
@@ -17,10 +19,16 @@ export const createStores = (context: StoreContext): SortStore => {
17
19
  const $props = get(props)
18
20
 
19
21
  // Initialise to default props
20
- const sort = memo({
21
- column: $props.initialSortColumn,
22
- order: $props.initialSortOrder || SortOrder.ASCENDING,
23
- })
22
+ const sort = memo<SortEntry[]>(
23
+ $props.initialSortColumn
24
+ ? [
25
+ {
26
+ column: $props.initialSortColumn,
27
+ order: $props.initialSortOrder || SortOrder.ASCENDING,
28
+ },
29
+ ]
30
+ : []
31
+ )
24
32
 
25
33
  return {
26
34
  sort,
@@ -32,30 +40,44 @@ export const initialise = (context: StoreContext) => {
32
40
 
33
41
  // Reset sort when initial sort props change
34
42
  initialSortColumn.subscribe(newSortColumn => {
35
- sort.update(state => ({ ...state, column: newSortColumn }))
43
+ sort.set(
44
+ newSortColumn
45
+ ? [
46
+ {
47
+ column: newSortColumn,
48
+ order: get(initialSortOrder) || SortOrder.ASCENDING,
49
+ },
50
+ ]
51
+ : []
52
+ )
36
53
  })
37
54
  initialSortOrder.subscribe(newSortOrder => {
38
- sort.update(state => ({
39
- ...state,
40
- order: newSortOrder || SortOrder.ASCENDING,
41
- }))
55
+ sort.update(state => {
56
+ if (!state.length) {
57
+ return state
58
+ }
59
+ return [
60
+ {
61
+ ...state[0],
62
+ order: newSortOrder || SortOrder.ASCENDING,
63
+ },
64
+ ...state.slice(1),
65
+ ]
66
+ })
42
67
  })
43
68
 
44
- // Derive if the current sort column exists in the schema
45
- const sortColumnExists = derived([sort, schema], ([$sort, $schema]) => {
46
- if (!$sort?.column || !$schema) {
47
- return true
69
+ // Remove any sort columns that don't exist in the schema
70
+ const validSorts = derived([sort, schema], ([$sort, $schema]) => {
71
+ if (!$schema) {
72
+ return $sort
48
73
  }
49
- return $schema[$sort.column] != null
74
+ return $sort.filter(sortEntry => $schema[sortEntry.column] != null)
50
75
  })
51
76
 
52
- // Clear sort state if our sort column does not exist
53
- sortColumnExists.subscribe(exists => {
54
- if (!exists) {
55
- sort.set({
56
- column: null,
57
- order: SortOrder.ASCENDING,
58
- })
77
+ validSorts.subscribe($validSorts => {
78
+ const current = get(sort)
79
+ if (JSON.stringify($validSorts) !== JSON.stringify(current)) {
80
+ sort.set($validSorts)
59
81
  }
60
82
  })
61
83
  }
@@ -13,8 +13,9 @@ import { DataFetchType } from "."
13
13
  import { APIClient } from "../api/types"
14
14
  import { QueryUtils } from "../utils"
15
15
  import { convertJSONSchemaToTableSchema } from "../utils/json"
16
+ import { normalizeSorts } from "./sort"
16
17
 
17
- const { buildQuery, limit: queryLimit, runQuery, sort } = QueryUtils
18
+ const { buildQuery, limit: queryLimit, multiSort, runQuery, sort } = QueryUtils
18
19
 
19
20
  interface DataFetchStore<TDefinition, TQuery, TRow extends Row> {
20
21
  rows: TRow[]
@@ -109,6 +110,7 @@ export default abstract class BaseDataFetch<
109
110
  sortColumn: null,
110
111
  sortOrder: SortOrder.ASCENDING,
111
112
  sortType: null,
113
+ sorts: null,
112
114
 
113
115
  // Pagination config
114
116
  paginate: true,
@@ -219,42 +221,22 @@ export default abstract class BaseDataFetch<
219
221
  }
220
222
  schema = this.enrichSchema(schema)
221
223
 
222
- // If an invalid sort column is specified, delete it
223
- if (this.options.sortColumn && !schema[this.options.sortColumn]) {
224
- this.options.sortColumn = null
225
- }
226
-
227
- // If no sort column, get the default column for this datasource
228
- if (!this.options.sortColumn) {
229
- this.options.sortColumn = this.getDefaultSortColumn(definition, schema)
230
- }
224
+ const normalizedSorts = normalizeSorts(
225
+ this.options,
226
+ schema,
227
+ this.getDefaultSortColumn(definition, schema)
228
+ )
231
229
 
232
- // If we don't have a sort column specified then just ensure we don't set
233
- // any sorting params
234
- if (!this.options.sortColumn) {
230
+ if (!normalizedSorts.length) {
231
+ this.options.sortColumn = null
235
232
  this.options.sortOrder = SortOrder.ASCENDING
236
233
  this.options.sortType = null
234
+ this.options.sorts = []
237
235
  } else {
238
- // Otherwise determine what sort type to use base on sort column
239
- this.options.sortType = SortType.STRING
240
- const fieldSchema = schema?.[this.options.sortColumn]
241
- if (
242
- fieldSchema?.type === FieldType.NUMBER ||
243
- fieldSchema?.type === FieldType.BIGINT ||
244
- fieldSchema?.responseType === FieldType.NUMBER ||
245
- ("calculationType" in fieldSchema && fieldSchema?.calculationType)
246
- ) {
247
- this.options.sortType = SortType.NUMBER
248
- }
249
-
250
- // If no sort order, default to ascending
251
- if (!this.options.sortOrder) {
252
- this.options.sortOrder = SortOrder.ASCENDING
253
- } else {
254
- // Ensure sortOrder matches the enum
255
- this.options.sortOrder =
256
- this.options.sortOrder.toLowerCase() as SortOrder
257
- }
236
+ this.options.sorts = normalizedSorts
237
+ this.options.sortColumn = normalizedSorts[0]?.field ?? null
238
+ this.options.sortOrder = normalizedSorts[0]?.order || SortOrder.ASCENDING
239
+ this.options.sortType = normalizedSorts[0]?.type || null
258
240
  }
259
241
 
260
242
  // Build the query
@@ -298,6 +280,7 @@ export default abstract class BaseDataFetch<
298
280
  sortColumn,
299
281
  sortOrder,
300
282
  sortType,
283
+ sorts,
301
284
  limit,
302
285
  clientSideSearching,
303
286
  clientSideSorting,
@@ -314,8 +297,34 @@ export default abstract class BaseDataFetch<
314
297
  }
315
298
 
316
299
  // If we don't support sorting, do a client-side sort
317
- if (!this.features.supportsSort && clientSideSorting && sortType) {
318
- rows = sort(rows, sortColumn as any, sortOrder, sortType)
300
+ if (!this.features.supportsSort && clientSideSorting) {
301
+ const activeSorts = (sorts || []).filter(
302
+ sortEntry => sortEntry?.field && sortEntry?.order
303
+ )
304
+ if (activeSorts.length > 1) {
305
+ rows = multiSort(
306
+ rows,
307
+ Object.fromEntries(
308
+ activeSorts.map(sortEntry => [
309
+ sortEntry.field,
310
+ {
311
+ direction: sortEntry.order || SortOrder.ASCENDING,
312
+ type: sortEntry.type,
313
+ },
314
+ ])
315
+ )
316
+ )
317
+ } else if (activeSorts.length === 1) {
318
+ const [entry] = activeSorts
319
+ rows = sort(
320
+ rows,
321
+ entry.field as any,
322
+ entry.order || sortOrder,
323
+ entry.type || sortType || SortType.STRING
324
+ )
325
+ } else if (sortType && sortColumn) {
326
+ rows = sort(rows, sortColumn as any, sortOrder, sortType)
327
+ }
319
328
  }
320
329
 
321
330
  // If we don't support pagination, do a client-side limit
@@ -1,6 +1,30 @@
1
1
  import { get } from "svelte/store"
2
2
  import BaseDataFetch from "./DataFetch"
3
- import { SortOrder, Table, TableDatasource } from "@budibase/types"
3
+ import {
4
+ SortField,
5
+ SortJson,
6
+ SortOrder,
7
+ SortType,
8
+ Table,
9
+ TableDatasource,
10
+ } from "@budibase/types"
11
+
12
+ const buildSortPayload = (sorts?: SortField[] | null) => {
13
+ if (!sorts?.length) {
14
+ return undefined
15
+ }
16
+ const sort: SortJson = {}
17
+ sorts.forEach(sortEntry => {
18
+ if (!sortEntry?.field) {
19
+ return
20
+ }
21
+ sort[sortEntry.field] = {
22
+ direction: sortEntry.order || SortOrder.ASCENDING,
23
+ ...(sortEntry.type ? { type: sortEntry.type as SortType } : {}),
24
+ }
25
+ })
26
+ return Object.keys(sort).length ? sort : undefined
27
+ }
4
28
 
5
29
  export default class TableFetch extends BaseDataFetch<TableDatasource, Table> {
6
30
  async determineFeatureFlags() {
@@ -29,19 +53,17 @@ export default class TableFetch extends BaseDataFetch<TableDatasource, Table> {
29
53
  }
30
54
 
31
55
  async getData() {
32
- const { datasource, limit, sortColumn, sortOrder, sortType, paginate } =
33
- this.options
56
+ const { datasource, limit, sorts, paginate } = this.options
34
57
  const { tableId } = datasource
35
58
  const { cursor, query } = get(this.store)
59
+ const sort = buildSortPayload(sorts)
36
60
 
37
61
  // Search table
38
62
  try {
39
63
  const res = await this.API.searchTable(tableId, {
40
64
  query,
41
65
  limit,
42
- sort: sortColumn,
43
- sortOrder: sortOrder ?? SortOrder.ASCENDING,
44
- sortType,
66
+ sort,
45
67
  paginate,
46
68
  bookmark: cursor,
47
69
  })
@@ -1,5 +1,8 @@
1
1
  import {
2
2
  SortOrder,
3
+ SortField,
4
+ SortJson,
5
+ SortType,
3
6
  ViewDatasource,
4
7
  ViewV2Enriched,
5
8
  ViewV2Type,
@@ -8,6 +11,23 @@ import BaseDataFetch from "./DataFetch"
8
11
  import { get } from "svelte/store"
9
12
  import { helpers } from "@budibase/shared-core"
10
13
 
14
+ const buildSortPayload = (sorts?: SortField[] | null) => {
15
+ if (!sorts?.length) {
16
+ return undefined
17
+ }
18
+ const sort: SortJson = {}
19
+ sorts.forEach(sortEntry => {
20
+ if (!sortEntry?.field) {
21
+ return
22
+ }
23
+ sort[sortEntry.field] = {
24
+ direction: sortEntry.order || SortOrder.ASCENDING,
25
+ ...(sortEntry.type ? { type: sortEntry.type as SortType } : {}),
26
+ }
27
+ })
28
+ return Object.keys(sort).length ? sort : undefined
29
+ }
30
+
11
31
  export default class ViewV2Fetch extends BaseDataFetch<
12
32
  ViewDatasource,
13
33
  ViewV2Enriched
@@ -40,8 +60,15 @@ export default class ViewV2Fetch extends BaseDataFetch<
40
60
  }
41
61
 
42
62
  async getData() {
43
- const { datasource, limit, sortColumn, sortOrder, sortType, paginate } =
44
- this.options
63
+ const {
64
+ datasource,
65
+ limit,
66
+ sortColumn,
67
+ sortOrder,
68
+ sortType,
69
+ sorts,
70
+ paginate,
71
+ } = this.options
45
72
  const { cursor, query, definition } = get(this.store)
46
73
 
47
74
  // If this is a calculation view and we have no calculations, return nothing
@@ -62,20 +89,23 @@ export default class ViewV2Fetch extends BaseDataFetch<
62
89
  // If sort/filter params are not defined, update options to store the
63
90
  // params built in to this view. This ensures that we can accurately
64
91
  // compare old and new params and skip a redundant API call.
65
- if (!sortColumn && definition?.sort?.field) {
66
- this.options.sortColumn = definition.sort.field
67
- this.options.sortOrder = definition.sort.order || SortOrder.ASCENDING
92
+ if (!sortColumn && definition?.sort && !sorts?.length) {
93
+ this.options.sorts = (
94
+ Array.isArray(definition.sort) ? definition.sort : [definition.sort]
95
+ ).filter(sortEntry => sortEntry?.field)
68
96
  }
69
97
 
98
+ const sortPayload = buildSortPayload(this.options.sorts)
99
+
70
100
  try {
71
101
  const request = {
72
102
  query,
73
103
  paginate,
74
104
  limit,
75
105
  bookmark: cursor,
76
- sort: sortColumn,
77
- sortOrder: sortOrder,
78
- sortType,
106
+ sort: sortPayload || sortColumn,
107
+ sortOrder: sortPayload ? undefined : sortOrder,
108
+ sortType: sortPayload ? undefined : sortType,
79
109
  }
80
110
  if (paginate) {
81
111
  const res = await this.API.viewV2.fetch(datasource.id, {
@@ -0,0 +1,73 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import { FieldType, SortOrder, SortType } from "@budibase/types"
3
+ import type { TableSchema } from "@budibase/types"
4
+ import { normalizeSorts } from "./sort"
5
+
6
+ describe("sort normalization", () => {
7
+ it("does not restore the single-sort options after sorts are explicitly cleared", () => {
8
+ const schema: TableSchema = {
9
+ name: {
10
+ name: "name",
11
+ type: FieldType.STRING,
12
+ },
13
+ }
14
+
15
+ expect(
16
+ normalizeSorts(
17
+ {
18
+ sorts: [],
19
+ sortColumn: "name",
20
+ sortOrder: SortOrder.DESCENDING,
21
+ sortType: null,
22
+ },
23
+ schema,
24
+ null
25
+ )
26
+ ).toEqual([])
27
+ })
28
+
29
+ it("normalizes every configured sort", () => {
30
+ const schema: TableSchema = {
31
+ name: {
32
+ name: "name",
33
+ type: FieldType.STRING,
34
+ },
35
+ count: {
36
+ name: "count",
37
+ type: FieldType.NUMBER,
38
+ },
39
+ }
40
+
41
+ expect(
42
+ normalizeSorts(
43
+ {
44
+ sorts: [
45
+ {
46
+ field: "name",
47
+ order: SortOrder.DESCENDING,
48
+ },
49
+ {
50
+ field: "count",
51
+ },
52
+ ],
53
+ sortColumn: null,
54
+ sortOrder: SortOrder.ASCENDING,
55
+ sortType: null,
56
+ },
57
+ schema,
58
+ null
59
+ )
60
+ ).toEqual([
61
+ {
62
+ field: "name",
63
+ order: SortOrder.DESCENDING,
64
+ type: SortType.STRING,
65
+ },
66
+ {
67
+ field: "count",
68
+ order: SortOrder.ASCENDING,
69
+ type: SortType.NUMBER,
70
+ },
71
+ ])
72
+ })
73
+ })
@@ -0,0 +1,70 @@
1
+ import { FieldType, SortOrder, SortType } from "@budibase/types"
2
+ import type { SortField, TableSchema } from "@budibase/types"
3
+
4
+ interface SortNormalizationOptions {
5
+ sorts?: SortField[] | null
6
+ sortColumn: string | null
7
+ sortOrder: SortOrder
8
+ sortType: SortType | null
9
+ }
10
+
11
+ const getSortType = (schema: TableSchema, field: string) => {
12
+ const fieldSchema = schema[field]
13
+ if (
14
+ fieldSchema?.type === FieldType.NUMBER ||
15
+ fieldSchema?.type === FieldType.BIGINT ||
16
+ ("responseType" in fieldSchema &&
17
+ fieldSchema.responseType === FieldType.NUMBER) ||
18
+ ("calculationType" in fieldSchema && fieldSchema.calculationType)
19
+ ) {
20
+ return SortType.NUMBER
21
+ }
22
+ return SortType.STRING
23
+ }
24
+
25
+ const normalizeSortOrder = (order?: SortOrder) =>
26
+ order?.toLowerCase() === SortOrder.DESCENDING
27
+ ? SortOrder.DESCENDING
28
+ : SortOrder.ASCENDING
29
+
30
+ export const normalizeSorts = (
31
+ options: SortNormalizationOptions,
32
+ schema: TableSchema,
33
+ defaultSortColumn: string | null
34
+ ): SortField[] => {
35
+ let normalizedSorts: SortField[] =
36
+ options.sorts
37
+ ?.filter(sortEntry => sortEntry?.field && schema[sortEntry.field])
38
+ .map(
39
+ sortEntry =>
40
+ ({
41
+ ...sortEntry,
42
+ order: normalizeSortOrder(sortEntry.order),
43
+ type: sortEntry.type || getSortType(schema, sortEntry.field),
44
+ }) satisfies SortField
45
+ ) || []
46
+
47
+ if (!normalizedSorts.length && options.sorts == null && options.sortColumn) {
48
+ if (schema[options.sortColumn]) {
49
+ normalizedSorts = [
50
+ {
51
+ field: options.sortColumn,
52
+ order: normalizeSortOrder(options.sortOrder),
53
+ type: options.sortType || getSortType(schema, options.sortColumn),
54
+ } satisfies SortField,
55
+ ]
56
+ }
57
+ }
58
+
59
+ if (!normalizedSorts.length && defaultSortColumn) {
60
+ normalizedSorts = [
61
+ {
62
+ field: defaultSortColumn,
63
+ order: SortOrder.ASCENDING,
64
+ type: getSortType(schema, defaultSortColumn),
65
+ } satisfies SortField,
66
+ ]
67
+ }
68
+
69
+ return normalizedSorts
70
+ }