@budibase/frontend-core 3.45.0 → 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 +4 -3
- package/src/components/ArrayValueInput.svelte +251 -0
- package/src/components/ChangePasswordModal.svelte +8 -4
- package/src/components/Chatbox/index.svelte +3 -3
- package/src/components/FilterField.svelte +21 -10
- package/src/components/PasswordRepeatInput.svelte +18 -4
- package/src/components/index.ts +1 -0
- 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.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": "^
|
|
25
|
+
"vitest": "^5.0.0"
|
|
25
26
|
},
|
|
26
|
-
"gitHead": "
|
|
27
|
+
"gitHead": "1444a702ce29cf158b10c31d3f205fc84c463169"
|
|
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>
|
|
@@ -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)
|
|
@@ -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"
|
|
@@ -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
|
}
|