@agent-native/core 0.90.3 → 0.90.5
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/bin/agent-native.js +25 -3
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +17 -0
- package/corpus/core/bin/agent-native.js +25 -3
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/durable-background.ts +18 -18
- package/corpus/core/src/agent/engine/ai-sdk-engine.ts +8 -2
- package/corpus/core/src/agent/engine/anthropic-engine.ts +11 -1
- package/corpus/core/src/agent/engine/index.ts +1 -0
- package/corpus/core/src/agent/engine/registry.ts +63 -2
- package/corpus/core/src/agent/production-agent.ts +22 -11
- package/corpus/core/src/agent/run-manager.ts +17 -6
- package/corpus/core/src/agent/run-store.ts +16 -11
- package/corpus/core/src/cli/create.ts +35 -0
- package/corpus/core/src/cli/design-connect.ts +263 -1
- package/corpus/core/src/cli/index.ts +84 -7
- package/corpus/core/src/cli/workspacify.ts +34 -0
- package/corpus/core/src/client/KeepTabOpenNotice.tsx +20 -15
- package/corpus/core/src/client/RunStuckBanner.tsx +14 -13
- package/corpus/core/src/client/agent-chat-adapter.ts +28 -17
- package/corpus/core/src/client/history/VersionHistoryPanel.tsx +77 -47
- package/corpus/core/src/client/history/index.ts +3 -0
- package/corpus/core/src/client/history/use-history.ts +28 -0
- package/corpus/core/src/client/index.ts +8 -0
- package/corpus/core/src/client/review/index.ts +5 -0
- package/corpus/core/src/client/review/use-review.ts +40 -2
- package/corpus/core/src/client/use-run-stuck-detection.ts +8 -5
- package/corpus/core/src/history/actions/create-resource-version.ts +11 -8
- package/corpus/core/src/history/index.ts +0 -1
- package/corpus/core/src/history/registry.ts +24 -12
- package/corpus/core/src/history/store.ts +84 -53
- package/corpus/core/src/review/actions/consume-review-feedback.ts +15 -5
- package/corpus/core/src/review/actions/delete-review-comment.ts +15 -3
- package/corpus/core/src/review/actions/resolve-review-thread.ts +12 -5
- package/corpus/core/src/review/index.ts +0 -5
- package/corpus/core/src/review/registry.ts +24 -12
- package/corpus/core/src/review/store.ts +10 -7
- package/corpus/core/src/server/agent-chat-plugin.ts +6 -0
- package/corpus/core/src/server/credential-provider.ts +13 -3
- package/corpus/templates/assets/actions/_preset-skeleton-validation.ts +11 -11
- package/corpus/templates/assets/actions/generate-image.ts +64 -28
- package/corpus/templates/assets/app/components/generation/GenerationResults.tsx +1 -1
- package/corpus/templates/assets/app/routes/brand-kits.$id_.presets.$presetId.tsx +91 -74
- package/corpus/templates/assets/server/lib/generation.ts +29 -4
- package/corpus/templates/assets/server/lib/image-processing.ts +131 -0
- package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +24 -2
- package/corpus/templates/calendar/AGENTS.md +3 -0
- package/corpus/templates/calendar/actions/create-event.ts +8 -34
- package/corpus/templates/calendar/actions/event-action-helpers.ts +52 -0
- package/corpus/templates/calendar/actions/manage-event-draft.ts +7 -34
- package/corpus/templates/calendar/actions/update-event.ts +10 -23
- package/corpus/templates/calendar/app/components/calendar/AttendeeAutocomplete.tsx +47 -0
- package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +23 -0
- package/corpus/templates/calendar/app/components/calendar/EventAttendeesSection.tsx +59 -2
- package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +40 -1
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +34 -1
- package/corpus/templates/calendar/app/hooks/use-events.ts +6 -0
- package/corpus/templates/calendar/app/i18n/zh-TW.ts +4 -0
- package/corpus/templates/calendar/app/i18n-data.ts +40 -0
- package/corpus/templates/calendar/changelog/2026-07-07-you-can-mark-event-guests-as-optional-while-inviting-them-or.md +6 -0
- package/corpus/templates/calendar/server/handlers/events.ts +1 -0
- package/corpus/templates/calendar/server/lib/google-calendar.ts +4 -0
- package/corpus/templates/calendar/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/calendar/shared/api.ts +2 -0
- package/corpus/templates/clips/.agents/skills/ai-video-tools/SKILL.md +29 -3
- package/corpus/templates/clips/actions/generate-workflow.ts +19 -8
- package/corpus/templates/clips/actions/get-clips-ai-prefs.ts +36 -0
- package/corpus/templates/clips/actions/lib/clips-ai-prefs.ts +28 -0
- package/corpus/templates/clips/actions/regenerate-chapters.ts +21 -7
- package/corpus/templates/clips/actions/regenerate-summary.ts +26 -8
- package/corpus/templates/clips/actions/regenerate-title.ts +44 -7
- package/corpus/templates/clips/actions/update-clips-ai-prefs.ts +50 -0
- package/corpus/templates/clips/app/components/meetings/meeting-card.tsx +7 -2
- package/corpus/templates/clips/app/hooks/use-auto-title.ts +25 -10
- package/corpus/templates/clips/app/i18n/ar-SA.ts +28 -6
- package/corpus/templates/clips/app/i18n/de-DE.ts +28 -6
- package/corpus/templates/clips/app/i18n/en-US.ts +29 -6
- package/corpus/templates/clips/app/i18n/es-ES.ts +29 -6
- package/corpus/templates/clips/app/i18n/fr-FR.ts +30 -6
- package/corpus/templates/clips/app/i18n/hi-IN.ts +26 -6
- package/corpus/templates/clips/app/i18n/ja-JP.ts +27 -6
- package/corpus/templates/clips/app/i18n/ko-KR.ts +28 -6
- package/corpus/templates/clips/app/i18n/pt-BR.ts +28 -6
- package/corpus/templates/clips/app/i18n/zh-CN.ts +26 -6
- package/corpus/templates/clips/app/i18n/zh-TW.ts +25 -6
- package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +73 -0
- package/corpus/templates/clips/app/routes/_app.settings._index.tsx +1 -0
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +70 -2
- package/corpus/templates/clips/changelog/2026-07-07-ai-tools-can-include-the-full-video-not-just-the-transcript-.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-07-clips-desktop-now-surfaces-meetings-and-dictate-directly-in-.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-07-meeting-notes-now-explain-exactly-how-to-start-granola-style.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-07-meeting-start-alerts-now-include-separate-start-notes-and-jo.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-07-recordings-start-more-predictably-and-newly-saved-clips-refr.md +6 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
- package/corpus/templates/clips/desktop/src/app.tsx +435 -105
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +27 -21
- package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +23 -22
- package/corpus/templates/clips/desktop/src/styles.css +167 -2
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +1 -1
- package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +3 -7
- package/corpus/templates/clips/desktop/src-tauri/src/tray_meetings.rs +5 -4
- package/corpus/templates/clips/server/routes/_agent-native/clips/user-prefs.put.ts +16 -8
- package/corpus/templates/clips/shared/clips-ai-prefs.ts +81 -0
- package/corpus/templates/content/AGENTS.md +1 -0
- package/corpus/templates/content/actions/_database-source-utils.ts +73 -8
- package/corpus/templates/content/actions/_database-utils.ts +2 -1
- package/corpus/templates/content/actions/prepare-builder-source-review.ts +20 -1
- package/corpus/templates/content/actions/stage-builder-source-bulk-update.ts +663 -0
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +33 -7
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +53 -4
- package/corpus/templates/content/app/components/editor/database/settings.tsx +24 -0
- package/corpus/templates/content/app/hooks/use-content-database.ts +70 -1
- package/corpus/templates/content/app/hooks/use-document-properties.ts +63 -5
- package/corpus/templates/content/app/i18n/zh-TW.ts +3 -0
- package/corpus/templates/content/app/i18n-data.ts +31 -0
- package/corpus/templates/content/changelog/2026-07-01-builder-source-rows-can-be-staged-in-bulk-with-a-reviewable.md +6 -0
- package/corpus/templates/content/changelog/2026-07-07-builder-backed-database-fields-now-edit-like-normal-content-fields.md +6 -0
- package/corpus/templates/content/parity/matrix.md +24 -24
- package/corpus/templates/content/parity/matrix.ts +2 -0
- package/corpus/templates/content/shared/api.ts +59 -4
- package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +16 -1
- package/corpus/templates/design/actions/add-localhost-screens.ts +1 -0
- package/corpus/templates/design/actions/connect-localhost.ts +26 -3
- package/corpus/templates/design/actions/open-visual-edit.ts +4 -37
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +362 -12
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +89 -14
- package/corpus/templates/design/app/components/design/bridge-security.ts +7 -1
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +1 -1
- package/corpus/templates/design/app/i18n/zh-TW.ts +3 -0
- package/corpus/templates/design/app/i18n-data.ts +41 -0
- package/corpus/templates/design/changelog/2026-07-07-local-visual-edit-keeps-bridge-access-when-opening-previousl.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-local-visual-edit-now-keeps-the-live-app-connection-authenti.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-local-visual-edit-uses-live-app-iframes-for-click-to-edit-pr.md +6 -0
- package/dist/agent/durable-background.d.ts +12 -12
- package/dist/agent/durable-background.js +18 -18
- package/dist/agent/durable-background.js.map +1 -1
- package/dist/agent/engine/ai-sdk-engine.d.ts.map +1 -1
- package/dist/agent/engine/ai-sdk-engine.js +8 -2
- package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
- package/dist/agent/engine/anthropic-engine.d.ts.map +1 -1
- package/dist/agent/engine/anthropic-engine.js +11 -1
- package/dist/agent/engine/anthropic-engine.js.map +1 -1
- package/dist/agent/engine/index.d.ts +1 -1
- package/dist/agent/engine/index.d.ts.map +1 -1
- package/dist/agent/engine/index.js +1 -1
- package/dist/agent/engine/index.js.map +1 -1
- package/dist/agent/engine/registry.d.ts +14 -0
- package/dist/agent/engine/registry.d.ts.map +1 -1
- package/dist/agent/engine/registry.js +56 -2
- package/dist/agent/engine/registry.js.map +1 -1
- package/dist/agent/production-agent.d.ts +2 -2
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +18 -11
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-manager.d.ts +7 -1
- package/dist/agent/run-manager.d.ts.map +1 -1
- package/dist/agent/run-manager.js +6 -1
- package/dist/agent/run-manager.js.map +1 -1
- package/dist/agent/run-store.d.ts +1 -1
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +15 -10
- package/dist/agent/run-store.js.map +1 -1
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/create.js +31 -0
- package/dist/cli/create.js.map +1 -1
- package/dist/cli/design-connect.d.ts.map +1 -1
- package/dist/cli/design-connect.js +197 -1
- package/dist/cli/design-connect.js.map +1 -1
- package/dist/cli/index.js +76 -9
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/workspacify.d.ts.map +1 -1
- package/dist/cli/workspacify.js +30 -0
- package/dist/cli/workspacify.js.map +1 -1
- package/dist/client/KeepTabOpenNotice.d.ts +9 -10
- package/dist/client/KeepTabOpenNotice.d.ts.map +1 -1
- package/dist/client/KeepTabOpenNotice.js +8 -4
- package/dist/client/KeepTabOpenNotice.js.map +1 -1
- package/dist/client/RunStuckBanner.d.ts.map +1 -1
- package/dist/client/RunStuckBanner.js +14 -13
- package/dist/client/RunStuckBanner.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +25 -17
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/history/VersionHistoryPanel.d.ts +1 -1
- package/dist/client/history/VersionHistoryPanel.d.ts.map +1 -1
- package/dist/client/history/VersionHistoryPanel.js +12 -5
- package/dist/client/history/VersionHistoryPanel.js.map +1 -1
- package/dist/client/history/index.d.ts +1 -1
- package/dist/client/history/index.d.ts.map +1 -1
- package/dist/client/history/index.js +1 -1
- package/dist/client/history/index.js.map +1 -1
- package/dist/client/history/use-history.d.ts +12 -0
- package/dist/client/history/use-history.d.ts.map +1 -1
- package/dist/client/history/use-history.js +6 -0
- package/dist/client/history/use-history.js.map +1 -1
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +2 -2
- package/dist/client/index.js.map +1 -1
- package/dist/client/review/index.d.ts +1 -1
- package/dist/client/review/index.d.ts.map +1 -1
- package/dist/client/review/index.js +1 -1
- package/dist/client/review/index.js.map +1 -1
- package/dist/client/review/use-review.d.ts +23 -0
- package/dist/client/review/use-review.d.ts.map +1 -1
- package/dist/client/review/use-review.js +8 -0
- package/dist/client/review/use-review.js.map +1 -1
- package/dist/client/use-run-stuck-detection.d.ts +1 -1
- package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
- package/dist/client/use-run-stuck-detection.js +6 -4
- package/dist/client/use-run-stuck-detection.js.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/history/actions/create-resource-version.js +7 -4
- package/dist/history/actions/create-resource-version.js.map +1 -1
- package/dist/history/index.d.ts +1 -1
- package/dist/history/index.d.ts.map +1 -1
- package/dist/history/index.js +1 -1
- package/dist/history/index.js.map +1 -1
- package/dist/history/registry.d.ts.map +1 -1
- package/dist/history/registry.js +14 -12
- package/dist/history/registry.js.map +1 -1
- package/dist/history/store.d.ts.map +1 -1
- package/dist/history/store.js +74 -49
- package/dist/history/store.js.map +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/review/actions/consume-review-feedback.d.ts +1 -0
- package/dist/review/actions/consume-review-feedback.js +8 -2
- package/dist/review/actions/consume-review-feedback.js.map +1 -1
- package/dist/review/actions/delete-review-comment.d.ts +2 -1
- package/dist/review/actions/delete-review-comment.js +11 -3
- package/dist/review/actions/delete-review-comment.js.map +1 -1
- package/dist/review/actions/resolve-review-thread.d.ts +2 -1
- package/dist/review/actions/resolve-review-thread.js +5 -2
- package/dist/review/actions/resolve-review-thread.js.map +1 -1
- package/dist/review/index.d.ts +1 -1
- package/dist/review/index.d.ts.map +1 -1
- package/dist/review/index.js +1 -1
- package/dist/review/index.js.map +1 -1
- package/dist/review/registry.d.ts.map +1 -1
- package/dist/review/registry.js +14 -12
- package/dist/review/registry.js.map +1 -1
- package/dist/review/store.d.ts +3 -3
- package/dist/review/store.d.ts.map +1 -1
- package/dist/review/store.js +7 -4
- package/dist/review/store.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +5 -0
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/credential-provider.d.ts +2 -0
- package/dist/server/credential-provider.d.ts.map +1 -1
- package/dist/server/credential-provider.js +13 -3
- package/dist/server/credential-provider.js.map +1 -1
- package/package.json +1 -1
|
@@ -60,6 +60,58 @@ export const workingLocationTypeInput = z
|
|
|
60
60
|
.enum(["homeOffice", "officeLocation", "customLocation"])
|
|
61
61
|
.optional();
|
|
62
62
|
|
|
63
|
+
export const attendeeObjectInput = z.object({
|
|
64
|
+
email: z.string(),
|
|
65
|
+
displayName: z.string().optional(),
|
|
66
|
+
optional: cliBoolean.optional(),
|
|
67
|
+
comment: z.string().optional(),
|
|
68
|
+
responseStatus: z
|
|
69
|
+
.enum(["accepted", "declined", "tentative", "needsAction"])
|
|
70
|
+
.optional(),
|
|
71
|
+
organizer: cliBoolean.optional(),
|
|
72
|
+
self: cliBoolean.optional(),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export const attendeesInput = z.union([
|
|
76
|
+
z.array(attendeeObjectInput),
|
|
77
|
+
z.string(),
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
export type NormalizedAttendee = {
|
|
81
|
+
email: string;
|
|
82
|
+
displayName?: string;
|
|
83
|
+
optional?: boolean;
|
|
84
|
+
comment?: string;
|
|
85
|
+
responseStatus?: "accepted" | "declined" | "tentative" | "needsAction";
|
|
86
|
+
organizer?: boolean;
|
|
87
|
+
self?: boolean;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export function normalizeAttendees(
|
|
91
|
+
input: z.infer<typeof attendeesInput> | undefined,
|
|
92
|
+
): NormalizedAttendee[] | undefined {
|
|
93
|
+
if (input === undefined) return undefined;
|
|
94
|
+
if (typeof input === "string") {
|
|
95
|
+
const emails = input
|
|
96
|
+
.split(/[\s,;]+/)
|
|
97
|
+
.map((s) => s.trim())
|
|
98
|
+
.filter((s) => s.length > 0 && s.includes("@"));
|
|
99
|
+
if (emails.length === 0) return [];
|
|
100
|
+
return emails.map((email) => ({ email }));
|
|
101
|
+
}
|
|
102
|
+
return input
|
|
103
|
+
.filter((a) => a.email && a.email.includes("@"))
|
|
104
|
+
.map((a) => ({
|
|
105
|
+
email: a.email,
|
|
106
|
+
...(a.displayName ? { displayName: a.displayName } : {}),
|
|
107
|
+
...(a.optional === true ? { optional: true } : {}),
|
|
108
|
+
...(a.comment ? { comment: a.comment } : {}),
|
|
109
|
+
...(a.responseStatus ? { responseStatus: a.responseStatus } : {}),
|
|
110
|
+
...(a.organizer === true ? { organizer: true } : {}),
|
|
111
|
+
...(a.self === true ? { self: true } : {}),
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
|
|
63
115
|
export function requireActionUserEmail(): string {
|
|
64
116
|
const email = getRequestUserEmail();
|
|
65
117
|
if (!email) throw new Error("no authenticated user");
|
|
@@ -11,11 +11,13 @@ import { z } from "zod";
|
|
|
11
11
|
import type { CalendarEventDraft } from "../shared/api.js";
|
|
12
12
|
import {
|
|
13
13
|
attachmentsInput,
|
|
14
|
+
attendeesInput,
|
|
14
15
|
availabilityInput,
|
|
15
16
|
buildReminderOverrides,
|
|
16
17
|
cliBoolean,
|
|
17
18
|
eventTypeInput,
|
|
18
19
|
googleColorIdInput,
|
|
20
|
+
normalizeAttendees,
|
|
19
21
|
reminderMethodInput,
|
|
20
22
|
reminderMinutesInput,
|
|
21
23
|
remindersInput,
|
|
@@ -25,18 +27,6 @@ import {
|
|
|
25
27
|
|
|
26
28
|
const DRAFT_PREFIX = "calendar-draft-";
|
|
27
29
|
|
|
28
|
-
const attendeesInput = z
|
|
29
|
-
.union([
|
|
30
|
-
z.array(
|
|
31
|
-
z.object({
|
|
32
|
-
email: z.string(),
|
|
33
|
-
displayName: z.string().optional(),
|
|
34
|
-
}),
|
|
35
|
-
),
|
|
36
|
-
z.string(),
|
|
37
|
-
])
|
|
38
|
-
.optional();
|
|
39
|
-
|
|
40
30
|
function sanitizeDraftId(id: string): string | null {
|
|
41
31
|
return /^[a-zA-Z0-9_-]{1,64}$/.test(id) ? id : null;
|
|
42
32
|
}
|
|
@@ -45,25 +35,6 @@ function draftKey(id: string) {
|
|
|
45
35
|
return `${DRAFT_PREFIX}${id}`;
|
|
46
36
|
}
|
|
47
37
|
|
|
48
|
-
function normalizeAttendees(
|
|
49
|
-
input: z.infer<typeof attendeesInput>,
|
|
50
|
-
): CalendarEventDraft["attendees"] | undefined {
|
|
51
|
-
if (input === undefined) return undefined;
|
|
52
|
-
if (typeof input === "string") {
|
|
53
|
-
const emails = input
|
|
54
|
-
.split(/[\s,;]+/)
|
|
55
|
-
.map((s) => s.trim())
|
|
56
|
-
.filter((s) => s.length > 0 && s.includes("@"));
|
|
57
|
-
return emails.map((email) => ({ email }));
|
|
58
|
-
}
|
|
59
|
-
return input
|
|
60
|
-
.filter((a) => a.email && a.email.includes("@"))
|
|
61
|
-
.map((a) => ({
|
|
62
|
-
email: a.email,
|
|
63
|
-
...(a.displayName ? { displayName: a.displayName } : {}),
|
|
64
|
-
}));
|
|
65
|
-
}
|
|
66
|
-
|
|
67
38
|
/**
|
|
68
39
|
* Deep link that reopens an unsent calendar event draft.
|
|
69
40
|
*
|
|
@@ -166,9 +137,11 @@ export default defineAction({
|
|
|
166
137
|
.optional()
|
|
167
138
|
.describe("Preselect Google Meet for this draft"),
|
|
168
139
|
addZoom: cliBoolean.optional().describe("Preselect Zoom for this draft"),
|
|
169
|
-
attendees: attendeesInput
|
|
170
|
-
|
|
171
|
-
|
|
140
|
+
attendees: attendeesInput
|
|
141
|
+
.optional()
|
|
142
|
+
.describe(
|
|
143
|
+
"Invitees — either an array of {email, displayName?, optional?} or a comma-separated string of emails. Set optional:true to mark a guest optional.",
|
|
144
|
+
),
|
|
172
145
|
accountEmail: z
|
|
173
146
|
.string()
|
|
174
147
|
.optional()
|
|
@@ -11,9 +11,11 @@ import type { CalendarEvent } from "../shared/api.js";
|
|
|
11
11
|
import {
|
|
12
12
|
availabilityInput,
|
|
13
13
|
attachmentsInput,
|
|
14
|
+
attendeesInput,
|
|
14
15
|
buildReminderOverrides,
|
|
15
16
|
cliBoolean,
|
|
16
17
|
googleColorIdInput,
|
|
18
|
+
normalizeAttendees,
|
|
17
19
|
normalizeGoogleEventId,
|
|
18
20
|
normalizeRecurrence,
|
|
19
21
|
reminderMethodInput,
|
|
@@ -24,27 +26,6 @@ import {
|
|
|
24
26
|
visibilityInput,
|
|
25
27
|
} from "./event-action-helpers.js";
|
|
26
28
|
|
|
27
|
-
const attendeeInput = z.object({
|
|
28
|
-
email: z.string(),
|
|
29
|
-
displayName: z.string().optional(),
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
const attendeesInput = z.union([z.array(attendeeInput), z.string()]);
|
|
33
|
-
|
|
34
|
-
function normalizeAttendees(
|
|
35
|
-
input: z.infer<typeof attendeesInput> | undefined,
|
|
36
|
-
): CalendarEvent["attendees"] | undefined {
|
|
37
|
-
if (input === undefined) return undefined;
|
|
38
|
-
if (typeof input === "string") {
|
|
39
|
-
const emails = input
|
|
40
|
-
.split(/[\s,;]+/)
|
|
41
|
-
.map((s) => s.trim())
|
|
42
|
-
.filter((s) => s.length > 0 && s.includes("@"));
|
|
43
|
-
return emails.map((email) => ({ email }));
|
|
44
|
-
}
|
|
45
|
-
return input.filter((a) => a.email && a.email.includes("@"));
|
|
46
|
-
}
|
|
47
|
-
|
|
48
29
|
function mergeAttendees(
|
|
49
30
|
existing: CalendarEvent["attendees"] | undefined,
|
|
50
31
|
additions: CalendarEvent["attendees"] | undefined,
|
|
@@ -71,6 +52,12 @@ function mergeAttendees(
|
|
|
71
52
|
email,
|
|
72
53
|
displayName: attendee.displayName ?? current?.displayName,
|
|
73
54
|
photoUrl: attendee.photoUrl ?? current?.photoUrl,
|
|
55
|
+
optional:
|
|
56
|
+
attendee.optional === true
|
|
57
|
+
? true
|
|
58
|
+
: attendee.optional === false
|
|
59
|
+
? undefined
|
|
60
|
+
: current?.optional,
|
|
74
61
|
});
|
|
75
62
|
}
|
|
76
63
|
|
|
@@ -157,12 +144,12 @@ export default defineAction({
|
|
|
157
144
|
attendees: attendeesInput
|
|
158
145
|
.optional()
|
|
159
146
|
.describe(
|
|
160
|
-
"Replace the event's attendee list. Accepts an array of {email, displayName?} or a comma-separated string of emails. Pass an empty array to clear all attendees.",
|
|
147
|
+
"Replace the event's attendee list. Accepts an array of {email, displayName?, optional?} or a comma-separated string of emails. Pass an empty array to clear all attendees. Set optional:true to mark a guest optional.",
|
|
161
148
|
),
|
|
162
149
|
addAttendees: attendeesInput
|
|
163
150
|
.optional()
|
|
164
151
|
.describe(
|
|
165
|
-
"Add invitees without dropping or resetting existing attendees. Accepts an array of {email, displayName?} or a comma-separated string of emails.",
|
|
152
|
+
"Add invitees without dropping or resetting existing attendees. Accepts an array of {email, displayName?, optional?} or a comma-separated string of emails. Set optional:true to mark a newly added guest optional.",
|
|
166
153
|
),
|
|
167
154
|
sendUpdates: z
|
|
168
155
|
.enum(["all", "none"])
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useT } from "@agent-native/core/client";
|
|
2
2
|
import {
|
|
3
3
|
IconAddressBook,
|
|
4
|
+
IconDots,
|
|
4
5
|
IconLoader2,
|
|
5
6
|
IconUserCircle,
|
|
6
7
|
IconUsersGroup,
|
|
@@ -18,6 +19,12 @@ import {
|
|
|
18
19
|
} from "react";
|
|
19
20
|
|
|
20
21
|
import { Button } from "@/components/ui/button";
|
|
22
|
+
import {
|
|
23
|
+
DropdownMenu,
|
|
24
|
+
DropdownMenuContent,
|
|
25
|
+
DropdownMenuItem,
|
|
26
|
+
DropdownMenuTrigger,
|
|
27
|
+
} from "@/components/ui/dropdown-menu";
|
|
21
28
|
import {
|
|
22
29
|
Popover,
|
|
23
30
|
PopoverAnchor,
|
|
@@ -39,6 +46,7 @@ export interface AttendeeRecipient {
|
|
|
39
46
|
email: string;
|
|
40
47
|
displayName?: string;
|
|
41
48
|
photoUrl?: string;
|
|
49
|
+
optional?: boolean;
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
export interface AttendeeAutocompleteHandle {
|
|
@@ -50,6 +58,7 @@ interface AttendeeAutocompleteProps {
|
|
|
50
58
|
selectedEmails?: string[];
|
|
51
59
|
onAdd: (attendee: AttendeeRecipient) => void;
|
|
52
60
|
onRemove?: (email: string) => void;
|
|
61
|
+
onToggleOptional?: (email: string, optional: boolean) => void;
|
|
53
62
|
inputId?: string;
|
|
54
63
|
placeholder?: string;
|
|
55
64
|
autoFocus?: boolean;
|
|
@@ -116,6 +125,7 @@ export const AttendeeAutocomplete = forwardRef<
|
|
|
116
125
|
selectedEmails,
|
|
117
126
|
onAdd,
|
|
118
127
|
onRemove,
|
|
128
|
+
onToggleOptional,
|
|
119
129
|
inputId,
|
|
120
130
|
placeholder = "Add guests",
|
|
121
131
|
autoFocus,
|
|
@@ -208,6 +218,8 @@ export const AttendeeAutocomplete = forwardRef<
|
|
|
208
218
|
email,
|
|
209
219
|
displayName,
|
|
210
220
|
photoUrl: person.photoUrl,
|
|
221
|
+
optional:
|
|
222
|
+
"optional" in person && person.optional === true ? true : undefined,
|
|
211
223
|
});
|
|
212
224
|
setInputValue("");
|
|
213
225
|
setOpen(false);
|
|
@@ -348,6 +360,41 @@ export const AttendeeAutocomplete = forwardRef<
|
|
|
348
360
|
<span className="truncate">
|
|
349
361
|
{attendee.displayName || attendee.email}
|
|
350
362
|
</span>
|
|
363
|
+
{attendee.optional && (
|
|
364
|
+
<span className="shrink-0 text-[10px] text-muted-foreground">
|
|
365
|
+
{t("attendees.optionalBadge")}
|
|
366
|
+
</span>
|
|
367
|
+
)}
|
|
368
|
+
{onToggleOptional && (
|
|
369
|
+
<DropdownMenu>
|
|
370
|
+
<DropdownMenuTrigger asChild>
|
|
371
|
+
<button
|
|
372
|
+
type="button"
|
|
373
|
+
onClick={(event) => event.stopPropagation()}
|
|
374
|
+
className="text-muted-foreground hover:text-foreground"
|
|
375
|
+
aria-label={t("attendees.guestOptions", {
|
|
376
|
+
email: attendee.email,
|
|
377
|
+
})}
|
|
378
|
+
>
|
|
379
|
+
<IconDots className="h-3 w-3" />
|
|
380
|
+
</button>
|
|
381
|
+
</DropdownMenuTrigger>
|
|
382
|
+
<DropdownMenuContent
|
|
383
|
+
align="start"
|
|
384
|
+
onClick={(event) => event.stopPropagation()}
|
|
385
|
+
>
|
|
386
|
+
<DropdownMenuItem
|
|
387
|
+
onSelect={() =>
|
|
388
|
+
onToggleOptional(attendee.email, !attendee.optional)
|
|
389
|
+
}
|
|
390
|
+
>
|
|
391
|
+
{attendee.optional
|
|
392
|
+
? t("attendees.markRequired")
|
|
393
|
+
: t("attendees.markOptional")}
|
|
394
|
+
</DropdownMenuItem>
|
|
395
|
+
</DropdownMenuContent>
|
|
396
|
+
</DropdownMenu>
|
|
397
|
+
)}
|
|
351
398
|
{onRemove && (
|
|
352
399
|
<button
|
|
353
400
|
type="button"
|
|
@@ -111,6 +111,12 @@ function uniqueAttendees(attendees: AttendeeRecipient[]) {
|
|
|
111
111
|
email,
|
|
112
112
|
displayName: existing?.displayName ?? attendee.displayName,
|
|
113
113
|
photoUrl: existing?.photoUrl ?? attendee.photoUrl,
|
|
114
|
+
optional:
|
|
115
|
+
attendee.optional === true
|
|
116
|
+
? true
|
|
117
|
+
: existing?.optional === true
|
|
118
|
+
? true
|
|
119
|
+
: undefined,
|
|
114
120
|
});
|
|
115
121
|
}
|
|
116
122
|
return Array.from(byEmail.values());
|
|
@@ -304,6 +310,7 @@ export function CreateEventPopover({
|
|
|
304
310
|
email: attendee.email,
|
|
305
311
|
displayName: attendee.displayName,
|
|
306
312
|
photoUrl: attendee.photoUrl,
|
|
313
|
+
optional: attendee.optional === true ? true : undefined,
|
|
307
314
|
})),
|
|
308
315
|
),
|
|
309
316
|
);
|
|
@@ -393,6 +400,7 @@ export function CreateEventPopover({
|
|
|
393
400
|
? attendees.map((attendee) => ({
|
|
394
401
|
email: attendee.email,
|
|
395
402
|
displayName: attendee.displayName,
|
|
403
|
+
...(attendee.optional === true ? { optional: true } : {}),
|
|
396
404
|
}))
|
|
397
405
|
: undefined,
|
|
398
406
|
addGoogleMeet: videoProvider === "google_meet",
|
|
@@ -494,6 +502,19 @@ export function CreateEventPopover({
|
|
|
494
502
|
);
|
|
495
503
|
}
|
|
496
504
|
|
|
505
|
+
function toggleAttendeeOptional(email: string, optional: boolean) {
|
|
506
|
+
setAttendees((prev) =>
|
|
507
|
+
prev.map((attendee) =>
|
|
508
|
+
attendee.email.toLowerCase() === email.toLowerCase()
|
|
509
|
+
? {
|
|
510
|
+
...attendee,
|
|
511
|
+
optional: optional ? true : undefined,
|
|
512
|
+
}
|
|
513
|
+
: attendee,
|
|
514
|
+
),
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
|
|
497
518
|
const effectiveAllDay = allDay && !timedOnlyStatus;
|
|
498
519
|
const currentStartISO =
|
|
499
520
|
!effectiveAllDay && date && startTime
|
|
@@ -639,6 +660,7 @@ export function CreateEventPopover({
|
|
|
639
660
|
? finalAttendees.map((attendee) => ({
|
|
640
661
|
email: attendee.email,
|
|
641
662
|
displayName: attendee.displayName,
|
|
663
|
+
...(attendee.optional === true ? { optional: true } : {}),
|
|
642
664
|
}))
|
|
643
665
|
: undefined,
|
|
644
666
|
};
|
|
@@ -925,6 +947,7 @@ export function CreateEventPopover({
|
|
|
925
947
|
attendees={attendees}
|
|
926
948
|
onAdd={addAttendee}
|
|
927
949
|
onRemove={removeAttendee}
|
|
950
|
+
onToggleOptional={toggleAttendeeOptional}
|
|
928
951
|
inputId="event-attendees"
|
|
929
952
|
placeholder={t("eventForm.attendeesPlaceholder")}
|
|
930
953
|
onEmptyEnter={() => formRef.current?.requestSubmit()}
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { useT } from "@agent-native/core/client";
|
|
2
2
|
import type { CalendarEvent } from "@shared/api";
|
|
3
|
-
import { IconMessageCircle, IconUser } from "@tabler/icons-react";
|
|
3
|
+
import { IconDots, IconMessageCircle, IconUser } from "@tabler/icons-react";
|
|
4
4
|
import { useEffect, useId, useMemo, useState } from "react";
|
|
5
5
|
|
|
6
6
|
import { AttendeeApolloPopover } from "@/components/calendar/ApolloPanel";
|
|
7
7
|
import { Button } from "@/components/ui/button";
|
|
8
|
+
import {
|
|
9
|
+
DropdownMenu,
|
|
10
|
+
DropdownMenuContent,
|
|
11
|
+
DropdownMenuItem,
|
|
12
|
+
DropdownMenuTrigger,
|
|
13
|
+
} from "@/components/ui/dropdown-menu";
|
|
8
14
|
import { Label } from "@/components/ui/label";
|
|
9
15
|
import {
|
|
10
16
|
Popover,
|
|
@@ -343,6 +349,8 @@ function AttendeeRow({
|
|
|
343
349
|
currentNote,
|
|
344
350
|
onResponseChange,
|
|
345
351
|
isRecurring,
|
|
352
|
+
canEditOptional,
|
|
353
|
+
onToggleOptional,
|
|
346
354
|
}: {
|
|
347
355
|
attendee: Attendee;
|
|
348
356
|
event: Pick<CalendarEvent, "id" | "accountEmail">;
|
|
@@ -352,15 +360,19 @@ function AttendeeRow({
|
|
|
352
360
|
currentNote?: string;
|
|
353
361
|
onResponseChange?: (status: RsvpStatus, note: string) => void;
|
|
354
362
|
isRecurring?: boolean;
|
|
363
|
+
canEditOptional?: boolean;
|
|
364
|
+
onToggleOptional?: (email: string, optional: boolean) => void;
|
|
355
365
|
}) {
|
|
356
366
|
const t = useT();
|
|
357
367
|
const displayStatus = inlineRsvp ? currentStatus : attendee.responseStatus;
|
|
358
368
|
const statusLabel =
|
|
359
369
|
getLocalizedRsvpStatusLabel(t, displayStatus) ?? t("eventForm.awaiting");
|
|
360
370
|
const comment = (inlineRsvp ? currentNote : attendee.comment)?.trim();
|
|
371
|
+
const showOptionalMenu =
|
|
372
|
+
canEditOptional && onToggleOptional && !attendee.organizer;
|
|
361
373
|
|
|
362
374
|
return (
|
|
363
|
-
<div className="rounded-xl px-1 py-1 transition-colors hover:bg-muted/40">
|
|
375
|
+
<div className="group rounded-xl px-1 py-1 transition-colors hover:bg-muted/40">
|
|
364
376
|
<AttendeeApolloPopover attendee={attendee}>
|
|
365
377
|
<div className="flex items-center gap-2.5">
|
|
366
378
|
<div className="relative shrink-0">
|
|
@@ -379,6 +391,11 @@ function AttendeeRow({
|
|
|
379
391
|
{t("eventForm.organizer")}
|
|
380
392
|
</span>
|
|
381
393
|
)}
|
|
394
|
+
{attendee.optional && !attendee.organizer && (
|
|
395
|
+
<span className="shrink-0 rounded bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
|
|
396
|
+
{t("attendees.optionalBadge")}
|
|
397
|
+
</span>
|
|
398
|
+
)}
|
|
382
399
|
</div>
|
|
383
400
|
{attendee.displayName && (
|
|
384
401
|
<div className="truncate text-[11px] text-muted-foreground/60">
|
|
@@ -391,6 +408,38 @@ function AttendeeRow({
|
|
|
391
408
|
: statusLabel}
|
|
392
409
|
</div>
|
|
393
410
|
</div>
|
|
411
|
+
{showOptionalMenu && (
|
|
412
|
+
<DropdownMenu>
|
|
413
|
+
<DropdownMenuTrigger asChild>
|
|
414
|
+
<Button
|
|
415
|
+
type="button"
|
|
416
|
+
variant="ghost"
|
|
417
|
+
size="icon"
|
|
418
|
+
className="h-7 w-7 shrink-0 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100 data-[state=open]:opacity-100"
|
|
419
|
+
aria-label={t("attendees.guestOptions", {
|
|
420
|
+
email: attendee.email,
|
|
421
|
+
})}
|
|
422
|
+
onClick={(event) => event.stopPropagation()}
|
|
423
|
+
>
|
|
424
|
+
<IconDots className="h-3.5 w-3.5" />
|
|
425
|
+
</Button>
|
|
426
|
+
</DropdownMenuTrigger>
|
|
427
|
+
<DropdownMenuContent
|
|
428
|
+
align="end"
|
|
429
|
+
onClick={(event) => event.stopPropagation()}
|
|
430
|
+
>
|
|
431
|
+
<DropdownMenuItem
|
|
432
|
+
onSelect={() =>
|
|
433
|
+
onToggleOptional(attendee.email, !attendee.optional)
|
|
434
|
+
}
|
|
435
|
+
>
|
|
436
|
+
{attendee.optional
|
|
437
|
+
? t("attendees.markRequired")
|
|
438
|
+
: t("attendees.markOptional")}
|
|
439
|
+
</DropdownMenuItem>
|
|
440
|
+
</DropdownMenuContent>
|
|
441
|
+
</DropdownMenu>
|
|
442
|
+
)}
|
|
394
443
|
</div>
|
|
395
444
|
{comment && (
|
|
396
445
|
<div className="ml-10 mt-1 flex items-start gap-1.5 rounded-md bg-muted/40 px-2 py-1 text-[11px] leading-relaxed text-muted-foreground">
|
|
@@ -425,6 +474,8 @@ function sortAttendees(attendees: Attendee[]) {
|
|
|
425
474
|
|
|
426
475
|
export function EventAttendeesSection({
|
|
427
476
|
event,
|
|
477
|
+
canEditOptional = false,
|
|
478
|
+
onToggleOptional,
|
|
428
479
|
}: {
|
|
429
480
|
event: Pick<
|
|
430
481
|
CalendarEvent,
|
|
@@ -436,6 +487,8 @@ export function EventAttendeesSection({
|
|
|
436
487
|
| "source"
|
|
437
488
|
| "recurringEventId"
|
|
438
489
|
>;
|
|
490
|
+
canEditOptional?: boolean;
|
|
491
|
+
onToggleOptional?: (email: string, optional: boolean) => void;
|
|
439
492
|
}) {
|
|
440
493
|
const t = useT();
|
|
441
494
|
const attendees = event.attendees ?? [];
|
|
@@ -523,6 +576,8 @@ export function EventAttendeesSection({
|
|
|
523
576
|
attendee={attendee}
|
|
524
577
|
event={event}
|
|
525
578
|
photoUrl={photos?.[attendee.email.toLowerCase()]}
|
|
579
|
+
canEditOptional={canEditOptional}
|
|
580
|
+
onToggleOptional={onToggleOptional}
|
|
526
581
|
/>
|
|
527
582
|
))}
|
|
528
583
|
|
|
@@ -557,6 +612,8 @@ export function EventAttendeesSection({
|
|
|
557
612
|
currentNote={selfNote}
|
|
558
613
|
onResponseChange={handleSelfResponseChange}
|
|
559
614
|
isRecurring={!!event.recurringEventId}
|
|
615
|
+
canEditOptional={canEditOptional}
|
|
616
|
+
onToggleOptional={onToggleOptional}
|
|
560
617
|
/>
|
|
561
618
|
</>
|
|
562
619
|
)}
|
|
@@ -185,6 +185,41 @@ export function EventDetailPanel({
|
|
|
185
185
|
})();
|
|
186
186
|
}, [event, promptGuestNotification, updateEvent]);
|
|
187
187
|
|
|
188
|
+
const handleToggleAttendeeOptional = useCallback(
|
|
189
|
+
(email: string, optional: boolean) => {
|
|
190
|
+
if (!event || updateEvent.isPending) return;
|
|
191
|
+
const existing = event.attendees || [];
|
|
192
|
+
const key = email.trim().toLowerCase();
|
|
193
|
+
if (!existing.some((attendee) => attendee.email.toLowerCase() === key)) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const attendees = existing.map((attendee) =>
|
|
197
|
+
attendee.email.toLowerCase() === key
|
|
198
|
+
? {
|
|
199
|
+
...attendee,
|
|
200
|
+
optional: optional ? true : undefined,
|
|
201
|
+
}
|
|
202
|
+
: attendee,
|
|
203
|
+
);
|
|
204
|
+
void (async () => {
|
|
205
|
+
const updates = { attendees };
|
|
206
|
+
const guestNotification = await promptGuestNotification({
|
|
207
|
+
event,
|
|
208
|
+
action: "update",
|
|
209
|
+
updates,
|
|
210
|
+
});
|
|
211
|
+
if (!guestNotification) return;
|
|
212
|
+
updateEvent.mutate({
|
|
213
|
+
id: event.id,
|
|
214
|
+
accountEmail: event.accountEmail,
|
|
215
|
+
...updates,
|
|
216
|
+
...guestNotification,
|
|
217
|
+
});
|
|
218
|
+
})();
|
|
219
|
+
},
|
|
220
|
+
[event, promptGuestNotification, updateEvent],
|
|
221
|
+
);
|
|
222
|
+
|
|
188
223
|
return (
|
|
189
224
|
<TooltipProvider>
|
|
190
225
|
{isOpen && (
|
|
@@ -414,7 +449,11 @@ export function EventDetailPanel({
|
|
|
414
449
|
|
|
415
450
|
{/* Attendees */}
|
|
416
451
|
{event.attendees && event.attendees.length > 0 && (
|
|
417
|
-
<EventAttendeesSection
|
|
452
|
+
<EventAttendeesSection
|
|
453
|
+
event={event}
|
|
454
|
+
canEditOptional={!isOverlay}
|
|
455
|
+
onToggleOptional={handleToggleAttendeeOptional}
|
|
456
|
+
/>
|
|
418
457
|
)}
|
|
419
458
|
|
|
420
459
|
{/* Research Meeting */}
|
|
@@ -294,6 +294,12 @@ function mergeAttendeesForPrompt(
|
|
|
294
294
|
email,
|
|
295
295
|
displayName: attendee.displayName ?? current?.displayName,
|
|
296
296
|
photoUrl: attendee.photoUrl ?? current?.photoUrl,
|
|
297
|
+
optional:
|
|
298
|
+
attendee.optional === true
|
|
299
|
+
? true
|
|
300
|
+
: attendee.optional === false
|
|
301
|
+
? undefined
|
|
302
|
+
: current?.optional,
|
|
297
303
|
});
|
|
298
304
|
}
|
|
299
305
|
|
|
@@ -939,6 +945,7 @@ export function EventDetailPopover({
|
|
|
939
945
|
email: attendee.email,
|
|
940
946
|
displayName: attendee.displayName,
|
|
941
947
|
photoUrl: attendee.photoUrl,
|
|
948
|
+
optional: attendee.optional === true ? true : undefined,
|
|
942
949
|
})),
|
|
943
950
|
[event.accountEmail, event.attendees],
|
|
944
951
|
);
|
|
@@ -1004,6 +1011,7 @@ export function EventDetailPopover({
|
|
|
1004
1011
|
email,
|
|
1005
1012
|
displayName: attendee.displayName,
|
|
1006
1013
|
photoUrl: attendee.photoUrl,
|
|
1014
|
+
...(attendee.optional === true ? { optional: true as const } : {}),
|
|
1007
1015
|
};
|
|
1008
1016
|
|
|
1009
1017
|
if (isDraft) {
|
|
@@ -1015,6 +1023,27 @@ export function EventDetailPopover({
|
|
|
1015
1023
|
[event.attendees, isDraft, saveField],
|
|
1016
1024
|
);
|
|
1017
1025
|
|
|
1026
|
+
const handleToggleAttendeeOptional = useCallback(
|
|
1027
|
+
(email: string, optional: boolean) => {
|
|
1028
|
+
const existing = event.attendees || [];
|
|
1029
|
+
const key = email.trim().toLowerCase();
|
|
1030
|
+
if (!existing.some((attendee) => attendee.email.toLowerCase() === key)) {
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
saveField({
|
|
1034
|
+
attendees: existing.map((attendee) =>
|
|
1035
|
+
attendee.email.toLowerCase() === key
|
|
1036
|
+
? {
|
|
1037
|
+
...attendee,
|
|
1038
|
+
optional: optional ? true : undefined,
|
|
1039
|
+
}
|
|
1040
|
+
: attendee,
|
|
1041
|
+
),
|
|
1042
|
+
});
|
|
1043
|
+
},
|
|
1044
|
+
[event.attendees, saveField],
|
|
1045
|
+
);
|
|
1046
|
+
|
|
1018
1047
|
const handleSaveMeetingLink = useCallback(() => {
|
|
1019
1048
|
const url = editMeetingLink.trim();
|
|
1020
1049
|
let saved = false;
|
|
@@ -1606,7 +1635,11 @@ export function EventDetailPopover({
|
|
|
1606
1635
|
|
|
1607
1636
|
{/* Attendees — always shown */}
|
|
1608
1637
|
{event.attendees && event.attendees.length > 0 ? (
|
|
1609
|
-
<EventAttendeesSection
|
|
1638
|
+
<EventAttendeesSection
|
|
1639
|
+
event={event}
|
|
1640
|
+
canEditOptional={!isOverlay}
|
|
1641
|
+
onToggleOptional={handleToggleAttendeeOptional}
|
|
1642
|
+
/>
|
|
1610
1643
|
) : !isOverlay ? (
|
|
1611
1644
|
<div className="px-4 py-1">
|
|
1612
1645
|
<div className="flex items-start gap-3">
|
|
@@ -136,6 +136,12 @@ export function mergeAttendeeLists(
|
|
|
136
136
|
email,
|
|
137
137
|
displayName: attendee.displayName ?? current?.displayName,
|
|
138
138
|
photoUrl: attendee.photoUrl ?? current?.photoUrl,
|
|
139
|
+
optional:
|
|
140
|
+
attendee.optional === true
|
|
141
|
+
? true
|
|
142
|
+
: attendee.optional === false
|
|
143
|
+
? undefined
|
|
144
|
+
: current?.optional,
|
|
139
145
|
});
|
|
140
146
|
}
|
|
141
147
|
|
|
@@ -126,7 +126,11 @@ const messages = {
|
|
|
126
126
|
contacts: "聯絡人",
|
|
127
127
|
contactsReconnect: "需要重新連線聯絡人或目錄存取權限。",
|
|
128
128
|
directory: "目錄",
|
|
129
|
+
guestOptions: "{{email}} 的來賓選項",
|
|
129
130
|
inviteEmail: "邀請 {{email}}",
|
|
131
|
+
markOptional: "標為可選",
|
|
132
|
+
markRequired: "標為必選",
|
|
133
|
+
optionalBadge: "可選",
|
|
130
134
|
otherContacts: "其他聯絡人",
|
|
131
135
|
removeAttendee: "移除 {{email}}",
|
|
132
136
|
searchingPeople: "正在搜尋人員",
|