/
dmitriysturov
/
workspaces_platform
Обзор
Документация
Войти
/
dmitriysturov
/
workspaces_platform
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
frontend/src/components/ProjectWorkspaceMemberAddPanel.tsx
147 строк
6 KB
dmitriysturov
vkr
04 июн 2026, 22:24
04 июн 2026, 22:24
971dfdc
Код
Авторство
О чём код?
import { useEffect, useMemo, useState } from 'react' import type { AccessGroup, AddProjectMemberRequest, ProjectMember, User, WorkspaceMember } from '../types' import { friendlyError, userDisplayName } from '../utils/format' import { Button } from './ui/Button' type ProjectWorkspaceMemberAddPanelProps = { workspaceMembers: WorkspaceMember[] projectMembers: ProjectMember[] userProfiles: Record<string, User> groups: AccessGroup[] isMutating: boolean canManage: boolean onAdd: (data: AddProjectMemberRequest) => Promise<unknown> } function labelFor(userId: string, profiles: Record<string, User>) { const profile = profiles[userId] return { name: userDisplayName(profile, profile?.email || userId), email: profile?.email || userId, } } export function ProjectWorkspaceMemberAddPanel({ workspaceMembers, projectMembers, userProfiles, groups, isMutating, canManage, onAdd, }: ProjectWorkspaceMemberAddPanelProps) { const [query, setQuery] = useState('') const [selectedUserId, setSelectedUserId] = useState('') const [selectedGroupIds, setSelectedGroupIds] = useState<Set<string>>(new Set()) const [error, setError] = useState<string | null>(null) const [success, setSuccess] = useState<string | null>(null) const projectMemberIds = useMemo(() => new Set(projectMembers.map((member) => member.userId)), [projectMembers]) const assignableGroups = groups.filter((group) => !(group.isSystem && group.systemType === 'OWNER')) const memberGroup = assignableGroups.find((group) => group.isSystem && group.systemType === 'MEMBER') const candidates = workspaceMembers .filter((member) => !projectMemberIds.has(member.userId)) .map((member) => ({ member, user: labelFor(member.userId, userProfiles) })) .filter(({ user }) => { const normalized = query.trim().toLowerCase() if (!normalized) { return true } return user.name.toLowerCase().includes(normalized) || user.email.toLowerCase().includes(normalized) }) useEffect(() => { function syncSelectedGroups() { if (selectedGroupIds.size === 0 && memberGroup) { setSelectedGroupIds(new Set([memberGroup.id])) } } syncSelectedGroups() }, [memberGroup, selectedGroupIds.size]) if (!canManage) { return null } async function handleAdd() { const userId = selectedUserId || candidates[0]?.member.userId if (!userId) { setError('Выберите участника рабочего пространства из списка.') return } try { setError(null) setSuccess(null) await onAdd({ userId, role: 'MEMBER', groupIds: [...selectedGroupIds] }) setQuery('') setSelectedUserId('') setSelectedGroupIds(new Set(memberGroup ? [memberGroup.id] : [])) setSuccess('Участник добавлен в проект.') } catch (err) { setError(friendlyError(err, 'Не удалось добавить участника в проект.')) } } return ( <section className="settings-block"> <header className="section-header"> <div> <h3>Добавить участника из рабочего пространства</h3> <p className="muted">Для внутренних участников используйте выбор по имени или email. Внешних пользователей приглашайте ниже по email.</p> </div> </header> {error && <div className="form-error">{error}</div>} {success && <div className="success">{success}</div>} <div className="member-add-row project-member-add-row"> <label className="member-add-field member-add-field-user"> <span>Поиск</span> <input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="name@example.com" /> </label> <label className="member-add-field member-add-field-user"> <span>Участник рабочего пространства</span> <select value={selectedUserId} onChange={(event) => setSelectedUserId(event.target.value)}> <option value="">Выберите участника</option> {candidates.map(({ member, user }) => ( <option key={member.userId} value={member.userId}> {user.name} · {user.email} </option> ))} </select> </label> <div className="member-add-field group-checkbox-field"> <span>Группы проекта</span> <div className="inline-checkboxes"> {assignableGroups.map((group) => ( <label key={group.id}> <input type="checkbox" checked={selectedGroupIds.has(group.id)} onChange={(event) => { const next = new Set(selectedGroupIds) if (event.currentTarget.checked) { next.add(group.id) } else { next.delete(group.id) } setSelectedGroupIds(next) }} /> <span>{group.name}</span> </label> ))} </div> </div> <Button type="button" isLoading={isMutating} disabled={candidates.length === 0} onClick={() => void handleAdd()}> Добавить </Button> </div> {candidates.length === 0 && <p className="compact-empty">Все доступные участники рабочего пространства уже добавлены в проект.</p>} </section> ) }