feat: split owned vs shared calendars per account in sidebar in Pro shell
This commit is contained in:
@@ -16,6 +16,35 @@ import { ContextMenu, ContextMenuItem, ContextMenuSeparator, ContextMenuSubMenu
|
|||||||
import { useContextMenu } from "@/hooks/use-context-menu";
|
import { useContextMenu } from "@/hooks/use-context-menu";
|
||||||
import type { IJMAPClient } from '@/lib/jmap/client-interface';
|
import type { IJMAPClient } from '@/lib/jmap/client-interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split a per-account calendar list into "owned" (the user's own) and
|
||||||
|
* "shared" sub-buckets, then group shared by the owning principal so each
|
||||||
|
* delegator gets its own sub-section.
|
||||||
|
*/
|
||||||
|
type AccountCalendarSplit = {
|
||||||
|
owned: Calendar[];
|
||||||
|
sharedGroups: { label: string; calendars: Calendar[] }[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function splitAccountCalendars(list: Calendar[]): AccountCalendarSplit {
|
||||||
|
const owned: Calendar[] = [];
|
||||||
|
const sharedBuckets = new Map<string, { label: string; calendars: Calendar[] }>();
|
||||||
|
for (const cal of list) {
|
||||||
|
if (cal.isShared) {
|
||||||
|
const key = cal.accountId || cal.accountName || cal.id;
|
||||||
|
const bucket = sharedBuckets.get(key);
|
||||||
|
if (bucket) {
|
||||||
|
bucket.calendars.push(cal);
|
||||||
|
} else {
|
||||||
|
sharedBuckets.set(key, { label: cal.accountName || key, calendars: [cal] });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
owned.push(cal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { owned, sharedGroups: Array.from(sharedBuckets.values()) };
|
||||||
|
}
|
||||||
|
|
||||||
interface CalendarSidebarPanelProps {
|
interface CalendarSidebarPanelProps {
|
||||||
calendars: Calendar[];
|
calendars: Calendar[];
|
||||||
selectedCalendarIds: string[];
|
selectedCalendarIds: string[];
|
||||||
@@ -128,14 +157,14 @@ export function CalendarSidebarPanel({
|
|||||||
list.push(cal);
|
list.push(cal);
|
||||||
byAccount.set(key, list);
|
byAccount.set(key, list);
|
||||||
}
|
}
|
||||||
const ordered: { key: string; label: string; calendars: Calendar[] }[] = [];
|
const ordered: { key: string; label: string; split: AccountCalendarSplit }[] = [];
|
||||||
// Active account first.
|
// Active account first.
|
||||||
if (activeLocalAccountId && byAccount.has(activeLocalAccountId)) {
|
if (activeLocalAccountId && byAccount.has(activeLocalAccountId)) {
|
||||||
const acct = localAccounts.find(a => a.id === activeLocalAccountId);
|
const acct = localAccounts.find(a => a.id === activeLocalAccountId);
|
||||||
ordered.push({
|
ordered.push({
|
||||||
key: activeLocalAccountId,
|
key: activeLocalAccountId,
|
||||||
label: acct?.label || acct?.email || acct?.username || activeLocalAccountId,
|
label: acct?.label || acct?.email || acct?.username || activeLocalAccountId,
|
||||||
calendars: byAccount.get(activeLocalAccountId)!,
|
split: splitAccountCalendars(byAccount.get(activeLocalAccountId)!),
|
||||||
});
|
});
|
||||||
byAccount.delete(activeLocalAccountId);
|
byAccount.delete(activeLocalAccountId);
|
||||||
}
|
}
|
||||||
@@ -145,7 +174,7 @@ export function CalendarSidebarPanel({
|
|||||||
ordered.push({
|
ordered.push({
|
||||||
key: acct.id,
|
key: acct.id,
|
||||||
label: acct.label || acct.email || acct.username,
|
label: acct.label || acct.email || acct.username,
|
||||||
calendars: byAccount.get(acct.id)!,
|
split: splitAccountCalendars(byAccount.get(acct.id)!),
|
||||||
});
|
});
|
||||||
byAccount.delete(acct.id);
|
byAccount.delete(acct.id);
|
||||||
}
|
}
|
||||||
@@ -154,7 +183,7 @@ export function CalendarSidebarPanel({
|
|||||||
const fallbackLabel = key === '__other__'
|
const fallbackLabel = key === '__other__'
|
||||||
? t('my_calendars')
|
? t('my_calendars')
|
||||||
: list[0]?.accountName || key;
|
: list[0]?.accountName || key;
|
||||||
ordered.push({ key, label: fallbackLabel, calendars: list });
|
ordered.push({ key, label: fallbackLabel, split: splitAccountCalendars(list) });
|
||||||
}
|
}
|
||||||
return ordered;
|
return ordered;
|
||||||
}, [multiAccountMode, calendars, localAccounts, activeLocalAccountId, t]);
|
}, [multiAccountMode, calendars, localAccounts, activeLocalAccountId, t]);
|
||||||
@@ -348,6 +377,7 @@ export function CalendarSidebarPanel({
|
|||||||
{localAccountGroups.map((group, idx) => {
|
{localAccountGroups.map((group, idx) => {
|
||||||
const expanded = !collapsedAccountGroups.has(group.key);
|
const expanded = !collapsedAccountGroups.has(group.key);
|
||||||
const isActive = group.key === activeLocalAccountId;
|
const isActive = group.key === activeLocalAccountId;
|
||||||
|
const { owned, sharedGroups } = group.split;
|
||||||
return (
|
return (
|
||||||
<div key={group.key} className={cn(idx === 0 ? "" : "mt-3")}>
|
<div key={group.key} className={cn(idx === 0 ? "" : "mt-3")}>
|
||||||
<button
|
<button
|
||||||
@@ -383,8 +413,28 @@ export function CalendarSidebarPanel({
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
{expanded && (
|
{expanded && (
|
||||||
<div className="space-y-0.5 mt-1">
|
<div className="mt-1 pl-3">
|
||||||
{group.calendars.map(renderCalendarItem)}
|
{owned.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<div className="px-1 mb-1 text-[10px] font-medium text-muted-foreground/80 uppercase tracking-wider">
|
||||||
|
{t('my_calendars')}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
{owned.map(renderCalendarItem)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{sharedGroups.map((sg) => (
|
||||||
|
<div key={`${group.key}-shared-${sg.label}`} className="mt-2">
|
||||||
|
<div className="px-1 mb-1 text-[10px] font-medium text-muted-foreground/80 uppercase tracking-wider flex items-center gap-1">
|
||||||
|
<Share2 className="w-3 h-3" />
|
||||||
|
{sg.label}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
{sg.calendars.map(renderCalendarItem)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -71,15 +71,14 @@ function prefixCalendarsWithLocalAccount(
|
|||||||
return calendars.map((cal) => ({ ...cal, localAccountId }));
|
return calendars.map((cal) => ({ ...cal, localAccountId }));
|
||||||
}
|
}
|
||||||
const prefix = buildCrossAccountIdPrefix(localAccountId);
|
const prefix = buildCrossAccountIdPrefix(localAccountId);
|
||||||
|
// Preserve each calendar's original `isShared` flag — it distinguishes
|
||||||
|
// the user's own calendars on the other account from calendars shared
|
||||||
|
// *into* that account by yet another user. The sidebar uses this split
|
||||||
|
// to render "My Calendars" vs "Shared" sub-sections per account.
|
||||||
return calendars.map((cal) => ({
|
return calendars.map((cal) => ({
|
||||||
...cal,
|
...cal,
|
||||||
id: `${prefix}${cal.id}`,
|
id: `${prefix}${cal.id}`,
|
||||||
localAccountId,
|
localAccountId,
|
||||||
// Make sure other accounts' calendars surface under their own section in
|
|
||||||
// the sidebar. The sidebar groups "shared" calendars by account label;
|
|
||||||
// promoting them keeps them visually separate from the active account's
|
|
||||||
// own calendars without inventing new grouping logic.
|
|
||||||
isShared: true,
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +95,6 @@ function prefixEventsWithLocalAccount(
|
|||||||
...event,
|
...event,
|
||||||
id: `${prefix}${event.id}`,
|
id: `${prefix}${event.id}`,
|
||||||
localAccountId,
|
localAccountId,
|
||||||
isShared: true,
|
|
||||||
calendarIds: event.calendarIds
|
calendarIds: event.calendarIds
|
||||||
? Object.fromEntries(
|
? Object.fromEntries(
|
||||||
Object.entries(event.calendarIds).map(([calId, v]) => [`${prefix}${calId}`, v]),
|
Object.entries(event.calendarIds).map(([calId, v]) => [`${prefix}${calId}`, v]),
|
||||||
|
|||||||
Reference in New Issue
Block a user