@@ -71,7 +74,7 @@ export default function SpaceSettingsDialog({controller: c}) {
{localError &&
{localError}
}
{manage && current?.can_invite && }
-
+
diff --git a/frontend/src/pages/MembersPage.jsx b/frontend/src/pages/MembersPage.jsx
index 38fd147..1d5f89b 100644
--- a/frontend/src/pages/MembersPage.jsx
+++ b/frontend/src/pages/MembersPage.jsx
@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useState } from 'react'
+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'
@@ -20,9 +20,10 @@ function Permissions({ value, onChange, disabled = false }) {
}
export default function MembersPage({ controller, user, embedded = false }) {
- const { spaces, activeSpaceId, loadSpaces, managedSpaceId } = controller
+ 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({can_view_parent:true,can_write_parent:false,can_invite:false})
const [url, setUrl] = useState('')
const [title, setTitle] = useState('')
@@ -40,15 +41,27 @@ export default function MembersPage({ controller, user, embedded = false }) {
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
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 { setRecord(await drive.members(parentId)); setError('') }
- catch (e) { setError(message(e)) }
+ 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])
@@ -114,6 +127,11 @@ export default function MembersPage({ controller, user, embedded = false }) {
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é.')}
@@ -125,11 +143,15 @@ export default function MembersPage({ controller, user, embedded = false }) {
{!embedded &&
Espaces & invitations
}
{error &&
{error}
}
{info &&
{info}
}
-
Nom de mon espace
+ {embedded && selectedManagedSpace &&
+
Gestion de « {selectedManagedSpace.display_name} »
+
Les accès et invitations ci-dessous concernent uniquement cet espace.
+
}
+ {!embedded &&
Nom de mon espace
setTitle(e.target.value)} maxLength={120}/>
Le nom visible peut être modifié sans changer le dossier enregistré sur le serveur.
-
- {user.is_admin &&
+
}
+ {!embedded && user.is_admin &&
Quota de mon espace racine
Utilisé avec tous les sous-espaces : {fmtSize(mine?.used_bytes || 0)}. La corbeille compte aussi.
@@ -160,7 +182,7 @@ export default function MembersPage({ controller, user, embedded = false }) {
}
- {!!record?.grants?.length &&
Utilisateurs ajoutés directement
+ {!!selectedRecord?.grants?.length &&
Utilisateurs ajoutés directement
{record.grants.map(g=>updateGrant(g.id,data)} remove={()=>revokeGrant(g.id)}/>)}
}
@@ -175,24 +197,27 @@ export default function MembersPage({ controller, user, embedded = false }) {
{url &&
}
Sous-utilisateurs et sous-espaces
- {!record?.children?.length &&
Aucun sous-utilisateur dans cet espace.
}
- {record?.children?.map(child=>
change(child.id,data)} onInvalid={setError}/>)}
+ {!selectedRecord?.children?.length && Aucun sous-utilisateur dans cet espace.
}
+ {selectedRecord?.children?.map(child=>change(child.id,data)} onManage={() => manageSubspace(child)} onInvalid={setError}/>)}
Invitations
-
{record?.invitations?.map(inv=>
Créée le {new Date(inv.created_at).toLocaleDateString('fr-FR')} · {inv.quota_bytes == null ? 'Sans limite dédiée' : fmtSize(inv.quota_bytes)} · {inv.used?'Utilisée':inv.revoked?'Révoquée':new Date(inv.expires_at){!inv.used&&!inv.revoked&&}
)}
+
{selectedRecord?.invitations?.map(inv=>
Créée le {new Date(inv.created_at).toLocaleDateString('fr-FR')} · {inv.quota_bytes == null ? 'Sans limite dédiée' : fmtSize(inv.quota_bytes)} · {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,onInvalid}) {
+function MemberRow({child,save,busy,onManage,onInvalid}) {
const [rights,setRights]=useState({can_view_parent:child.can_view_parent,can_write_parent:child.can_write_parent,can_invite:child.can_invite})
const [quota,setQuota]=useState(fromQuota(child.quota_bytes))
useEffect(()=>{setRights({can_view_parent:child.can_view_parent,can_write_parent:child.can_write_parent,can_invite:child.can_invite});setQuota(fromQuota(child.quota_bytes))},[child])
- if (!child.has_account) return