@budibase/frontend-core 3.44.1 → 3.46.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": "v3.44.1",
3
+ "version": "v3.46.0",
4
4
  "description": "Budibase frontend core libraries used in builder and client",
5
5
  "author": "Budibase",
6
6
  "license": "MPL-2.0",
@@ -17,11 +17,12 @@
17
17
  "@budibase/types": "*",
18
18
  "ai": "^7.0.77",
19
19
  "dayjs": "^1.10.8",
20
+ "highlight.js": "^11.11.1",
20
21
  "lodash": "4.18.1",
21
22
  "socket.io-client": "^4.7.5"
22
23
  },
23
24
  "devDependencies": {
24
- "vitest": "^4.1.0"
25
+ "vitest": "^5.0.0"
25
26
  },
26
- "gitHead": "1709761edbab48686390d5d2336283e31b0d1dce"
27
+ "gitHead": "1444a702ce29cf158b10c31d3f205fc84c463169"
27
28
  }
@@ -26,8 +26,8 @@ export interface PagedChannels<T> {
26
26
 
27
27
  export interface ChatLinksEndpoints {
28
28
  fetchChatIdentityLinks: (
29
- provider?: ChatIdentityLinkProvider,
30
- agentId?: string
29
+ provider: ChatIdentityLinkProvider,
30
+ agentId: string
31
31
  ) => Promise<ChatIdentityLink[]>
32
32
  fetchSlackChannels: (
33
33
  agentId: string,
@@ -43,12 +43,8 @@ export const buildChatLinksEndpoints = (
43
43
  API: BaseAPIClient
44
44
  ): ChatLinksEndpoints => ({
45
45
  fetchChatIdentityLinks: async (provider, agentId) => {
46
- const params = new URLSearchParams({
47
- ...(provider ? { provider } : {}),
48
- ...(agentId ? { agentId } : {}),
49
- }).toString()
50
- const query = params ? `?${params}` : ""
51
- return await API.get({ url: `/api/chat-links${query}` })
46
+ const query = new URLSearchParams({ provider, agentId }).toString()
47
+ return await API.get({ url: `/api/chat-links?${query}` })
52
48
  },
53
49
  fetchSlackChannels: async (agentId, cursor) => {
54
50
  const query = new URLSearchParams({
@@ -0,0 +1,251 @@
1
+ <script lang="ts">
2
+ import { tick } from "svelte"
3
+ import {
4
+ Icon,
5
+ Popover,
6
+ PopoverAlignment,
7
+ type PopoverAPI,
8
+ } from "@budibase/bbui"
9
+
10
+ interface Props {
11
+ value?: string[]
12
+ disabled?: boolean
13
+ placeholder?: string
14
+ onchange?: (value: string[]) => void
15
+ }
16
+
17
+ let {
18
+ value = [],
19
+ disabled = false,
20
+ placeholder = "Add values",
21
+ onchange,
22
+ }: Props = $props()
23
+
24
+ let popover = $state<PopoverAPI>()
25
+ let anchor = $state<HTMLElement>()
26
+ let addInput = $state<HTMLInputElement>()
27
+ let content = $state<HTMLElement>()
28
+ let draft = $state("")
29
+ let open = $state(false)
30
+
31
+ const show = () => {
32
+ if (!disabled) {
33
+ popover?.show()
34
+ }
35
+ }
36
+
37
+ const onTriggerKeydown = (event: KeyboardEvent) => {
38
+ if (event.key === "Enter" || event.key === " ") {
39
+ event.preventDefault()
40
+ if (disabled) {
41
+ return
42
+ }
43
+ if (open) {
44
+ popover?.hide()
45
+ } else {
46
+ popover?.show()
47
+ }
48
+ }
49
+ }
50
+
51
+ const onPopoverKeydown = (event: KeyboardEvent) => {
52
+ if (event.key === "Escape") {
53
+ event.stopPropagation()
54
+ popover?.hide()
55
+ }
56
+ }
57
+
58
+ const onOpen = async () => {
59
+ open = true
60
+ await tick()
61
+ addInput?.focus()
62
+ }
63
+
64
+ const onClose = () => {
65
+ open = false
66
+ const active = document.activeElement
67
+ if (
68
+ !active ||
69
+ active === document.body ||
70
+ (content && content.contains(active))
71
+ ) {
72
+ anchor?.focus()
73
+ }
74
+ }
75
+
76
+ const displayValue = $derived(
77
+ value.length ? `(${value.length}) ${value.join(", ")}` : ""
78
+ )
79
+
80
+ const add = () => {
81
+ const entries = draft
82
+ .split(",")
83
+ .map(entry => entry.trim())
84
+ .filter(Boolean)
85
+ const next = entries.reduce(
86
+ (acc, entry) => (acc.includes(entry) ? acc : [...acc, entry]),
87
+ [...value]
88
+ )
89
+ draft = ""
90
+ if (next.length !== value.length) {
91
+ onchange?.(next)
92
+ }
93
+ }
94
+
95
+ const remove = (index: number) => {
96
+ onchange?.(value.filter((_, i) => i !== index))
97
+ }
98
+
99
+ const onKeydown = (event: KeyboardEvent) => {
100
+ if (event.key === "Enter") {
101
+ event.preventDefault()
102
+ add()
103
+ }
104
+ }
105
+ </script>
106
+
107
+ <div
108
+ bind:this={anchor}
109
+ class="spectrum-InputGroup array-input"
110
+ class:is-disabled={disabled}
111
+ role="button"
112
+ tabindex={disabled ? -1 : 0}
113
+ aria-haspopup="dialog"
114
+ aria-expanded={open}
115
+ aria-disabled={disabled}
116
+ onclick={show}
117
+ onkeydown={onTriggerKeydown}
118
+ >
119
+ <div class="spectrum-Textfield spectrum-InputGroup-textfield">
120
+ <input
121
+ {disabled}
122
+ readonly
123
+ type="text"
124
+ class="spectrum-Textfield-input spectrum-InputGroup-input"
125
+ {placeholder}
126
+ value={displayValue}
127
+ />
128
+ </div>
129
+ {#if !disabled}
130
+ <button
131
+ type="button"
132
+ class="spectrum-Picker spectrum-Picker--sizeM spectrum-InputGroup-button"
133
+ tabindex="-1"
134
+ >
135
+ <Icon name="list-bullets" />
136
+ </button>
137
+ {/if}
138
+ </div>
139
+
140
+ <Popover
141
+ bind:this={popover}
142
+ {anchor}
143
+ align={PopoverAlignment.Left}
144
+ widthMode="fixed-to-anchor"
145
+ resizable={false}
146
+ on:open={onOpen}
147
+ on:close={onClose}
148
+ >
149
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
150
+ <div bind:this={content} class="array-popover" onkeydown={onPopoverKeydown}>
151
+ {#each value as entry, index}
152
+ <div class="array-entry">
153
+ <span class="array-entry-label">{entry}</span>
154
+ <button
155
+ type="button"
156
+ class="array-entry-remove"
157
+ aria-label="Remove {entry}"
158
+ onclick={() => remove(index)}
159
+ >
160
+ <Icon name="x" size="XS" />
161
+ </button>
162
+ </div>
163
+ {/each}
164
+ <div class="array-add">
165
+ <input
166
+ bind:this={addInput}
167
+ type="text"
168
+ class="spectrum-Textfield-input array-add-input"
169
+ placeholder="Enter value"
170
+ bind:value={draft}
171
+ onkeydown={onKeydown}
172
+ onblur={add}
173
+ />
174
+ <button
175
+ type="button"
176
+ class="array-entry-remove"
177
+ aria-label="Add value"
178
+ onclick={add}
179
+ >
180
+ <Icon name="plus" size="XS" />
181
+ </button>
182
+ </div>
183
+ </div>
184
+ </Popover>
185
+
186
+ <style>
187
+ .array-input {
188
+ width: 100%;
189
+ min-width: 0;
190
+ cursor: pointer;
191
+ }
192
+ .array-input .spectrum-Textfield {
193
+ width: 100%;
194
+ min-width: 0;
195
+ }
196
+ .array-input .spectrum-Textfield-input {
197
+ pointer-events: none;
198
+ width: 100%;
199
+ }
200
+ .array-input.is-disabled {
201
+ pointer-events: none;
202
+ }
203
+ .array-input:focus-visible {
204
+ outline: none;
205
+ box-shadow: 0 0 0 1px var(--spectrum-global-color-blue-400);
206
+ border-radius: 4px;
207
+ }
208
+
209
+ .array-popover {
210
+ display: flex;
211
+ flex-direction: column;
212
+ gap: var(--spacing-xs);
213
+ padding: var(--spacing-s);
214
+ }
215
+ .array-entry,
216
+ .array-add {
217
+ display: flex;
218
+ align-items: center;
219
+ justify-content: space-between;
220
+ gap: var(--spacing-s);
221
+ min-height: 28px;
222
+ }
223
+ .array-entry-label {
224
+ flex: 1;
225
+ overflow: hidden;
226
+ text-overflow: ellipsis;
227
+ white-space: nowrap;
228
+ padding-left: var(--spacing-xs);
229
+ }
230
+ .array-entry-remove {
231
+ display: flex;
232
+ padding: 4px;
233
+ border: 0;
234
+ background: transparent;
235
+ color: inherit;
236
+ cursor: pointer;
237
+ }
238
+ .array-add-input {
239
+ flex: 1;
240
+ border: 1px solid var(--spectrum-global-color-gray-400);
241
+ border-radius: 4px;
242
+ background: var(--spectrum-global-color-gray-50);
243
+ color: inherit;
244
+ padding: 4px var(--spacing-s);
245
+ height: 28px;
246
+ }
247
+ .array-add-input:focus {
248
+ outline: none;
249
+ border-color: var(--spectrum-global-color-blue-400);
250
+ }
251
+ </style>
@@ -1,12 +1,16 @@
1
1
  <script lang="ts">
2
2
  import { ModalContent, Body, notifications } from "@budibase/bbui"
3
- import PasswordRepeatInput from "./PasswordRepeatInput.svelte"
4
3
  import type { APIClient } from "@budibase/frontend-core"
4
+ import {
5
+ DEFAULT_PASSWORD_POLICY,
6
+ resolveTranslationGroup,
7
+ } from "@budibase/shared-core"
8
+ import type { PasswordPolicy } from "@budibase/types"
5
9
  import { createEventDispatcher } from "svelte"
6
- import { resolveTranslationGroup } from "@budibase/shared-core"
10
+ import PasswordRepeatInput from "./PasswordRepeatInput.svelte"
7
11
 
8
12
  export let API: APIClient
9
- export let passwordMinLength: string | undefined = undefined
13
+ export let passwordPolicy: PasswordPolicy = DEFAULT_PASSWORD_POLICY
10
14
  export let notifySuccess = notifications.success
11
15
  export let notifyError = notifications.error
12
16
  // Get the default translations for the password modal and derive a type from it.
@@ -55,7 +59,7 @@
55
59
  <PasswordRepeatInput
56
60
  bind:password
57
61
  bind:error
58
- minLength={passwordMinLength}
62
+ policy={passwordPolicy}
59
63
  labels={resolvedLabels}
60
64
  />
61
65
  </ModalContent>
@@ -14,7 +14,7 @@
14
14
  EscalationContextDoc,
15
15
  EscalationRespondResult,
16
16
  } from "@budibase/types"
17
- import { EscalateToolResultStatus } from "@budibase/types"
17
+ import { ApprovalToolResultStatus } from "@budibase/types"
18
18
  import { Header } from "@budibase/shared-core"
19
19
  import { tick, untrack } from "svelte"
20
20
  import { createAPIClient } from "@budibase/frontend-core"
@@ -87,7 +87,7 @@
87
87
  // Only a genuinely-raised escalation gets the approval card
88
88
  const isRaisedEscalation = (output: unknown) =>
89
89
  (output as { status?: string } | undefined)?.status ===
90
- EscalateToolResultStatus.PENDING_APPROVAL
90
+ ApprovalToolResultStatus.PENDING_APPROVAL
91
91
 
92
92
  // The escalate part's input/output are loosely typed by the AI SDK, so the
93
93
  // casts live here rather than cluttering the template.
@@ -326,7 +326,7 @@
326
326
  | { status?: string; escalationId?: string }
327
327
  | undefined
328
328
  if (
329
- output?.status === EscalateToolResultStatus.PENDING_APPROVAL &&
329
+ output?.status === ApprovalToolResultStatus.PENDING_APPROVAL &&
330
330
  output.escalationId
331
331
  ) {
332
332
  ids.push(output.escalationId)
@@ -9,6 +9,7 @@
9
9
  Button,
10
10
  Select,
11
11
  } from "@budibase/bbui"
12
+ import ArrayValueInput from "./ArrayValueInput.svelte"
12
13
 
13
14
  import FilterUsers from "./FilterUsers.svelte"
14
15
  import { FieldType, ArrayOperator } from "@budibase/types"
@@ -22,10 +23,10 @@
22
23
  export let bindings = []
23
24
  export let allowBindings = false
24
25
  export let schemaFields
25
- export let panel
26
- export let drawerTitle
27
- export let toReadable
28
- export let toRuntime
26
+ export let panel = undefined
27
+ export let drawerTitle = undefined
28
+ export let toReadable = undefined
29
+ export let toRuntime = undefined
29
30
  export let evaluationContext = {}
30
31
  export let bindingValueType = Constants.FilterValueType.BINDING
31
32
  export let useConditionValueControls = false
@@ -82,9 +83,11 @@
82
83
  } else {
83
84
  return !value
84
85
  }
85
- return links.every(link =>
86
- getSchema(filter)?.constraints?.inclusion?.includes(link)
87
- )
86
+ const inclusion = getSchema(filter)?.constraints?.inclusion
87
+ if (!inclusion?.length) {
88
+ return true
89
+ }
90
+ return links.every(link => inclusion.includes(link))
88
91
  }
89
92
 
90
93
  const isValidBoolean = value => {
@@ -195,6 +198,12 @@
195
198
  value={readableValue}
196
199
  on:change={onChange}
197
200
  />
201
+ {:else if filter.type === FieldType.ARRAY && useConditionValueControls && !getFieldOptions(filter.field).length}
202
+ <ArrayValueInput
203
+ disabled={filter.noValue}
204
+ value={Array.isArray(readableValue) ? readableValue : []}
205
+ onchange={value => onChange({ detail: value })}
206
+ />
198
207
  {:else if filter.type === FieldType.ARRAY || (filter.type === FieldType.OPTIONS && (filter.operator === ArrayOperator.ONE_OF || filter.operator === ArrayOperator.NOT_ONE_OF))}
199
208
  <Multiselect
200
209
  disabled={filter.noValue}
@@ -295,15 +304,17 @@
295
304
  border-bottom-right-radius: 0px;
296
305
  }
297
306
 
307
+ .field :global(.spectrum-InputGroup.spectrum-Datepicker) {
308
+ min-width: unset;
309
+ }
310
+
298
311
  .field-wrap.bindings
299
312
  .field
300
313
  :global(.spectrum-InputGroup.spectrum-Datepicker) {
301
- min-width: unset;
302
314
  border-radius: 0px;
303
315
  }
304
316
 
305
- .field-wrap.bindings
306
- .field
317
+ .field
307
318
  :global(
308
319
  .spectrum-InputGroup.spectrum-Datepicker
309
320
  .spectrum-Textfield-input.spectrum-InputGroup-input
@@ -1,18 +1,32 @@
1
1
  <script lang="ts">
2
2
  import { FancyForm, FancyInput } from "@budibase/bbui"
3
+ import {
4
+ DEFAULT_PASSWORD_POLICY,
5
+ validatePasswordPolicy,
6
+ } from "@budibase/shared-core"
7
+ import type { PasswordPolicy } from "@budibase/types"
3
8
  import { createValidationStore, requiredValidator } from "../utils/validation"
4
9
 
5
10
  export let passwordForm: FancyForm | undefined = undefined
6
11
  export let password: string
7
12
  export let error: string
8
- export let minLength = "12"
13
+ export let policy: PasswordPolicy = DEFAULT_PASSWORD_POLICY
9
14
  export let labels: any = {}
10
15
 
11
16
  const validatePassword = (value: string | undefined) => {
12
- if (!value || value.length < parseInt(minLength)) {
17
+ const result = validatePasswordPolicy({ password: value, policy })
18
+ if (!result.valid) {
19
+ if (!value || value.length < policy.minLength) {
20
+ return (
21
+ labels?.minLengthText?.replace(
22
+ "{minLength}",
23
+ policy.minLength.toString()
24
+ ) || result.error
25
+ )
26
+ }
13
27
  return (
14
- labels?.minLengthText?.replace("{minLength}", minLength) ||
15
- `Please enter at least ${minLength} characters. We recommend using machine generated or random passwords.`
28
+ result.error ||
29
+ `Password must contain no more than ${policy.maxLength} characters.`
16
30
  )
17
31
  }
18
32
  return null
@@ -1,9 +1,8 @@
1
1
  <script lang="ts">
2
2
  import { Avatar, AbsTooltip, TooltipPosition } from "@budibase/bbui"
3
3
  import { helpers } from "@budibase/shared-core"
4
- import type { User } from "@budibase/types"
5
4
 
6
- export let user: User | undefined
5
+ export let user: helpers.UserDisplayInfo | undefined
7
6
  export let size: "XS" | "S" | "M" = "S"
8
7
  export let tooltipPosition: TooltipPosition = TooltipPosition.Top
9
8
  export let showTooltip: boolean = true
@@ -6,6 +6,7 @@ export { default as Updating } from "./Updating.svelte"
6
6
  export { Grid } from "./grid"
7
7
  export { default as ClientAppSkeleton } from "./ClientAppSkeleton.svelte"
8
8
  export { default as CoreFilterBuilder } from "./CoreFilterBuilder.svelte"
9
+ export { default as FilterField } from "./FilterField.svelte"
9
10
  export { default as FilterUsers } from "./FilterUsers.svelte"
10
11
  export { default as ChangePasswordModal } from "./ChangePasswordModal.svelte"
11
12
  export { default as ProfileModal } from "./ProfileModal.svelte"
@@ -0,0 +1,51 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import { format } from "./jsonFormatter"
3
+
4
+ describe("JSON formatter", () => {
5
+ it("escapes markup in raw string values", () => {
6
+ const formatted = format("<img src=x onerror=alert(window.xssproof)>")
7
+
8
+ expect(formatted).toContain(
9
+ "&lt;img src=x onerror=alert(window.xssproof)&gt;"
10
+ )
11
+ expect(formatted).not.toContain("<img")
12
+ })
13
+
14
+ it("escapes markup in JSON string values and keys", () => {
15
+ const formatted = format({
16
+ "<script>": "<img src=x>",
17
+ })
18
+
19
+ expect(formatted).not.toContain("<script>")
20
+ expect(formatted).not.toContain("<img")
21
+ })
22
+
23
+ it("preserves raw strings without adding JSON quotes", () => {
24
+ expect(format("hello & goodbye")).toBe(
25
+ '<span class="json-highlight">hello &amp; goodbye</span>'
26
+ )
27
+ })
28
+
29
+ it("formats undefined binding results", () => {
30
+ expect(format(undefined)).toBe(
31
+ '<span class="json-highlight">undefined</span>'
32
+ )
33
+ })
34
+
35
+ it("preserves indentation when formatting objects", () => {
36
+ expect(format({ value: 42 }).replace(/<[^>]*>/g, "")).toBe(
37
+ "{\n &quot;value&quot;: 42\n}"
38
+ )
39
+ })
40
+
41
+ it("highlights numbers with negative exponents as one token", () => {
42
+ expect(format(1e-7)).toContain('<span class="hljs-number">1e-7</span>')
43
+ })
44
+
45
+ it.each([true, false, null])("highlights the literal %s", value => {
46
+ const formatted = format(value)
47
+
48
+ expect(formatted).toContain('class="hljs-literal"')
49
+ expect(formatted.replace(/<[^>]*>/g, "")).toBe(String(value))
50
+ })
51
+ })
@@ -1,71 +1,14 @@
1
- import { JSONValue } from "@budibase/types"
2
-
3
- export type ColorsOptions = {
4
- keyColor?: string
5
- numberColor?: string
6
- stringColor?: string
7
- trueColor?: string
8
- falseColor?: string
9
- nullColor?: string
10
- }
11
-
12
- const defaultColors: ColorsOptions = {
13
- keyColor: "dimgray",
14
- numberColor: "lightskyblue",
15
- stringColor: "lightcoral",
16
- trueColor: "lightseagreen",
17
- falseColor: "#f66578",
18
- nullColor: "cornflowerblue",
19
- }
20
-
21
- const entityMap = {
22
- "&": "&amp;",
23
- "<": "&lt;",
24
- ">": "&gt;",
25
- '"': "&quot;",
26
- "'": "&#39;",
27
- "`": "&#x60;",
28
- "=": "&#x3D;",
29
- }
30
-
31
- function escapeHtml(html: string) {
32
- return String(html).replace(/[&<>"'`=]/g, function (s) {
33
- return entityMap[s as keyof typeof entityMap]
34
- })
35
- }
36
-
37
- export function format(json: JSONValue, colorOptions: ColorsOptions = {}) {
38
- const valueType = typeof json
39
- let jsonString =
40
- typeof json === "string" ? json : JSON.stringify(json, null, 2) || valueType
41
- let colors = Object.assign({}, defaultColors, colorOptions)
42
- jsonString = jsonString
43
- .replace(/&/g, "&")
44
- .replace(/</g, "<")
45
- .replace(/>/g, ">")
46
- return jsonString.replace(
47
- /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+]?\d+)?)/g,
48
- (match: string) => {
49
- let color = colors.numberColor
50
- let style = ""
51
- if (/^"/.test(match)) {
52
- if (/:$/.test(match)) {
53
- color = colors.keyColor
54
- } else {
55
- color = colors.stringColor
56
- match = '"' + escapeHtml(match.substr(1, match.length - 2)) + '"'
57
- style = "word-wrap:break-word;white-space:pre-wrap;"
58
- }
59
- } else {
60
- color = /true/.test(match)
61
- ? colors.trueColor
62
- : /false/.test(match)
63
- ? colors.falseColor
64
- : /null/.test(match)
65
- ? colors.nullColor
66
- : color
67
- }
68
- return `<span style="${style}color:${color}">${match}</span>`
69
- }
70
- )
1
+ import hljs from "highlight.js/lib/core"
2
+ import jsonLanguage from "highlight.js/lib/languages/json"
3
+ import type { JSONValue } from "@budibase/types"
4
+
5
+ hljs.registerLanguage("json", jsonLanguage)
6
+
7
+ export const format = (json: JSONValue | undefined) => {
8
+ const text =
9
+ typeof json === "string"
10
+ ? json
11
+ : (JSON.stringify(json, null, 2) ?? typeof json)
12
+ const highlighted = hljs.highlight(text, { language: "json" }).value
13
+ return `<span class="json-highlight">${highlighted}</span>`
71
14
  }