@budibase/frontend-core 3.47.0 → 3.49.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.
|
|
3
|
+
"version": "v3.49.0",
|
|
4
4
|
"description": "Budibase frontend core libraries used in builder and client",
|
|
5
5
|
"author": "Budibase",
|
|
6
6
|
"license": "MPL-2.0",
|
|
@@ -24,5 +24,5 @@
|
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"vitest": "^5.0.0"
|
|
26
26
|
},
|
|
27
|
-
"gitHead": "
|
|
27
|
+
"gitHead": "c601aad0b5346eca1152d70c3168e14afb7bdd3b"
|
|
28
28
|
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { Body, Button, Icon } from "@budibase/bbui"
|
|
3
|
-
import
|
|
3
|
+
import { APPROVAL_REQUIRED_TITLE_PREFIX } from "@budibase/shared-core"
|
|
4
|
+
import type {
|
|
5
|
+
EscalationContextDoc,
|
|
6
|
+
EscalationReviewContext,
|
|
7
|
+
} from "@budibase/types"
|
|
4
8
|
|
|
5
9
|
interface Props {
|
|
6
10
|
title?: string
|
|
7
11
|
summary?: string
|
|
12
|
+
reviewContext?: EscalationReviewContext
|
|
8
13
|
// Live resolution from the poll (not the frozen tool output).
|
|
9
14
|
resolution: EscalationContextDoc["resolution"]
|
|
10
15
|
// Message relayed from resolve ("Response recorded." etc.) - set as soon as
|
|
@@ -20,6 +25,7 @@
|
|
|
20
25
|
let {
|
|
21
26
|
title,
|
|
22
27
|
summary,
|
|
28
|
+
reviewContext,
|
|
23
29
|
resolution,
|
|
24
30
|
statusMessage,
|
|
25
31
|
showApproval = false,
|
|
@@ -32,6 +38,14 @@
|
|
|
32
38
|
let isResolved = $derived(!!statusMessage || resolution !== "pending")
|
|
33
39
|
let expanded = $state(true)
|
|
34
40
|
let showDetails = $derived(!showApproval || expanded)
|
|
41
|
+
// The card heading already reads "Approval required", so drop the matching
|
|
42
|
+
// prefix that operation escalations put on the title.
|
|
43
|
+
let displayTitle = $derived(
|
|
44
|
+
title?.startsWith(APPROVAL_REQUIRED_TITLE_PREFIX)
|
|
45
|
+
? title.slice(APPROVAL_REQUIRED_TITLE_PREFIX.length).trim()
|
|
46
|
+
: title
|
|
47
|
+
)
|
|
48
|
+
let hasSharedParameters = $derived(Boolean(reviewContext?.parameters?.length))
|
|
35
49
|
|
|
36
50
|
const toggleDetails = () => {
|
|
37
51
|
expanded = !expanded
|
|
@@ -44,11 +58,11 @@
|
|
|
44
58
|
type="button"
|
|
45
59
|
class="escalation-card-header escalation-card-toggle"
|
|
46
60
|
aria-expanded={expanded}
|
|
47
|
-
aria-label={`${expanded ? "Collapse" : "Expand"} approval details${
|
|
61
|
+
aria-label={`${expanded ? "Collapse" : "Expand"} approval details${displayTitle ? ` for ${displayTitle}` : ""}`}
|
|
48
62
|
onclick={toggleDetails}
|
|
49
63
|
>
|
|
50
64
|
<Icon name={isResolved ? "check-circle" : "clock"} size="M" />
|
|
51
|
-
<span class="escalation-card-
|
|
65
|
+
<span class="escalation-card-heading">Approval required</span>
|
|
52
66
|
<span class="escalation-card-header-end">
|
|
53
67
|
{#if !isResolved}
|
|
54
68
|
<span class="escalation-card-badge">Test mode</span>
|
|
@@ -61,11 +75,23 @@
|
|
|
61
75
|
{:else}
|
|
62
76
|
<div class="escalation-card-header">
|
|
63
77
|
<Icon name={isResolved ? "check-circle" : "clock"} size="M" />
|
|
64
|
-
<span class="escalation-card-
|
|
78
|
+
<span class="escalation-card-heading">Approval required</span>
|
|
65
79
|
</div>
|
|
66
80
|
{/if}
|
|
67
81
|
|
|
68
82
|
{#if showDetails}
|
|
83
|
+
{#if displayTitle}
|
|
84
|
+
<div class="escalation-card-title">{displayTitle}</div>
|
|
85
|
+
{/if}
|
|
86
|
+
|
|
87
|
+
{#if reviewContext}
|
|
88
|
+
<Body size="S" color="var(--spectrum-global-color-gray-700)">
|
|
89
|
+
{reviewContext.requestedBy} is requesting approval for
|
|
90
|
+
<strong>{reviewContext.action}</strong> as part of
|
|
91
|
+
<strong>{reviewContext.operation}</strong>.
|
|
92
|
+
</Body>
|
|
93
|
+
{/if}
|
|
94
|
+
|
|
69
95
|
{#if summary}
|
|
70
96
|
<Body size="S" color="var(--spectrum-global-color-gray-700)">
|
|
71
97
|
{summary}
|
|
@@ -85,6 +111,24 @@
|
|
|
85
111
|
Awaiting a human response.
|
|
86
112
|
</Body>
|
|
87
113
|
{/if}
|
|
114
|
+
|
|
115
|
+
{#if hasSharedParameters && reviewContext?.parameters}
|
|
116
|
+
<div class="escalation-card-divider"></div>
|
|
117
|
+
<div class="escalation-card-parameters-heading">
|
|
118
|
+
<span>Tool parameters</span>
|
|
119
|
+
{#if reviewContext.toolName}
|
|
120
|
+
<code>{reviewContext.toolName}</code>
|
|
121
|
+
{/if}
|
|
122
|
+
</div>
|
|
123
|
+
<div class="escalation-card-parameters-list">
|
|
124
|
+
{#each reviewContext.parameters as parameter (parameter.name)}
|
|
125
|
+
<div class="escalation-card-parameter">
|
|
126
|
+
<code>{parameter.name}</code>
|
|
127
|
+
<pre class="escalation-card-parameters">{parameter.value}</pre>
|
|
128
|
+
</div>
|
|
129
|
+
{/each}
|
|
130
|
+
</div>
|
|
131
|
+
{/if}
|
|
88
132
|
{/if}
|
|
89
133
|
|
|
90
134
|
{#if showApproval && !isResolved}
|
|
@@ -129,25 +173,22 @@
|
|
|
129
173
|
color: inherit;
|
|
130
174
|
font: inherit;
|
|
131
175
|
}
|
|
176
|
+
.escalation-card-heading,
|
|
177
|
+
.escalation-card-title,
|
|
178
|
+
.escalation-card-parameters-heading {
|
|
179
|
+
font-weight: 600;
|
|
180
|
+
font-size: 14px;
|
|
181
|
+
color: var(--spectrum-global-color-gray-900);
|
|
182
|
+
}
|
|
183
|
+
.escalation-card-title {
|
|
184
|
+
margin-top: var(--spacing-xs);
|
|
185
|
+
}
|
|
132
186
|
.escalation-card-header-end {
|
|
133
187
|
display: flex;
|
|
134
188
|
align-items: center;
|
|
135
189
|
gap: var(--spacing-xs);
|
|
136
190
|
margin-left: auto;
|
|
137
191
|
}
|
|
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
|
-
}
|
|
146
|
-
.escalation-card-title {
|
|
147
|
-
font-weight: 600;
|
|
148
|
-
font-size: 14px;
|
|
149
|
-
color: var(--spectrum-global-color-gray-900);
|
|
150
|
-
}
|
|
151
192
|
.escalation-card-badge {
|
|
152
193
|
border-radius: 999px;
|
|
153
194
|
border: 1px solid var(--spectrum-global-color-blue-400);
|
|
@@ -158,9 +199,61 @@
|
|
|
158
199
|
line-height: 1;
|
|
159
200
|
padding: 4px 8px;
|
|
160
201
|
}
|
|
202
|
+
.escalation-card-chevron {
|
|
203
|
+
display: flex;
|
|
204
|
+
color: var(--spectrum-global-color-gray-700);
|
|
205
|
+
transition: transform 130ms ease-in-out;
|
|
206
|
+
}
|
|
207
|
+
.escalation-card-chevron.expanded {
|
|
208
|
+
transform: rotate(180deg);
|
|
209
|
+
}
|
|
161
210
|
.escalation-card-actions {
|
|
162
211
|
display: flex;
|
|
163
212
|
gap: var(--spacing-s);
|
|
164
213
|
margin-top: var(--spacing-xs);
|
|
165
214
|
}
|
|
215
|
+
.escalation-card-divider {
|
|
216
|
+
height: 1px;
|
|
217
|
+
margin: var(--spacing-xs) calc(-1 * var(--spacing-m));
|
|
218
|
+
background: var(--spectrum-global-color-gray-300);
|
|
219
|
+
}
|
|
220
|
+
.escalation-card-parameters-heading {
|
|
221
|
+
display: flex;
|
|
222
|
+
align-items: center;
|
|
223
|
+
gap: var(--spacing-xs);
|
|
224
|
+
}
|
|
225
|
+
.escalation-card-parameters-heading code {
|
|
226
|
+
min-width: 0;
|
|
227
|
+
padding: 2px 5px;
|
|
228
|
+
border-radius: 4px;
|
|
229
|
+
background: var(--spectrum-global-color-gray-200);
|
|
230
|
+
font-size: 12px;
|
|
231
|
+
font-weight: 400;
|
|
232
|
+
overflow-wrap: anywhere;
|
|
233
|
+
white-space: normal;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.escalation-card-parameters-list,
|
|
237
|
+
.escalation-card-parameter {
|
|
238
|
+
display: flex;
|
|
239
|
+
flex-direction: column;
|
|
240
|
+
gap: var(--spacing-xs);
|
|
241
|
+
}
|
|
242
|
+
.escalation-card-parameters {
|
|
243
|
+
box-sizing: content-box;
|
|
244
|
+
min-height: 12px;
|
|
245
|
+
max-height: 360px;
|
|
246
|
+
margin: 0;
|
|
247
|
+
padding: var(--spacing-s);
|
|
248
|
+
overflow: auto;
|
|
249
|
+
border: 1px solid var(--spectrum-global-color-gray-300);
|
|
250
|
+
border-radius: 4px;
|
|
251
|
+
background: var(--spectrum-global-color-gray-100);
|
|
252
|
+
color: var(--spectrum-global-color-gray-800);
|
|
253
|
+
font-family: monospace;
|
|
254
|
+
font-size: 12px;
|
|
255
|
+
line-height: 16px;
|
|
256
|
+
white-space: pre-wrap;
|
|
257
|
+
overflow-wrap: anywhere;
|
|
258
|
+
}
|
|
166
259
|
</style>
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
DraftChatConversation,
|
|
13
13
|
AgentMessageMetadata,
|
|
14
14
|
EscalationContextDoc,
|
|
15
|
+
EscalationReviewContext,
|
|
15
16
|
EscalationRespondResult,
|
|
16
17
|
} from "@budibase/types"
|
|
17
18
|
import { ApprovalToolResultStatus } from "@budibase/types"
|
|
@@ -93,7 +94,12 @@
|
|
|
93
94
|
// casts live here rather than cluttering the template.
|
|
94
95
|
const escalationCardProps = (part: { input?: unknown; output?: unknown }) => {
|
|
95
96
|
const output = part.output as
|
|
96
|
-
| {
|
|
97
|
+
| {
|
|
98
|
+
escalationId?: string
|
|
99
|
+
title?: string
|
|
100
|
+
summary?: string
|
|
101
|
+
reviewContext?: EscalationReviewContext
|
|
102
|
+
}
|
|
97
103
|
| undefined
|
|
98
104
|
const input = part.input as { title?: string; summary?: string } | undefined
|
|
99
105
|
const escalationId = output?.escalationId
|
|
@@ -101,6 +107,7 @@
|
|
|
101
107
|
escalationId,
|
|
102
108
|
title: output?.title ?? input?.title,
|
|
103
109
|
summary: output?.summary ?? input?.summary,
|
|
110
|
+
reviewContext: output?.reviewContext,
|
|
104
111
|
resolution:
|
|
105
112
|
(escalationId && escalationState?.[escalationId]?.resolution) ||
|
|
106
113
|
"pending",
|
|
@@ -266,7 +273,25 @@
|
|
|
266
273
|
|
|
267
274
|
let resolvedConversationId = $state<string | undefined>()
|
|
268
275
|
|
|
276
|
+
// The consumer must get plain objects, otherwise reading them keeps
|
|
277
|
+
// subscribing to the live chat state and every streamed token counts as a
|
|
278
|
+
// change. $state.snapshot cannot type UIMessage, hence the manual clone.
|
|
279
|
+
const snapshotMessages = (
|
|
280
|
+
nextMessages: UIMessage<AgentMessageMetadata>[]
|
|
281
|
+
): UIMessage<AgentMessageMetadata>[] =>
|
|
282
|
+
nextMessages.map(message => JSON.parse(JSON.stringify(message)))
|
|
283
|
+
|
|
284
|
+
const publishChatMessages = (
|
|
285
|
+
nextMessages: UIMessage<AgentMessageMetadata>[] = chatInstance.messages
|
|
286
|
+
) => {
|
|
287
|
+
chat = {
|
|
288
|
+
...chat,
|
|
289
|
+
messages: snapshotMessages(nextMessages),
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
269
293
|
const chatInstance = new Chat<UIMessage<AgentMessageMetadata>>({
|
|
294
|
+
messages: chat?.messages || [],
|
|
270
295
|
transport: new DefaultChatTransport({
|
|
271
296
|
headers: () => ({ [Header.WORKSPACE_ID]: workspaceId }),
|
|
272
297
|
prepareSendMessagesRequest: ({ messages }) => {
|
|
@@ -286,13 +311,13 @@
|
|
|
286
311
|
}
|
|
287
312
|
},
|
|
288
313
|
}),
|
|
289
|
-
messages: chat?.messages || [],
|
|
290
314
|
onFinish: async () => {
|
|
291
315
|
isPreparingResponse = false
|
|
292
|
-
|
|
316
|
+
publishChatMessages()
|
|
293
317
|
},
|
|
294
318
|
onError: error => {
|
|
295
319
|
resetPendingResponse()
|
|
320
|
+
publishChatMessages()
|
|
296
321
|
|
|
297
322
|
console.error(error)
|
|
298
323
|
let message = error.message || "Failed to send message"
|
|
@@ -350,6 +375,7 @@
|
|
|
350
375
|
message: UIMessage<AgentMessageMetadata>
|
|
351
376
|
) {
|
|
352
377
|
chatInstance.messages = [...chatInstance.messages, message]
|
|
378
|
+
publishChatMessages()
|
|
353
379
|
}
|
|
354
380
|
|
|
355
381
|
let lastAssistantUsage = $derived(
|
|
@@ -490,7 +516,18 @@
|
|
|
490
516
|
inputValue = ""
|
|
491
517
|
promptHistoryIndex = undefined
|
|
492
518
|
onpromptsubmitted?.(text)
|
|
493
|
-
|
|
519
|
+
|
|
520
|
+
const userMessage: UIMessage<AgentMessageMetadata> = {
|
|
521
|
+
id: Helpers.uuid(),
|
|
522
|
+
role: "user",
|
|
523
|
+
parts: [{ type: "text", text }],
|
|
524
|
+
}
|
|
525
|
+
publishChatMessages([...(chat.messages ?? []), userMessage])
|
|
526
|
+
chatInstance.sendMessage({
|
|
527
|
+
id: userMessage.id,
|
|
528
|
+
role: "user",
|
|
529
|
+
parts: userMessage.parts,
|
|
530
|
+
})
|
|
494
531
|
isPreparingResponse = false
|
|
495
532
|
}
|
|
496
533
|
|
|
@@ -758,6 +795,7 @@
|
|
|
758
795
|
<EscalationCard
|
|
759
796
|
title={card.title}
|
|
760
797
|
summary={card.summary}
|
|
798
|
+
reviewContext={card.reviewContext}
|
|
761
799
|
resolution={card.resolution}
|
|
762
800
|
statusMessage={card.escalationId
|
|
763
801
|
? resolveMessages[card.escalationId]
|
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import { Select, Multiselect } from "@budibase/bbui"
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
fetchData,
|
|
5
|
+
fetchUsersById,
|
|
6
|
+
loadTranslationsByGroup,
|
|
7
|
+
Utils,
|
|
8
|
+
} from "@budibase/frontend-core"
|
|
4
9
|
import { createAPIClient } from "../api"
|
|
5
10
|
|
|
6
11
|
export let API = createAPIClient()
|
|
7
12
|
|
|
13
|
+
/** @type {string | string[] | null} */
|
|
8
14
|
export let value = null
|
|
9
|
-
export let disabled
|
|
15
|
+
export let disabled = false
|
|
10
16
|
export let multiselect = false
|
|
11
17
|
|
|
18
|
+
let searchTerm = null
|
|
19
|
+
let appliedTerm = ""
|
|
20
|
+
let selectedCache = {}
|
|
21
|
+
let loadingIds = new Set()
|
|
22
|
+
|
|
12
23
|
$: fetch = fetchData({
|
|
13
24
|
API,
|
|
14
25
|
datasource: {
|
|
@@ -19,10 +30,57 @@
|
|
|
19
30
|
},
|
|
20
31
|
})
|
|
21
32
|
|
|
22
|
-
$:
|
|
33
|
+
$: search(searchTerm || "")
|
|
34
|
+
$: selectedIds = !value ? [] : Array.isArray(value) ? value : [value]
|
|
35
|
+
$: pageIds = new Set(($fetch.rows || []).map(user => user._id))
|
|
36
|
+
$: loadMissing(
|
|
37
|
+
selectedIds.filter(id => !pageIds.has(id) && !selectedCache[id])
|
|
38
|
+
)
|
|
39
|
+
$: options = sortSelectedFirst(
|
|
40
|
+
[
|
|
41
|
+
...($fetch.rows || []),
|
|
42
|
+
...Object.values(selectedCache).filter(
|
|
43
|
+
user => selectedIds.includes(user._id) && !pageIds.has(user._id)
|
|
44
|
+
),
|
|
45
|
+
],
|
|
46
|
+
selectedIds
|
|
47
|
+
)
|
|
23
48
|
|
|
24
49
|
$: component = multiselect ? Multiselect : Select
|
|
25
50
|
const pickerLabels = loadTranslationsByGroup("picker")
|
|
51
|
+
|
|
52
|
+
const search = Utils.debounce(term => {
|
|
53
|
+
if (term === appliedTerm) {
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
appliedTerm = term
|
|
57
|
+
fetch.update({ query: term ? { string: { email: term } } : {} })
|
|
58
|
+
}, 250)
|
|
59
|
+
|
|
60
|
+
const loadMissing = async ids => {
|
|
61
|
+
const wanted = ids.filter(id => !loadingIds.has(id))
|
|
62
|
+
if (!wanted.length) {
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
wanted.forEach(id => loadingIds.add(id))
|
|
66
|
+
const users = await fetchUsersById(API, wanted)
|
|
67
|
+
wanted.forEach(id => loadingIds.delete(id))
|
|
68
|
+
if (Object.keys(users).length) {
|
|
69
|
+
selectedCache = { ...selectedCache, ...users }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const sortSelectedFirst = (users, ids) => {
|
|
74
|
+
const selected = new Set(ids)
|
|
75
|
+
return [...users].sort((a, b) => {
|
|
76
|
+
const aSelected = selected.has(a._id)
|
|
77
|
+
const bSelected = selected.has(b._id)
|
|
78
|
+
if (aSelected !== bSelected) {
|
|
79
|
+
return aSelected ? -1 : 1
|
|
80
|
+
}
|
|
81
|
+
return (a.email || "").localeCompare(b.email || "")
|
|
82
|
+
})
|
|
83
|
+
}
|
|
26
84
|
</script>
|
|
27
85
|
|
|
28
86
|
<div class="user-control">
|
|
@@ -37,5 +95,6 @@
|
|
|
37
95
|
{disabled}
|
|
38
96
|
searchPlaceholder={pickerLabels.searchPlaceholder}
|
|
39
97
|
popoverAutoWidth
|
|
98
|
+
bind:searchTerm
|
|
40
99
|
/>
|
|
41
100
|
</div>
|
package/src/fetch/DataFetch.ts
CHANGED
|
@@ -84,6 +84,7 @@ export default abstract class BaseDataFetch<
|
|
|
84
84
|
}
|
|
85
85
|
store: Writable<DataFetchStore<TDefinition, TQuery, TRow>>
|
|
86
86
|
derivedStore: Readable<DataFetchDerivedStore<TDefinition, TQuery, TRow>>
|
|
87
|
+
lastLoadId: number | undefined
|
|
87
88
|
|
|
88
89
|
/**
|
|
89
90
|
* Constructs a new DataFetch instance.
|
|
@@ -203,11 +204,22 @@ export default abstract class BaseDataFetch<
|
|
|
203
204
|
async getInitialData() {
|
|
204
205
|
const { filter, paginate } = this.options
|
|
205
206
|
|
|
207
|
+
// Load using IDs to track invocations and ensure we're handling the
|
|
208
|
+
// latest update
|
|
209
|
+
this.lastLoadId = Math.random()
|
|
210
|
+
const thisLoadId = this.lastLoadId
|
|
211
|
+
|
|
206
212
|
// Fetch datasource definition and extract sort properties if configured
|
|
207
213
|
const definition = await this.getDefinition()
|
|
208
214
|
|
|
209
215
|
// Determine feature flags
|
|
210
216
|
const features = await this.determineFeatureFlags()
|
|
217
|
+
|
|
218
|
+
// In case a newer load started while we waited, abandon this one before
|
|
219
|
+
// it writes its query to the store
|
|
220
|
+
if (thisLoadId !== this.lastLoadId) {
|
|
221
|
+
return
|
|
222
|
+
}
|
|
211
223
|
this.features = {
|
|
212
224
|
supportsSearch: !!features?.supportsSearch,
|
|
213
225
|
supportsSort: !!features?.supportsSort,
|
|
@@ -259,6 +271,11 @@ export default abstract class BaseDataFetch<
|
|
|
259
271
|
|
|
260
272
|
// Actually fetch data
|
|
261
273
|
const page = await this.getPage()
|
|
274
|
+
|
|
275
|
+
// In case fetching takes longer than a newer load, abandon these results
|
|
276
|
+
if (thisLoadId !== this.lastLoadId) {
|
|
277
|
+
return
|
|
278
|
+
}
|
|
262
279
|
this.store.update($store => ({
|
|
263
280
|
...$store,
|
|
264
281
|
loading: false,
|
package/src/utils/index.ts
CHANGED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { StrippedUser, User } from "@budibase/types"
|
|
2
|
+
import { APIClient } from "../api"
|
|
3
|
+
|
|
4
|
+
export type PickerUser = User | StrippedUser | { _id: string; email: string }
|
|
5
|
+
|
|
6
|
+
// Resolves user ids that fall outside a picker's first page. Ids that no
|
|
7
|
+
// longer match a user get a placeholder so callers stop re-requesting them
|
|
8
|
+
// and still have something to render.
|
|
9
|
+
export const fetchUsersById = async (
|
|
10
|
+
API: APIClient,
|
|
11
|
+
ids: string[]
|
|
12
|
+
): Promise<Record<string, PickerUser>> => {
|
|
13
|
+
const wanted = Array.from(new Set(ids.filter(Boolean)))
|
|
14
|
+
const users: Record<string, PickerUser> = {}
|
|
15
|
+
if (!wanted.length) {
|
|
16
|
+
return users
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const res = await API.searchUsers({
|
|
20
|
+
query: { oneOf: { _id: wanted } },
|
|
21
|
+
limit: wanted.length,
|
|
22
|
+
})
|
|
23
|
+
for (const user of res?.data || []) {
|
|
24
|
+
if (user?._id) {
|
|
25
|
+
users[user._id] = user
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
for (const id of wanted) {
|
|
29
|
+
if (!users[id]) {
|
|
30
|
+
users[id] = { _id: id, email: id }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error("Failed to load users by id", error)
|
|
35
|
+
}
|
|
36
|
+
return users
|
|
37
|
+
}
|