@budibase/frontend-core 3.27.3 → 3.27.5

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.27.3",
3
+ "version": "3.27.5",
4
4
  "description": "Budibase frontend core libraries used in builder and client",
5
5
  "author": "Budibase",
6
6
  "license": "MPL-2.0",
@@ -24,5 +24,5 @@
24
24
  "devDependencies": {
25
25
  "vitest": "^3.2.4"
26
26
  },
27
- "gitHead": "a2ccba6d3b5002fab734abfe9563b7cf2b3a6592"
27
+ "gitHead": "46316dfbbf8e41a547bc8a4977ccd819a7515b9a"
28
28
  }
package/src/api/rows.ts CHANGED
@@ -11,7 +11,11 @@ import {
11
11
  import { BaseAPIClient } from "./types"
12
12
 
13
13
  export interface RowEndpoints {
14
- fetchRow: (tableId: string, rowId: string) => Promise<FindRowResponse>
14
+ fetchRow: (
15
+ tableId: string,
16
+ rowId: string,
17
+ suppressErrors?: boolean
18
+ ) => Promise<FindRowResponse>
15
19
  saveRow: (
16
20
  row: SaveRowRequest,
17
21
  suppressErrors?: boolean
@@ -34,10 +38,12 @@ export const buildRowEndpoints = (API: BaseAPIClient): RowEndpoints => ({
34
38
  * Fetches data about a certain row in a data source.
35
39
  * @param sourceId the ID of the table or view to fetch from
36
40
  * @param rowId the ID of the row to fetch
41
+ * @param suppressErrors whether or not to suppress error notifications
37
42
  */
38
- fetchRow: async (sourceId, rowId) => {
43
+ fetchRow: async (sourceId, rowId, suppressErrors = false) => {
39
44
  return await API.get({
40
45
  url: `/api/${sourceId}/rows/${rowId}`,
46
+ suppressErrors,
41
47
  })
42
48
  },
43
49
 
package/src/api/user.ts CHANGED
@@ -43,7 +43,10 @@ export interface UserEndpoints {
43
43
  deleteUser: (userId: string) => Promise<DeleteUserResponse>
44
44
  deleteUsers: (users: UserIdentifier[]) => Promise<BulkUserDeleted | undefined>
45
45
  onboardUsers: (data: InviteUsersRequest) => Promise<InviteUsersResponse>
46
- getUserInvite: (code: string) => Promise<CheckInviteResponse>
46
+ getUserInvite: (
47
+ code: string,
48
+ tenantId?: string
49
+ ) => Promise<CheckInviteResponse>
47
50
  getUserInvites: () => Promise<GetUserInvitesResponse>
48
51
  inviteUsers: (users: InviteUsersRequest) => Promise<InviteUsersResponse>
49
52
  removeUserInvites: (
@@ -223,9 +226,10 @@ export const buildUserEndpoints = (API: BaseAPIClient): UserEndpoints => ({
223
226
  * Retrieves the invitation associated with a provided code.
224
227
  * @param code The unique code for the target invite
225
228
  */
226
- getUserInvite: async code => {
229
+ getUserInvite: async (code, tenantId) => {
230
+ const query = tenantId ? `?tenantId=${encodeURIComponent(tenantId)}` : ""
227
231
  return await API.get({
228
- url: `/api/global/users/invite/${code}`,
232
+ url: `/api/global/users/invite/${code}${query}`,
229
233
  })
230
234
  },
231
235
 
@@ -31,10 +31,13 @@
31
31
  chat: ChatConversationLike
32
32
  persistConversation?: boolean
33
33
  conversationStarters?: { prompt: string }[]
34
+ initialPrompt?: string
34
35
  onchatsaved?: (_event: {
35
36
  detail: { chatId?: string; chat: ChatConversationLike }
36
37
  }) => void
37
38
  isAgentPreviewChat?: boolean
39
+ readOnly?: boolean
40
+ readOnlyReason?: "disabled" | "deleted" | "offline"
38
41
  }
39
42
 
40
43
  let {
@@ -42,8 +45,11 @@
42
45
  chat = $bindable(),
43
46
  persistConversation = true,
44
47
  conversationStarters = [],
48
+ initialPrompt = "",
45
49
  onchatsaved,
46
50
  isAgentPreviewChat = false,
51
+ readOnly = false,
52
+ readOnlyReason,
47
53
  }: Props = $props()
48
54
 
49
55
  let API = $state(
@@ -60,6 +66,7 @@
60
66
  let textareaElement = $state<HTMLTextAreaElement>()
61
67
  let expandedTools = $state<Record<string, boolean>>({})
62
68
  let inputValue = $state("")
69
+ let lastInitialPrompt = $state("")
63
70
  let reasoningTimers = $state<Record<string, number>>({})
64
71
 
65
72
  const getReasoningText = (message: UIMessage<AgentMessageMetadata>) =>
@@ -78,6 +85,9 @@
78
85
  part => isToolUIPart(part) && part.state === "output-error"
79
86
  )
80
87
 
88
+ const getMessageError = (message: UIMessage<AgentMessageMetadata>) =>
89
+ message.metadata?.error
90
+
81
91
  $effect(() => {
82
92
  const interval = setInterval(() => {
83
93
  let updated = false
@@ -138,6 +148,20 @@
138
148
  tick().then(() => textareaElement?.focus())
139
149
  }
140
150
 
151
+ $effect(() => {
152
+ if (!initialPrompt) {
153
+ lastInitialPrompt = ""
154
+ return
155
+ }
156
+
157
+ if (initialPrompt === lastInitialPrompt) {
158
+ return
159
+ }
160
+
161
+ lastInitialPrompt = initialPrompt
162
+ applyConversationStarter(initialPrompt)
163
+ })
164
+
141
165
  const chatInstance = new Chat<UIMessage<AgentMessageMetadata>>({
142
166
  transport: new DefaultChatTransport({
143
167
  headers: () => ({ [Header.APP_ID]: workspaceId }),
@@ -151,6 +175,7 @@
151
175
  chatAppId,
152
176
  agentId: chat?.agentId,
153
177
  transient: !persistConversation,
178
+ isPreview: isAgentPreviewChat,
154
179
  title: chat?.title,
155
180
  messages,
156
181
  },
@@ -194,12 +219,20 @@
194
219
  let isBusy = $derived(
195
220
  chatInstance.status === "streaming" || chatInstance.status === "submitted"
196
221
  )
197
- let hasMessages = $derived(Boolean(chat?.messages?.length))
222
+ let hasMessages = $derived(Boolean(messages?.length))
198
223
  let showConversationStarters = $derived(
199
224
  !isBusy &&
200
225
  !hasMessages &&
201
226
  conversationStarters.length > 0 &&
202
- !isAgentPreviewChat
227
+ !isAgentPreviewChat &&
228
+ !readOnly
229
+ )
230
+ let readOnlyMessage = $derived(
231
+ readOnlyReason === "deleted"
232
+ ? "This agent was deleted. Select another agent to resume chatting."
233
+ : readOnlyReason === "offline"
234
+ ? "This agent is no longer live. Make it live in Settings to resume chatting."
235
+ : "This agent is disabled. Enable it in Settings to resume chatting."
203
236
  )
204
237
 
205
238
  let lastChatId = $state<string | undefined>(chat?._id)
@@ -258,6 +291,10 @@
258
291
  }
259
292
 
260
293
  const handleKeyDown = async (event: KeyboardEvent) => {
294
+ if (readOnly) {
295
+ return
296
+ }
297
+
261
298
  if (event.key === "Enter" && !event.shiftKey) {
262
299
  event.preventDefault()
263
300
  await sendMessage()
@@ -265,6 +302,10 @@
265
302
  }
266
303
 
267
304
  const sendMessage = async () => {
305
+ if (readOnly) {
306
+ return
307
+ }
308
+
268
309
  const chatAppIdFromEnsure = await ensureChatApp()
269
310
 
270
311
  if (!chat) {
@@ -347,7 +388,9 @@
347
388
  mounted = true
348
389
  ensureChatApp()
349
390
  tick().then(() => {
350
- textareaElement?.focus()
391
+ if (!readOnly) {
392
+ textareaElement?.focus()
393
+ }
351
394
  })
352
395
  }
353
396
  })
@@ -385,7 +428,7 @@
385
428
  {/each}
386
429
  </div>
387
430
  </div>
388
- {:else}
431
+ {:else if !hasMessages}
389
432
  <div class="empty-state">
390
433
  <div class="empty-state-icon">
391
434
  <Icon
@@ -409,9 +452,13 @@
409
452
  {@const reasoningText = getReasoningText(message)}
410
453
  {@const reasoningId = `${message.id}-reasoning`}
411
454
  {@const toolError = hasToolError(message)}
455
+ {@const messageError = getMessageError(message)}
412
456
  {@const reasoningStreaming = isReasoningStreaming(message)}
413
457
  {@const isThinking =
414
- reasoningStreaming && !toolError && !message.metadata?.completedAt}
458
+ reasoningStreaming &&
459
+ !toolError &&
460
+ !messageError &&
461
+ !message.metadata?.completedAt}
415
462
  <div class="message assistant">
416
463
  {#if reasoningText}
417
464
  <div class="reasoning-part">
@@ -487,7 +534,7 @@
487
534
  <div class="tool-name-wrapper">
488
535
  <span class="tool-name">{getToolName(part)}</span>
489
536
  </div>
490
- {#if isRunning || isError}
537
+ {#if isRunning || isError || isSuccess}
491
538
  <span class="tool-status">
492
539
  {#if isRunning}
493
540
  <ProgressCircle size="S" />
@@ -497,6 +544,12 @@
497
544
  size="S"
498
545
  color="var(--spectrum-global-color-red-600)"
499
546
  />
547
+ {:else if isSuccess}
548
+ <Icon
549
+ name="check"
550
+ size="S"
551
+ color="var(--spectrum-global-color-green-600)"
552
+ />
500
553
  {/if}
501
554
  </span>
502
555
  {/if}
@@ -556,16 +609,26 @@
556
609
  {/each}
557
610
  </div>
558
611
 
559
- <div class="input-wrapper">
560
- <textarea
561
- bind:value={inputValue}
562
- bind:this={textareaElement}
563
- class="input spectrum-Textfield-input"
564
- onkeydown={handleKeyDown}
565
- placeholder="Ask anything"
566
- disabled={isBusy}
567
- ></textarea>
568
- </div>
612
+ {#if readOnly}
613
+ <div class="input-wrapper">
614
+ <div class="read-only-notice">
615
+ <Body size="S" color="var(--spectrum-global-color-gray-700)">
616
+ {readOnlyMessage}
617
+ </Body>
618
+ </div>
619
+ </div>
620
+ {:else}
621
+ <div class="input-wrapper">
622
+ <textarea
623
+ bind:value={inputValue}
624
+ bind:this={textareaElement}
625
+ class="input spectrum-Textfield-input"
626
+ onkeydown={handleKeyDown}
627
+ placeholder="Ask anything"
628
+ disabled={isBusy}
629
+ ></textarea>
630
+ </div>
631
+ {/if}
569
632
  </div>
570
633
 
571
634
  <style>
@@ -602,36 +665,45 @@
602
665
  .starter-section {
603
666
  display: flex;
604
667
  flex-direction: column;
605
- gap: var(--spacing-s);
668
+ align-items: center;
669
+ gap: var(--spacing-xl);
670
+ margin: auto 0;
606
671
  }
607
672
 
608
673
  .starter-title {
609
- font-size: 12px;
610
- text-transform: uppercase;
611
- letter-spacing: 0.08em;
612
- color: var(--spectrum-global-color-gray-600);
674
+ font-size: 14px;
675
+ letter-spacing: 0;
676
+ color: var(--spectrum-global-color-gray-700);
677
+ text-align: center;
613
678
  }
614
679
 
615
680
  .starter-grid {
616
681
  display: grid;
617
682
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
618
- gap: var(--spacing-s);
683
+ gap: var(--spacing-m);
684
+ width: min(520px, 100%);
685
+ margin: 0 auto;
619
686
  }
620
687
 
621
688
  .starter-card {
622
- border: 1px solid var(--grey-3);
689
+ border: 1px solid var(--spectrum-global-color-gray-200);
623
690
  border-radius: 12px;
624
691
  padding: var(--spacing-m);
625
- background: var(--grey-2);
626
- color: var(--spectrum-global-color-gray-900);
692
+ background: var(--spectrum-global-color-gray-50);
693
+ color: var(--spectrum-global-color-gray-800);
627
694
  font: inherit;
628
- text-align: left;
695
+ font-size: 14px;
696
+ line-height: 1.4;
697
+ text-align: center;
629
698
  cursor: pointer;
699
+ display: flex;
700
+ align-items: center;
701
+ justify-content: center;
630
702
  }
631
703
 
632
704
  .starter-card:hover {
633
- border-color: var(--grey-4);
634
- background: var(--grey-1);
705
+ border-color: var(--spectrum-global-color-gray-300);
706
+ background: var(--spectrum-global-color-gray-100);
635
707
  }
636
708
 
637
709
  .message {
@@ -674,6 +746,14 @@
674
746
  line-height: 1.4;
675
747
  }
676
748
 
749
+ .read-only-notice {
750
+ border: 1px solid var(--spectrum-global-color-gray-200);
751
+ border-radius: 10px;
752
+ padding: var(--spacing-m);
753
+ background-color: var(--spectrum-global-color-gray-50);
754
+ text-align: center;
755
+ }
756
+
677
757
  .input {
678
758
  width: 100%;
679
759
  height: 100px;