- Updated routing for mail settings to redirect to the new settings layout. - Introduced new account layout and section components for better organization. - Replaced hardcoded paths with constants for account and mail settings to enhance maintainability. - Removed deprecated mail settings layout and integrated it into the new settings structure. - Enhanced user experience by streamlining navigation between account and mail settings.
631 lines
22 KiB
TypeScript
631 lines
22 KiB
TypeScript
"use client"
|
|
|
|
import { useEffect, useMemo, useState } from "react"
|
|
import { Link2, Pencil, Plus, Trash2 } from "lucide-react"
|
|
import { toast } from "sonner"
|
|
import { AccountAvatar } from "@/components/gmail/account-avatar"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Checkbox } from "@/components/ui/checkbox"
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog"
|
|
import { Input } from "@/components/ui/input"
|
|
import { Label } from "@/components/ui/label"
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select"
|
|
import { AGENDA_COLOR_PALETTE } from "@/lib/agenda/agenda-colors"
|
|
import { isCalendarEnabledForAccount, isReservedAgendaViewName } from "@/lib/agenda/agenda-calendar-visibility"
|
|
import { calendarColor } from "@/lib/agenda/agenda-events"
|
|
import { useAgendaSettingsStore } from "@/lib/agenda/agenda-store"
|
|
import type { AgendaCalendarView, AgendaExternalCalendar } from "@/lib/agenda/agenda-settings-types"
|
|
import { useMergedAgendaCalendars } from "@/lib/agenda/use-visible-agenda-calendars"
|
|
import { useLabels } from "@/lib/api/hooks/use-folder-label-queries"
|
|
import { useMailAccounts } from "@/lib/api/hooks/use-mail-queries"
|
|
import { MAIL_SETTINGS_PAGE_MASONRY_SECTION_CLASS } from "@/lib/mail-chrome-classes"
|
|
import { MAIL_SETTINGS_BASE_PATH } from "@/lib/mail-settings/settings-nav"
|
|
import { cn } from "@/lib/utils"
|
|
|
|
function CalendarCheckboxRow({
|
|
calendarId,
|
|
label,
|
|
color,
|
|
checked,
|
|
onToggle,
|
|
}: {
|
|
calendarId: string
|
|
label: string
|
|
color: string
|
|
checked: boolean
|
|
onToggle: () => void
|
|
}) {
|
|
return (
|
|
<label className="flex cursor-pointer items-center gap-2.5 rounded-lg px-2 py-1.5 hover:bg-mail-nav-hover">
|
|
<Checkbox checked={checked} onCheckedChange={onToggle} />
|
|
<span
|
|
aria-hidden
|
|
className="size-3.5 shrink-0 rounded-[4px]"
|
|
style={{ backgroundColor: color }}
|
|
/>
|
|
<span className="min-w-0 truncate text-sm text-foreground/85">{label}</span>
|
|
<span className="sr-only">{calendarId}</span>
|
|
</label>
|
|
)
|
|
}
|
|
|
|
export function ExternalCalendarDialog({
|
|
open,
|
|
onOpenChange,
|
|
calendar,
|
|
accounts,
|
|
}: {
|
|
open: boolean
|
|
onOpenChange: (open: boolean) => void
|
|
calendar: AgendaExternalCalendar | null
|
|
accounts: { id: string; name: string; email: string }[]
|
|
}) {
|
|
const addExternalCalendar = useAgendaSettingsStore((s) => s.addExternalCalendar)
|
|
const updateExternalCalendar = useAgendaSettingsStore((s) => s.updateExternalCalendar)
|
|
const [name, setName] = useState("")
|
|
const [url, setUrl] = useState("")
|
|
const [color, setColor] = useState(AGENDA_COLOR_PALETTE[0].value)
|
|
const [accountId, setAccountId] = useState<string>("all")
|
|
|
|
useEffect(() => {
|
|
if (!open) return
|
|
setName(calendar?.display_name ?? "")
|
|
setUrl(calendar?.url ?? "")
|
|
setColor(calendar?.color ?? AGENDA_COLOR_PALETTE[0].value)
|
|
setAccountId(calendar?.account_id ?? "all")
|
|
}, [open, calendar])
|
|
|
|
const submit = () => {
|
|
const displayName = name.trim()
|
|
const feedUrl = url.trim()
|
|
if (!displayName || !feedUrl) return
|
|
try {
|
|
const parsed = new URL(feedUrl)
|
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
toast.error("L'URL doit commencer par http:// ou https://")
|
|
return
|
|
}
|
|
} catch {
|
|
toast.error("URL iCal invalide")
|
|
return
|
|
}
|
|
|
|
const payload = {
|
|
display_name: displayName,
|
|
url: feedUrl,
|
|
color,
|
|
account_id: accountId === "all" ? null : accountId,
|
|
}
|
|
|
|
if (calendar) {
|
|
updateExternalCalendar(calendar.id, payload)
|
|
toast.success("Calendrier externe mis à jour")
|
|
} else {
|
|
addExternalCalendar(payload)
|
|
toast.success("Calendrier externe ajouté")
|
|
}
|
|
onOpenChange(false)
|
|
}
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="sm:max-w-md" aria-describedby={undefined}>
|
|
<DialogHeader>
|
|
<DialogTitle>
|
|
{calendar ? "Modifier le calendrier externe" : "Ajouter un calendrier iCal"}
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="flex flex-col gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="ext-cal-name">Nom</Label>
|
|
<Input
|
|
id="ext-cal-name"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="Calendrier externe"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="ext-cal-url">Lien iCal</Label>
|
|
<Input
|
|
id="ext-cal-url"
|
|
value={url}
|
|
onChange={(e) => setUrl(e.target.value)}
|
|
placeholder="https://…/calendar.ics"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Compte mail</Label>
|
|
<Select value={accountId} onValueChange={setAccountId}>
|
|
<SelectTrigger className="h-9">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">Tous les comptes</SelectItem>
|
|
{accounts.map((account) => (
|
|
<SelectItem key={account.id} value={account.id}>
|
|
{account.email}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Couleur</Label>
|
|
<div className="flex flex-wrap gap-2">
|
|
{AGENDA_COLOR_PALETTE.map((entry) => (
|
|
<button
|
|
key={entry.value}
|
|
type="button"
|
|
title={entry.label}
|
|
aria-label={entry.label}
|
|
onClick={() => setColor(entry.value)}
|
|
className={cn(
|
|
"size-7 rounded-full transition-transform hover:scale-110",
|
|
color === entry.value &&
|
|
"ring-2 ring-foreground/70 ring-offset-2 ring-offset-background",
|
|
)}
|
|
style={{ backgroundColor: entry.value }}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="ghost" onClick={() => onOpenChange(false)}>
|
|
Annuler
|
|
</Button>
|
|
<Button onClick={submit} disabled={!name.trim() || !url.trim()}>
|
|
{calendar ? "Enregistrer" : "Ajouter"}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|
|
|
|
export function CalendarViewDialog({
|
|
open,
|
|
onOpenChange,
|
|
view,
|
|
calendarOptions,
|
|
labelOptions,
|
|
}: {
|
|
open: boolean
|
|
onOpenChange: (open: boolean) => void
|
|
view: AgendaCalendarView | null
|
|
calendarOptions: { id: string; label: string; color: string }[]
|
|
labelOptions: { id: string; label: string }[]
|
|
}) {
|
|
const addCalendarView = useAgendaSettingsStore((s) => s.addCalendarView)
|
|
const updateCalendarView = useAgendaSettingsStore((s) => s.updateCalendarView)
|
|
const [name, setName] = useState("")
|
|
const [calendarIds, setCalendarIds] = useState<string[]>([])
|
|
const [labelIds, setLabelIds] = useState<string[]>([])
|
|
|
|
useEffect(() => {
|
|
if (!open) return
|
|
setName(view?.name ?? "")
|
|
setCalendarIds(view?.calendar_ids ?? [])
|
|
setLabelIds(view?.label_ids ?? [])
|
|
}, [open, view])
|
|
|
|
const toggleCalendar = (calendarId: string) => {
|
|
setCalendarIds((current) =>
|
|
current.includes(calendarId)
|
|
? current.filter((id) => id !== calendarId)
|
|
: [...current, calendarId],
|
|
)
|
|
}
|
|
|
|
const toggleLabel = (labelId: string) => {
|
|
setLabelIds((current) =>
|
|
current.includes(labelId)
|
|
? current.filter((id) => id !== labelId)
|
|
: [...current, labelId],
|
|
)
|
|
}
|
|
|
|
const submit = () => {
|
|
const trimmed = name.trim()
|
|
if (!trimmed) return
|
|
if (isReservedAgendaViewName(trimmed)) {
|
|
toast.error('« Tous les agendas » est réservé à l\'affichage sans vue.')
|
|
return
|
|
}
|
|
if (calendarIds.length === 0) {
|
|
toast.error("Sélectionnez au moins un agenda")
|
|
return
|
|
}
|
|
const payload = {
|
|
name: trimmed,
|
|
calendar_ids: calendarIds,
|
|
label_ids: labelIds,
|
|
}
|
|
if (view) {
|
|
updateCalendarView(view.id, payload)
|
|
toast.success("Vue mise à jour")
|
|
} else {
|
|
addCalendarView(payload)
|
|
toast.success("Vue créée")
|
|
}
|
|
onOpenChange(false)
|
|
}
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="sm:max-w-lg" aria-describedby={undefined}>
|
|
<DialogHeader>
|
|
<DialogTitle>{view ? "Modifier la vue" : "Nouvelle vue d'agendas"}</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="flex max-h-[min(70vh,520px)] flex-col gap-4 overflow-y-auto pr-1">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="view-name">Nom de la vue</Label>
|
|
<Input
|
|
id="view-name"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="Travail, Perso…"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Agendas inclus</Label>
|
|
<div className="max-h-44 space-y-0.5 overflow-y-auto rounded-lg border border-border p-1">
|
|
{calendarOptions.length === 0 ? (
|
|
<p className="px-2 py-3 text-xs text-muted-foreground">
|
|
Aucun agenda disponible.
|
|
</p>
|
|
) : (
|
|
calendarOptions.map((calendar) => (
|
|
<CalendarCheckboxRow
|
|
key={calendar.id}
|
|
calendarId={calendar.id}
|
|
label={calendar.label}
|
|
color={calendar.color}
|
|
checked={calendarIds.includes(calendar.id)}
|
|
onToggle={() => toggleCalendar(calendar.id)}
|
|
/>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Libellés associés</Label>
|
|
<p className="text-[11px] text-muted-foreground">
|
|
Pour filtrer les invitations importées depuis le mail (optionnel).
|
|
</p>
|
|
<div className="max-h-36 space-y-0.5 overflow-y-auto rounded-lg border border-border p-1">
|
|
{labelOptions.length === 0 ? (
|
|
<p className="px-2 py-3 text-xs text-muted-foreground">Aucun libellé.</p>
|
|
) : (
|
|
labelOptions.map((label) => (
|
|
<label
|
|
key={label.id}
|
|
className="flex cursor-pointer items-center gap-2.5 rounded-lg px-2 py-1.5 hover:bg-mail-nav-hover"
|
|
>
|
|
<Checkbox
|
|
checked={labelIds.includes(label.id)}
|
|
onCheckedChange={() => toggleLabel(label.id)}
|
|
/>
|
|
<span className="truncate text-sm">{label.label}</span>
|
|
</label>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="ghost" onClick={() => onOpenChange(false)}>
|
|
Annuler
|
|
</Button>
|
|
<Button onClick={submit} disabled={!name.trim()}>
|
|
{view ? "Enregistrer" : "Créer"}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|
|
|
|
export function AgendaCalendarsSettingsFields({
|
|
variant = "panel",
|
|
}: {
|
|
variant?: "panel" | "page"
|
|
}) {
|
|
const { data: accounts = [] } = useMailAccounts()
|
|
const { data: labels = [] } = useLabels()
|
|
const { calendars, externalCalendars } = useMergedAgendaCalendars()
|
|
const accountEnabledCalendarIds = useAgendaSettingsStore(
|
|
(s) => s.accountEnabledCalendarIds,
|
|
)
|
|
const toggleAccountCalendar = useAgendaSettingsStore((s) => s.toggleAccountCalendar)
|
|
const removeExternalCalendar = useAgendaSettingsStore((s) => s.removeExternalCalendar)
|
|
const calendarViews = useAgendaSettingsStore((s) => s.calendarViews)
|
|
const defaultCalendarViewId = useAgendaSettingsStore((s) => s.defaultCalendarViewId)
|
|
const setDefaultCalendarViewId = useAgendaSettingsStore((s) => s.setDefaultCalendarViewId)
|
|
const removeCalendarView = useAgendaSettingsStore((s) => s.removeCalendarView)
|
|
|
|
const [externalDialogOpen, setExternalDialogOpen] = useState(false)
|
|
const [editingExternal, setEditingExternal] = useState<AgendaExternalCalendar | null>(
|
|
null,
|
|
)
|
|
const [viewDialogOpen, setViewDialogOpen] = useState(false)
|
|
const [editingView, setEditingView] = useState<AgendaCalendarView | null>(null)
|
|
|
|
const allCalendarIds = useMemo(
|
|
() => calendars.map((calendar) => calendar.id),
|
|
[calendars],
|
|
)
|
|
|
|
const calendarOptions = useMemo(
|
|
() =>
|
|
calendars.map((calendar) => ({
|
|
id: calendar.id,
|
|
label: calendar.display_name,
|
|
color: calendarColor(calendar),
|
|
})),
|
|
[calendars],
|
|
)
|
|
|
|
const labelOptions = useMemo(
|
|
() => labels.map((label) => ({ id: label.id, label: label.name })),
|
|
[labels],
|
|
)
|
|
|
|
const isPage = variant === "page"
|
|
|
|
const sectionShell = (position: "first" | "rest") =>
|
|
cn(
|
|
"space-y-3",
|
|
isPage
|
|
? MAIL_SETTINGS_PAGE_MASONRY_SECTION_CLASS
|
|
: cn("px-4 py-3", position === "rest" && "border-t border-border"),
|
|
)
|
|
|
|
return (
|
|
<>
|
|
<section className={sectionShell("first")}>
|
|
<div>
|
|
<h2 className="text-sm font-medium text-foreground">Agendas par compte mail</h2>
|
|
<p className="mt-0.5 text-[11px] leading-snug text-muted-foreground">
|
|
Choisissez les agendas visibles pour chaque compte Ultimail.
|
|
</p>
|
|
</div>
|
|
{accounts.length === 0 ? (
|
|
<p className="text-xs text-muted-foreground">
|
|
Aucun compte mail connecté.{" "}
|
|
<a href={`${MAIL_SETTINGS_BASE_PATH}/accounts`} className="text-[#1a73e8] hover:underline">
|
|
Ajouter un compte
|
|
</a>
|
|
</p>
|
|
) : (
|
|
<div className="divide-y divide-border overflow-hidden rounded-lg border border-border">
|
|
{accounts.map((account) => (
|
|
<div key={account.id} className="p-3">
|
|
<div className="mb-2 flex items-center gap-2">
|
|
<AccountAvatar account={account} size="sm" />
|
|
<div className="min-w-0">
|
|
<p className="truncate text-sm font-medium">{account.name}</p>
|
|
<p className="truncate text-xs text-muted-foreground">{account.email}</p>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-0.5">
|
|
{calendars.length === 0 ? (
|
|
<p className="text-xs text-muted-foreground">Aucun agenda.</p>
|
|
) : (
|
|
calendars.map((calendar) => (
|
|
<CalendarCheckboxRow
|
|
key={`${account.id}-${calendar.id}`}
|
|
calendarId={calendar.id}
|
|
label={calendar.display_name}
|
|
color={calendarColor(calendar)}
|
|
checked={isCalendarEnabledForAccount(
|
|
calendar.id,
|
|
account.id,
|
|
accountEnabledCalendarIds,
|
|
)}
|
|
onToggle={() =>
|
|
toggleAccountCalendar(account.id, calendar.id, allCalendarIds)
|
|
}
|
|
/>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</section>
|
|
|
|
<section className={sectionShell("rest")}>
|
|
<div className="flex items-start justify-between gap-2">
|
|
<div>
|
|
<h2 className="text-sm font-medium text-foreground">Calendriers externes</h2>
|
|
<p className="mt-0.5 text-[11px] leading-snug text-muted-foreground">
|
|
Abonnez un flux iCal (Google, Outlook, etc.).
|
|
</p>
|
|
</div>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
className="h-8 shrink-0 gap-1 rounded-full"
|
|
onClick={() => {
|
|
setEditingExternal(null)
|
|
setExternalDialogOpen(true)
|
|
}}
|
|
>
|
|
<Plus className="size-3.5" />
|
|
Ajouter
|
|
</Button>
|
|
</div>
|
|
{externalCalendars.length === 0 ? (
|
|
<p className="text-xs text-muted-foreground">Aucun calendrier externe.</p>
|
|
) : (
|
|
<ul className="space-y-2">
|
|
{externalCalendars.map((calendar) => (
|
|
<li
|
|
key={calendar.id}
|
|
className="flex items-center gap-2 rounded-lg border border-border px-2 py-2"
|
|
>
|
|
<Link2 className="size-4 shrink-0 text-muted-foreground" />
|
|
<span
|
|
aria-hidden
|
|
className="size-3 shrink-0 rounded-full"
|
|
style={{ backgroundColor: calendar.color }}
|
|
/>
|
|
<div className="min-w-0 flex-1">
|
|
<p className="truncate text-sm">{calendar.display_name}</p>
|
|
<p className="truncate text-[11px] text-muted-foreground">{calendar.url}</p>
|
|
</div>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
className="size-8 shrink-0"
|
|
aria-label={`Modifier ${calendar.display_name}`}
|
|
onClick={() => {
|
|
setEditingExternal(calendar)
|
|
setExternalDialogOpen(true)
|
|
}}
|
|
>
|
|
<Pencil className="size-4" />
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
className="size-8 shrink-0 text-destructive"
|
|
aria-label={`Supprimer ${calendar.display_name}`}
|
|
onClick={() => removeExternalCalendar(calendar.id)}
|
|
>
|
|
<Trash2 className="size-4" />
|
|
</Button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</section>
|
|
|
|
<section className={sectionShell("rest")}>
|
|
<div className="flex items-start justify-between gap-2">
|
|
<div>
|
|
<h2 className="text-sm font-medium text-foreground">Vues d'agendas</h2>
|
|
<p className="mt-0.5 text-[11px] leading-snug text-muted-foreground">
|
|
Regroupez des agendas et libellés, puis ouvrez une vue depuis la barre latérale.
|
|
</p>
|
|
</div>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
className="h-8 shrink-0 gap-1 rounded-full"
|
|
onClick={() => {
|
|
setEditingView(null)
|
|
setViewDialogOpen(true)
|
|
}}
|
|
>
|
|
<Plus className="size-3.5" />
|
|
Nouvelle vue
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-[minmax(0,0.72fr)_minmax(9.75rem,1.18fr)] items-center gap-2 py-1">
|
|
<Label className="text-xs font-normal text-muted-foreground">Vue par défaut</Label>
|
|
<Select
|
|
value={defaultCalendarViewId ?? "none"}
|
|
onValueChange={(value) =>
|
|
setDefaultCalendarViewId(value === "none" ? null : value)
|
|
}
|
|
>
|
|
<SelectTrigger className="h-8 w-full text-xs">
|
|
<SelectValue placeholder="Aucune vue" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="none">Aucune vue</SelectItem>
|
|
{calendarViews.map((view) => (
|
|
<SelectItem key={view.id} value={view.id}>
|
|
{view.name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{calendarViews.length === 0 ? (
|
|
<p className="text-xs text-muted-foreground">Aucune vue enregistrée.</p>
|
|
) : (
|
|
<ul className="space-y-2">
|
|
{calendarViews.map((view) => (
|
|
<li
|
|
key={view.id}
|
|
className="flex items-center gap-2 rounded-lg border border-border px-2 py-2"
|
|
>
|
|
<div className="min-w-0 flex-1">
|
|
<p className="truncate text-sm font-medium">{view.name}</p>
|
|
<p className="text-[11px] text-muted-foreground">
|
|
{view.calendar_ids.length} agenda
|
|
{view.calendar_ids.length > 1 ? "s" : ""}
|
|
{view.label_ids.length > 0
|
|
? ` · ${view.label_ids.length} libellé${view.label_ids.length > 1 ? "s" : ""}`
|
|
: ""}
|
|
</p>
|
|
</div>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
className="size-8 shrink-0"
|
|
aria-label={`Modifier ${view.name}`}
|
|
onClick={() => {
|
|
setEditingView(view)
|
|
setViewDialogOpen(true)
|
|
}}
|
|
>
|
|
<Pencil className="size-4" />
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
className="size-8 shrink-0 text-destructive"
|
|
aria-label={`Supprimer ${view.name}`}
|
|
onClick={() => removeCalendarView(view.id)}
|
|
>
|
|
<Trash2 className="size-4" />
|
|
</Button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</section>
|
|
|
|
<ExternalCalendarDialog
|
|
open={externalDialogOpen}
|
|
onOpenChange={setExternalDialogOpen}
|
|
calendar={editingExternal}
|
|
accounts={accounts}
|
|
/>
|
|
<CalendarViewDialog
|
|
open={viewDialogOpen}
|
|
onOpenChange={setViewDialogOpen}
|
|
view={editingView}
|
|
calendarOptions={calendarOptions}
|
|
labelOptions={labelOptions}
|
|
/>
|
|
</>
|
|
)
|
|
}
|