211 lines
8.3 KiB
TypeScript
211 lines
8.3 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import { updateAsset } from '../../../services/assets-api';
|
|
import type { Asset, Organization, ShareItem } from '../../../types/assets';
|
|
import Modal from '../../../components/ui/Modal';
|
|
import Button from '../../../components/ui/Button';
|
|
import { ChevronDown, ChevronUp } from 'lucide-react';
|
|
import { useToast } from '../../../hooks/use-toast';
|
|
|
|
interface ShareAssetModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
asset: Asset | null;
|
|
organizations: Organization[];
|
|
onSuccess: () => void;
|
|
}
|
|
|
|
export const ShareAssetModal: React.FC<ShareAssetModalProps> = ({
|
|
isOpen,
|
|
onClose,
|
|
asset,
|
|
organizations,
|
|
onSuccess
|
|
}) => {
|
|
const { success, error } = useToast();
|
|
const [sharesList, setSharesList] = useState<ShareItem[]>([]);
|
|
const [isSavingShare, setIsSavingShare] = useState(false);
|
|
const [expandedOrgId, setExpandedOrgId] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (asset) {
|
|
setSharesList(
|
|
asset.sharedWith?.map(s => ({
|
|
organizationId: s.organizationId,
|
|
userId: s.userId
|
|
})) || []
|
|
);
|
|
}
|
|
}, [asset]);
|
|
|
|
const isOrgSharedEntirely = (orgId: string) => {
|
|
return sharesList.some(s => s.organizationId === orgId && s.userId === null);
|
|
};
|
|
|
|
const isUserSharedSpecifically = (orgId: string, userId: string) => {
|
|
return sharesList.some(s => s.organizationId === orgId && s.userId === userId);
|
|
};
|
|
|
|
const handleToggleOrg = (orgId: string) => {
|
|
const isShared = isOrgSharedEntirely(orgId);
|
|
if (isShared) {
|
|
setSharesList(prev => prev.filter(s => s.organizationId !== orgId));
|
|
} else {
|
|
setSharesList(prev => [
|
|
...prev.filter(s => s.organizationId !== orgId),
|
|
{ organizationId: orgId, userId: null }
|
|
]);
|
|
}
|
|
};
|
|
|
|
const handleToggleUser = (orgId: string, userId: string) => {
|
|
const isShared = isUserSharedSpecifically(orgId, userId);
|
|
if (isShared) {
|
|
setSharesList(prev => prev.filter(s => !(s.organizationId === orgId && s.userId === userId)));
|
|
} else {
|
|
setSharesList(prev => [
|
|
...prev.filter(s => !(s.organizationId === orgId && s.userId === null)),
|
|
{ organizationId: orgId, userId }
|
|
]);
|
|
}
|
|
};
|
|
|
|
const handleShareSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!asset) return;
|
|
|
|
setIsSavingShare(true);
|
|
try {
|
|
await updateAsset(asset.id, {
|
|
shares: sharesList
|
|
});
|
|
success('Share permissions updated', 'The asset visibility settings have been updated.');
|
|
onSuccess();
|
|
onClose();
|
|
} catch (err: any) {
|
|
console.error('Failed to update share permissions', err);
|
|
error('Failed to update share permissions', err.response?.data?.error || 'Something went wrong.');
|
|
} finally {
|
|
setIsSavingShare(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Modal
|
|
isOpen={isOpen && !!asset}
|
|
onClose={onClose}
|
|
title="Share Settings"
|
|
subtitle={asset?.title}
|
|
size="md"
|
|
footer={
|
|
<>
|
|
<Button
|
|
type="button"
|
|
onClick={onClose}
|
|
variant="ghost"
|
|
size="sm"
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
type="submit"
|
|
form="share-asset-form"
|
|
disabled={isSavingShare}
|
|
variant="primary"
|
|
size="sm"
|
|
>
|
|
{isSavingShare ? 'Saving...' : 'Update Shares'}
|
|
</Button>
|
|
</>
|
|
}
|
|
>
|
|
{asset && (
|
|
<form id="share-asset-form" onSubmit={handleShareSubmit} className="space-y-4">
|
|
<p className="text-xs text-ink-600 leading-relaxed font-sans">
|
|
Select organizations or expand to specify exact users that can access this asset:
|
|
</p>
|
|
|
|
<div className="max-h-64 overflow-y-auto border border-ink-200 rounded-xl divide-y divide-ink-250 bg-ink-50 scrollbar-thin">
|
|
{organizations.length === 0 ? (
|
|
<p className="p-4 text-xs text-ink-500 text-center font-medium font-sans">No partner organizations registered yet.</p>
|
|
) : (
|
|
organizations.map(org => {
|
|
const isEntireShared = isOrgSharedEntirely(org.id);
|
|
const isExpanded = expandedOrgId === org.id;
|
|
const activeUsers = org.users || [];
|
|
const specificSharedCount = sharesList.filter(s => s.organizationId === org.id && s.userId !== null).length;
|
|
|
|
return (
|
|
<div key={org.id} className="flex flex-col">
|
|
<div className="flex items-center justify-between p-3 hover:bg-ink-100 transition-colors">
|
|
<label className="flex items-center gap-3 cursor-pointer flex-1 select-none">
|
|
<input
|
|
type="checkbox"
|
|
checked={isEntireShared}
|
|
onChange={() => handleToggleOrg(org.id)}
|
|
className="w-4 h-4 rounded border-ink-300 text-ink-900 focus:ring-ink-900/10 cursor-pointer"
|
|
/>
|
|
<div className="flex flex-col">
|
|
<span className="text-xs font-bold text-ink-900 font-sans">{org.name}</span>
|
|
{specificSharedCount > 0 && !isEntireShared && (
|
|
<span className="text-[10px] text-ink-500 font-semibold font-sans">
|
|
Shared with {specificSharedCount} specific {specificSharedCount === 1 ? 'user' : 'users'}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</label>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={() => setExpandedOrgId(isExpanded ? null : org.id)}
|
|
className="p-1 rounded-lg text-ink-400 hover:text-ink-900 hover:bg-ink-200 transition-colors flex items-center gap-1 text-[11px] font-bold cursor-pointer font-sans"
|
|
>
|
|
<span>Users</span>
|
|
{isExpanded ? <ChevronUp className="w-3.5 h-3.5" /> : <ChevronDown className="w-3.5 h-3.5" />}
|
|
</button>
|
|
</div>
|
|
|
|
<AnimatePresence>
|
|
{isExpanded && (
|
|
<motion.div
|
|
initial={{ height: 0, opacity: 0 }}
|
|
animate={{ height: 'auto', opacity: 1 }}
|
|
exit={{ height: 0, opacity: 0 }}
|
|
className="bg-ink-100 border-t border-b border-ink-200 overflow-hidden divide-y divide-ink-150"
|
|
>
|
|
{activeUsers.length === 0 ? (
|
|
<p className="p-3 text-[10px] text-ink-500 italic font-sans">No users found in this organization.</p>
|
|
) : (
|
|
activeUsers.map(userItem => {
|
|
const isUserShared = isUserSharedSpecifically(org.id, userItem.id);
|
|
|
|
return (
|
|
<label key={userItem.id} className="flex items-center gap-3 py-2 px-8 cursor-pointer hover:bg-ink-200/50 transition-all select-none">
|
|
<input
|
|
type="checkbox"
|
|
disabled={isEntireShared}
|
|
checked={isEntireShared || isUserShared}
|
|
onChange={() => handleToggleUser(org.id, userItem.id)}
|
|
className="w-3.5 h-3.5 rounded border-ink-300 text-ink-900 focus:ring-ink-900/10 cursor-pointer disabled:opacity-50"
|
|
/>
|
|
<span className={`text-[11px] font-semibold font-sans ${isEntireShared ? 'text-ink-400' : 'text-ink-800'}`}>
|
|
{userItem.email}
|
|
</span>
|
|
</label>
|
|
);
|
|
})
|
|
)}
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
</form>
|
|
)}
|
|
</Modal>
|
|
);
|
|
};
|