@budibase/frontend-core 3.38.5 → 3.39.15

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.38.5",
3
+ "version": "3.39.15",
4
4
  "description": "Budibase frontend core libraries used in builder and client",
5
5
  "author": "Budibase",
6
6
  "license": "MPL-2.0",
@@ -21,7 +21,7 @@
21
21
  "socket.io-client": "^4.7.5"
22
22
  },
23
23
  "devDependencies": {
24
- "vitest": "^3.2.4"
24
+ "vitest": "^4.1.0"
25
25
  },
26
- "gitHead": "5795ec6c95812463cba1d8cede8295f6c2b915cb"
26
+ "gitHead": "e409b00d4727dd9f3c5ba22401aa3d0d8c062cba"
27
27
  }
package/src/api/agents.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  DisconnectAgentSharePointSiteResponse,
8
8
  DuplicateAgentResponse,
9
9
  FetchAgentKnowledgeResponse,
10
+ FetchAgentFileUrlResponse,
10
11
  FetchAgentKnowledgeSourceEntriesResponse,
11
12
  FetchAgentKnowledgeSourceOptionsResponse,
12
13
  FetchAgentsResponse,
@@ -79,6 +80,10 @@ export interface AgentEndpoints {
79
80
  agentId: string,
80
81
  fileId: string
81
82
  ) => Promise<{ deleted: true }>
83
+ fetchAgentFileUrl: (
84
+ agentId: string,
85
+ fileId: string
86
+ ) => Promise<FetchAgentFileUrlResponse>
82
87
  fetchAgentKnowledgeSourceOptions: (
83
88
  datasourceId: string,
84
89
  authConfigId: string
@@ -104,6 +109,7 @@ export interface AgentEndpoints {
104
109
  agentId: string,
105
110
  sourceId: string
106
111
  ) => Promise<SyncAgentKnowledgeSourcesResponse>
112
+ resetAgentKnowledgeBaseStore: (agentId: string) => Promise<void>
107
113
  }
108
114
 
109
115
  export const buildAgentEndpoints = (API: BaseAPIClient): AgentEndpoints => ({
@@ -249,6 +255,12 @@ export const buildAgentEndpoints = (API: BaseAPIClient): AgentEndpoints => ({
249
255
  })
250
256
  },
251
257
 
258
+ fetchAgentFileUrl: async (agentId: string, fileId: string) => {
259
+ return await API.get<FetchAgentFileUrlResponse>({
260
+ url: `/api/agent/${agentId}/files/${fileId}/url`,
261
+ })
262
+ },
263
+
252
264
  fetchAgentKnowledgeSourceOptions: async (
253
265
  datasourceId: string,
254
266
  authConfigId: string
@@ -302,4 +314,10 @@ export const buildAgentEndpoints = (API: BaseAPIClient): AgentEndpoints => ({
302
314
  url: `/api/agent/${agentId}/knowledge-sources/${encodeURIComponent(sourceId)}/sync`,
303
315
  })
304
316
  },
317
+
318
+ resetAgentKnowledgeBaseStore: async (agentId: string) => {
319
+ await API.post({
320
+ url: `/api/agent/${agentId}/knowledge/store/reset`,
321
+ })
322
+ },
305
323
  })
@@ -10,6 +10,8 @@ import {
10
10
  SearchAutomationLogsResponse,
11
11
  TestAutomationRequest,
12
12
  TestAutomationResponse,
13
+ TestEmailConnectionRequest,
14
+ TestEmailConnectionResponse,
13
15
  TestProgressState,
14
16
  TriggerAutomationRequest,
15
17
  TriggerAutomationResponse,
@@ -44,6 +46,9 @@ export interface AutomationEndpoints {
44
46
  data: TestAutomationRequest,
45
47
  options?: { async?: boolean }
46
48
  ) => Promise<TestAutomationResponse>
49
+ testEmailConnection: (
50
+ data: TestEmailConnectionRequest
51
+ ) => Promise<TestEmailConnectionResponse>
47
52
  getAutomationTestStatus: (automationId: string) => Promise<TestProgressState>
48
53
  getAutomationDefinitions: () => Promise<GetAutomationStepDefinitionsResponse>
49
54
  getAutomationLogs: (
@@ -91,6 +96,16 @@ export const buildAutomationEndpoints = (
91
96
  })
92
97
  },
93
98
 
99
+ testEmailConnection: async data => {
100
+ return await API.post<
101
+ TestEmailConnectionRequest,
102
+ TestEmailConnectionResponse
103
+ >({
104
+ url: "/api/automations/email/test-connection",
105
+ body: data,
106
+ })
107
+ },
108
+
94
109
  /**
95
110
  * Gets a list of all automations.
96
111
  */
@@ -10,6 +10,7 @@ import {
10
10
  FetchPublishedChatAppsResponse,
11
11
  UpdateChatAppRequest,
12
12
  AgentMessageMetadata,
13
+ FetchAgentFileUrlResponse,
13
14
  } from "@budibase/types"
14
15
  import { Header } from "@budibase/shared-core"
15
16
  import { BaseAPIClient } from "./types"
@@ -38,6 +39,11 @@ export interface ChatAppEndpoints {
38
39
  ) => Promise<FetchAgentHistoryResponse>
39
40
  fetchChatApp: (workspaceId?: string) => Promise<ChatApp | null>
40
41
  setChatAppAgent: (chatAppId: string, agentId: string) => Promise<ChatAppAgent>
42
+ fetchChatAppAgentFileUrl: (
43
+ chatAppId: string,
44
+ agentId: string,
45
+ fileId: string
46
+ ) => Promise<FetchAgentFileUrlResponse>
41
47
  createChatConversation: (
42
48
  chat: CreateChatConversationRequest,
43
49
  workspaceId?: string
@@ -181,6 +187,16 @@ export const buildChatAppEndpoints = (
181
187
  })
182
188
  },
183
189
 
190
+ fetchChatAppAgentFileUrl: async (
191
+ chatAppId: string,
192
+ agentId: string,
193
+ fileId: string
194
+ ) => {
195
+ return await API.get<FetchAgentFileUrlResponse>({
196
+ url: `/api/chatapps/${chatAppId}/agents/${agentId}/files/${fileId}/url`,
197
+ })
198
+ },
199
+
184
200
  createChatConversation: async (
185
201
  chat: CreateChatConversationRequest,
186
202
  workspaceId?: string
@@ -18,6 +18,7 @@
18
18
  name?: string
19
19
  icon?: string
20
20
  iconColor?: string
21
+ allowKnowledgeSourceDownload?: boolean
21
22
  }
22
23
 
23
24
  export let selectedAgentId: string | null = null
@@ -79,6 +80,10 @@
79
80
 
80
81
  $: readOnlyReason = getReadOnlyReason(agentAvailability)
81
82
 
83
+ $: allowKnowledgeSourceDownload =
84
+ enabledAgentList.find(agent => agent.agentId === selectedAgentId)
85
+ ?.allowKnowledgeSourceDownload !== false
86
+
82
87
  const selectAgent = (agentId: string) => {
83
88
  dispatch("agentSelected", { agentId })
84
89
  }
@@ -127,6 +132,7 @@
127
132
  {workspaceId}
128
133
  {conversationStarters}
129
134
  {initialPrompt}
135
+ {allowKnowledgeSourceDownload}
130
136
  readOnly={Boolean(readOnlyReason)}
131
137
  {readOnlyReason}
132
138
  onchatsaved={event => dispatch("chatSaved", event.detail)}
@@ -41,6 +41,7 @@
41
41
  isAgentPreviewChat?: boolean
42
42
  readOnly?: boolean
43
43
  readOnlyReason?: "disabled" | "deleted" | "offline"
44
+ allowKnowledgeSourceDownload?: boolean
44
45
  }
45
46
 
46
47
  let {
@@ -53,6 +54,7 @@
53
54
  isAgentPreviewChat = false,
54
55
  readOnly = false,
55
56
  readOnlyReason,
57
+ allowKnowledgeSourceDownload = true,
56
58
  }: Props = $props()
57
59
 
58
60
  let API = $state(
@@ -76,11 +78,51 @@
76
78
  let inputValue = $state("")
77
79
  let lastInitialPrompt = $state("")
78
80
  let isPreparingResponse = $state(false)
79
-
80
81
  const resetPendingResponse = () => {
81
82
  isPreparingResponse = false
82
83
  }
83
84
 
85
+ const openRagSource = async (
86
+ source: NonNullable<AgentMessageMetadata["ragSources"]>[number]
87
+ ) => {
88
+ if (!allowKnowledgeSourceDownload) {
89
+ notifications.error("Source downloads are disabled for this agent")
90
+ return
91
+ }
92
+ if (!source.fileId) {
93
+ return
94
+ }
95
+
96
+ try {
97
+ const resolvedUrl =
98
+ !isAgentPreviewChat && chat?.chatAppId && chat?.agentId
99
+ ? (
100
+ await API.fetchChatAppAgentFileUrl(
101
+ chat.chatAppId,
102
+ chat.agentId,
103
+ source.fileId
104
+ )
105
+ ).url
106
+ : chat?.agentId
107
+ ? (await API.fetchAgentFileUrl(chat.agentId, source.fileId)).url
108
+ : undefined
109
+ if (!resolvedUrl) {
110
+ notifications.error("Could not resolve source file URL")
111
+ return
112
+ }
113
+
114
+ const link = document.createElement("a")
115
+ link.href = resolvedUrl
116
+ link.download = source.filename || "source.pdf"
117
+ link.target = "_blank"
118
+ link.rel = "noopener noreferrer"
119
+ link.click()
120
+ } catch (error) {
121
+ console.error("Failed to resolve knowledge source URL", error)
122
+ notifications.error("Failed to download source file")
123
+ }
124
+ }
125
+
84
126
  const getReasoningText = (message: UIMessage<AgentMessageMetadata>) =>
85
127
  (message.parts ?? [])
86
128
  .filter(isReasoningUIPart)
@@ -112,7 +154,32 @@
112
154
  return true
113
155
  }
114
156
 
115
- return Boolean(message.metadata?.ragSources?.length)
157
+ return Boolean(getVisibleRagSources(message).length)
158
+ }
159
+
160
+ const getVisibleRagSources = (message: UIMessage<AgentMessageMetadata>) => {
161
+ const ragSources = message.metadata?.ragSources || []
162
+ const uniqueByFileId = new Set<string>()
163
+ const visible: NonNullable<AgentMessageMetadata["ragSources"]> = []
164
+
165
+ for (const source of ragSources) {
166
+ const filename = source.filename?.trim()
167
+ if (!source.fileId || !filename) {
168
+ continue
169
+ }
170
+
171
+ if (uniqueByFileId.has(source.fileId)) {
172
+ continue
173
+ }
174
+
175
+ uniqueByFileId.add(source.fileId)
176
+ visible.push({
177
+ ...source,
178
+ filename,
179
+ })
180
+ }
181
+
182
+ return visible
116
183
  }
117
184
 
118
185
  const hasToolError = (message: UIMessage<AgentMessageMetadata>) =>
@@ -691,7 +758,7 @@
691
758
  </div>
692
759
  {/if}
693
760
  {/each}
694
- {#if message.metadata?.ragSources?.length}
761
+ {#if getVisibleRagSources(message).length}
695
762
  <div class="sources">
696
763
  <div class="sources-header">
697
764
  <span class="sources-icon">
@@ -704,11 +771,19 @@
704
771
  <span class="sources-title">Sources</span>
705
772
  </div>
706
773
  <ul>
707
- {#each message.metadata.ragSources as source (source.sourceId)}
774
+ {#each getVisibleRagSources(message) as source (source.fileId)}
708
775
  <li class="source-item">
709
- <span class="source-name"
710
- >{source.filename || source.sourceId}</span
711
- >
776
+ {#if allowKnowledgeSourceDownload}
777
+ <button
778
+ type="button"
779
+ class="source-link"
780
+ onclick={() => openRagSource(source)}
781
+ >
782
+ {source.filename}
783
+ </button>
784
+ {:else}
785
+ <span class="source-name">{source.filename}</span>
786
+ {/if}
712
787
  </li>
713
788
  {/each}
714
789
  </ul>
@@ -1202,4 +1277,16 @@
1202
1277
  .source-name {
1203
1278
  font-weight: 400;
1204
1279
  }
1280
+
1281
+ .source-link {
1282
+ border: none;
1283
+ background: transparent;
1284
+ padding: 0;
1285
+ margin: 0;
1286
+ font-weight: 400;
1287
+ color: var(--spectrum-global-color-blue-700);
1288
+ text-decoration: underline;
1289
+ cursor: pointer;
1290
+ text-align: left;
1291
+ }
1205
1292
  </style>
@@ -16,7 +16,6 @@
16
16
  import { QueryUtils, Constants } from "@budibase/frontend-core"
17
17
  import { getContext, createEventDispatcher } from "svelte"
18
18
  import FilterField from "./FilterField.svelte"
19
- import ConditionField from "./ConditionField.svelte"
20
19
  import { utils } from "@budibase/shared-core"
21
20
 
22
21
  const dispatch = createEventDispatcher()
@@ -35,8 +34,12 @@
35
34
  export let behaviourFilters = false
36
35
  export let allowBindings = false
37
36
  export let allowOnEmpty = true
38
- export let builderType = "filter"
39
37
  export let docsURL = "https://docs.budibase.com/docs/searchfilter-data"
38
+ export let prefix = "Show data which matches"
39
+ export let filterTypeLabel = "filter"
40
+ export let drawerTitle = null
41
+ export let bindingValueType = Constants.FilterValueType.BINDING
42
+ export let useConditionValueControls = false
40
43
 
41
44
  export let bindings
42
45
  export let panel
@@ -57,10 +60,6 @@
57
60
  schemaFields = [...schemaFields, { name: "_id", type: "string" }]
58
61
  }
59
62
  }
60
- $: prefix =
61
- builderType === "filter"
62
- ? "Show data which matches"
63
- : "Run branch when matching"
64
63
 
65
64
  // We still may need to migrate this even though the backend does it automatically now
66
65
  // for query definitions. This is because we might be editing saved filter definitions
@@ -111,17 +110,9 @@
111
110
  }
112
111
 
113
112
  const getValidOperatorsForType = filter => {
114
- if (builderType === "condition") {
115
- return [OperatorOptions.Equals, OperatorOptions.NotEquals]
116
- }
117
-
118
- if (!filter?.field && !filter?.name) {
119
- return []
120
- }
121
-
122
113
  return QueryUtils.getValidOperatorsForType(
123
- filter,
124
- filter.field || filter.name,
114
+ filter?.type ? filter : { ...filter, type: FieldType.STRING },
115
+ filter?.field || filter?.name,
125
116
  datasource
126
117
  )
127
118
  }
@@ -234,9 +225,6 @@
234
225
  } else if (addFilter) {
235
226
  targetGroup.filters.push({
236
227
  valueType: FilterValueType.VALUE,
237
- ...(builderType === "condition"
238
- ? { operator: OperatorOptions.Equals.value, type: FieldType.STRING }
239
- : {}),
240
228
  })
241
229
  } else if (group) {
242
230
  editable.groups[groupIdx] = {
@@ -257,11 +245,6 @@
257
245
  filters: [
258
246
  {
259
247
  valueType: FilterValueType.VALUE,
260
- ...(builderType === "condition"
261
- ? {
262
- operator: OperatorOptions.Equals.value,
263
- }
264
- : {}),
265
248
  },
266
249
  ],
267
250
  })
@@ -307,7 +290,7 @@
307
290
  popoverAutoWidth
308
291
  />
309
292
  </span>
310
- <span>of the following {builderType} groups:</span>
293
+ <span>of the following {filterTypeLabel} groups:</span>
311
294
  </div>
312
295
  {/if}
313
296
  {#if editableFilters?.groups?.length}
@@ -337,7 +320,7 @@
337
320
  popoverAutoWidth
338
321
  />
339
322
  </span>
340
- <span>of the following {builderType}s are matched:</span>
323
+ <span>of the following {filterTypeLabel}s are matched:</span>
341
324
  </div>
342
325
  <div class="group-actions">
343
326
  <Icon
@@ -367,8 +350,22 @@
367
350
 
368
351
  <div class="filters">
369
352
  {#each group.filters as filter, filterIdx}
370
- <div class="filter">
371
- {#if builderType === "filter"}
353
+ <div
354
+ class="filter"
355
+ class:has-extra-column={!!$$slots["extra-column"]}
356
+ >
357
+ {#if $$slots["field-column"]}
358
+ <slot
359
+ name="field-column"
360
+ {filter}
361
+ {groupIdx}
362
+ {filterIdx}
363
+ onUpdate={f =>
364
+ onFilterFieldUpdate(f, groupIdx, filterIdx)}
365
+ {sanitizeOperator}
366
+ {sanitizeValue}
367
+ />
368
+ {:else}
372
369
  <Select
373
370
  value={filter.field}
374
371
  options={fieldOptions}
@@ -380,28 +377,10 @@
380
377
  placeholder="Column"
381
378
  popoverAutoWidth
382
379
  />
383
- {:else}
384
- <ConditionField
385
- placeholder="Value"
386
- {filter}
387
- drawerTitle={"Edit Binding"}
388
- {bindings}
389
- {panel}
390
- {toReadable}
391
- {toRuntime}
392
- {evaluationContext}
393
- on:change={e => {
394
- const updated = {
395
- ...filter,
396
- field: e.detail.field,
397
- }
398
- onFilterFieldUpdate(updated, groupIdx, filterIdx)
399
- }}
400
- />
401
380
  {/if}
402
381
  <Select
403
- value={filter.operator}
404
- disabled={!filter.field && builderType === "filter"}
382
+ value={filter.operator || OperatorOptions.Equals.value}
383
+ disabled={!filter.field && !$$slots["field-column"]}
405
384
  options={getValidOperatorsForType(filter)}
406
385
  on:change={e => {
407
386
  const updated = { ...filter, operator: e.detail }
@@ -411,33 +390,55 @@
411
390
  placeholder={false}
412
391
  popoverAutoWidth
413
392
  />
414
- <FilterField
415
- placeholder="Value"
416
- disabled={!filter.field && builderType === "filter"}
417
- drawerTitle={builderType === "condition"
418
- ? "Edit binding"
419
- : null}
420
- {allowBindings}
421
- filter={{
422
- ...filter,
423
- ...(builderType === "condition"
424
- ? { type: FieldType.STRING }
425
- : {}),
426
- }}
427
- {schemaFields}
428
- {bindings}
429
- {panel}
430
- {toReadable}
431
- {toRuntime}
432
- {evaluationContext}
433
- on:change={e => {
434
- onFilterFieldUpdate(
435
- { ...filter, ...e.detail },
436
- groupIdx,
437
- filterIdx
438
- )
439
- }}
440
- />
393
+ {#if $$slots["extra-column"]}
394
+ <slot
395
+ name="extra-column"
396
+ {filter}
397
+ {groupIdx}
398
+ {filterIdx}
399
+ onUpdate={f =>
400
+ onFilterFieldUpdate(f, groupIdx, filterIdx)}
401
+ {sanitizeOperator}
402
+ {sanitizeValue}
403
+ />
404
+ {/if}
405
+ {#if $$slots["value-column"]}
406
+ <slot
407
+ name="value-column"
408
+ {filter}
409
+ {groupIdx}
410
+ {filterIdx}
411
+ onUpdate={f =>
412
+ onFilterFieldUpdate(f, groupIdx, filterIdx)}
413
+ {sanitizeOperator}
414
+ {sanitizeValue}
415
+ />
416
+ {:else}
417
+ <FilterField
418
+ placeholder="Value"
419
+ disabled={!filter.field && !$$slots["field-column"]}
420
+ {drawerTitle}
421
+ {allowBindings}
422
+ filter={{
423
+ ...filter,
424
+ }}
425
+ {schemaFields}
426
+ {bindings}
427
+ {panel}
428
+ {toReadable}
429
+ {toRuntime}
430
+ {evaluationContext}
431
+ {bindingValueType}
432
+ {useConditionValueControls}
433
+ on:change={e => {
434
+ onFilterFieldUpdate(
435
+ { ...filter, ...e.detail },
436
+ groupIdx,
437
+ filterIdx
438
+ )
439
+ }}
440
+ />
441
+ {/if}
441
442
 
442
443
  <ActionButton
443
444
  size="M"
@@ -478,7 +479,7 @@
478
479
  popoverAutoWidth
479
480
  />
480
481
  </span>
481
- <span>when all {builderType}s are empty</span>
482
+ <span>when all {filterTypeLabel}s are empty</span>
482
483
  </div>
483
484
  {/if}
484
485
  <div class="add-group">
@@ -492,7 +493,7 @@
492
493
  })
493
494
  }}
494
495
  >
495
- Add {builderType} group
496
+ Add {filterTypeLabel} group
496
497
  </Button>
497
498
  {#if docsURL}
498
499
  <a href={docsURL} target="_blank">
@@ -563,7 +564,13 @@
563
564
  .filter {
564
565
  display: grid;
565
566
  gap: var(--spacing-l);
566
- grid-template-columns: minmax(150px, 1fr) 170px minmax(200px, 1fr) 40px 40px;
567
+ grid-template-columns: minmax(150px, 1fr) 170px minmax(200px, 1fr) 40px;
568
+ }
569
+
570
+ .filter.has-extra-column {
571
+ grid-template-columns:
572
+ minmax(150px, 1fr) 170px 120px minmax(200px, 1fr)
573
+ 40px;
567
574
  }
568
575
 
569
576
  .filters-footer {
@@ -7,6 +7,7 @@
7
7
  Icon,
8
8
  Drawer,
9
9
  Button,
10
+ Select,
10
11
  } from "@budibase/bbui"
11
12
 
12
13
  import FilterUsers from "./FilterUsers.svelte"
@@ -25,6 +26,8 @@
25
26
  export let toReadable
26
27
  export let toRuntime
27
28
  export let evaluationContext = {}
29
+ export let bindingValueType = Constants.FilterValueType.BINDING
30
+ export let useConditionValueControls = false
28
31
 
29
32
  const dispatch = createEventDispatcher()
30
33
  const { OperatorOptions, FilterValueType } = Constants
@@ -60,7 +63,7 @@
60
63
  const onConfirmBinding = () => {
61
64
  dispatch("change", {
62
65
  value: toRuntime ? toRuntime(bindings, drawerValue) : drawerValue,
63
- valueType: drawerValue ? FilterValueType.BINDING : FilterValueType.VALUE,
66
+ valueType: drawerValue ? bindingValueType : FilterValueType.VALUE,
64
67
  })
65
68
  }
66
69
 
@@ -177,7 +180,14 @@
177
180
  />
178
181
  {:else}
179
182
  <div>
180
- {#if [FieldType.STRING, FieldType.LONGFORM, FieldType.NUMBER, FieldType.BIGINT, FieldType.FORMULA, FieldType.AI, FieldType.BARCODEQR].includes(filter.type)}
183
+ {#if filter.type === FieldType.NUMBER && useConditionValueControls}
184
+ <Input
185
+ type="number"
186
+ disabled={filter.noValue}
187
+ value={readableValue}
188
+ on:change={onChange}
189
+ />
190
+ {:else if [FieldType.STRING, FieldType.LONGFORM, FieldType.NUMBER, FieldType.BIGINT, FieldType.FORMULA, FieldType.AI, FieldType.BARCODEQR].includes(filter.type)}
181
191
  <Input
182
192
  disabled={filter.noValue}
183
193
  value={readableValue}
@@ -200,6 +210,18 @@
200
210
  wrapText
201
211
  on:change={onChange}
202
212
  />
213
+ {:else if filter.type === FieldType.BOOLEAN && useConditionValueControls}
214
+ <Select
215
+ placeholder={false}
216
+ disabled={filter.noValue}
217
+ options={[
218
+ { label: "True", value: "true" },
219
+ { label: "False", value: "false" },
220
+ ]}
221
+ value={readableValue}
222
+ popoverAutoWidth
223
+ on:change={onChange}
224
+ />
203
225
  {:else if filter.type === FieldType.BOOLEAN}
204
226
  <Combobox
205
227
  disabled={filter.noValue}
@@ -0,0 +1,26 @@
1
+ import { describe, expect, it, vi } from "vitest"
2
+ import type { UICondition } from "@budibase/types"
3
+
4
+ vi.mock("../../../utils", () => ({
5
+ derivedMemo: (value: unknown) => value,
6
+ QueryUtils: {},
7
+ }))
8
+
9
+ import { getEnabledConditions } from "./conditions"
10
+
11
+ describe("grid condition evaluation helpers", () => {
12
+ it("filters disabled conditions", () => {
13
+ const conditions = [
14
+ { disabled: true },
15
+ { disabled: false },
16
+ { disabled: true },
17
+ ] as UICondition[]
18
+
19
+ expect(getEnabledConditions(conditions)).toEqual([{ disabled: false }])
20
+ })
21
+
22
+ it("returns an empty array when no conditions are supplied", () => {
23
+ expect(getEnabledConditions(undefined)).toEqual([])
24
+ expect(getEnabledConditions([])).toEqual([])
25
+ })
26
+ })
@@ -26,6 +26,13 @@ export const createStores = (): ConditionStore => {
26
26
  }
27
27
  }
28
28
 
29
+ export const getEnabledConditions = (conditions: UICondition[] | undefined) => {
30
+ if (!conditions?.length) {
31
+ return []
32
+ }
33
+ return conditions.filter(condition => !condition.disabled)
34
+ }
35
+
29
36
  export const deriveStores = (context: StoreContext): ConditionDerivedStore => {
30
37
  const { columns, props } = context
31
38
 
@@ -77,15 +84,20 @@ export const deriveStores = (context: StoreContext): ConditionDerivedStore => {
77
84
  export const initialise = (context: StoreContext) => {
78
85
  const { metadata, conditions, rows } = context
79
86
 
80
- // Recompute all metadata if conditions change
81
- conditions.subscribe($conditions => {
87
+ const recomputeAllMetadata = () => {
82
88
  let newMetadata: Record<string, any> = {}
89
+ const $conditions = get(conditions)
83
90
  if ($conditions?.length) {
84
91
  for (let row of get(rows)) {
85
92
  newMetadata[row._id] = evaluateConditions(row, $conditions, context)
86
93
  }
87
94
  }
88
95
  metadata.set(newMetadata)
96
+ }
97
+
98
+ // Recompute all metadata if conditions change
99
+ conditions.subscribe(() => {
100
+ recomputeAllMetadata()
89
101
  })
90
102
 
91
103
  // Recompute metadata for specific rows when they change
@@ -196,6 +208,8 @@ const evaluateConditions = (
196
208
  }
197
209
  }
198
210
 
211
+ allConditions = getEnabledConditions(allConditions)
212
+
199
213
  // Pre-process button conditions to set default visibility for show conditions
200
214
  const buttonShowConditions = new Set()
201
215
  for (let condition of allConditions) {
@@ -7,9 +7,10 @@ export default class QueryFetch extends BaseDataFetch<QueryDatasource, Query> {
7
7
  async determineFeatureFlags() {
8
8
  const definition = await this.getDefinition()
9
9
  const supportsPagination =
10
- !!definition?.fields?.pagination?.type &&
11
- !!definition?.fields?.pagination?.location &&
12
- !!definition?.fields?.pagination?.pageParam
10
+ (!!definition?.fields?.pagination?.type &&
11
+ !!definition?.fields?.pagination?.location &&
12
+ !!definition?.fields?.pagination?.pageParam) ||
13
+ !!definition?.fields?.pagination?.enabled
13
14
  return { supportsPagination }
14
15
  }
15
16
 
@@ -41,7 +42,7 @@ export default class QueryFetch extends BaseDataFetch<QueryDatasource, Query> {
41
42
  const { datasource, limit, paginate } = this.options
42
43
  const { supportsPagination } = this.features
43
44
  const { cursor, definition } = get(this.store)
44
- const type = definition?.fields?.pagination?.type
45
+ const paginationType = definition?.fields?.pagination?.type
45
46
 
46
47
  // Set the default query params
47
48
  const parameters = Helpers.cloneDeep(datasource.queryParams || {})
@@ -54,7 +55,11 @@ export default class QueryFetch extends BaseDataFetch<QueryDatasource, Query> {
54
55
  // Add pagination to query if supported
55
56
  const queryPayload: ExecuteQueryRequest = { parameters }
56
57
  if (paginate && supportsPagination) {
57
- const requestCursor = type === "page" ? parseInt(cursor || "1") : cursor
58
+ // For SQL queries (pagination.enabled), use page-based pagination
59
+ // For REST queries, use the configured type (page or cursor)
60
+ const isPageBased =
61
+ paginationType === "page" || definition?.fields?.pagination?.enabled
62
+ const requestCursor = isPageBased ? parseInt(cursor || "1") : cursor
58
63
  queryPayload.pagination = { page: requestCursor, limit }
59
64
  }
60
65
 
@@ -67,7 +72,10 @@ export default class QueryFetch extends BaseDataFetch<QueryDatasource, Query> {
67
72
  let nextCursor = null
68
73
  let hasNextPage = false
69
74
  if (paginate && supportsPagination) {
70
- if (type === "page") {
75
+ const isPageBased =
76
+ paginationType === "page" || definition?.fields?.pagination?.enabled
77
+
78
+ if (isPageBased) {
71
79
  // For "page number" pagination, increment the existing page number
72
80
  nextCursor = queryPayload.pagination!.page! + 1
73
81
  hasNextPage = data?.length === limit && limit > 0
@@ -1,175 +0,0 @@
1
- <script>
2
- import { Input, Icon, Drawer, Button } from "@budibase/bbui"
3
- import { isJSBinding } from "@budibase/string-templates"
4
- import { createEventDispatcher } from "svelte"
5
-
6
- export let filter
7
- export let disabled = false
8
- export let bindings = []
9
- export let panel
10
- export let drawerTitle
11
- export let toReadable
12
- export let toRuntime
13
- export let evaluationContext = {}
14
-
15
- const dispatch = createEventDispatcher()
16
-
17
- let bindingDrawer
18
- let fieldValue
19
-
20
- $: fieldValue = filter?.field
21
- $: readableValue = toReadable ? toReadable(bindings, fieldValue) : fieldValue
22
- $: drawerValue = toDrawerValue(fieldValue)
23
- $: isJS = isJSBinding(fieldValue)
24
-
25
- const drawerOnChange = e => {
26
- drawerValue = e.detail
27
- }
28
-
29
- const onChange = e => {
30
- fieldValue = e.detail
31
- dispatch("change", {
32
- field: toRuntime ? toRuntime(bindings, fieldValue) : fieldValue,
33
- })
34
- }
35
-
36
- const onConfirmBinding = () => {
37
- dispatch("change", {
38
- field: toRuntime ? toRuntime(bindings, drawerValue) : drawerValue,
39
- })
40
- }
41
-
42
- /**
43
- * Converts arrays into strings. The CodeEditor expects a string or encoded JS
44
- *
45
- * @param{string} fieldValue
46
- */
47
- const toDrawerValue = fieldValue => {
48
- return Array.isArray(fieldValue) ? fieldValue.join(",") : readableValue
49
- }
50
- </script>
51
-
52
- <div>
53
- <Drawer
54
- on:drawerHide
55
- on:drawerShow
56
- bind:this={bindingDrawer}
57
- title={drawerTitle || ""}
58
- forceModal
59
- >
60
- <Button
61
- cta
62
- slot="buttons"
63
- on:click={() => {
64
- onConfirmBinding()
65
- bindingDrawer.hide()
66
- }}
67
- >
68
- Confirm
69
- </Button>
70
- <svelte:component
71
- this={panel}
72
- slot="body"
73
- value={drawerValue}
74
- allowJS
75
- allowHelpers
76
- allowHBS
77
- on:change={drawerOnChange}
78
- {bindings}
79
- context={evaluationContext}
80
- />
81
- </Drawer>
82
-
83
- <div class="field-wrap" class:bindings={true}>
84
- <div class="field">
85
- <Input
86
- disabled={filter.noValue}
87
- readonly={isJS}
88
- value={isJS ? "(JavaScript function)" : readableValue}
89
- on:change={onChange}
90
- />
91
- </div>
92
-
93
- <div class="binding-control">
94
- {#if !disabled}
95
- <!-- svelte-ignore a11y-click-events-have-key-events -->
96
- <!-- svelte-ignore a11y-no-static-element-interactions -->
97
- <div
98
- class="icon binding"
99
- on:click={() => {
100
- bindingDrawer.show()
101
- }}
102
- >
103
- <Icon size="S" weight="fill" name="lightning" />
104
- </div>
105
- {/if}
106
- </div>
107
- </div>
108
- </div>
109
-
110
- <style>
111
- .field-wrap {
112
- display: flex;
113
- }
114
- .field {
115
- flex: 1;
116
- }
117
-
118
- .field-wrap.bindings .field :global(.spectrum-Form-itemField),
119
- .field-wrap.bindings .field :global(input),
120
- .field-wrap.bindings .field :global(.spectrum-Picker) {
121
- border-top-right-radius: 0px;
122
- border-bottom-right-radius: 0px;
123
- }
124
-
125
- .field-wrap.bindings
126
- .field
127
- :global(.spectrum-InputGroup.spectrum-Datepicker) {
128
- min-width: unset;
129
- border-radius: 0px;
130
- }
131
-
132
- .field-wrap.bindings
133
- .field
134
- :global(
135
- .spectrum-InputGroup.spectrum-Datepicker
136
- .spectrum-Textfield-input.spectrum-InputGroup-input
137
- ) {
138
- width: 100%;
139
- }
140
-
141
- .binding-control .icon {
142
- border: 1px solid
143
- var(
144
- --spectrum-textfield-m-border-color,
145
- var(--spectrum-alias-border-color)
146
- );
147
- border-left: 0px;
148
- border-top-right-radius: 4px;
149
- border-bottom-right-radius: 4px;
150
- justify-content: center;
151
- align-items: center;
152
- display: flex;
153
- flex-direction: row;
154
- box-sizing: border-box;
155
- width: 31px;
156
- color: var(--spectrum-alias-text-color);
157
- background-color: var(--spectrum-global-color-gray-75);
158
- transition:
159
- background-color var(--spectrum-global-animation-duration-100, 130ms),
160
- box-shadow var(--spectrum-global-animation-duration-100, 130ms),
161
- border-color var(--spectrum-global-animation-duration-100, 130ms);
162
- height: calc(var(--spectrum-alias-item-height-m));
163
- }
164
-
165
- .binding-control .icon:hover {
166
- cursor: pointer;
167
- background-color: var(--spectrum-global-color-gray-50);
168
- border-color: var(--spectrum-alias-border-color-hover);
169
- color: var(--spectrum-alias-text-color-hover);
170
- }
171
-
172
- .binding-control .icon.binding:hover {
173
- color: var(--yellow);
174
- }
175
- </style>