import React, { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Project } from '../../entities/Project'; import { AccessLevel, grantShare, listShares, revokeShare } from '../../utils/sharesService'; interface ProjectShareModalProps { isOpen: boolean; onClose: () => void; project: Project; } interface ShareRow { user_id: number; access_level: AccessLevel; created_at: string; email?: string; // best-effort; may stay undefined without a lookup API } const isValidEmail = (value: string) => /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(value); const ProjectShareModal: React.FC = ({ isOpen, onClose, project }) => { const { t } = useTranslation(); const [targetEmail, setTargetEmail] = useState(''); const [access, setAccess] = useState('ro'); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const [rows, setRows] = useState(null); const [loadingList, setLoadingList] = useState(false); const projectUid: string | null = useMemo(() => { // Prefer stable uid if present; fallback to id string if needed // Share APIs require resource_uid; projects list provides uid return (project as any).uid || null; }, [project]); useEffect(() => { if (!isOpen) return; setTargetEmail(''); setAccess('ro'); setError(null); if (!projectUid) return; const load = async () => { setLoadingList(true); try { const data = await listShares('project', projectUid); setRows(data); } catch (err: any) { setError(err.message || 'Failed to load shares'); setRows([]); } finally { setLoadingList(false); } }; load(); }, [isOpen, projectUid]); if (!isOpen) return null; const onSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(null); if (!projectUid) { setError(t('errors.generic', 'Something went wrong')); return; } if (!isValidEmail(targetEmail)) { setError(t('errors.invalidEmail', 'Invalid email address')); return; } setSubmitting(true); try { await grantShare({ resource_type: 'project', resource_uid: projectUid, target_user_email: targetEmail, access_level: access }); setTargetEmail(''); // refresh list const data = await listShares('project', projectUid); setRows(data); } catch (err: any) { setError(err.message || 'Failed to share'); } finally { setSubmitting(false); } }; const onRevoke = async (userId: number) => { if (!projectUid) return; try { await revokeShare('project', projectUid, userId); const data = await listShares('project', projectUid); setRows(data); } catch (err: any) { setError(err.message || 'Failed to revoke share'); } }; const accessLabel = (al: AccessLevel) => (al === 'rw' ? t('shares.readWrite', 'Read & write') : t('shares.readOnly', 'Read only')); return (
e.stopPropagation()}>

{t('projects.shareProject', 'Share project')}

{project?.name}

setTargetEmail(e.target.value)} className="w-full rounded border px-3 py-2 bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-gray-100" placeholder={t('shares.emailPlaceholder', 'name@example.com')!} />
{error &&
{error}
}
{t('shares.currentShares', 'Users with access')}
{loadingList ? (
{t('common.loading', 'Loading...')}
) : !rows || rows.length === 0 ? (
{t('shares.noShares', 'Not shared yet')}
) : (
    {rows.map((r) => (
  • {r.email || `#${r.user_id}`}
    {accessLabel(r.access_level)}
  • ))}
)}
); }; export default ProjectShareModal;