@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@budibase/frontend-core",
3
- "version": "v3.45.0",
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": "^4.1.0"
25
+ "vitest": "^5.0.0"
25
26
  },
26
- "gitHead": "39146fbea58fca5022556582a42e57a794fdf8ad"
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 { 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>
@@ -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
- <div class="escalation-card-header">
37
- <Icon name={isResolved ? "check-circle" : "clock"} size="M" />
38
- <span class="escalation-card-title">{title || "Approval required"}</span>
39
- {#if showApproval && !isResolved}
40
- <span class="escalation-card-badge">Test mode</span>
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
- {#if summary}
45
- <Body size="S" color="var(--spectrum-global-color-gray-700)">
46
- {summary}
47
- </Body>
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 { 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)
@@ -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 toggleTool = (toolId: string) => {
499
- expandedTools = { ...expandedTools, [toolId]: !expandedTools[toolId] }
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(scrollToBottom)
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" bind:this={chatAreaElement}>
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={() => toggleTool(toolId)}
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-y: auto;
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 auto;
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
- position: relative;
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
- position: absolute;
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(--background);
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
- 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
@@ -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"
@@ -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, workspaceId, ...rest } = query
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
+ "&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
  }