import { useCallback, useEffect, useRef, useState } from 'react' import { Check, Copy, KeyRound, Link2, ShieldCheck, Trash2, Users } from 'lucide-react' import { drive, message } from '@/api/client' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import QuotaField from '@/components/drive/QuotaField' import {fromQuota, toQuota} from '@/lib/quota-utils' import {fmtSize} from '@/lib/drive-format' import PermissionsEditor, {PRESETS} from '@/components/drive/PermissionsEditor' export default function MembersPage({ controller, user, embedded = false }) { const { spaces, activeSpaceId, loadSpaces, managedSpaceId, setManagedSpaceId, setSpaceDialog } = controller const [parentId, setParentId] = useState(managedSpaceId || user.space_id) const [record, setRecord] = useState(null) const reloadSequence = useRef(0) const [rights, setRights] = useState(PRESETS.reader) const [url, setUrl] = useState('') const [title, setTitle] = useState('') const [busy, setBusy] = useState(false) const [error, setError] = useState('') const [info, setInfo] = useState('') const [expires, setExpires] = useState('7') const [rootQuota, setRootQuota] = useState('') const [userQuery, setUserQuery] = useState('') const [userResults, setUserResults] = useState([]) const [searching, setSearching] = useState(false) const [selectedAccount, setSelectedAccount] = useState(null) const [directRights, setDirectRights] = useState(PRESETS.reader) const mine = spaces.find(s => s.id === user.space_id) const managed = spaces.filter(s => !s.is_private && s.can_invite && (!embedded || s.id === managedSpaceId)) const selectedManagedSpace = managed.find(space => space.id === parentId) // Never render stale information from a previously selected parent space. const selectedRecord = record?.parent?.id === parentId ? record : null // An account must never appear among its own directly added members. const directGrants = (selectedRecord?.grants || []).filter(grant => Number(grant.user_id) !== Number(user.id) && Number(grant.user_id) !== Number(selectedRecord.parent.user_id) ) const managedKey = managed.map(space => space.id).join(',') useEffect(() => { if (managedSpaceId) setParentId(managedSpaceId) }, [managedSpaceId]) useEffect(() => { if (managed.length && !managed.some(space => space.id === parentId)) setParentId(managed[0].id) }, [managedKey, parentId]) const reload = useCallback(async () => { const sequence = ++reloadSequence.current if (!managedKey.split(',').includes(String(parentId))) {setRecord(null);return} try { const nextRecord = await drive.members(parentId) if (sequence !== reloadSequence.current) return // Responses for a previously selected space must never overwrite this one. if (nextRecord?.parent?.id !== parentId) throw new Error('Réponse incohérente pour cet espace.') setRecord(nextRecord) setError('') } catch (e) { if (sequence === reloadSequence.current) setError(e?.response ? message(e) : e.message || message(e)) } }, [parentId,managedKey]) useEffect(() => { reload() }, [reload]) useEffect(() => { setTitle(mine?.display_name || '') }, [mine?.display_name]) useEffect(() => { setRootQuota(fromQuota(mine?.quota_bytes)) }, [mine?.quota_bytes]) const create = async () => { setBusy(true);setError('');setUrl('') try { const inv = await drive.invite({ permissions:rights, expires_days:Number(expires), parent_space:parentId }) setUrl(`${window.location.origin}/invite/${inv.token}`) setInfo('Invitation créée ! Le lien n’est affiché qu’une seule fois : copie-le maintenant.') await reload() } catch(e) { setError(e?.response ? message(e) : (e.message || message(e))) } finally { setBusy(false) } } const change = async (id, data) => { setBusy(true);setError('') try {await drive.memberPatch(id,data);await reload();await loadSpaces();controller.reloadStats();setInfo('Paramètres de l’espace mis à jour.')} catch(e){setError(e?.response ? message(e) : (e.message || message(e)))}finally{setBusy(false)} } const revoke = async id => { setBusy(true) try {await drive.revokeInvite(id);await reload();setInfo('Invitation révoquée.')} catch(e){setError(message(e))}finally{setBusy(false)} } const saveRootQuota = async () => { setBusy(true);setError('') try { await drive.spaceQuota(user.space_id, toQuota(rootQuota)) await loadSpaces(); await reload(); controller.reloadStats() setInfo('Limite de stockage enregistrée.') } catch(e) {setError(e?.response ? message(e) : (e.message || message(e)))} finally {setBusy(false)} } useEffect(() => { if (selectedAccount?.username === userQuery || userQuery.trim().length < 2) { setUserResults([]); return } let alive = true const timer = setTimeout(async () => { setSearching(true) try { const users = await drive.searchUsers(userQuery.trim(), parentId); if(alive) setUserResults(users) } catch(e) { if(alive) setError(message(e)) } finally { if(alive) setSearching(false) } }, 300) return () => { alive = false; clearTimeout(timer) } }, [userQuery, parentId, selectedAccount?.username]) const addExisting = async () => { if (!selectedAccount) return setBusy(true); setError('') try { await drive.addUser(selectedAccount.id,parentId,directRights) setInfo(`Accès accordé à ${selectedAccount.first_name} ${selectedAccount.last_name}`.trim()) setSelectedAccount(null);setUserQuery('');setUserResults([]) await reload();await loadSpaces() } catch(e) { setError(message(e)) } finally { setBusy(false) } } const updateGrant = async (grantId, data) => { setBusy(true);setError('') try { await drive.grantPatch(grantId,data);await reload();setInfo('Droits mis à jour.') } catch(e){setError(message(e))}finally{setBusy(false)} } const revokeGrant = async grantId => { setBusy(true);setError('') try {await drive.grantRemove(grantId);await reload();setInfo('Accès révoqué.')} catch(e){setError(message(e))}finally{setBusy(false)} } const manageSubspace = child => { // Close the access dialog before opening the *child's* settings. setManagedSpaceId(null) setSpaceDialog({ mode: 'manage', spaceId: child.id }) } const rename = async () => { setBusy(true) try {await drive.renameSpace(title);await loadSpaces();setInfo('Nom de ton espace enregistré.')} catch(e){setError(message(e))}finally{setBusy(false)} } return
{!embedded &&

Espaces & invitations

} {error &&
{error}
} {info &&
{info}
} {embedded && selectedManagedSpace &&

Gestion de « {selectedManagedSpace.display_name} »

} {!embedded && mine?.user_id === user.id &&

Nom de mon espace

setTitle(e.target.value)} maxLength={120}/>
} {!embedded && user.is_admin &&

Quota de mon espace racine

} {managed.length > 0 ? <> {managed.length > 1 &&
}
Ajouter un utilisateur existant

Cherche son identifiant, vérifie son nom et prénom, puis autorise l'accès à cet espace. Ses fichiers personnels restent privés.

{setUserQuery(e.target.value);setSelectedAccount(null)}} placeholder="Rechercher par nom d’utilisateur…" aria-label="Rechercher un utilisateur" /> {searching &&

Recherche en cours…

} {!!userResults.length &&
{userResults.map(account=>)}
} {selectedAccount &&

Utilisateur sélectionné : {[selectedAccount.first_name,selectedAccount.last_name].filter(Boolean).join(' ')} (@{selectedAccount.username})

} {!!directGrants.length &&

Utilisateurs ajoutés directement

{directGrants.map(g=>updateGrant(g.id,data)} remove={()=>revokeGrant(g.id)}/>)}
}
Créer une invitation
{url &&
}

Sous-utilisateurs et sous-espaces

{!selectedRecord?.children?.length &&

Aucun sous-utilisateur dans cet espace.

} {selectedRecord?.children?.map(child=>change(child.id,data)} onManage={() => manageSubspace(child)} onInvalid={setError}/>)}

Invitations

{selectedRecord?.invitations?.map(inv=>
Créée le {new Date(inv.created_at).toLocaleDateString('fr-FR')} · {inv.used?'Utilisée':inv.revoked?'Révoquée':new Date(inv.expires_at){!inv.used&&!inv.revoked&&}
)}
:

La création de sous-utilisateurs n’est pas autorisée pour ton compte.

}
} function MemberRow({child,save,busy,onManage,onInvalid,maxRights}) { const [rights,setRights]=useState(child.parent_permissions || PRESETS.reader) const [quota,setQuota]=useState(fromQuota(child.quota_bytes)) useEffect(()=>{setRights(child.parent_permissions || PRESETS.reader);setQuota(fromQuota(child.quota_bytes))},[child]) if (!child.has_account) return
{child.display_name}

Sous-espace partagé indépendant

{child.can_invite && }
return
{child.display_name}
{[child.first_name,child.last_name].filter(Boolean).join(' ') || child.username} · @{child.username} · {child.is_active?'Actif':'Suspendu'}
{child.can_edit_quota && }

Limite : {child.quota_bytes == null ? 'Sans limite' : fmtSize(child.quota_bytes)} · Utilisé : {fmtSize(child.used_bytes || 0)}{child.reserved_bytes > 0 ? ` · Transferts réservés : ${fmtSize(child.reserved_bytes)}` : ''} · Disponible : {child.available_bytes == null ? 'Sans limite' : fmtSize(child.available_bytes)}

} function DirectMember({grant,save,remove,busy,maxRights}) { const [permissions,setPermissions]=useState(grant.permissions || PRESETS.reader) useEffect(()=>setPermissions(grant.permissions || PRESETS.reader),[grant]) return

{[grant.first_name,grant.last_name].filter(Boolean).join(' ') || grant.username}

@{grant.username}

}