@budibase/frontend-core 3.41.1 → 3.41.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 +2 -2
- package/src/api/agents.ts +20 -5
- package/src/api/chatApps.ts +3 -3
- package/src/api/index.ts +1 -1
- package/src/components/Chatbox/index.svelte +6 -6
- package/src/components/grid/cells/HeaderCell.svelte +17 -14
- package/src/components/grid/stores/datasources/nonPlus.ts +16 -6
- package/src/components/grid/stores/datasources/table.ts +16 -6
- package/src/components/grid/stores/datasources/viewV2.ts +59 -44
- package/src/components/grid/stores/rows.ts +5 -2
- package/src/components/grid/stores/sort.ts +47 -25
- package/src/fetch/DataFetch.ts +44 -35
- package/src/fetch/TableFetch.ts +28 -6
- package/src/fetch/ViewV2Fetch.ts +38 -8
- package/src/fetch/sort.test.ts +73 -0
- package/src/fetch/sort.ts +70 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@budibase/frontend-core",
|
|
3
|
-
"version": "3.41.
|
|
3
|
+
"version": "3.41.3",
|
|
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": "
|
|
26
|
+
"gitHead": "dd1b8d888c49ad09b82b00fc5b93ceff707bbd6a"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
356
|
+
url: `/api/agent/${agentId}/operations/${operationId}/knowledge-sources/sharepoint/entries?${query.toString()}`,
|
|
342
357
|
})
|
|
343
358
|
},
|
|
344
359
|
|
package/src/api/chatApps.ts
CHANGED
|
@@ -92,7 +92,7 @@ export const buildChatAppEndpoints = (
|
|
|
92
92
|
headers: {
|
|
93
93
|
"Content-Type": "application/json",
|
|
94
94
|
Accept: "application/json",
|
|
95
|
-
[Header.
|
|
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.
|
|
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.
|
|
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.
|
|
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?: (
|
|
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?: (
|
|
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
|
-
|
|
57
|
-
|
|
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.
|
|
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.
|
|
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
|
-
$:
|
|
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
|
-
|
|
155
|
-
|
|
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
|
-
|
|
163
|
-
|
|
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={
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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.
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
//
|
|
45
|
-
const
|
|
46
|
-
if (!$
|
|
47
|
-
return
|
|
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[
|
|
74
|
+
return $sort.filter(sortEntry => $schema[sortEntry.column] != null)
|
|
50
75
|
})
|
|
51
76
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (
|
|
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
|
}
|
package/src/fetch/DataFetch.ts
CHANGED
|
@@ -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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
233
|
-
|
|
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
|
-
|
|
239
|
-
this.options.
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
318
|
-
|
|
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
|
package/src/fetch/TableFetch.ts
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
import { get } from "svelte/store"
|
|
2
2
|
import BaseDataFetch from "./DataFetch"
|
|
3
|
-
import {
|
|
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,
|
|
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
|
|
43
|
-
sortOrder: sortOrder ?? SortOrder.ASCENDING,
|
|
44
|
-
sortType,
|
|
66
|
+
sort,
|
|
45
67
|
paginate,
|
|
46
68
|
bookmark: cursor,
|
|
47
69
|
})
|
package/src/fetch/ViewV2Fetch.ts
CHANGED
|
@@ -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 {
|
|
44
|
-
|
|
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?.
|
|
66
|
-
this.options.
|
|
67
|
-
|
|
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
|
+
}
|