Files
SRCmail/components/settings/sharing-settings.tsx
T
Bernd Rodler e7acf56753 feat: P2.3 Folder Sharing + P2.5 Email Import + P2.6 Contact Import + P2.7 Free/Busy
- P2.3: Folder sharing system — ShareFolderDialog, sharing-store, sharing-settings
- P2.5: Email import (.eml, .tgz, .zip) with dedup and progress
- P2.6: Contact import (vCard + CSV) with auto-mapping
- P2.7: Free/Busy view grid with color-coded slots
2026-08-07 13:32:33 +02:00

280 lines
9.3 KiB
TypeScript

"use client";
import { useEffect, useState, useCallback } from "react";
import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
import { Avatar } from "@/components/ui/avatar";
import {
Loader2,
RefreshCw,
Check,
X,
Folder,
Calendar,
BookUser,
HardDrive,
Trash2,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { useAuthStore } from "@/stores/auth-store";
import { useSharingStore, type SharedResourceKind, type SharedFolder } from "@/stores/sharing-store";
const ICON_CLASS = "w-4 h-4 shrink-0";
function KindIcon({ kind }: { kind: SharedResourceKind }) {
switch (kind) {
case "mailbox":
return <Folder className={cn(ICON_CLASS, "text-blue-600/80")} />;
case "calendar":
return <Calendar className={cn(ICON_CLASS, "text-emerald-600/80")} />;
case "addressBook":
return <BookUser className={cn(ICON_CLASS, "text-violet-600/80")} />;
case "file":
return <HardDrive className={cn(ICON_CLASS, "text-amber-600/80")} />;
}
}
function KindLabel({ kind }: { kind: SharedResourceKind }) {
switch (kind) {
case "mailbox":
return "Mail";
case "calendar":
return "Calendar";
case "addressBook":
return "Contacts";
case "file":
return "Files";
}
}
export function SharingSettings() {
const t = useTranslations("settings");
const tSharing = useTranslations("sharing");
const client = useAuthStore((s) => s.client);
const {
sharedByMe,
sharedWithMe,
loading,
fetchShares,
revokeShare,
changeRole,
acceptShare,
declineShare,
} = useSharingStore();
const [activeTab, setActiveTab] = useState<"byMe" | "withMe">("byMe");
const handleRefresh = useCallback(() => {
if (client) fetchShares(client);
}, [client, fetchShares]);
useEffect(() => {
if (client) handleRefresh();
}, [client, handleRefresh]);
const handleRevoke = async (share: SharedFolder) => {
if (!client) return;
await revokeShare(
client,
share.resourceId,
share.resourceKind,
share.principalId,
share.accountId,
);
};
const handleChangeRole = async (share: SharedFolder, role: string) => {
if (!client) return;
await changeRole(
client,
share.resourceId,
share.resourceKind,
share.principalId,
role,
share.accountId,
);
};
const handleAccept = async (share: SharedFolder) => {
if (!client) return;
await acceptShare(client, share);
};
const handleDecline = async (share: SharedFolder) => {
if (!client) return;
await declineShare(client, share);
};
return (
<div>
<div className="flex items-center gap-1 border-b border-border mb-4">
<button
onClick={() => setActiveTab("byMe")}
className={cn(
"px-4 py-2 text-sm font-medium border-b-2 transition-colors -mb-px",
activeTab === "byMe"
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-foreground",
)}
>
{tSharing("tab_shared_by_me")}
</button>
<button
onClick={() => setActiveTab("withMe")}
className={cn(
"px-4 py-2 text-sm font-medium border-b-2 transition-colors -mb-px",
activeTab === "withMe"
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-foreground",
)}
>
{tSharing("tab_shared_with_me")}
</button>
<div className="flex-1" />
<button
onClick={handleRefresh}
disabled={loading}
className="p-2 rounded-md hover:bg-muted text-muted-foreground disabled:opacity-50 transition-colors"
title={t("refresh")}
>
<RefreshCw
className={cn("w-4 h-4", loading && "animate-spin")}
/>
</button>
</div>
{loading && (
<div className="flex items-center justify-center py-8 text-muted-foreground">
<Loader2 className="w-5 h-5 animate-spin me-2" />
{t("loading")}
</div>
)}
{!loading && activeTab === "byMe" && (
<>
{sharedByMe.length === 0 ? (
<div className="text-sm text-muted-foreground py-8 text-center">
{tSharing("no_shares_by_me")}
</div>
) : (
<div className="space-y-1">
{sharedByMe.map((share) => (
<div
key={share.id}
className="flex items-center gap-3 px-3 py-2.5 rounded-md border border-border bg-card"
>
<KindIcon kind={share.resourceKind} />
<div className="flex-1 min-w-0">
<div className="text-sm font-medium truncate">
{share.resourceName}
</div>
<div className="text-xs text-muted-foreground flex items-center gap-1">
<KindLabel kind={share.resourceKind} />
<span className="mx-1 opacity-40">|</span>
<Avatar
name={share.principalName}
email={share.principalEmail ?? undefined}
size="sm"
className="shrink-0 me-1"
/>
<span className="truncate">{share.principalName}</span>
</div>
</div>
<select
value={share.role}
onChange={(e) => handleChangeRole(share, e.target.value)}
className="appearance-none rounded-md border border-input bg-background px-2 py-1 text-xs focus:outline-none focus:ring-2 focus:ring-ring"
>
<option value="read">
{tSharing("preset.read")}
</option>
<option value="readWrite">
{tSharing("preset.readWrite")}
</option>
<option value="manager">
{tSharing("preset.manager")}
</option>
</select>
<button
onClick={() => handleRevoke(share)}
className="p-1.5 rounded-md hover:bg-destructive/10 text-muted-foreground hover:text-destructive transition-colors"
title={tSharing("remove")}
>
<Trash2 className="w-4 h-4" />
</button>
</div>
))}
</div>
)}
</>
)}
{!loading && activeTab === "withMe" && (
<>
{sharedWithMe.length === 0 ? (
<div className="text-sm text-muted-foreground py-8 text-center">
{tSharing("no_shares_with_me")}
</div>
) : (
<div className="space-y-1">
{sharedWithMe.map((share) => (
<div
key={share.id}
className="flex items-center gap-3 px-3 py-2.5 rounded-md border border-border bg-card"
>
<KindIcon kind={share.resourceKind} />
<div className="flex-1 min-w-0">
<div className="text-sm font-medium truncate">
{share.resourceName}
</div>
<div className="text-xs text-muted-foreground flex items-center gap-1">
<KindLabel kind={share.resourceKind} />
<span className="mx-1 opacity-40">|</span>
<span className="truncate">
{tSharing("shared_by")}: {share.principalName}
</span>
</div>
</div>
<span className="text-xs bg-muted rounded px-2 py-0.5 text-muted-foreground">
{tSharing(`preset.${share.role}`)}
</span>
{share.pending ? (
<div className="flex items-center gap-1">
<Button
size="sm"
variant="default"
onClick={() => handleAccept(share)}
className="h-7 px-2 text-xs"
>
<Check className="w-3 h-3 me-1" />
{tSharing("accept")}
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => handleDecline(share)}
className="h-7 px-2 text-xs"
>
<X className="w-3 h-3 me-1" />
{tSharing("decline")}
</Button>
</div>
) : (
<Button
size="sm"
variant="ghost"
onClick={() => handleDecline(share)}
className="h-7 px-2 text-xs text-muted-foreground hover:text-destructive"
>
{tSharing("remove")}
</Button>
)}
</div>
))}
</div>
)}
</>
)}
</div>
);
}