@agent-native/core 0.171.1 → 0.172.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/corpus/README.md +1 -1
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +4 -5
- 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/agent/actions/set-tool-approval-policy.d.ts +8 -0
- package/dist/agent/actions/set-tool-approval-policy.js +26 -0
- package/dist/agent/harness/migrations.js +8 -0
- package/dist/agent/harness/runner.js +114 -33
- package/dist/agent/harness/store.d.ts +9 -0
- package/dist/agent/harness/store.js +87 -23
- package/dist/agent/production-agent.d.ts +3 -0
- package/dist/agent/production-agent.js +23 -1
- package/dist/agent/run-manager.js +40 -9
- package/dist/agent/tool-approval-migrations.d.ts +21 -0
- package/dist/agent/tool-approval-migrations.js +32 -0
- package/dist/agent/tool-approval-store.d.ts +11 -0
- package/dist/agent/tool-approval-store.js +76 -1
- package/dist/cli/code-agent-executor.js +20 -3
- package/dist/client/AgentPanel.d.ts +4 -1
- package/dist/client/AgentPanel.js +11 -3
- package/dist/client/AssistantChat.d.ts +5 -5
- package/dist/client/AssistantChat.js +24 -11
- package/dist/client/DefaultSpinner.d.ts +3 -1
- package/dist/client/DefaultSpinner.js +2 -2
- package/dist/client/chat/tool-call-display.d.ts +5 -5
- package/dist/client/chat/tool-call-display.js +27 -11
- 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/resources/McpIntegrationDialog.js +5 -5
- package/dist/client/resources/mcp-integration-catalog.js +1 -1
- package/dist/coding-tools/index.d.ts +31 -0
- package/dist/coding-tools/index.js +139 -3
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/localization/core-messages/ar-SA.js +3 -0
- package/dist/localization/core-messages/de-DE.js +3 -0
- package/dist/localization/core-messages/en-US.d.ts +3 -0
- package/dist/localization/core-messages/en-US.js +3 -0
- package/dist/localization/core-messages/es-ES.js +3 -0
- package/dist/localization/core-messages/fr-FR.js +3 -0
- package/dist/localization/core-messages/hi-IN.js +3 -0
- package/dist/localization/core-messages/ja-JP.js +3 -0
- package/dist/localization/core-messages/ko-KR.js +3 -0
- package/dist/localization/core-messages/pt-BR.js +3 -0
- package/dist/localization/core-messages/zh-CN.js +3 -0
- package/dist/localization/core-messages/zh-TW.js +3 -0
- package/dist/localization/core-messages.d.ts +3 -0
- package/dist/localization/default-messages.d.ts +1 -0
- package/dist/localization/default-messages.js +7 -6
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
- package/dist/provider-api/actions/provider-api.d.ts +11 -11
- package/dist/server/action-discovery.js +5 -0
- package/dist/server/agent-chat/prompt-resources.d.ts +9 -0
- package/dist/server/agent-chat/prompt-resources.js +12 -2
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/release-schema.js +4 -1
- package/dist/triggers/dispatcher.d.ts +24 -0
- package/dist/triggers/dispatcher.js +76 -18
- package/dist/vite/action-types-plugin.js +4 -0
- package/package.json +2 -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(
|
|
@@ -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(() => {
|
|
@@ -13,16 +13,18 @@ export interface PeopleSearchResponse {
|
|
|
13
13
|
scopeRequired?: boolean;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
export type PeopleSearchScope = "all" | "directory";
|
|
17
|
+
|
|
16
18
|
const PEOPLE_CONTACTS_STALE_TIME = 10 * 60_000;
|
|
17
19
|
const PEOPLE_CONTACTS_GC_TIME = 30 * 60_000;
|
|
18
20
|
const PEOPLE_SEARCH_STALE_TIME = 60_000;
|
|
19
21
|
const PEOPLE_SEARCH_GC_TIME = 5 * 60_000;
|
|
20
22
|
|
|
21
|
-
export
|
|
22
|
-
"action",
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
export function peopleContactsQueryKey(scope: PeopleSearchScope = "all") {
|
|
24
|
+
return ["action", "search-people", { scope }] as const;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const PEOPLE_CONTACTS_QUERY_KEY = peopleContactsQueryKey();
|
|
26
28
|
|
|
27
29
|
function sourceRank(source?: PeopleSearchResult["source"]) {
|
|
28
30
|
switch (source) {
|
|
@@ -115,13 +117,14 @@ export function filterPeopleResults(
|
|
|
115
117
|
|
|
116
118
|
export function prefetchPeopleContacts(
|
|
117
119
|
queryClient: ReturnType<typeof useQueryClient>,
|
|
120
|
+
scope: PeopleSearchScope = "all",
|
|
118
121
|
) {
|
|
119
122
|
return queryClient.prefetchQuery({
|
|
120
|
-
queryKey:
|
|
123
|
+
queryKey: peopleContactsQueryKey(scope),
|
|
121
124
|
queryFn: () =>
|
|
122
125
|
callAction<PeopleSearchResponse>(
|
|
123
126
|
"search-people",
|
|
124
|
-
{ scope
|
|
127
|
+
{ scope },
|
|
125
128
|
{ method: "GET" },
|
|
126
129
|
),
|
|
127
130
|
staleTime: PEOPLE_CONTACTS_STALE_TIME,
|
|
@@ -129,13 +132,16 @@ export function prefetchPeopleContacts(
|
|
|
129
132
|
});
|
|
130
133
|
}
|
|
131
134
|
|
|
132
|
-
export function usePeopleContacts(
|
|
135
|
+
export function usePeopleContacts(
|
|
136
|
+
scope: PeopleSearchScope = "all",
|
|
137
|
+
enabled = true,
|
|
138
|
+
) {
|
|
133
139
|
return useQuery({
|
|
134
|
-
queryKey:
|
|
140
|
+
queryKey: peopleContactsQueryKey(scope),
|
|
135
141
|
queryFn: () =>
|
|
136
142
|
callAction<PeopleSearchResponse>(
|
|
137
143
|
"search-people",
|
|
138
|
-
{ scope
|
|
144
|
+
{ scope },
|
|
139
145
|
{ method: "GET" },
|
|
140
146
|
),
|
|
141
147
|
enabled,
|
|
@@ -145,14 +151,18 @@ export function usePeopleContacts(enabled = true) {
|
|
|
145
151
|
});
|
|
146
152
|
}
|
|
147
153
|
|
|
148
|
-
export function usePeopleSearch(
|
|
154
|
+
export function usePeopleSearch(
|
|
155
|
+
query: string,
|
|
156
|
+
enabled = true,
|
|
157
|
+
scope: PeopleSearchScope = "all",
|
|
158
|
+
) {
|
|
149
159
|
const q = query.trim();
|
|
150
160
|
return useQuery({
|
|
151
|
-
queryKey: ["action", "search-people", { q, scope
|
|
161
|
+
queryKey: ["action", "search-people", { q, scope }],
|
|
152
162
|
queryFn: () =>
|
|
153
163
|
callAction<PeopleSearchResponse>(
|
|
154
164
|
"search-people",
|
|
155
|
-
{ q, scope
|
|
165
|
+
{ q, scope },
|
|
156
166
|
{ method: "GET" },
|
|
157
167
|
),
|
|
158
168
|
enabled: enabled && q.length > 0,
|
|
@@ -112,18 +112,33 @@ export function computeTimedEventLayout(
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
const totalCols = overlapLayers.length;
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
const sameStartGroups = new Map<number, EventEntry[]>();
|
|
116
|
+
const sameStartIndexes = new Map<EventEntry, number>();
|
|
117
|
+
for (const entry of group) {
|
|
118
|
+
const entries = sameStartGroups.get(entry.bounds.start) ?? [];
|
|
119
|
+
sameStartIndexes.set(entry, entries.length);
|
|
120
|
+
entries.push(entry);
|
|
121
|
+
sameStartGroups.set(entry.bounds.start, entries);
|
|
122
|
+
}
|
|
119
123
|
|
|
120
124
|
for (const entry of group) {
|
|
121
125
|
const col = eventColumns.get(entry.event)!;
|
|
126
|
+
const sameStartEntries = sameStartGroups.get(entry.bounds.start)!;
|
|
127
|
+
const sameStartIndex = sameStartIndexes.get(entry)!;
|
|
128
|
+
const sameStartWidth = 100 / sameStartEntries.length;
|
|
129
|
+
const isGroupStart = entry.bounds.start === group[0].bounds.start;
|
|
130
|
+
const useStartLanes = sameStartEntries.length > 1;
|
|
131
|
+
|
|
132
|
+
// Later events sit over the earlier event instead of forcing both cards
|
|
133
|
+
// into equal columns. Events with the same start time still get lanes so
|
|
134
|
+
// their titles remain independently readable.
|
|
135
|
+
const left = useStartLanes ? sameStartIndex * sameStartWidth : 0;
|
|
136
|
+
const width = 100 - left;
|
|
122
137
|
|
|
123
138
|
result.set(entry.event.id, {
|
|
124
|
-
left
|
|
139
|
+
left,
|
|
125
140
|
width,
|
|
126
|
-
indent:
|
|
141
|
+
indent: isGroupStart && sameStartIndex === 0 ? 0 : OVERLAP_INDENT_PX,
|
|
127
142
|
col,
|
|
128
143
|
totalCols,
|
|
129
144
|
stackOrder: stackOrder++,
|
|
@@ -896,6 +896,9 @@ export default function CalendarView() {
|
|
|
896
896
|
createEvent.mutate(payload, {
|
|
897
897
|
onSuccess: (result) => {
|
|
898
898
|
discardedCommittingDraftsRef.current.delete(draftId);
|
|
899
|
+
if (result?.videoConferenceError === "zoom") {
|
|
900
|
+
toast.error(t("eventForm.zoomAddFailed"));
|
|
901
|
+
}
|
|
899
902
|
const createdEventId = result?.id;
|
|
900
903
|
if (createdEventId) {
|
|
901
904
|
const undo = () => {
|
|
@@ -130,6 +130,14 @@ export function isGoogleNotFoundError(error: unknown): boolean {
|
|
|
130
130
|
);
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
export function isGoogleEventAbsentError(error: unknown): boolean {
|
|
134
|
+
return (
|
|
135
|
+
isGoogleNotFoundError(error) ||
|
|
136
|
+
(error instanceof GoogleApiError && error.status === 410) ||
|
|
137
|
+
(error instanceof Error && /^Google API error \(410\):/.test(error.message))
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
133
141
|
export async function googleFetch(
|
|
134
142
|
url: string,
|
|
135
143
|
accessToken: string,
|
|
@@ -364,6 +372,7 @@ export function calendarListEvents(
|
|
|
364
372
|
timeMax?: string;
|
|
365
373
|
q?: string;
|
|
366
374
|
singleEvents?: boolean;
|
|
375
|
+
showDeleted?: boolean;
|
|
367
376
|
orderBy?: string;
|
|
368
377
|
maxResults?: number;
|
|
369
378
|
pageToken?: string;
|