@budibase/frontend-core 3.26.3 → 3.27.1

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,12 +1,14 @@
1
1
  {
2
2
  "name": "@budibase/frontend-core",
3
- "version": "3.26.3",
3
+ "version": "3.27.1",
4
4
  "description": "Budibase frontend core libraries used in builder and client",
5
5
  "author": "Budibase",
6
6
  "license": "MPL-2.0",
7
7
  "svelte": "./src/index.ts",
8
8
  "scripts": {
9
- "check:types": "yarn svelte-check"
9
+ "check:types": "yarn svelte-check",
10
+ "test": "vitest run",
11
+ "test:watch": "vitest"
10
12
  },
11
13
  "dependencies": {
12
14
  "@ai-sdk/svelte": "^4.0.48",
@@ -19,5 +21,8 @@
19
21
  "shortid": "2.2.15",
20
22
  "socket.io-client": "^4.7.5"
21
23
  },
22
- "gitHead": "69ab5e17bf9ccdac19b3bccd30352b936942129a"
24
+ "devDependencies": {
25
+ "vitest": "^3.2.4"
26
+ },
27
+ "gitHead": "e16e4f2acb1dbf82177c69b4cbfd23c3bdf16544"
23
28
  }
@@ -62,6 +62,22 @@
62
62
  let inputValue = $state("")
63
63
  let reasoningTimers = $state<Record<string, number>>({})
64
64
 
65
+ const getReasoningText = (message: UIMessage<AgentMessageMetadata>) =>
66
+ (message.parts ?? [])
67
+ .filter(isReasoningUIPart)
68
+ .map(p => p.text)
69
+ .join("")
70
+
71
+ const isReasoningStreaming = (message: UIMessage<AgentMessageMetadata>) =>
72
+ (message.parts ?? []).some(
73
+ part => isReasoningUIPart(part) && part.state === "streaming"
74
+ )
75
+
76
+ const hasToolError = (message: UIMessage<AgentMessageMetadata>) =>
77
+ (message.parts ?? []).some(
78
+ part => isToolUIPart(part) && part.state === "output-error"
79
+ )
80
+
65
81
  $effect(() => {
66
82
  const interval = setInterval(() => {
67
83
  let updated = false
@@ -71,24 +87,33 @@
71
87
  if (message.role !== "assistant") continue
72
88
  const createdAt = message.metadata?.createdAt
73
89
  const completedAt = message.metadata?.completedAt
90
+ const id = `${message.id}-reasoning`
91
+
92
+ if (!createdAt) continue
74
93
 
75
- for (const [index, part] of (message.parts ?? []).entries()) {
76
- if (!isReasoningUIPart(part)) continue
77
-
78
- const id = `${message.id}-reasoning-${index}`
79
-
80
- if (completedAt && createdAt) {
81
- const finalElapsed = (completedAt - createdAt) / 1000
82
- if (newTimers[id] !== finalElapsed) {
83
- newTimers[id] = finalElapsed
84
- updated = true
85
- }
86
- } else if (part.state === "streaming" && createdAt) {
87
- const newElapsed = (Date.now() - createdAt) / 1000
88
- if (newTimers[id] !== newElapsed) {
89
- newTimers[id] = newElapsed
90
- updated = true
91
- }
94
+ if (completedAt) {
95
+ const finalElapsed = (completedAt - createdAt) / 1000
96
+ if (newTimers[id] !== finalElapsed) {
97
+ newTimers[id] = finalElapsed
98
+ updated = true
99
+ }
100
+ continue
101
+ }
102
+
103
+ const toolError = hasToolError(message)
104
+ if (toolError) {
105
+ if (newTimers[id] == null) {
106
+ newTimers[id] = (Date.now() - createdAt) / 1000
107
+ updated = true
108
+ }
109
+ continue
110
+ }
111
+
112
+ if (isReasoningStreaming(message)) {
113
+ const newElapsed = (Date.now() - createdAt) / 1000
114
+ if (newTimers[id] !== newElapsed) {
115
+ newTimers[id] = newElapsed
116
+ updated = true
92
117
  }
93
118
  }
94
119
  }
@@ -381,48 +406,48 @@
381
406
  <MarkdownViewer value={getUserMessageText(message)} />
382
407
  </div>
383
408
  {:else if message.role === "assistant"}
409
+ {@const reasoningText = getReasoningText(message)}
410
+ {@const reasoningId = `${message.id}-reasoning`}
411
+ {@const toolError = hasToolError(message)}
412
+ {@const reasoningStreaming = isReasoningStreaming(message)}
413
+ {@const isThinking =
414
+ reasoningStreaming && !toolError && !message.metadata?.completedAt}
384
415
  <div class="message assistant">
416
+ {#if reasoningText}
417
+ <div class="reasoning-part">
418
+ <button
419
+ class="reasoning-toggle"
420
+ type="button"
421
+ onclick={() =>
422
+ (expandedTools = {
423
+ ...expandedTools,
424
+ [reasoningId]: !expandedTools[reasoningId],
425
+ })}
426
+ >
427
+ <span class="reasoning-icon" class:shimmer={isThinking}>
428
+ <Icon
429
+ name="brain"
430
+ size="M"
431
+ color="var(--spectrum-global-color-gray-600)"
432
+ />
433
+ </span>
434
+ <span class="reasoning-label" class:shimmer={isThinking}>
435
+ {isThinking ? "Thinking" : "Thought for"}
436
+ {#if reasoningTimers[reasoningId]}
437
+ <span class="reasoning-timer"
438
+ >{reasoningTimers[reasoningId].toFixed(1)}s</span
439
+ >
440
+ {/if}
441
+ </span>
442
+ </button>
443
+ {#if expandedTools[reasoningId]}
444
+ <div class="reasoning-content">{reasoningText}</div>
445
+ {/if}
446
+ </div>
447
+ {/if}
385
448
  {#each message.parts ?? [] as part, partIndex}
386
449
  {#if isTextUIPart(part)}
387
450
  <MarkdownViewer value={part.text} />
388
- {:else if isReasoningUIPart(part)}
389
- {@const reasoningId = `${message.id}-reasoning-${partIndex}`}
390
- <div class="reasoning-part">
391
- <button
392
- class="reasoning-toggle"
393
- type="button"
394
- onclick={() =>
395
- (expandedTools = {
396
- ...expandedTools,
397
- [reasoningId]: !expandedTools[reasoningId],
398
- })}
399
- >
400
- <span
401
- class="reasoning-icon"
402
- class:shimmer={part.state === "streaming"}
403
- >
404
- <Icon
405
- name="brain"
406
- size="M"
407
- color="var(--spectrum-global-color-gray-600)"
408
- />
409
- </span>
410
- <span
411
- class="reasoning-label"
412
- class:shimmer={part.state === "streaming"}
413
- >
414
- {part.state === "streaming" ? "Thinking" : "Thought for"}
415
- {#if reasoningTimers[reasoningId]}
416
- <span class="reasoning-timer"
417
- >{reasoningTimers[reasoningId].toFixed(1)}s</span
418
- >
419
- {/if}
420
- </span>
421
- </button>
422
- {#if expandedTools[reasoningId]}
423
- <div class="reasoning-content">{part.text}</div>
424
- {/if}
425
- </div>
426
451
  {:else if isToolUIPart(part)}
427
452
  {@const toolId = `${message.id}-${getToolName(part)}-${partIndex}`}
428
453
  {@const isRunning =
@@ -639,12 +664,6 @@
639
664
  max-width: 100%;
640
665
  }
641
666
 
642
- .message.system {
643
- align-self: flex-start;
644
- background: none;
645
- padding-left: 0;
646
- }
647
-
648
667
  .input-wrapper {
649
668
  position: sticky;
650
669
  bottom: 0;
@@ -97,7 +97,7 @@
97
97
  }
98
98
 
99
99
  // Reset state if this search is invalid
100
- if (!schema?.tableId || !isOpen) {
100
+ if (!schema?.tableId || !isOpen || !primaryDisplay) {
101
101
  lastSearchString = null
102
102
  candidateIndex = null
103
103
  searchResults = []
@@ -159,6 +159,7 @@
159
159
  primaryDisplay = await cache.actions.getPrimaryDisplayForTableId(
160
160
  schema.tableId
161
161
  )
162
+ searching = false
162
163
  }
163
164
 
164
165
  // Show initial list of results
@@ -38,7 +38,8 @@ export const createActions = (context: StoreContext): CacheActionStore => {
38
38
 
39
39
  const getPrimaryDisplayForTableId = async (tableId: string) => {
40
40
  const table = await fetchTable(tableId)
41
- const display = table?.primaryDisplay || table?.schema?.[0]?.name
41
+ const firstSchemaKey = Object.keys(table?.schema || {})[0]
42
+ const display = table?.primaryDisplay || firstSchemaKey
42
43
  return display
43
44
  }
44
45
 
package/src/constants.ts CHANGED
@@ -11,7 +11,6 @@ import { BpmCorrelationKey } from "@budibase/shared-core"
11
11
  import { BBReferenceFieldSubType, FieldType } from "@budibase/types"
12
12
 
13
13
  export const BannedSearchTypes = [
14
- FieldType.LINK,
15
14
  FieldType.ATTACHMENT_SINGLE,
16
15
  FieldType.ATTACHMENTS,
17
16
  FieldType.FORMULA,
@@ -8,10 +8,12 @@ export function getTableFields(tables, linkField) {
8
8
  const linkFields = getFields(tables, Object.values(table.schema), {
9
9
  allowLinks: false,
10
10
  })
11
- return linkFields.map(field => ({
12
- ...field,
13
- name: `${linkField.name}.${field.name}`,
14
- }))
11
+ return linkFields
12
+ .sort((a, b) => a.name.localeCompare(b.name))
13
+ .map(field => ({
14
+ ...field,
15
+ name: `${linkField.name}.${field.name}`,
16
+ }))
15
17
  }
16
18
 
17
19
  export function getFields(
@@ -19,18 +21,22 @@ export function getFields(
19
21
  fields,
20
22
  { allowLinks } = { allowLinks: true }
21
23
  ) {
24
+ const result = []
22
25
  let filteredFields = fields.filter(
23
- field => !BannedSearchTypes.includes(field.type)
26
+ field =>
27
+ !BannedSearchTypes.includes(field.type) &&
28
+ (allowLinks || field.type !== "link")
24
29
  )
25
- if (allowLinks) {
26
- const linkFields = fields.filter(field => field.type === "link")
27
- for (let linkField of linkFields) {
30
+ for (const field of filteredFields) {
31
+ result.push(field)
32
+
33
+ if (allowLinks && field.type === "link") {
28
34
  // only allow one depth of SQL relationship filtering
29
- filteredFields = filteredFields.concat(getTableFields(tables, linkField))
35
+ result.push(...getTableFields(tables, field))
30
36
  }
31
37
  }
32
38
  const staticFormulaFields = fields.filter(
33
39
  field => field.type === "formula" && field.formulaType === "static"
34
40
  )
35
- return filteredFields.concat(staticFormulaFields)
41
+ return result.concat(staticFormulaFields)
36
42
  }
@@ -0,0 +1,93 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import { getFields, getTableFields } from "./searchFields.js"
3
+
4
+ const field = (name, type, extra = {}) => ({
5
+ name,
6
+ type,
7
+ ...extra,
8
+ })
9
+
10
+ const table = (id, schema) => ({
11
+ _id: id,
12
+ name: id,
13
+ sql: true,
14
+ schema,
15
+ })
16
+
17
+ describe("search fields", () => {
18
+ const relatedTable = table("t2", {
19
+ title: field("title", "string"),
20
+ otherLink: field("otherLink", "link", { tableId: "t3" }),
21
+ })
22
+ const baseTable = table("t1", {
23
+ name: field("name", "string"),
24
+ rel: field("rel", "link", { tableId: "t2" }),
25
+ })
26
+ const tables = [baseTable, relatedTable]
27
+
28
+ describe("getTableFields", () => {
29
+ it("excludes nested link fields when allowLinks is false", () => {
30
+ const fields = getTableFields(tables, baseTable.schema.rel)
31
+ expect(fields.map(field => field.name)).toEqual(["rel.title"])
32
+ })
33
+
34
+ it("returns empty for non-sql related tables", () => {
35
+ const nonSqlTable = {
36
+ ...relatedTable,
37
+ sql: false,
38
+ }
39
+ const fields = getTableFields([baseTable, nonSqlTable], {
40
+ ...baseTable.schema.rel,
41
+ tableId: nonSqlTable._id,
42
+ })
43
+ expect(fields).toEqual([])
44
+ })
45
+ })
46
+
47
+ describe("getFields", () => {
48
+ it("excludes link fields when allowLinks is false", () => {
49
+ const fields = getFields(tables, Object.values(baseTable.schema), {
50
+ allowLinks: false,
51
+ })
52
+ expect(fields.map(field => field.name)).toEqual(["name"])
53
+ })
54
+
55
+ it("includes one relationship depth when allowLinks is true", () => {
56
+ const fields = getFields(tables, Object.values(baseTable.schema), {
57
+ allowLinks: true,
58
+ })
59
+ const names = fields.map(field => field.name)
60
+ expect(names).toEqual(
61
+ expect.arrayContaining(["name", "rel", "rel.title"])
62
+ )
63
+ expect(names).not.toEqual(expect.arrayContaining(["rel.otherLink"]))
64
+ })
65
+
66
+ it("appends static formula fields even when allowLinks is false", () => {
67
+ const withFormula = table("t4", {
68
+ name: field("name", "string"),
69
+ calc: field("calc", "formula", { formulaType: "static" }),
70
+ rel: field("rel", "link", { tableId: "t2" }),
71
+ })
72
+ const fields = getFields(tables, Object.values(withFormula.schema), {
73
+ allowLinks: false,
74
+ })
75
+ expect(fields.map(field => field.name)).toEqual(
76
+ expect.arrayContaining(["name", "calc"])
77
+ )
78
+ })
79
+
80
+ it("filters out banned field types", () => {
81
+ const withBanned = table("t5", {
82
+ name: field("name", "string"),
83
+ meta: field("meta", "json"),
84
+ files: field("files", "attachment"),
85
+ file: field("file", "attachment_single"),
86
+ })
87
+ const fields = getFields(tables, Object.values(withBanned.schema), {
88
+ allowLinks: false,
89
+ })
90
+ expect(fields.map(field => field.name)).toEqual(["name"])
91
+ })
92
+ })
93
+ })
@@ -0,0 +1,23 @@
1
+ import { defineConfig } from "vite"
2
+ import path from "path"
3
+
4
+ export default defineConfig(() => {
5
+ return {
6
+ test: {
7
+ globals: true,
8
+ include: ["src/**/*.test.*", "src/**/*.spec.*"],
9
+ },
10
+ resolve: {
11
+ alias: [
12
+ {
13
+ find: "@budibase/types",
14
+ replacement: path.resolve("../types/src"),
15
+ },
16
+ {
17
+ find: "@budibase/shared-core",
18
+ replacement: path.resolve("../shared-core/src"),
19
+ },
20
+ ],
21
+ },
22
+ }
23
+ })