@agent-native/core 0.171.0 → 0.171.3
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/corpus/README.md +1 -1
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +4 -5
- package/corpus/templates/analytics/server/lib/workspace-feature-flags.ts +84 -20
- package/corpus/templates/calendar/actions/create-event.ts +11 -3
- package/corpus/templates/calendar/app/components/calendar/AddCalendarDialog.tsx +56 -51
- package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +3 -0
- package/corpus/templates/calendar/app/components/calendar/DayView.tsx +1 -1
- package/corpus/templates/calendar/app/components/calendar/PeopleSearchDialog.tsx +54 -51
- package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +1 -1
- package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +4 -1
- package/corpus/templates/calendar/app/hooks/use-people.ts +23 -13
- package/corpus/templates/calendar/app/lib/event-layout.ts +21 -6
- package/corpus/templates/calendar/app/pages/CalendarView.tsx +3 -0
- package/corpus/templates/calendar/changelog/2026-08-24-overlapping-events-use-space-better.md +5 -0
- package/corpus/templates/calendar/server/lib/google-api.ts +9 -0
- package/corpus/templates/calendar/server/lib/google-calendar.ts +59 -2
- package/corpus/templates/calendar/server/lib/people-search.ts +43 -28
- package/corpus/templates/calendar/shared/api.ts +2 -0
- package/corpus/templates/clips/app/components/player/playback-comment-overlay.tsx +3 -3
- package/corpus/templates/mail/app/root.tsx +29 -0
- package/dist/action.d.ts +4 -1
- package/dist/action.js +2 -0
- package/dist/client/DefaultSpinner.d.ts +3 -1
- package/dist/client/DefaultSpinner.js +2 -2
- package/dist/client/chat-first/apps-rail.d.ts +1 -1
- package/dist/client/chat-first/apps-rail.js +11 -8
- package/dist/client/chat-first/copy.js +1 -0
- package/dist/client/chat-first/types.d.ts +1 -0
- package/dist/client/use-action.js +8 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/action-routes.js +6 -2
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
package/corpus/README.md
CHANGED
|
@@ -2450,17 +2450,15 @@ export function Sidebar({ mobile }: { mobile?: boolean } = {}) {
|
|
|
2450
2450
|
: "text-muted-foreground hover:bg-sidebar-accent/50",
|
|
2451
2451
|
)}
|
|
2452
2452
|
>
|
|
2453
|
-
<
|
|
2454
|
-
|
|
2455
|
-
onClick={toggleDashOpen}
|
|
2453
|
+
<Link
|
|
2454
|
+
to="/dashboards"
|
|
2456
2455
|
className="flex min-w-0 flex-1 items-center gap-3 px-3 py-2 text-start"
|
|
2457
|
-
aria-expanded={dashOpen}
|
|
2458
2456
|
>
|
|
2459
2457
|
<IconChartBar className="h-4 w-4 shrink-0" />
|
|
2460
2458
|
<span className="min-w-0 flex-1 truncate">
|
|
2461
2459
|
{t("navigation.dashboards")}
|
|
2462
2460
|
</span>
|
|
2463
|
-
</
|
|
2461
|
+
</Link>
|
|
2464
2462
|
<SidebarSectionSettingsPopover
|
|
2465
2463
|
label={t("navigation.dashboards")}
|
|
2466
2464
|
sortMode={dashboardSortMode}
|
|
@@ -2477,6 +2475,7 @@ export function Sidebar({ mobile }: { mobile?: boolean } = {}) {
|
|
|
2477
2475
|
? t("sidebar.collapseDashboards")
|
|
2478
2476
|
: t("sidebar.expandDashboards")
|
|
2479
2477
|
}
|
|
2478
|
+
aria-expanded={dashOpen}
|
|
2480
2479
|
>
|
|
2481
2480
|
<IconChevronDown
|
|
2482
2481
|
className={cn(
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
|
|
3
3
|
import { signA2AToken } from "@agent-native/core/a2a";
|
|
4
|
+
import {
|
|
5
|
+
ActionContractError,
|
|
6
|
+
AgentActionStopError,
|
|
7
|
+
} from "@agent-native/core/action";
|
|
4
8
|
import { isFeatureFlagEnabled } from "@agent-native/core/feature-flags";
|
|
5
9
|
import { fetchOrgApps, type OrgApp } from "@agent-native/core/mcp";
|
|
6
10
|
import { getOrgDomain } from "@agent-native/core/org";
|
|
@@ -9,6 +13,7 @@ import { VERIFIED_FLEET_FLAG_MUTATIONS } from "../../shared/feature-flags.js";
|
|
|
9
13
|
import type { AnalyticsAdminContext } from "./db-admin-connections.js";
|
|
10
14
|
|
|
11
15
|
const TARGET_TIMEOUT_MS = 3_000;
|
|
16
|
+
const VERIFICATION_ATTEMPTS = 2;
|
|
12
17
|
const CONCURRENCY = 4;
|
|
13
18
|
|
|
14
19
|
export type FleetFlagState =
|
|
@@ -197,6 +202,8 @@ export type WorkspaceFeatureFlagFailurePhase =
|
|
|
197
202
|
| "unsupported-target"
|
|
198
203
|
| "target-action"
|
|
199
204
|
| "persistence"
|
|
205
|
+
| "verification-timeout"
|
|
206
|
+
| "verification-network"
|
|
200
207
|
| "verification";
|
|
201
208
|
|
|
202
209
|
const FAILURE_MESSAGES: Record<WorkspaceFeatureFlagFailurePhase, string> = {
|
|
@@ -211,16 +218,42 @@ const FAILURE_MESSAGES: Record<WorkspaceFeatureFlagFailurePhase, string> = {
|
|
|
211
218
|
"target-action": "The target app could not complete the feature flag action.",
|
|
212
219
|
persistence:
|
|
213
220
|
"The target app did not persist the requested feature flag rules.",
|
|
221
|
+
"verification-timeout":
|
|
222
|
+
"The feature flag change persisted, but the target app timed out during verification.",
|
|
223
|
+
"verification-network":
|
|
224
|
+
"The feature flag change persisted, but Analytics could not reach the target app during verification.",
|
|
214
225
|
verification: "Analytics could not verify the persisted feature flag change.",
|
|
215
226
|
};
|
|
216
227
|
|
|
217
|
-
export class WorkspaceFeatureFlagFailure extends
|
|
228
|
+
export class WorkspaceFeatureFlagFailure extends AgentActionStopError {
|
|
218
229
|
constructor(readonly phase: WorkspaceFeatureFlagFailurePhase) {
|
|
219
|
-
|
|
230
|
+
const message = `[${phase}] ${FAILURE_MESSAGES[phase]}`;
|
|
231
|
+
const errorCode = `workspace_feature_flag_${phase.replace("-", "_")}`;
|
|
232
|
+
super(message, {
|
|
233
|
+
errorCode,
|
|
234
|
+
details: { phase },
|
|
235
|
+
toolResult: JSON.stringify({ error: errorCode, phase, message }),
|
|
236
|
+
});
|
|
220
237
|
this.name = "WorkspaceFeatureFlagFailure";
|
|
221
238
|
}
|
|
222
239
|
}
|
|
223
240
|
|
|
241
|
+
class WorkspaceFeatureFlagSetupFailure extends ActionContractError {
|
|
242
|
+
readonly phase: "directory" | "token-generation";
|
|
243
|
+
|
|
244
|
+
constructor(phase: "directory" | "token-generation") {
|
|
245
|
+
const message = `[${phase}] ${FAILURE_MESSAGES[phase]}`;
|
|
246
|
+
const errorCode = `workspace_feature_flag_${phase.replace("-", "_")}`;
|
|
247
|
+
super(message, {
|
|
248
|
+
errorCode,
|
|
249
|
+
details: { phase },
|
|
250
|
+
statusCode: 503,
|
|
251
|
+
});
|
|
252
|
+
this.name = "WorkspaceFeatureFlagSetupFailure";
|
|
253
|
+
this.phase = phase;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
224
257
|
type TargetFailureReason = "token-generation" | "timeout" | "network";
|
|
225
258
|
|
|
226
259
|
class TargetCallFailure extends Error {
|
|
@@ -242,6 +275,16 @@ export function classifyWorkspaceFeatureFlagTargetFailure(
|
|
|
242
275
|
return "network";
|
|
243
276
|
}
|
|
244
277
|
|
|
278
|
+
function isSyntaxError(error: unknown): boolean {
|
|
279
|
+
return (
|
|
280
|
+
error instanceof SyntaxError ||
|
|
281
|
+
(!!error &&
|
|
282
|
+
typeof error === "object" &&
|
|
283
|
+
"name" in error &&
|
|
284
|
+
error.name === "SyntaxError")
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
245
288
|
function targetFailure(error: unknown): WorkspaceFeatureFlagFailure {
|
|
246
289
|
const reason = classifyWorkspaceFeatureFlagTargetFailure(error);
|
|
247
290
|
return new WorkspaceFeatureFlagFailure(reason);
|
|
@@ -259,10 +302,10 @@ async function resolveTargetApp(
|
|
|
259
302
|
serviceOrgId: admin.orgId,
|
|
260
303
|
});
|
|
261
304
|
} catch {
|
|
262
|
-
throw new
|
|
305
|
+
throw new WorkspaceFeatureFlagSetupFailure("directory");
|
|
263
306
|
}
|
|
264
307
|
const app = apps.find((candidate) => candidate.id === appId);
|
|
265
|
-
if (!app) throw new
|
|
308
|
+
if (!app) throw new WorkspaceFeatureFlagSetupFailure("directory");
|
|
266
309
|
return app;
|
|
267
310
|
}
|
|
268
311
|
|
|
@@ -302,12 +345,41 @@ async function callTarget(
|
|
|
302
345
|
let parsed: unknown = null;
|
|
303
346
|
try {
|
|
304
347
|
parsed = await response.json();
|
|
305
|
-
} catch {
|
|
306
|
-
|
|
348
|
+
} catch (error) {
|
|
349
|
+
const successfulResponse = response.status >= 200 && response.status < 300;
|
|
350
|
+
if (successfulResponse && !isSyntaxError(error))
|
|
351
|
+
throw new TargetCallFailure(
|
|
352
|
+
classifyWorkspaceFeatureFlagTargetFailure(error),
|
|
353
|
+
);
|
|
354
|
+
// Invalid legacy payloads and non-success statuses are classified below.
|
|
307
355
|
}
|
|
308
356
|
return { status: response.status, body: parsed };
|
|
309
357
|
}
|
|
310
358
|
|
|
359
|
+
async function readBackTarget(
|
|
360
|
+
app: OrgApp,
|
|
361
|
+
admin: AnalyticsAdminContext,
|
|
362
|
+
orgDomain: string,
|
|
363
|
+
): Promise<{ status: number; body: unknown }> {
|
|
364
|
+
for (let attempt = 1; attempt <= VERIFICATION_ATTEMPTS; attempt += 1) {
|
|
365
|
+
try {
|
|
366
|
+
return await callTarget(app, admin, "list-feature-flags", {}, orgDomain);
|
|
367
|
+
} catch (error) {
|
|
368
|
+
const reason = classifyWorkspaceFeatureFlagTargetFailure(error);
|
|
369
|
+
const retryable = reason === "timeout" || reason === "network";
|
|
370
|
+
if (retryable && attempt < VERIFICATION_ATTEMPTS) continue;
|
|
371
|
+
throw new WorkspaceFeatureFlagFailure(
|
|
372
|
+
reason === "timeout"
|
|
373
|
+
? "verification-timeout"
|
|
374
|
+
: reason === "network"
|
|
375
|
+
? "verification-network"
|
|
376
|
+
: reason,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
throw new WorkspaceFeatureFlagFailure("verification");
|
|
381
|
+
}
|
|
382
|
+
|
|
311
383
|
export function classifyWorkspaceFeatureFlagList(
|
|
312
384
|
app: OrgApp,
|
|
313
385
|
result: { status: number; body: unknown },
|
|
@@ -404,9 +476,10 @@ export async function setWorkspaceFeatureFlag(
|
|
|
404
476
|
try {
|
|
405
477
|
orgDomain = (await getOrgDomain(admin.orgId))?.trim().toLowerCase();
|
|
406
478
|
} catch {
|
|
407
|
-
throw new
|
|
479
|
+
throw new WorkspaceFeatureFlagSetupFailure("token-generation");
|
|
408
480
|
}
|
|
409
|
-
if (!orgDomain)
|
|
481
|
+
if (!orgDomain)
|
|
482
|
+
throw new WorkspaceFeatureFlagSetupFailure("token-generation");
|
|
410
483
|
let result: Awaited<ReturnType<typeof callTarget>>;
|
|
411
484
|
try {
|
|
412
485
|
result = await callTarget(
|
|
@@ -417,6 +490,8 @@ export async function setWorkspaceFeatureFlag(
|
|
|
417
490
|
orgDomain,
|
|
418
491
|
);
|
|
419
492
|
} catch (error) {
|
|
493
|
+
if (classifyWorkspaceFeatureFlagTargetFailure(error) === "token-generation")
|
|
494
|
+
throw new WorkspaceFeatureFlagSetupFailure("token-generation");
|
|
420
495
|
throw targetFailure(error);
|
|
421
496
|
}
|
|
422
497
|
if (result.status === 401 || result.status === 403)
|
|
@@ -465,18 +540,7 @@ export async function setWorkspaceFeatureFlag(
|
|
|
465
540
|
return mutation;
|
|
466
541
|
}
|
|
467
542
|
|
|
468
|
-
|
|
469
|
-
try {
|
|
470
|
-
readBack = await callTarget(
|
|
471
|
-
app,
|
|
472
|
-
admin,
|
|
473
|
-
"list-feature-flags",
|
|
474
|
-
{},
|
|
475
|
-
orgDomain,
|
|
476
|
-
);
|
|
477
|
-
} catch (error) {
|
|
478
|
-
throw targetFailure(error);
|
|
479
|
-
}
|
|
543
|
+
const readBack = await readBackTarget(app, admin, orgDomain);
|
|
480
544
|
if (readBack.status === 401 || readBack.status === 403)
|
|
481
545
|
throw new WorkspaceFeatureFlagFailure("authorization");
|
|
482
546
|
if (readBack.status === 404 || readBack.status === 405)
|
|
@@ -216,10 +216,16 @@ export default defineAction({
|
|
|
216
216
|
};
|
|
217
217
|
|
|
218
218
|
let zoomMeetingLink: string | undefined;
|
|
219
|
+
let videoConferenceError: CalendarEvent["videoConferenceError"];
|
|
219
220
|
if (args.addZoom) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
221
|
+
try {
|
|
222
|
+
const zoom = await prepareZoomMeetingPatch(email, calEvent);
|
|
223
|
+
zoomMeetingLink = zoom.meetingLink;
|
|
224
|
+
Object.assign(calEvent, zoom.patch);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
videoConferenceError = "zoom";
|
|
227
|
+
console.error("[create-event] Zoom meeting provisioning failed", error);
|
|
228
|
+
}
|
|
223
229
|
}
|
|
224
230
|
|
|
225
231
|
const result = await googleCalendar.createEvent(calEvent, {
|
|
@@ -238,6 +244,8 @@ export default defineAction({
|
|
|
238
244
|
if (result.meetLink) calEvent.hangoutLink = result.meetLink;
|
|
239
245
|
if (result.conferenceData) calEvent.conferenceData = result.conferenceData;
|
|
240
246
|
if (zoomMeetingLink) calEvent.meetingLink = zoomMeetingLink;
|
|
247
|
+
if (videoConferenceError)
|
|
248
|
+
calEvent.videoConferenceError = videoConferenceError;
|
|
241
249
|
|
|
242
250
|
try {
|
|
243
251
|
emit(
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { callAction } from "@agent-native/core/client/hooks";
|
|
2
1
|
import { useT } from "@agent-native/core/client/i18n";
|
|
3
2
|
import {
|
|
4
3
|
IconSearch,
|
|
@@ -8,7 +7,7 @@ import {
|
|
|
8
7
|
IconLink,
|
|
9
8
|
IconCalendarPlus,
|
|
10
9
|
} from "@tabler/icons-react";
|
|
11
|
-
import { useState, useEffect,
|
|
10
|
+
import { useState, useEffect, useMemo, useRef } from "react";
|
|
12
11
|
import { toast } from "sonner";
|
|
13
12
|
|
|
14
13
|
import { Button } from "@/components/ui/button";
|
|
@@ -31,17 +30,12 @@ import {
|
|
|
31
30
|
useAddOverlayPerson,
|
|
32
31
|
useRemoveOverlayPerson,
|
|
33
32
|
} from "@/hooks/use-overlay-people";
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
interface SearchResponse {
|
|
42
|
-
results: SearchResult[];
|
|
43
|
-
scopeRequired?: boolean;
|
|
44
|
-
}
|
|
33
|
+
import {
|
|
34
|
+
filterPeopleResults,
|
|
35
|
+
mergePeopleResults,
|
|
36
|
+
usePeopleContacts,
|
|
37
|
+
usePeopleSearch,
|
|
38
|
+
} from "@/hooks/use-people";
|
|
45
39
|
|
|
46
40
|
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
47
41
|
const URL_REGEX = /^(https?|webcal):\/\/.+/i;
|
|
@@ -67,7 +61,7 @@ export function AddCalendarDialog({
|
|
|
67
61
|
|
|
68
62
|
return (
|
|
69
63
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
70
|
-
<DialogContent className="sm:max-w-[440px] gap-0 p-0 top-[
|
|
64
|
+
<DialogContent className="sm:max-w-[440px] max-h-[calc(85vh-1rem)] gap-0 overflow-y-auto p-0 top-[15vh] !translate-y-0">
|
|
71
65
|
<DialogHeader className="px-4 pt-4 pb-0">
|
|
72
66
|
<DialogTitle className="text-base">
|
|
73
67
|
{t("eventForm.addCalendar")}
|
|
@@ -89,7 +83,7 @@ export function AddCalendarDialog({
|
|
|
89
83
|
</TabsList>
|
|
90
84
|
|
|
91
85
|
<TabsContent value="people" className="mt-0">
|
|
92
|
-
<PeopleTab
|
|
86
|
+
<PeopleTab open={open} />
|
|
93
87
|
</TabsContent>
|
|
94
88
|
|
|
95
89
|
<TabsContent value="url" className="mt-0 px-4 pb-4 pt-3">
|
|
@@ -103,15 +97,12 @@ export function AddCalendarDialog({
|
|
|
103
97
|
|
|
104
98
|
// ─── People tab ──────────────────────────────────────────────────────────────
|
|
105
99
|
|
|
106
|
-
function PeopleTab({
|
|
100
|
+
function PeopleTab({ open }: { open: boolean }) {
|
|
107
101
|
const t = useT();
|
|
108
102
|
const [query, setQuery] = useState("");
|
|
109
|
-
const [
|
|
110
|
-
const [scopeRequired, setScopeRequired] = useState(false);
|
|
111
|
-
const [searching, setSearching] = useState(false);
|
|
103
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
112
104
|
const [activeIndex, setActiveIndex] = useState(-1);
|
|
113
105
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
114
|
-
const debounceRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
|
115
106
|
const listRef = useRef<HTMLDivElement>(null);
|
|
116
107
|
const shouldScrollActiveResultRef = useRef(false);
|
|
117
108
|
|
|
@@ -119,46 +110,57 @@ function PeopleTab({ onClose: _ }: { onClose: () => void }) {
|
|
|
119
110
|
const overlayPeople = Array.isArray(rawOverlayPeople) ? rawOverlayPeople : [];
|
|
120
111
|
const addPerson = useAddOverlayPerson();
|
|
121
112
|
const removePerson = useRemoveOverlayPerson();
|
|
113
|
+
const contacts = usePeopleContacts("directory", open);
|
|
114
|
+
const directorySearch = usePeopleSearch(searchQuery, open, "directory");
|
|
122
115
|
|
|
123
|
-
const overlayEmails =
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
116
|
+
const overlayEmails = useMemo(
|
|
117
|
+
() => new Set(overlayPeople.map((p) => p.email.toLowerCase())),
|
|
118
|
+
[overlayPeople],
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const results = useMemo(
|
|
122
|
+
() =>
|
|
123
|
+
filterPeopleResults(
|
|
124
|
+
mergePeopleResults(
|
|
125
|
+
contacts.data?.results,
|
|
126
|
+
directorySearch.data?.results,
|
|
127
|
+
),
|
|
128
|
+
query,
|
|
129
|
+
new Set(),
|
|
130
|
+
),
|
|
131
|
+
[contacts.data?.results, directorySearch.data?.results, query],
|
|
132
|
+
);
|
|
133
|
+
const selectableResults = results.filter(
|
|
134
|
+
(r) => !overlayEmails.has(r.email.toLowerCase()),
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const searching =
|
|
138
|
+
contacts.isLoading ||
|
|
139
|
+
contacts.isFetching ||
|
|
140
|
+
directorySearch.isLoading ||
|
|
141
|
+
directorySearch.isFetching;
|
|
142
|
+
const scopeRequired = Boolean(
|
|
143
|
+
contacts.data?.scopeRequired || directorySearch.data?.scopeRequired,
|
|
144
|
+
);
|
|
142
145
|
|
|
143
146
|
useEffect(() => {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}, [query
|
|
147
|
+
const timeout = window.setTimeout(
|
|
148
|
+
() => setSearchQuery(query),
|
|
149
|
+
query.trim() ? 300 : 0,
|
|
150
|
+
);
|
|
151
|
+
return () => window.clearTimeout(timeout);
|
|
152
|
+
}, [query]);
|
|
150
153
|
|
|
151
154
|
useEffect(() => {
|
|
152
155
|
setActiveIndex(results.length > 0 ? 0 : -1);
|
|
153
156
|
}, [results]);
|
|
154
157
|
|
|
155
158
|
useEffect(() => {
|
|
159
|
+
if (!open) return;
|
|
156
160
|
setQuery("");
|
|
157
|
-
|
|
158
|
-
setScopeRequired(false);
|
|
161
|
+
setSearchQuery("");
|
|
159
162
|
setActiveIndex(-1);
|
|
160
|
-
|
|
161
|
-
}, [search]);
|
|
163
|
+
}, [open]);
|
|
162
164
|
|
|
163
165
|
useEffect(() => {
|
|
164
166
|
if (
|
|
@@ -198,7 +200,10 @@ function PeopleTab({ onClose: _ }: { onClose: () => void }) {
|
|
|
198
200
|
return;
|
|
199
201
|
}
|
|
200
202
|
const trimmed = query.trim();
|
|
201
|
-
if (
|
|
203
|
+
if (
|
|
204
|
+
EMAIL_REGEX.test(trimmed) &&
|
|
205
|
+
!overlayEmails.has(trimmed.toLowerCase())
|
|
206
|
+
) {
|
|
202
207
|
handleAdd(trimmed);
|
|
203
208
|
setQuery("");
|
|
204
209
|
}
|
|
@@ -229,7 +234,7 @@ function PeopleTab({ onClose: _ }: { onClose: () => void }) {
|
|
|
229
234
|
className="max-h-48 overflow-y-auto border-t border-border"
|
|
230
235
|
>
|
|
231
236
|
{results.map((person) => {
|
|
232
|
-
const alreadyAdded = overlayEmails.has(person.email);
|
|
237
|
+
const alreadyAdded = overlayEmails.has(person.email.toLowerCase());
|
|
233
238
|
const selectableIdx = selectableResults.indexOf(person);
|
|
234
239
|
const isActive = !alreadyAdded && selectableIdx === activeIndex;
|
|
235
240
|
return (
|
|
@@ -856,6 +856,9 @@ export function CreateEventPopover({
|
|
|
856
856
|
deletePersistedDraft(activeDraftId);
|
|
857
857
|
onDraftCreated?.(activeDraftId);
|
|
858
858
|
}
|
|
859
|
+
if (result?.videoConferenceError === "zoom") {
|
|
860
|
+
toast.error(t("eventForm.zoomAddFailed"));
|
|
861
|
+
}
|
|
859
862
|
const eventId = result?.id;
|
|
860
863
|
const undo = eventId
|
|
861
864
|
? () => {
|
|
@@ -297,7 +297,7 @@ const DayEventCard = memo(function DayEventCard({
|
|
|
297
297
|
style={{
|
|
298
298
|
...posStyle,
|
|
299
299
|
left: `calc(${li.left}% + ${li.indent}px)`,
|
|
300
|
-
width: `calc(${li.width}% - ${li.indent
|
|
300
|
+
width: `calc(${li.width}% - ${li.indent + 2}px)`,
|
|
301
301
|
zIndex:
|
|
302
302
|
isBeingDragged && isDragging
|
|
303
303
|
? 100
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { callAction } from "@agent-native/core/client/hooks";
|
|
2
1
|
import { useT } from "@agent-native/core/client/i18n";
|
|
3
2
|
import {
|
|
4
3
|
IconSearch,
|
|
@@ -6,7 +5,7 @@ import {
|
|
|
6
5
|
IconUserPlus,
|
|
7
6
|
IconLoader2,
|
|
8
7
|
} from "@tabler/icons-react";
|
|
9
|
-
import { useState, useEffect,
|
|
8
|
+
import { useState, useEffect, useMemo, useRef } from "react";
|
|
10
9
|
|
|
11
10
|
import {
|
|
12
11
|
Dialog,
|
|
@@ -19,17 +18,12 @@ import {
|
|
|
19
18
|
useAddOverlayPerson,
|
|
20
19
|
useRemoveOverlayPerson,
|
|
21
20
|
} from "@/hooks/use-overlay-people";
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
interface SearchResponse {
|
|
30
|
-
results: SearchResult[];
|
|
31
|
-
scopeRequired?: boolean;
|
|
32
|
-
}
|
|
21
|
+
import {
|
|
22
|
+
filterPeopleResults,
|
|
23
|
+
mergePeopleResults,
|
|
24
|
+
usePeopleContacts,
|
|
25
|
+
usePeopleSearch,
|
|
26
|
+
} from "@/hooks/use-people";
|
|
33
27
|
|
|
34
28
|
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
35
29
|
|
|
@@ -44,12 +38,8 @@ export function PeopleSearchDialog({
|
|
|
44
38
|
}: PeopleSearchDialogProps) {
|
|
45
39
|
const t = useT();
|
|
46
40
|
const [query, setQuery] = useState("");
|
|
47
|
-
const [
|
|
48
|
-
const [scopeRequired, setScopeRequired] = useState(false);
|
|
49
|
-
const [searching, setSearching] = useState(false);
|
|
41
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
50
42
|
const [activeIndex, setActiveIndex] = useState(-1);
|
|
51
|
-
const inputRef = useRef<HTMLInputElement>(null);
|
|
52
|
-
const debounceRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
|
53
43
|
const listRef = useRef<HTMLDivElement>(null);
|
|
54
44
|
const shouldScrollActiveResultRef = useRef(false);
|
|
55
45
|
|
|
@@ -57,52 +47,61 @@ export function PeopleSearchDialog({
|
|
|
57
47
|
const overlayPeople = Array.isArray(rawOverlayPeople) ? rawOverlayPeople : [];
|
|
58
48
|
const addPerson = useAddOverlayPerson();
|
|
59
49
|
const removePerson = useRemoveOverlayPerson();
|
|
50
|
+
const contacts = usePeopleContacts("directory", open);
|
|
51
|
+
const directorySearch = usePeopleSearch(searchQuery, open, "directory");
|
|
52
|
+
|
|
53
|
+
const overlayEmails = useMemo(
|
|
54
|
+
() => new Set(overlayPeople.map((p) => p.email.toLowerCase())),
|
|
55
|
+
[overlayPeople],
|
|
56
|
+
);
|
|
60
57
|
|
|
61
|
-
const
|
|
58
|
+
const results = useMemo(
|
|
59
|
+
() =>
|
|
60
|
+
filterPeopleResults(
|
|
61
|
+
mergePeopleResults(
|
|
62
|
+
contacts.data?.results,
|
|
63
|
+
directorySearch.data?.results,
|
|
64
|
+
),
|
|
65
|
+
query,
|
|
66
|
+
new Set(),
|
|
67
|
+
),
|
|
68
|
+
[contacts.data?.results, directorySearch.data?.results, query],
|
|
69
|
+
);
|
|
62
70
|
|
|
63
71
|
// Selectable results (exclude already-added)
|
|
64
|
-
const selectableResults = results.filter(
|
|
72
|
+
const selectableResults = results.filter(
|
|
73
|
+
(r) => !overlayEmails.has(r.email.toLowerCase()),
|
|
74
|
+
);
|
|
65
75
|
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
setResults(data.results ?? []);
|
|
75
|
-
setScopeRequired(data.scopeRequired ?? false);
|
|
76
|
-
} catch {
|
|
77
|
-
// ignore
|
|
78
|
-
} finally {
|
|
79
|
-
setSearching(false);
|
|
80
|
-
}
|
|
81
|
-
}, []);
|
|
76
|
+
const searching =
|
|
77
|
+
contacts.isLoading ||
|
|
78
|
+
contacts.isFetching ||
|
|
79
|
+
directorySearch.isLoading ||
|
|
80
|
+
directorySearch.isFetching;
|
|
81
|
+
const scopeRequired = Boolean(
|
|
82
|
+
contacts.data?.scopeRequired || directorySearch.data?.scopeRequired,
|
|
83
|
+
);
|
|
82
84
|
|
|
83
85
|
useEffect(() => {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}, [query
|
|
86
|
+
const timeout = window.setTimeout(
|
|
87
|
+
() => setSearchQuery(query),
|
|
88
|
+
query.trim() ? 300 : 0,
|
|
89
|
+
);
|
|
90
|
+
return () => window.clearTimeout(timeout);
|
|
91
|
+
}, [query]);
|
|
90
92
|
|
|
91
93
|
// Reset active index when results change
|
|
92
94
|
useEffect(() => {
|
|
93
95
|
setActiveIndex(results.length > 0 ? 0 : -1);
|
|
94
96
|
}, [results]);
|
|
95
97
|
|
|
96
|
-
// Load org contacts when dialog opens
|
|
97
98
|
useEffect(() => {
|
|
98
99
|
if (open) {
|
|
99
100
|
setQuery("");
|
|
100
|
-
|
|
101
|
-
setScopeRequired(false);
|
|
101
|
+
setSearchQuery("");
|
|
102
102
|
setActiveIndex(-1);
|
|
103
|
-
search("");
|
|
104
103
|
}
|
|
105
|
-
}, [open
|
|
104
|
+
}, [open]);
|
|
106
105
|
|
|
107
106
|
function handleAdd(email: string, name?: string) {
|
|
108
107
|
addPerson.mutate({ email, name });
|
|
@@ -145,7 +144,10 @@ export function PeopleSearchDialog({
|
|
|
145
144
|
}
|
|
146
145
|
// Otherwise, try adding as a typed email
|
|
147
146
|
const trimmed = query.trim();
|
|
148
|
-
if (
|
|
147
|
+
if (
|
|
148
|
+
EMAIL_REGEX.test(trimmed) &&
|
|
149
|
+
!overlayEmails.has(trimmed.toLowerCase())
|
|
150
|
+
) {
|
|
149
151
|
handleAdd(trimmed);
|
|
150
152
|
setQuery("");
|
|
151
153
|
}
|
|
@@ -154,7 +156,7 @@ export function PeopleSearchDialog({
|
|
|
154
156
|
|
|
155
157
|
return (
|
|
156
158
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
157
|
-
<DialogContent className="sm:max-w-[440px] gap-0 p-0">
|
|
159
|
+
<DialogContent className="sm:max-w-[440px] max-h-[calc(85vh-1rem)] gap-0 overflow-y-auto p-0 top-[15vh] !translate-y-0">
|
|
158
160
|
<DialogHeader className="px-4 pt-4 pb-0">
|
|
159
161
|
<DialogTitle className="text-base">
|
|
160
162
|
{t("eventForm.people")}
|
|
@@ -165,7 +167,6 @@ export function PeopleSearchDialog({
|
|
|
165
167
|
<div className="relative px-4 pt-3 pb-2">
|
|
166
168
|
<IconSearch className="absolute left-7 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
167
169
|
<input
|
|
168
|
-
ref={inputRef}
|
|
169
170
|
value={query}
|
|
170
171
|
onChange={(e) => setQuery(e.target.value)}
|
|
171
172
|
onKeyDown={handleKeyDown}
|
|
@@ -185,7 +186,9 @@ export function PeopleSearchDialog({
|
|
|
185
186
|
className="max-h-48 overflow-y-auto border-t border-border"
|
|
186
187
|
>
|
|
187
188
|
{results.map((person) => {
|
|
188
|
-
const alreadyAdded = overlayEmails.has(
|
|
189
|
+
const alreadyAdded = overlayEmails.has(
|
|
190
|
+
person.email.toLowerCase(),
|
|
191
|
+
);
|
|
189
192
|
const selectableIdx = selectableResults.indexOf(person);
|
|
190
193
|
const isActive = !alreadyAdded && selectableIdx === activeIndex;
|
|
191
194
|
return (
|
|
@@ -367,7 +367,7 @@ const WeekEventCard = memo(function WeekEventCard({
|
|
|
367
367
|
style={{
|
|
368
368
|
...style,
|
|
369
369
|
left: `calc(${li.left}% + ${li.indent}px)`,
|
|
370
|
-
width: `calc(${li.width}% - ${li.indent
|
|
370
|
+
width: `calc(${li.width}% - ${li.indent + 2}px)`,
|
|
371
371
|
zIndex:
|
|
372
372
|
isBeingDragged && isDragging
|
|
373
373
|
? 100
|
|
@@ -272,7 +272,10 @@ export function AppLayout({ children }: AppLayoutProps) {
|
|
|
272
272
|
|
|
273
273
|
useEffect(() => {
|
|
274
274
|
if (!hasAccounts) return;
|
|
275
|
-
void
|
|
275
|
+
void Promise.all([
|
|
276
|
+
prefetchPeopleContacts(queryClient),
|
|
277
|
+
prefetchPeopleContacts(queryClient, "directory"),
|
|
278
|
+
]);
|
|
276
279
|
}, [hasAccounts, queryClient]);
|
|
277
280
|
|
|
278
281
|
useEffect(() => {
|