@budibase/frontend-core 3.45.0 → 3.47.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 +4 -3
- package/src/components/ArrayValueInput.svelte +251 -0
- package/src/components/ChangePasswordModal.svelte +8 -4
- package/src/components/Chatbox/EscalationCard.svelte +76 -23
- package/src/components/Chatbox/index.svelte +63 -45
- package/src/components/FilterField.svelte +21 -10
- package/src/components/PasswordRepeatInput.svelte +18 -4
- package/src/components/index.ts +1 -0
- package/src/fetch/UserFetch.ts +10 -3
- package/src/utils/jsonFormatter.test.ts +51 -0
- package/src/utils/jsonFormatter.ts +13 -70
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@budibase/frontend-core",
|
|
3
|
-
"version": "v3.
|
|
3
|
+
"version": "v3.47.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": "^
|
|
25
|
+
"vitest": "^5.0.0"
|
|
25
26
|
},
|
|
26
|
-
"gitHead": "
|
|
27
|
+
"gitHead": "adfff86585b1d393941175f58cbe74f2005616b4"
|
|
27
28
|
}
|
|
@@ -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
|
|
10
|
+
import PasswordRepeatInput from "./PasswordRepeatInput.svelte"
|
|
7
11
|
|
|
8
12
|
export let API: APIClient
|
|
9
|
-
export let
|
|
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
|
-
|
|
62
|
+
policy={passwordPolicy}
|
|
59
63
|
labels={resolvedLabels}
|
|
60
64
|
/>
|
|
61
65
|
</ModalContent>
|
|
@@ -30,31 +30,64 @@
|
|
|
30
30
|
|
|
31
31
|
// Resolved either by this card's action (statusMessage) or elsewhere (poll).
|
|
32
32
|
let isResolved = $derived(!!statusMessage || resolution !== "pending")
|
|
33
|
+
let expanded = $state(true)
|
|
34
|
+
let showDetails = $derived(!showApproval || expanded)
|
|
35
|
+
|
|
36
|
+
const toggleDetails = () => {
|
|
37
|
+
expanded = !expanded
|
|
38
|
+
}
|
|
33
39
|
</script>
|
|
34
40
|
|
|
35
41
|
<div class="escalation-card" aria-live="polite">
|
|
36
|
-
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
{#if showApproval}
|
|
43
|
+
<button
|
|
44
|
+
type="button"
|
|
45
|
+
class="escalation-card-header escalation-card-toggle"
|
|
46
|
+
aria-expanded={expanded}
|
|
47
|
+
aria-label={`${expanded ? "Collapse" : "Expand"} approval details${title ? ` for ${title}` : ""}`}
|
|
48
|
+
onclick={toggleDetails}
|
|
49
|
+
>
|
|
50
|
+
<Icon name={isResolved ? "check-circle" : "clock"} size="M" />
|
|
51
|
+
<span class="escalation-card-title">{title || "Approval required"}</span>
|
|
52
|
+
<span class="escalation-card-header-end">
|
|
53
|
+
{#if !isResolved}
|
|
54
|
+
<span class="escalation-card-badge">Test mode</span>
|
|
55
|
+
{/if}
|
|
56
|
+
<span class="escalation-card-chevron" class:expanded>
|
|
57
|
+
<Icon name="caret-down" size="S" />
|
|
58
|
+
</span>
|
|
59
|
+
</span>
|
|
60
|
+
</button>
|
|
61
|
+
{:else}
|
|
62
|
+
<div class="escalation-card-header">
|
|
63
|
+
<Icon name={isResolved ? "check-circle" : "clock"} size="M" />
|
|
64
|
+
<span class="escalation-card-title">{title || "Approval required"}</span>
|
|
65
|
+
</div>
|
|
66
|
+
{/if}
|
|
67
|
+
|
|
68
|
+
{#if showDetails}
|
|
69
|
+
{#if summary}
|
|
70
|
+
<Body size="S" color="var(--spectrum-global-color-gray-700)">
|
|
71
|
+
{summary}
|
|
72
|
+
</Body>
|
|
41
73
|
{/if}
|
|
42
|
-
</div>
|
|
43
74
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
75
|
+
{#if isResolved}
|
|
76
|
+
<Body size="XS" color="var(--spectrum-global-color-gray-600)">
|
|
77
|
+
{statusMessage || "Response recorded."}
|
|
78
|
+
</Body>
|
|
79
|
+
{:else if showApproval}
|
|
80
|
+
<Body size="XS" color="var(--spectrum-global-color-gray-600)">
|
|
81
|
+
Approve or reject here to simulate a reviewer response.
|
|
82
|
+
</Body>
|
|
83
|
+
{:else}
|
|
84
|
+
<Body size="XS" color="var(--spectrum-global-color-gray-600)">
|
|
85
|
+
Awaiting a human response.
|
|
86
|
+
</Body>
|
|
87
|
+
{/if}
|
|
48
88
|
{/if}
|
|
49
89
|
|
|
50
|
-
{#if isResolved}
|
|
51
|
-
<Body size="XS" color="var(--spectrum-global-color-gray-600)">
|
|
52
|
-
{statusMessage || "Response recorded."}
|
|
53
|
-
</Body>
|
|
54
|
-
{:else if showApproval}
|
|
55
|
-
<Body size="XS" color="var(--spectrum-global-color-gray-600)">
|
|
56
|
-
Approve or reject here to simulate a reviewer response.
|
|
57
|
-
</Body>
|
|
90
|
+
{#if showApproval && !isResolved}
|
|
58
91
|
<div class="escalation-card-actions">
|
|
59
92
|
<Button cta disabled={resolving} on:click={() => onApprove?.()}>
|
|
60
93
|
Approve
|
|
@@ -63,10 +96,6 @@
|
|
|
63
96
|
Reject
|
|
64
97
|
</Button>
|
|
65
98
|
</div>
|
|
66
|
-
{:else}
|
|
67
|
-
<Body size="XS" color="var(--spectrum-global-color-gray-600)">
|
|
68
|
-
Awaiting a human response.
|
|
69
|
-
</Body>
|
|
70
99
|
{/if}
|
|
71
100
|
</div>
|
|
72
101
|
|
|
@@ -89,13 +118,37 @@
|
|
|
89
118
|
align-items: center;
|
|
90
119
|
gap: var(--spacing-s);
|
|
91
120
|
}
|
|
121
|
+
.escalation-card-toggle {
|
|
122
|
+
width: 100%;
|
|
123
|
+
margin: 0;
|
|
124
|
+
padding: 0;
|
|
125
|
+
border: none;
|
|
126
|
+
background: none;
|
|
127
|
+
cursor: pointer;
|
|
128
|
+
text-align: left;
|
|
129
|
+
color: inherit;
|
|
130
|
+
font: inherit;
|
|
131
|
+
}
|
|
132
|
+
.escalation-card-header-end {
|
|
133
|
+
display: flex;
|
|
134
|
+
align-items: center;
|
|
135
|
+
gap: var(--spacing-xs);
|
|
136
|
+
margin-left: auto;
|
|
137
|
+
}
|
|
138
|
+
.escalation-card-chevron {
|
|
139
|
+
display: flex;
|
|
140
|
+
color: var(--spectrum-global-color-gray-700);
|
|
141
|
+
transition: transform 130ms ease-in-out;
|
|
142
|
+
}
|
|
143
|
+
.escalation-card-chevron.expanded {
|
|
144
|
+
transform: rotate(180deg);
|
|
145
|
+
}
|
|
92
146
|
.escalation-card-title {
|
|
93
147
|
font-weight: 600;
|
|
94
148
|
font-size: 14px;
|
|
95
149
|
color: var(--spectrum-global-color-gray-900);
|
|
96
150
|
}
|
|
97
151
|
.escalation-card-badge {
|
|
98
|
-
margin-left: auto;
|
|
99
152
|
border-radius: 999px;
|
|
100
153
|
border: 1px solid var(--spectrum-global-color-blue-400);
|
|
101
154
|
color: var(--spectrum-global-color-blue-700);
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
EscalationContextDoc,
|
|
15
15
|
EscalationRespondResult,
|
|
16
16
|
} from "@budibase/types"
|
|
17
|
-
import {
|
|
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
|
-
|
|
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 ===
|
|
329
|
+
output?.status === ApprovalToolResultStatus.PENDING_APPROVAL &&
|
|
330
330
|
output.escalationId
|
|
331
331
|
) {
|
|
332
332
|
ids.push(output.escalationId)
|
|
@@ -412,6 +412,10 @@
|
|
|
412
412
|
}
|
|
413
413
|
})
|
|
414
414
|
|
|
415
|
+
// Tool/reasoning toggles mutate the chat DOM; the observer below would
|
|
416
|
+
// otherwise pin the viewport to the bottom as height changes.
|
|
417
|
+
let skipAutoScroll = false
|
|
418
|
+
|
|
415
419
|
const scrollToBottom = async () => {
|
|
416
420
|
await tick()
|
|
417
421
|
if (chatAreaElement) {
|
|
@@ -421,6 +425,9 @@
|
|
|
421
425
|
|
|
422
426
|
$effect(() => {
|
|
423
427
|
if (messages?.length) {
|
|
428
|
+
if (skipAutoScroll) {
|
|
429
|
+
return
|
|
430
|
+
}
|
|
424
431
|
scrollToBottom()
|
|
425
432
|
}
|
|
426
433
|
})
|
|
@@ -495,8 +502,17 @@
|
|
|
495
502
|
await sendMessage()
|
|
496
503
|
}
|
|
497
504
|
|
|
498
|
-
const
|
|
499
|
-
|
|
505
|
+
const toggleExpanded = (id: string) => {
|
|
506
|
+
const area = chatAreaElement
|
|
507
|
+
const previousScrollTop = area?.scrollTop ?? 0
|
|
508
|
+
skipAutoScroll = true
|
|
509
|
+
expandedTools = { ...expandedTools, [id]: !expandedTools[id] }
|
|
510
|
+
tick().then(() => {
|
|
511
|
+
if (area) {
|
|
512
|
+
area.scrollTop = previousScrollTop
|
|
513
|
+
}
|
|
514
|
+
skipAutoScroll = false
|
|
515
|
+
})
|
|
500
516
|
}
|
|
501
517
|
|
|
502
518
|
const formatToolOutput = (output: unknown): string =>
|
|
@@ -530,7 +546,12 @@
|
|
|
530
546
|
$effect(() => {
|
|
531
547
|
if (!chatAreaElement) return
|
|
532
548
|
|
|
533
|
-
const obs = new MutationObserver(
|
|
549
|
+
const obs = new MutationObserver(() => {
|
|
550
|
+
if (skipAutoScroll) {
|
|
551
|
+
return
|
|
552
|
+
}
|
|
553
|
+
scrollToBottom()
|
|
554
|
+
})
|
|
534
555
|
obs.observe(chatAreaElement, {
|
|
535
556
|
childList: true,
|
|
536
557
|
subtree: true,
|
|
@@ -543,8 +564,8 @@
|
|
|
543
564
|
})
|
|
544
565
|
</script>
|
|
545
566
|
|
|
546
|
-
<div class="chat-area"
|
|
547
|
-
<div class="chatbox">
|
|
567
|
+
<div class="chat-area">
|
|
568
|
+
<div class="chatbox" bind:this={chatAreaElement}>
|
|
548
569
|
{#if !hasMessages && !isRequestPending}
|
|
549
570
|
<div class="empty-state">
|
|
550
571
|
<div class="empty-state-icon">
|
|
@@ -597,35 +618,13 @@
|
|
|
597
618
|
interactive={!!reasoningText}
|
|
598
619
|
expanded={Boolean(expandedTools[reasoningId])}
|
|
599
620
|
content={reasoningText}
|
|
600
|
-
ontoggle={() =>
|
|
601
|
-
(expandedTools = {
|
|
602
|
-
...expandedTools,
|
|
603
|
-
[reasoningId]: !expandedTools[reasoningId],
|
|
604
|
-
})}
|
|
621
|
+
ontoggle={() => toggleExpanded(reasoningId)}
|
|
605
622
|
/>
|
|
606
623
|
{/if}
|
|
607
624
|
{#each message.parts ?? [] as part, partIndex}
|
|
608
625
|
{#if isTextUIPart(part)}
|
|
609
626
|
<MarkdownViewer value={part.text} />
|
|
610
|
-
{:else if isToolUIPart(part) && isRaisedEscalation(part.output)}
|
|
611
|
-
{@const card = escalationCardProps(part)}
|
|
612
|
-
<EscalationCard
|
|
613
|
-
title={card.title}
|
|
614
|
-
summary={card.summary}
|
|
615
|
-
resolution={card.resolution}
|
|
616
|
-
statusMessage={card.escalationId
|
|
617
|
-
? resolveMessages[card.escalationId]
|
|
618
|
-
: undefined}
|
|
619
|
-
showApproval={showInlineApproval}
|
|
620
|
-
resolving={!!card.escalationId &&
|
|
621
|
-
!!resolvingEscalations[card.escalationId]}
|
|
622
|
-
onApprove={() =>
|
|
623
|
-
card.escalationId && handleResolve(card.escalationId, true)}
|
|
624
|
-
onReject={() =>
|
|
625
|
-
card.escalationId &&
|
|
626
|
-
handleResolve(card.escalationId, false)}
|
|
627
|
-
/>
|
|
628
|
-
{:else if isToolUIPart(part)}
|
|
627
|
+
{:else if isToolUIPart(part) && !isRaisedEscalation(part.output)}
|
|
629
628
|
{@const rawToolName = getToolName(part)}
|
|
630
629
|
{@const displayToolName = formatToolName(
|
|
631
630
|
rawToolName,
|
|
@@ -642,7 +641,7 @@
|
|
|
642
641
|
class="tool-header"
|
|
643
642
|
class:tool-header-expanded={expandedTools[toolId]}
|
|
644
643
|
type="button"
|
|
645
|
-
onclick={() =>
|
|
644
|
+
onclick={() => toggleExpanded(toolId)}
|
|
646
645
|
>
|
|
647
646
|
<span
|
|
648
647
|
class="tool-chevron"
|
|
@@ -753,6 +752,27 @@
|
|
|
753
752
|
</ul>
|
|
754
753
|
</div>
|
|
755
754
|
{/if}
|
|
755
|
+
{#each message.parts ?? [] as part}
|
|
756
|
+
{#if isToolUIPart(part) && isRaisedEscalation(part.output)}
|
|
757
|
+
{@const card = escalationCardProps(part)}
|
|
758
|
+
<EscalationCard
|
|
759
|
+
title={card.title}
|
|
760
|
+
summary={card.summary}
|
|
761
|
+
resolution={card.resolution}
|
|
762
|
+
statusMessage={card.escalationId
|
|
763
|
+
? resolveMessages[card.escalationId]
|
|
764
|
+
: undefined}
|
|
765
|
+
showApproval={showInlineApproval}
|
|
766
|
+
resolving={!!card.escalationId &&
|
|
767
|
+
!!resolvingEscalations[card.escalationId]}
|
|
768
|
+
onApprove={() =>
|
|
769
|
+
card.escalationId && handleResolve(card.escalationId, true)}
|
|
770
|
+
onReject={() =>
|
|
771
|
+
card.escalationId &&
|
|
772
|
+
handleResolve(card.escalationId, false)}
|
|
773
|
+
/>
|
|
774
|
+
{/if}
|
|
775
|
+
{/each}
|
|
756
776
|
</div>
|
|
757
777
|
{/if}
|
|
758
778
|
{/if}
|
|
@@ -803,7 +823,7 @@
|
|
|
803
823
|
flex: 1 1 0;
|
|
804
824
|
display: flex;
|
|
805
825
|
flex-direction: column;
|
|
806
|
-
overflow
|
|
826
|
+
overflow: hidden;
|
|
807
827
|
min-height: 0;
|
|
808
828
|
font-family: var(--chat-font-sans, var(--font-sans));
|
|
809
829
|
--font-serif: var(--chat-font-sans, var(--font-sans));
|
|
@@ -819,7 +839,9 @@
|
|
|
819
839
|
flex-direction: column;
|
|
820
840
|
gap: 24px;
|
|
821
841
|
width: 100%;
|
|
822
|
-
flex: 1 1
|
|
842
|
+
flex: 1 1 0;
|
|
843
|
+
min-height: 0;
|
|
844
|
+
overflow-y: auto;
|
|
823
845
|
padding: 48px 0 24px 0;
|
|
824
846
|
}
|
|
825
847
|
|
|
@@ -868,14 +890,13 @@
|
|
|
868
890
|
}
|
|
869
891
|
|
|
870
892
|
.input-wrapper {
|
|
871
|
-
position: sticky;
|
|
872
|
-
bottom: 0;
|
|
873
893
|
width: 100%;
|
|
874
894
|
display: flex;
|
|
875
895
|
flex-direction: column;
|
|
876
896
|
flex-shrink: 0;
|
|
877
897
|
line-height: 1.4;
|
|
878
898
|
gap: 6px;
|
|
899
|
+
padding-top: 8px;
|
|
879
900
|
}
|
|
880
901
|
|
|
881
902
|
.input-footer {
|
|
@@ -983,7 +1004,9 @@
|
|
|
983
1004
|
}
|
|
984
1005
|
|
|
985
1006
|
.tool-part {
|
|
986
|
-
|
|
1007
|
+
display: flex;
|
|
1008
|
+
flex-direction: column;
|
|
1009
|
+
align-items: stretch;
|
|
987
1010
|
margin-top: var(--spacing-l);
|
|
988
1011
|
margin-bottom: 0;
|
|
989
1012
|
}
|
|
@@ -1076,22 +1099,17 @@
|
|
|
1076
1099
|
}
|
|
1077
1100
|
|
|
1078
1101
|
.tool-details {
|
|
1079
|
-
|
|
1080
|
-
top: 100%;
|
|
1081
|
-
left: 0;
|
|
1082
|
-
margin-top: var(--spacing-m);
|
|
1102
|
+
margin-top: var(--spacing-s);
|
|
1083
1103
|
width: 100%;
|
|
1084
1104
|
max-width: 100%;
|
|
1085
1105
|
box-sizing: border-box;
|
|
1086
1106
|
display: flex;
|
|
1087
1107
|
flex-direction: column;
|
|
1088
1108
|
gap: var(--spacing-s);
|
|
1089
|
-
background: var(--
|
|
1109
|
+
background: var(--spectrum-global-color-gray-75);
|
|
1090
1110
|
border: 1px solid var(--spectrum-global-color-gray-200);
|
|
1091
1111
|
border-radius: 6px;
|
|
1092
1112
|
padding: var(--spacing-m);
|
|
1093
|
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
1094
|
-
z-index: 1;
|
|
1095
1113
|
overflow-x: hidden;
|
|
1096
1114
|
min-width: 0;
|
|
1097
1115
|
}
|
|
@@ -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
|
-
|
|
86
|
-
|
|
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
|
|
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
|
|
13
|
+
export let policy: PasswordPolicy = DEFAULT_PASSWORD_POLICY
|
|
9
14
|
export let labels: any = {}
|
|
10
15
|
|
|
11
16
|
const validatePassword = (value: string | undefined) => {
|
|
12
|
-
|
|
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
|
-
|
|
15
|
-
`
|
|
28
|
+
result.error ||
|
|
29
|
+
`Password must contain no more than ${policy.maxLength} characters.`
|
|
16
30
|
)
|
|
17
31
|
}
|
|
18
32
|
return null
|
package/src/components/index.ts
CHANGED
|
@@ -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"
|
package/src/fetch/UserFetch.ts
CHANGED
|
@@ -11,7 +11,6 @@ import BaseDataFetch, { DataFetchParams } from "./DataFetch"
|
|
|
11
11
|
|
|
12
12
|
interface UserFetchQuery {
|
|
13
13
|
appId?: string
|
|
14
|
-
workspaceId?: string
|
|
15
14
|
paginated?: boolean
|
|
16
15
|
string?: {
|
|
17
16
|
email: string
|
|
@@ -36,6 +35,7 @@ export default class UserFetch extends BaseDataFetch<
|
|
|
36
35
|
super({
|
|
37
36
|
...opts,
|
|
38
37
|
datasource: {
|
|
38
|
+
...opts.datasource,
|
|
39
39
|
type: "user",
|
|
40
40
|
},
|
|
41
41
|
})
|
|
@@ -56,9 +56,14 @@ export default class UserFetch extends BaseDataFetch<
|
|
|
56
56
|
async getData() {
|
|
57
57
|
const { limit, paginate } = this.options
|
|
58
58
|
const { cursor, query } = get(this.store)
|
|
59
|
+
const {
|
|
60
|
+
groupIds,
|
|
61
|
+
workspaceId: datasourceWorkspaceId,
|
|
62
|
+
workspaceRoleId,
|
|
63
|
+
} = this.options.datasource
|
|
59
64
|
|
|
60
65
|
// Convert old format to new one - we now allow use of the lucene format
|
|
61
|
-
const { appId, paginated,
|
|
66
|
+
const { appId, paginated, ...rest } = query
|
|
62
67
|
|
|
63
68
|
const finalQuery: SearchFilters = utils.isSupportedUserSearch(rest)
|
|
64
69
|
? rest
|
|
@@ -69,7 +74,9 @@ export default class UserFetch extends BaseDataFetch<
|
|
|
69
74
|
bookmark: cursor ?? undefined,
|
|
70
75
|
query: finalQuery ?? undefined,
|
|
71
76
|
appId: appId,
|
|
72
|
-
workspaceId,
|
|
77
|
+
workspaceId: datasourceWorkspaceId,
|
|
78
|
+
groupIds,
|
|
79
|
+
workspaceRoleId,
|
|
73
80
|
paginate: paginated || paginate,
|
|
74
81
|
limit,
|
|
75
82
|
}
|
|
@@ -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
|
+
"<img src=x onerror=alert(window.xssproof)>"
|
|
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 & 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 "value": 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
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
numberColor: "lightskyblue",
|
|
15
|
-
stringColor: "lightcoral",
|
|
16
|
-
trueColor: "lightseagreen",
|
|
17
|
-
falseColor: "#f66578",
|
|
18
|
-
nullColor: "cornflowerblue",
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const entityMap = {
|
|
22
|
-
"&": "&",
|
|
23
|
-
"<": "<",
|
|
24
|
-
">": ">",
|
|
25
|
-
'"': """,
|
|
26
|
-
"'": "'",
|
|
27
|
-
"`": "`",
|
|
28
|
-
"=": "=",
|
|
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
|
}
|