@budibase/frontend-core 3.23.47 → 3.24.0

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.23.47",
3
+ "version": "3.24.0",
4
4
  "description": "Budibase frontend core libraries used in builder and client",
5
5
  "author": "Budibase",
6
6
  "license": "MPL-2.0",
@@ -12,11 +12,11 @@
12
12
  "@budibase/bbui": "*",
13
13
  "@budibase/shared-core": "*",
14
14
  "@budibase/types": "*",
15
- "ai": "^5.0.93",
15
+ "ai": "^6.0.3",
16
16
  "dayjs": "^1.10.8",
17
17
  "lodash": "4.17.21",
18
18
  "shortid": "2.2.15",
19
19
  "socket.io-client": "^4.7.5"
20
20
  },
21
- "gitHead": "534845c38ed0fe209406ebe45fd87f3de79f1da5"
21
+ "gitHead": "7cfa950d39730aa88a1c26c60f8094d6f0b0606d"
22
22
  }
package/src/api/agents.ts CHANGED
@@ -1,101 +1,23 @@
1
1
  import {
2
- AgentChat,
3
- ChatAgentRequest,
4
2
  CreateAgentRequest,
5
3
  CreateAgentResponse,
6
- FetchAgentHistoryResponse,
7
4
  FetchAgentsResponse,
8
5
  ToolMetadata,
9
6
  UpdateAgentRequest,
10
7
  UpdateAgentResponse,
11
8
  } from "@budibase/types"
12
9
 
13
- import { Header } from "@budibase/shared-core"
14
10
  import { BaseAPIClient } from "./types"
15
- import { readUIMessageStream, UIMessage, UIMessageChunk } from "ai"
16
- import { createSseToJsonTransformStream } from "../utils/utils"
17
11
 
18
12
  export interface AgentEndpoints {
19
- agentChatStream: (
20
- chat: AgentChat,
21
- workspaceId: string
22
- ) => Promise<AsyncIterable<UIMessage>>
23
-
24
- removeChat: (chatId: string) => Promise<void>
25
- fetchChats: (agentId: string) => Promise<FetchAgentHistoryResponse>
26
-
27
- fetchTools: () => Promise<ToolMetadata[]>
13
+ fetchTools: (aiconfigId?: string) => Promise<ToolMetadata[]>
28
14
  fetchAgents: () => Promise<FetchAgentsResponse>
29
15
  createAgent: (agent: CreateAgentRequest) => Promise<CreateAgentResponse>
30
16
  updateAgent: (agent: UpdateAgentRequest) => Promise<UpdateAgentResponse>
31
17
  deleteAgent: (agentId: string) => Promise<{ deleted: true }>
32
18
  }
33
19
 
34
- const throwOnErrorChunk = () =>
35
- new TransformStream<UIMessageChunk, UIMessageChunk>({
36
- transform(chunk, controller) {
37
- if (chunk.type === "error") {
38
- throw new Error(chunk.errorText || "Agent action failed")
39
- }
40
- controller.enqueue(chunk)
41
- },
42
- })
43
-
44
20
  export const buildAgentEndpoints = (API: BaseAPIClient): AgentEndpoints => ({
45
- agentChatStream: async (chat, workspaceId) => {
46
- const body: ChatAgentRequest = chat
47
-
48
- const response = await fetch("/api/agent/chat/stream", {
49
- method: "POST",
50
- headers: {
51
- "Content-Type": "application/json",
52
- Accept: "application/json",
53
- [Header.APP_ID]: workspaceId,
54
- },
55
- body: JSON.stringify(body),
56
- credentials: "same-origin",
57
- })
58
-
59
- if (!response.ok) {
60
- const errorBody = await response.json()
61
- throw new Error(
62
- errorBody.message || `HTTP error! status: ${response.status}`
63
- )
64
- }
65
-
66
- if (!response.body) {
67
- throw new Error("Failed to get response body")
68
- }
69
-
70
- const chunkStream = response.body
71
- .pipeThrough(new TextDecoderStream())
72
- .pipeThrough(createSseToJsonTransformStream<UIMessageChunk>())
73
- .pipeThrough(throwOnErrorChunk())
74
-
75
- return readUIMessageStream({
76
- stream: chunkStream,
77
- terminateOnError: true,
78
- })
79
- },
80
-
81
- removeChat: async (chatId: string) => {
82
- return await API.delete({
83
- url: `/api/agent/chats/${chatId}`,
84
- })
85
- },
86
-
87
- fetchChats: async (agentId: string) => {
88
- return await API.get({
89
- url: `/api/agent/${agentId}/chats`,
90
- })
91
- },
92
-
93
- fetchTools: async () => {
94
- return await API.get({
95
- url: `/api/agent/tools`,
96
- })
97
- },
98
-
99
21
  fetchAgents: async () => {
100
22
  return await API.get({
101
23
  url: "/api/agent",
@@ -121,4 +43,13 @@ export const buildAgentEndpoints = (API: BaseAPIClient): AgentEndpoints => ({
121
43
  url: `/api/agent/${agentId}`,
122
44
  })
123
45
  },
46
+
47
+ fetchTools: async (aiconfigId?: string) => {
48
+ const query = aiconfigId
49
+ ? `?aiconfigId=${encodeURIComponent(aiconfigId)}`
50
+ : ""
51
+ return await API.get({
52
+ url: `/api/agent/tools${query}`,
53
+ })
54
+ },
124
55
  })
@@ -0,0 +1,190 @@
1
+ import {
2
+ ChatAgentRequest,
3
+ ChatConversation,
4
+ ChatConversationRequest,
5
+ CreateChatConversationRequest,
6
+ ChatApp,
7
+ FetchAgentHistoryResponse,
8
+ UpdateChatAppRequest,
9
+ } from "@budibase/types"
10
+ import { Header } from "@budibase/shared-core"
11
+ import { BaseAPIClient } from "./types"
12
+ import { readUIMessageStream, UIMessage, UIMessageChunk } from "ai"
13
+ import { createSseToJsonTransformStream } from "../utils/utils"
14
+
15
+ export interface ChatAppEndpoints {
16
+ streamChatConversation: (
17
+ chat: ChatConversationRequest,
18
+ workspaceId: string
19
+ ) => Promise<AsyncIterable<UIMessage>>
20
+ deleteChatConversation: (
21
+ chatConversationId: string,
22
+ chatAppId: string
23
+ ) => Promise<void>
24
+ fetchChatConversation: (
25
+ chatAppId: string,
26
+ chatConversationId: string
27
+ ) => Promise<ChatConversation>
28
+ fetchChatHistory: (chatAppId: string) => Promise<FetchAgentHistoryResponse>
29
+ fetchChatApp: (workspaceId?: string) => Promise<ChatApp | null>
30
+ setChatAppAgent: (chatAppId: string, agentId: string) => Promise<ChatApp>
31
+ createChatConversation: (
32
+ chat: CreateChatConversationRequest,
33
+ workspaceId?: string
34
+ ) => Promise<ChatConversation>
35
+ updateChatApp: (chatApp: UpdateChatAppRequest) => Promise<ChatApp>
36
+ }
37
+
38
+ const throwOnErrorChunk = () =>
39
+ new TransformStream<UIMessageChunk, UIMessageChunk>({
40
+ transform(chunk, controller) {
41
+ if (chunk.type === "error") {
42
+ throw new Error(chunk.errorText || "Agent action failed")
43
+ }
44
+ controller.enqueue(chunk)
45
+ },
46
+ })
47
+
48
+ export const buildChatAppEndpoints = (
49
+ API: BaseAPIClient
50
+ ): ChatAppEndpoints => ({
51
+ streamChatConversation: async (chat, workspaceId) => {
52
+ if (!chat.chatAppId) {
53
+ throw new Error("chatAppId is required to stream a chat conversation")
54
+ }
55
+
56
+ const body: ChatAgentRequest = chat
57
+ const conversationId = chat._id || "new"
58
+
59
+ const response = await fetch(
60
+ `/api/chatapps/${chat.chatAppId}/conversations/${conversationId}/stream`,
61
+ {
62
+ method: "POST",
63
+ headers: {
64
+ "Content-Type": "application/json",
65
+ Accept: "application/json",
66
+ [Header.APP_ID]: workspaceId,
67
+ },
68
+ body: JSON.stringify(body),
69
+ credentials: "same-origin",
70
+ }
71
+ )
72
+
73
+ if (!response.ok) {
74
+ const errorBody = await response.json()
75
+ throw new Error(
76
+ errorBody.message || `HTTP error! status: ${response.status}`
77
+ )
78
+ }
79
+
80
+ if (!response.body) {
81
+ throw new Error("Failed to get response body")
82
+ }
83
+
84
+ const chunkStream = response.body
85
+ .pipeThrough(new TextDecoderStream())
86
+ .pipeThrough(createSseToJsonTransformStream<UIMessageChunk>())
87
+ .pipeThrough(throwOnErrorChunk())
88
+
89
+ return readUIMessageStream({
90
+ stream: chunkStream,
91
+ terminateOnError: true,
92
+ })
93
+ },
94
+
95
+ deleteChatConversation: async (
96
+ chatConversationId: string,
97
+ chatAppId: string
98
+ ) => {
99
+ return await API.delete({
100
+ url: `/api/chatapps/${chatAppId}/conversations/${chatConversationId}`,
101
+ })
102
+ },
103
+
104
+ fetchChatConversation: async (
105
+ chatAppId: string,
106
+ chatConversationId: string
107
+ ) => {
108
+ return await API.get({
109
+ url: `/api/chatapps/${chatAppId}/conversations/${chatConversationId}`,
110
+ })
111
+ },
112
+
113
+ fetchChatHistory: async (chatAppId: string) => {
114
+ return await API.get({
115
+ url: `/api/chatapps/${chatAppId}/conversations`,
116
+ })
117
+ },
118
+
119
+ fetchChatApp: async (workspaceId?: string) => {
120
+ const url = "/api/chatapps"
121
+ const headers = workspaceId
122
+ ? {
123
+ [Header.APP_ID]: workspaceId,
124
+ }
125
+ : undefined
126
+ return await API.get({
127
+ url,
128
+ ...(headers && { headers }),
129
+ })
130
+ },
131
+
132
+ setChatAppAgent: async (chatAppId: string, agentId: string) => {
133
+ if (!chatAppId) {
134
+ throw new Error("chatAppId is required to set chat app agent")
135
+ }
136
+ if (!agentId) {
137
+ throw new Error("agentId is required to set chat app agent")
138
+ }
139
+ return await API.post({
140
+ url: `/api/chatapps/${chatAppId}/agent`,
141
+ body: { agentId } as any,
142
+ })
143
+ },
144
+
145
+ createChatConversation: async (
146
+ chat: CreateChatConversationRequest,
147
+ workspaceId?: string
148
+ ) => {
149
+ const resolvedWorkspaceId = workspaceId || API.getAppID()
150
+ const { chatAppId } = chat
151
+ if (!chatAppId) {
152
+ throw new Error("chatAppId is required to create a chat conversation")
153
+ }
154
+
155
+ const headers: Record<string, string> = {
156
+ "Content-Type": "application/json",
157
+ Accept: "application/json",
158
+ }
159
+
160
+ if (resolvedWorkspaceId) {
161
+ headers[Header.APP_ID] = resolvedWorkspaceId
162
+ }
163
+
164
+ const response = await fetch(`/api/chatapps/${chatAppId}/conversations`, {
165
+ method: "POST",
166
+ headers,
167
+ credentials: "same-origin",
168
+ body: JSON.stringify(chat),
169
+ })
170
+
171
+ if (!response.ok) {
172
+ const errorBody = await response.json().catch(() => null)
173
+ throw new Error(
174
+ errorBody?.message || `HTTP error! status: ${response.status}`
175
+ )
176
+ }
177
+
178
+ return (await response.json()) as ChatConversation
179
+ },
180
+
181
+ updateChatApp: async (chatApp: UpdateChatAppRequest) => {
182
+ if (!chatApp._id) {
183
+ throw new Error("chatAppId is required to update a chat app")
184
+ }
185
+ return await API.put({
186
+ url: `/api/chatapps/${chatApp._id}`,
187
+ body: chatApp as any,
188
+ })
189
+ },
190
+ })
package/src/api/index.ts CHANGED
@@ -47,6 +47,7 @@ import { buildMigrationEndpoints } from "./migrations"
47
47
  import { buildRowActionEndpoints } from "./rowActions"
48
48
  import { buildOAuth2Endpoints } from "./oauth2"
49
49
  import { buildAgentEndpoints } from "./agents"
50
+ import { buildChatAppEndpoints } from "./chatApps"
50
51
  import { buildFeatureFlagEndpoints } from "./features"
51
52
  import { buildNavigationEndpoints } from "./navigation"
52
53
  import { buildWorkspaceAppEndpoints } from "./workspaceApps"
@@ -313,6 +314,7 @@ export const createAPIClient = (config: APIClientConfig = {}): APIClient => {
313
314
  ...buildLogsEndpoints(API),
314
315
  ...buildMigrationEndpoints(API),
315
316
  ...buildAgentEndpoints(API),
317
+ ...buildChatAppEndpoints(API),
316
318
  ...buildFeatureFlagEndpoints(API),
317
319
  deployment: buildDeploymentEndpoints(API),
318
320
  viewV2: buildViewV2Endpoints(API),
@@ -5,6 +5,9 @@ import {
5
5
  FetchPluginResponse,
6
6
  UploadPluginRequest,
7
7
  UploadPluginResponse,
8
+ PluginUpdateCheckResponse,
9
+ PluginUpdateApplyRequest,
10
+ PluginUpdateApplyResponse,
8
11
  } from "@budibase/types"
9
12
  import { BaseAPIClient } from "./types"
10
13
 
@@ -13,6 +16,11 @@ export interface PluginEndpoins {
13
16
  createPlugin: (data: CreatePluginRequest) => Promise<CreatePluginResponse>
14
17
  getPlugins: () => Promise<FetchPluginResponse>
15
18
  deletePlugin: (pluginId: string) => Promise<DeletePluginResponse>
19
+ checkPluginUpdates: (token?: string) => Promise<PluginUpdateCheckResponse>
20
+ applyPluginUpdates: (
21
+ data: PluginUpdateApplyRequest,
22
+ token?: string
23
+ ) => Promise<PluginUpdateApplyResponse>
16
24
  }
17
25
 
18
26
  export const buildPluginEndpoints = (API: BaseAPIClient): PluginEndpoins => ({
@@ -58,4 +66,20 @@ export const buildPluginEndpoints = (API: BaseAPIClient): PluginEndpoins => ({
58
66
  url: `/api/plugin/${encodeURIComponent(pluginId)}`,
59
67
  })
60
68
  },
69
+
70
+ checkPluginUpdates: async token => {
71
+ const url = token
72
+ ? `/api/plugin/updates?token=${encodeURIComponent(token)}`
73
+ : "/api/plugin/updates"
74
+ return await API.get({
75
+ url,
76
+ })
77
+ },
78
+
79
+ applyPluginUpdates: async (data, token) => {
80
+ return await API.post({
81
+ url: "/api/plugin/updates",
82
+ body: { ...data, token },
83
+ })
84
+ },
61
85
  })
package/src/api/types.ts CHANGED
@@ -35,6 +35,7 @@ import { UserEndpoints } from "./user"
35
35
  import { ViewEndpoints } from "./views"
36
36
  import { ViewV2Endpoints } from "./viewsV2"
37
37
  import { AgentEndpoints } from "./agents"
38
+ import { ChatAppEndpoints } from "./chatApps"
38
39
  import { NavigationEndpoints } from "./navigation"
39
40
  import { WorkspaceAppEndpoints } from "./workspaceApps"
40
41
  import { ResourceEndpoints } from "./resource"
@@ -113,6 +114,7 @@ export type APIError = {
113
114
  export type APIClient = BaseAPIClient &
114
115
  AIEndpoints &
115
116
  AgentEndpoints &
117
+ ChatAppEndpoints &
116
118
  AnalyticsEndpoints &
117
119
  AppEndpoints &
118
120
  AttachmentEndpoints &
@@ -1,6 +1,10 @@
1
1
  <script lang="ts">
2
2
  import { MarkdownViewer, notifications } from "@budibase/bbui"
3
- import type { AgentChat } from "@budibase/types"
3
+ import type {
4
+ ChatConversation,
5
+ ChatConversationRequest,
6
+ } from "@budibase/types"
7
+ import { Header } from "@budibase/shared-core"
4
8
  import BBAI from "../../icons/BBAI.svelte"
5
9
  import { tick } from "svelte"
6
10
  import { onDestroy } from "svelte"
@@ -10,14 +14,21 @@
10
14
  import type { UIMessage } from "ai"
11
15
  import { v4 as uuidv4 } from "uuid"
12
16
 
13
- export let API = createAPIClient()
14
-
15
17
  export let workspaceId: string
16
- export let chat: AgentChat
18
+ export let API = createAPIClient({
19
+ attachHeaders: headers => {
20
+ if (workspaceId) {
21
+ headers[Header.APP_ID] = workspaceId
22
+ }
23
+ },
24
+ })
25
+ type ChatConversationLike = ChatConversation | ChatConversationRequest
26
+
27
+ export let chat: ChatConversationLike
17
28
  export let loading: boolean = false
18
29
 
19
30
  const dispatch = createEventDispatcher<{
20
- chatSaved: { chatId?: string; chat: AgentChat }
31
+ chatSaved: { chatId?: string; chat: ChatConversationLike }
21
32
  }>()
22
33
 
23
34
  let inputValue = ""
@@ -25,12 +36,32 @@
25
36
  let observer: MutationObserver
26
37
  let textareaElement: HTMLTextAreaElement
27
38
  let lastFocusedChatId: string | undefined
28
- let lastFocusedNewChat: AgentChat | undefined
39
+ let lastFocusedNewChat: ChatConversationLike | undefined
29
40
 
30
41
  $: if (chat?.messages?.length) {
31
42
  scrollToBottom()
32
43
  }
33
44
 
45
+ const ensureChatApp = async (): Promise<string | undefined> => {
46
+ if (chat?.chatAppId) {
47
+ return chat.chatAppId
48
+ }
49
+ try {
50
+ const chatApp = await API.fetchChatApp(workspaceId)
51
+ if (chatApp?._id) {
52
+ const baseChat = chat || { title: "", messages: [], chatAppId: "" }
53
+ chat = {
54
+ ...baseChat,
55
+ chatAppId: chatApp._id,
56
+ }
57
+ return chatApp._id
58
+ }
59
+ } catch (err) {
60
+ console.error(err)
61
+ }
62
+ return undefined
63
+ }
64
+
34
65
  async function scrollToBottom() {
35
66
  await tick()
36
67
  if (chatAreaElement) {
@@ -46,8 +77,41 @@
46
77
  }
47
78
 
48
79
  async function prompt() {
80
+ const resolvedChatAppId = await ensureChatApp()
81
+
49
82
  if (!chat) {
50
- chat = { title: "", messages: [] }
83
+ chat = { title: "", messages: [], chatAppId: "" }
84
+ }
85
+
86
+ const chatAppId = chat.chatAppId || resolvedChatAppId
87
+
88
+ if (!chatAppId) {
89
+ notifications.error("Chat app could not be created")
90
+ return
91
+ }
92
+
93
+ if (!chat._id && (!chat.messages || chat.messages.length === 0)) {
94
+ try {
95
+ const newChat = await API.createChatConversation(
96
+ {
97
+ chatAppId,
98
+ title: chat.title,
99
+ },
100
+ workspaceId
101
+ )
102
+
103
+ chat = {
104
+ ...chat,
105
+ ...newChat,
106
+ chatAppId,
107
+ }
108
+ } catch (err: any) {
109
+ console.error(err)
110
+ notifications.error(
111
+ err?.message || "Could not start a new chat conversation"
112
+ )
113
+ return
114
+ }
51
115
  }
52
116
 
53
117
  const userMessage: UIMessage = {
@@ -56,8 +120,9 @@
56
120
  parts: [{ type: "text", text: inputValue }],
57
121
  }
58
122
 
59
- const updatedChat: AgentChat = {
123
+ const updatedChat: ChatConversationLike = {
60
124
  ...chat,
125
+ chatAppId: chat.chatAppId,
61
126
  messages: [...chat.messages, userMessage],
62
127
  }
63
128
 
@@ -68,16 +133,41 @@
68
133
  loading = true
69
134
 
70
135
  try {
71
- const messageStream = await API.agentChatStream(updatedChat, workspaceId)
136
+ const messageStream = await API.streamChatConversation(
137
+ updatedChat,
138
+ workspaceId
139
+ )
140
+
141
+ let streamedMessages: UIMessage[] = [...updatedChat.messages]
72
142
 
73
143
  for await (const message of messageStream) {
144
+ streamedMessages = [...streamedMessages, message]
74
145
  chat = {
75
146
  ...updatedChat,
76
- messages: [...updatedChat.messages, message],
147
+ messages: streamedMessages,
77
148
  }
78
149
  scrollToBottom()
79
150
  }
80
151
 
152
+ // When a chat is created for the first time the server generates the ID.
153
+ // If we don't have it locally yet, retrieve the saved conversation so
154
+ // subsequent prompts append to the same document instead of creating a new one.
155
+ if (!chat._id && chat.chatAppId) {
156
+ try {
157
+ const history = await API.fetchChatHistory(chat.chatAppId)
158
+ const lastMessageId = chat.messages[chat.messages.length - 1]?.id
159
+ const savedConversation =
160
+ history?.find(convo =>
161
+ convo.messages.some(message => message.id === lastMessageId)
162
+ ) || history?.[0]
163
+ if (savedConversation) {
164
+ chat = savedConversation
165
+ }
166
+ } catch (historyError) {
167
+ console.error(historyError)
168
+ }
169
+ }
170
+
81
171
  loading = false
82
172
  dispatch("chatSaved", { chatId: chat._id, chat })
83
173
  } catch (err: any) {
@@ -93,6 +183,8 @@
93
183
  }
94
184
 
95
185
  onMount(async () => {
186
+ await ensureChatApp()
187
+
96
188
  // Ensure we always autoscroll to reveal new messages
97
189
  observer = new MutationObserver(async () => {
98
190
  await tick()
@@ -211,7 +303,7 @@
211
303
  on:keydown={handleKeyDown}
212
304
  placeholder="Ask anything"
213
305
  disabled={loading}
214
- />
306
+ ></textarea>
215
307
  </div>
216
308
  </div>
217
309
 
@@ -6,17 +6,17 @@
6
6
  </script>
7
7
 
8
8
  <div class:sideNav id="clientAppSkeletonLoader" class="skeleton">
9
- <div class="animation" class:noAnimation />
9
+ <div class="animation" class:noAnimation></div>
10
10
 
11
11
  {#if !hideDevTools}
12
- <div class="devTools" />
12
+ <div class="devTools"></div>
13
13
  {/if}
14
14
  <div class="main">
15
- <div class="nav" />
15
+ <div class="nav"></div>
16
16
  <div class="body">
17
- <div class="bodyVerticalPadding" />
17
+ <div class="bodyVerticalPadding"></div>
18
18
  <div class="bodyHorizontal">
19
- <div class="bodyHorizontalPadding" />
19
+ <div class="bodyHorizontalPadding"></div>
20
20
  <svg
21
21
  class="svg"
22
22
  xmlns="http://www.w3.org/2000/svg"
@@ -42,13 +42,13 @@
42
42
  mask="url(#mask)"
43
43
  />
44
44
  </svg>
45
- <div class="bodyHorizontalPadding" />
45
+ <div class="bodyHorizontalPadding"></div>
46
46
  </div>
47
- <div class="bodyVerticalPadding" />
47
+ <div class="bodyVerticalPadding"></div>
48
48
  </div>
49
49
  </div>
50
50
  {#if !hideFooter}
51
- <div class="footer" />
51
+ <div class="footer"></div>
52
52
  {/if}
53
53
  </div>
54
54
 
@@ -57,7 +57,7 @@
57
57
  on:wheel|stopPropagation
58
58
  spellcheck="false"
59
59
  use:clickOutside={close}
60
- />
60
+ ></textarea>
61
61
  </GridPopover>
62
62
  {/if}
63
63
 
@@ -12,7 +12,12 @@
12
12
  export let schema
13
13
  export let maximum
14
14
 
15
- const { API, notifications, props, datasource } = getContext("grid")
15
+ const {
16
+ API,
17
+ notifications,
18
+ props: gridProps,
19
+ datasource,
20
+ } = getContext("grid")
16
21
  const imageExtensions = ["png", "tiff", "gif", "raw", "jpg", "jpeg"]
17
22
 
18
23
  let isOpen = false
@@ -89,7 +94,7 @@
89
94
  {#each value || [] as attachment}
90
95
  {#if isImage(attachment.extension)}
91
96
  <img
92
- class:light={!$props?.darkMode &&
97
+ class:light={!$gridProps?.darkMode &&
93
98
  schema.type === FieldType.SIGNATURE_SINGLE}
94
99
  src={attachment.url}
95
100
  alt={attachment.extension}
@@ -111,7 +116,7 @@
111
116
  on:change={e => onChange(e.detail)}
112
117
  maximum={maximum || schema.constraints?.length?.maximum}
113
118
  {processFiles}
114
- handleFileTooLarge={$props.isCloud ? handleFileTooLarge : null}
119
+ handleFileTooLarge={$gridProps.isCloud ? handleFileTooLarge : null}
115
120
  />
116
121
  </div>
117
122
  </GridPopover>
@@ -74,7 +74,7 @@
74
74
  on:wheel|stopPropagation
75
75
  spellcheck="false"
76
76
  use:clickOutside={close}
77
- />
77
+ ></textarea>
78
78
  </GridPopover>
79
79
  {/if}
80
80
 
@@ -11,7 +11,12 @@
11
11
  export let readonly = false
12
12
  export let api
13
13
 
14
- const { API, notifications, props, datasource } = getContext("grid")
14
+ const {
15
+ API,
16
+ notifications,
17
+ props: gridProps,
18
+ datasource,
19
+ } = getContext("grid")
15
20
 
16
21
  let isOpen = false
17
22
  let modal
@@ -73,7 +78,7 @@
73
78
  <!-- svelte-ignore a11y-click-events-have-key-events -->
74
79
  <div
75
80
  class="signature-cell"
76
- class:light={!$props?.darkMode}
81
+ class:light={!$gridProps?.darkMode}
77
82
  class:editable
78
83
  bind:this={anchor}
79
84
  on:click={editable ? open : null}
@@ -88,7 +93,7 @@
88
93
  onConfirm={saveSignature}
89
94
  title={schema?.name}
90
95
  {value}
91
- darkMode={$props.darkMode}
96
+ darkMode={$gridProps.darkMode}
92
97
  bind:this={modal}
93
98
  />
94
99
 
@@ -98,7 +103,7 @@
98
103
  {#if value?.key}
99
104
  <div class="signature-wrap">
100
105
  <CoreSignature
101
- darkMode={$props.darkMode}
106
+ darkMode={$gridProps.darkMode}
102
107
  editable={false}
103
108
  {value}
104
109
  on:change={saveSignature}
@@ -8,7 +8,7 @@
8
8
  const {
9
9
  renderedRows,
10
10
  hoveredRowId,
11
- props,
11
+ props: gridProps,
12
12
  width,
13
13
  rows,
14
14
  focusedRow,
@@ -26,7 +26,7 @@
26
26
 
27
27
  let container
28
28
 
29
- $: buttons = getButtons($props)
29
+ $: buttons = getButtons($gridProps)
30
30
  $: columnsWidth = $scrollableColumns.reduce(
31
31
  (total, col) => (total += col.width),
32
32
  0
@@ -116,11 +116,11 @@
116
116
  class="buttons"
117
117
  class:offset={$showVScrollbar && $showHScrollbar}
118
118
  >
119
- {#if $props.buttonsCollapsed}
119
+ {#if $gridProps.buttonsCollapsed}
120
120
  {#if rowButtons.length > 0}
121
121
  <CollapsedButtonGroup
122
122
  buttons={makeCollapsedButtons(rowButtons, row)}
123
- text={$props.buttonsCollapsedText || "Action"}
123
+ text={$gridProps.buttonsCollapsedText || "Action"}
124
124
  align="right"
125
125
  offset={5}
126
126
  size="S"
@@ -128,7 +128,7 @@
128
128
  on:mouseenter={() => ($hoveredRowId = row._id)}
129
129
  />
130
130
  {:else}
131
- <div class="button-placeholder-collapsed" />
131
+ <div class="button-placeholder-collapsed"></div>
132
132
  {/if}
133
133
  {:else}
134
134
  {#each rowButtons as button}
@@ -143,13 +143,13 @@
143
143
  on:click={() => handleClick(button, row)}
144
144
  >
145
145
  {#if button.icon}
146
- <i class="{button.icon} S" />
146
+ <i class="{button.icon} S"></i>
147
147
  {/if}
148
148
  {button.text || "Button"}
149
149
  </Button>
150
150
  {/each}
151
151
  {#if rowButtons.length === 0}
152
- <div class="button-placeholder" />
152
+ <div class="button-placeholder"></div>
153
153
  {/if}
154
154
  {/if}
155
155
  </div>
@@ -14,7 +14,7 @@
14
14
  dispatch,
15
15
  isDragging,
16
16
  config,
17
- props,
17
+ props: gridProps,
18
18
  } = getContext("grid")
19
19
 
20
20
  let body
@@ -66,7 +66,7 @@
66
66
  </div>
67
67
  {/if}
68
68
  </GridScrollWrapper>
69
- {#if $props.buttons?.length}
69
+ {#if $gridProps.buttons?.length}
70
70
  <ButtonColumn />
71
71
  {/if}
72
72
  </div>
@@ -23,7 +23,7 @@
23
23
  isSelectingCells,
24
24
  selectedCellMap,
25
25
  selectedCellCount,
26
- props,
26
+ props: gridProps,
27
27
  buttonColumnWidth,
28
28
  } = getContext("grid")
29
29
 
@@ -32,7 +32,7 @@
32
32
  $hoveredRowId === row._id && (!$selectedCellCount || !$isSelectingCells)
33
33
  $: rowFocused = $focusedRow?._id === row._id
34
34
  $: reorderSource = $reorder.sourceColumn
35
- $: hasButtons = $props?.buttons?.length > 0
35
+ $: hasButtons = $gridProps?.buttons?.length > 0
36
36
  $: needsButtonSpacer = hasButtons && $buttonColumnWidth > 0
37
37
  </script>
38
38
 
@@ -188,14 +188,17 @@
188
188
  class:floating={offset > 0}
189
189
  style="--offset:{offset}px; --sticky-width:{width}px;"
190
190
  >
191
- <div class="underlay sticky" transition:fade|local={{ duration: 130 }} />
192
- <div class="underlay" transition:fade|local={{ duration: 130 }} />
191
+ <div
192
+ class="underlay sticky"
193
+ transition:fade|local={{ duration: 130 }}
194
+ ></div>
195
+ <div class="underlay" transition:fade|local={{ duration: 130 }}></div>
193
196
  <div class="sticky-column" transition:fade|local={{ duration: 130 }}>
194
197
  <div class="row">
195
198
  <GutterCell expandable on:expand={addViaModal} rowHovered>
196
199
  <Icon name="plus" color="var(--spectrum-global-color-gray-500)" />
197
200
  {#if isAdding}
198
- <div in:fade={{ duration: 130 }} class="loading-overlay" />
201
+ <div in:fade={{ duration: 130 }} class="loading-overlay"></div>
199
202
  {/if}
200
203
  </GutterCell>
201
204
  {#if $displayColumn}
@@ -214,7 +217,7 @@
214
217
  <div class="readonly-overlay">Can't edit auto column</div>
215
218
  {/if}
216
219
  {#if isAdding}
217
- <div in:fade={{ duration: 130 }} class="loading-overlay" />
220
+ <div in:fade={{ duration: 130 }} class="loading-overlay"></div>
218
221
  {/if}
219
222
  </DataCell>
220
223
  {/if}
@@ -240,7 +243,7 @@
240
243
  <div class="readonly-overlay">Can't edit auto column</div>
241
244
  {/if}
242
245
  {#if isAdding}
243
- <div in:fade={{ duration: 130 }} class="loading-overlay" />
246
+ <div in:fade={{ duration: 130 }} class="loading-overlay"></div>
244
247
  {/if}
245
248
  </DataCell>
246
249
  {/each}
@@ -65,7 +65,7 @@
65
65
  }
66
66
  </script>
67
67
 
68
- <div bind:this={anchor} {style} class="menu-anchor" />
68
+ <div bind:this={anchor} {style} class="menu-anchor"></div>
69
69
 
70
70
  {#if $menu.visible}
71
71
  {#key style}
@@ -1,4 +1,4 @@
1
- <div class="grid-popover-container" />
1
+ <div class="grid-popover-container"></div>
2
2
 
3
3
  <style>
4
4
  .grid-popover-container {
@@ -35,7 +35,7 @@
35
35
  {#if visible}
36
36
  <div class="reorder-wrapper">
37
37
  <GridScrollWrapper scrollVertically>
38
- <div class="reorder-overlay" {style} />
38
+ <div class="reorder-overlay" {style}></div>
39
39
  </GridScrollWrapper>
40
40
  </div>
41
41
  {/if}
@@ -24,7 +24,7 @@
24
24
  on:dblclick={() => resize.actions.resetSize(column)}
25
25
  style={getStyle(column, $scrollLeft)}
26
26
  >
27
- <div class="resize-indicator" />
27
+ <div class="resize-indicator"></div>
28
28
  </div>
29
29
  {/each}
30
30
  {/if}
@@ -123,7 +123,7 @@
123
123
  on:mousedown={startVDragging}
124
124
  on:touchstart={startVDragging}
125
125
  class:dragging={isDraggingV}
126
- />
126
+ ></div>
127
127
  {/if}
128
128
  {#if $showHScrollbar}
129
129
  <!-- svelte-ignore a11y-no-static-element-interactions -->
@@ -133,7 +133,7 @@
133
133
  on:mousedown={startHDragging}
134
134
  on:touchstart={startHDragging}
135
135
  class:dragging={isDraggingH}
136
- />
136
+ ></div>
137
137
  {/if}
138
138
 
139
139
  <style>
@@ -1,5 +1,5 @@
1
1
  import { writable, derived, get, Writable, Readable } from "svelte/store"
2
- import { DataFetch, fetchData } from "../../../fetch"
2
+ import { DataFetch, DataFetchDefinition, fetchData } from "../../../fetch"
3
3
  import { NewRowID, RowPageSize } from "../lib/constants"
4
4
  import {
5
5
  generateRowID,
@@ -252,7 +252,18 @@ export const createActions = (context: StoreContext): RowActionStore => {
252
252
  })
253
253
 
254
254
  // Subscribe to changes of this fetch model
255
- unsubscribe = newFetch.subscribe(async $fetch => {
255
+ type GridFetchSnapshot = {
256
+ rows: Row[]
257
+ loading: boolean
258
+ loaded: boolean
259
+ resetKey: string
260
+ hasNextPage: boolean
261
+ error: { message: string; status: number; url: string } | null
262
+ definition?: DataFetchDefinition | null
263
+ }
264
+
265
+ const fetchStore = newFetch as unknown as Readable<GridFetchSnapshot>
266
+ unsubscribe = fetchStore.subscribe(async $fetch => {
256
267
  if ($fetch.error) {
257
268
  // Present a helpful error to the user
258
269
  let message = "An unknown error occurred"
@@ -481,7 +492,7 @@ export const createActions = (context: StoreContext): RowActionStore => {
481
492
  })
482
493
 
483
494
  // Create rows
484
- let saved = []
495
+ let saved: UIRow[] = []
485
496
  let failed = 0
486
497
  for (let i = 0; i < count; i++) {
487
498
  try {
@@ -700,7 +711,7 @@ export const createActions = (context: StoreContext): RowActionStore => {
700
711
 
701
712
  // Update rows
702
713
  const $columnLookupMap = get(columnLookupMap)
703
- let updated = []
714
+ let updated: UIRow[] = []
704
715
  let failed = 0
705
716
  for (let i = 0; i < count; i++) {
706
717
  const rowId = rowIds[i]
@@ -779,7 +790,7 @@ export const createActions = (context: StoreContext): RowActionStore => {
779
790
  if (resetRows) {
780
791
  rowCacheMap = {}
781
792
  }
782
- let rowsToAppend = []
793
+ let rowsToAppend: Row[] = []
783
794
  let newRow
784
795
  const $hasBudibaseIdentifiers = get(hasBudibaseIdentifiers)
785
796
  for (let i = 0; i < newRows.length; i++) {
@@ -11,9 +11,13 @@ export default class JSONArrayFetch extends FieldFetch<JSONArrayFieldDatasource>
11
11
  try {
12
12
  const { fieldName } = datasource
13
13
  const table = await this.API.fetchTableDefinition(datasource.tableId)
14
+
15
+ // For Postgres JSON columns, the schema may be directly available on the field
16
+ // Otherwise, fall back to fetching the schema
14
17
  const schema =
15
- table.schema[fieldName].schema ??
18
+ table.schema?.[fieldName]?.schema ??
16
19
  getJSONArrayDatasourceSchema(table.schema, datasource)
20
+
17
21
  return { schema }
18
22
  } catch (error) {
19
23
  return null
package/svelte.config.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"
2
2
 
3
3
  const config = {
4
- preprocess: vitePreprocess(),
4
+ preprocess: vitePreprocess({ script: true }),
5
5
  }
6
6
 
7
7
  export default config
File without changes