/
kan64
/
spreadsheet-lab
Обзор
Документация
Войти
/
kan64
/
spreadsheet-lab
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
develop
client/src/components/ShareDialog.tsx
254 строки
9 KB
Anton Kravchenkov
feat: Add avatar color
04 мар 2026, 23:25
04 мар 2026, 23:25
f05c921
Код
Авторство
О чём код?
import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { apiGetPermissions, apiGrantPermission, apiRevokePermission, apiSetSheetPublic, apiSearchUsers, isUnauthorizedError, handleUnauthorized, type SheetPermissionDto, type UserSearchResult, } from '../utils/api'; import { SearchableDropdown } from './SearchableDropdown'; import { SelectDropdown } from './SelectDropdown'; import { resolveAvatarColor } from '../utils/avatarUtils'; export interface SharedUser { userId: string; username: string; avatarColor?: string; } interface ShareDialogProps { sheetId: string; isPublic: boolean; isOwner: boolean; onClose: () => void; onPublicChanged: (pub: boolean) => void; onPermissionsChanged: (users: SharedUser[]) => void; } export const ShareDialog: React.FC<ShareDialogProps> = ({ sheetId, isPublic, isOwner, onClose, onPublicChanged, onPermissionsChanged, }) => { const [pub, setPub] = useState(isPublic); const [permissions, setPermissions] = useState<SheetPermissionDto[]>([]); const [allUsers, setAllUsers] = useState<UserSearchResult[]>([]); const [searchQuery, setSearchQuery] = useState(''); const [selectedUser, setSelectedUser] = useState<UserSearchResult | null>(null); const [permission, setPermission] = useState<'READ' | 'EDIT' | 'OWNER'>('READ'); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const sortedPermissions = useMemo( () => [...permissions].sort((a, b) => a.username.localeCompare(b.username)), [permissions], ); const grantedIds = useMemo(() => new Set(permissions.map((p) => p.userId)), [permissions]); const filteredUsers = useMemo(() => { const available = allUsers.filter((u) => !grantedIds.has(u.id)); if (!searchQuery.trim()) return available; const q = searchQuery.trim().toLowerCase(); return available.filter((u) => u.username.toLowerCase().includes(q)); }, [allUsers, grantedIds, searchQuery]); const loadPermissions = useCallback(async () => { if (!isOwner) return; try { const data = await apiGetPermissions(sheetId); setPermissions(data); } catch (err) { if (isUnauthorizedError(err)) { handleUnauthorized(); return; } console.error(err); } }, [sheetId, isOwner]); const loadAllUsers = useCallback(async () => { try { const data = await apiSearchUsers(''); setAllUsers(data); } catch (err) { if (isUnauthorizedError(err)) { handleUnauthorized(); return; } console.error(err); } }, []); useEffect(() => { loadPermissions(); loadAllUsers(); }, [loadPermissions, loadAllUsers]); useEffect(() => { onPermissionsChanged(permissions.map((p) => ({ userId: p.userId, username: p.username }))); }, [permissions, onPermissionsChanged]); const handleSelectUser = (user: UserSearchResult) => { setSelectedUser(user); setSearchQuery(user.username); }; const handleTogglePublic = async () => { try { await apiSetSheetPublic(sheetId, !pub); setPub(!pub); onPublicChanged(!pub); } catch (err) { if (isUnauthorizedError(err)) { handleUnauthorized(); return; } setError(err instanceof Error ? err.message : 'Ошибка'); } }; const handleGrant = async () => { const target = selectedUser?.username ?? searchQuery.trim(); if (!target) return; setError(''); setLoading(true); try { await apiGrantPermission(sheetId, target, permission); setSearchQuery(''); setSelectedUser(null); await loadPermissions(); } catch (err) { if (isUnauthorizedError(err)) { handleUnauthorized(); return; } setError(err instanceof Error ? err.message : 'Ошибка'); } finally { setLoading(false); } }; const handleRevoke = async (userId: string) => { try { await apiRevokePermission(sheetId, userId); setPermissions((prev) => prev.filter((p) => p.userId !== userId)); } catch (err) { if (isUnauthorizedError(err)) { handleUnauthorized(); return; } setError(err instanceof Error ? err.message : 'Ошибка'); } }; const permissionOptions = [ { value: 'READ' as const, label: 'Читатель' }, { value: 'EDIT' as const, label: 'Редактор' }, { value: 'OWNER' as const, label: 'Владелец' }, ]; return ( <div className="share-dialog-overlay" onMouseDown={onClose}> <div className="share-dialog" onMouseDown={(e) => e.stopPropagation()}> <div className="share-dialog-header"> <h3>Доступ к таблице</h3> <button className="share-dialog-close" onClick={onClose}> <svg width="18" height="18" viewBox="0 0 18 18" fill="none"> <path d="M4 4L14 14M14 4L4 14" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" /> </svg> </button> </div> {isOwner && ( <> <div className="share-toggle-row"> <div className="share-toggle-info"> <span className="share-toggle-label"> {pub ? 'Публичная таблица' : 'Приватная таблица'} </span> <span className="share-toggle-hint"> {pub ? 'Доступна для редактирования всем пользователям' : 'Доступна только владельцу и приглашённым'} </span> </div> <button className={`share-toggle-btn ${pub ? 'active' : ''}`} onClick={handleTogglePublic} title={pub ? 'Сделать приватной' : 'Сделать публичной'} > <span className="share-toggle-knob" /> </button> </div> <div className="share-separator" /> <div className="share-grant-section"> <h4>Добавить пользователя</h4> <div className="share-grant-row"> <SearchableDropdown<UserSearchResult> value={searchQuery} onValueChange={(v) => { setSearchQuery(v); setSelectedUser(null); }} items={filteredUsers} onSelect={handleSelectUser} itemLabel={(u) => u.username} renderItem={(user) => ( <> <span className="share-search-item-avatar" style={{ backgroundColor: resolveAvatarColor(user.username, user.avatarColor), color: 'white' }}> {user.username.charAt(0).toUpperCase()} </span> <span className="share-search-item-name">{user.username}</span> </> )} placeholder="Выберите пользователя..." className="share-search-wrapper" /> <SelectDropdown<'READ' | 'EDIT' | 'OWNER'> value={permission} onChange={setPermission} options={permissionOptions} className="share-grant-select-wrap" /> <button className="share-grant-btn" onClick={handleGrant} disabled={loading || !searchQuery.trim()}> Добавить </button> </div> </div> {error && <div className="share-error">{error}</div>} {sortedPermissions.length > 0 && ( <> <div className="share-separator" /> <div className="share-permissions-list"> <h4>Пользователи с доступом</h4> {sortedPermissions.map((p) => ( <div key={p.id} className="share-permission-item"> <div className="share-permission-user"> <span className="share-permission-avatar" style={{ backgroundColor: resolveAvatarColor(p.username, p.avatarColor), color: 'white' }}> {p.username.charAt(0).toUpperCase()} </span> <span className="share-permission-username">{p.username}</span> </div> <span className="share-permission-level"> {p.permission === 'OWNER' ? 'Владелец' : p.permission === 'EDIT' ? 'Редактор' : 'Читатель'} </span> <button className="share-permission-revoke" onClick={() => handleRevoke(p.userId)} title="Отозвать доступ" > <svg width="16" height="16" viewBox="0 0 16 16" fill="none"> <path d="M4 4L12 12M12 4L4 12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" /> </svg> </button> </div> ))} </div> </> )} </> )} {!isOwner && ( <div className="share-readonly-info"> <p>Вы не являетесь владельцем этой таблицы и не можете управлять доступом.</p> </div> )} </div> </div> ); };