import React, { useState, useRef } from 'react'; import styled, { css } from 'styled-components'; import TaskAssignee from 'shared/components/TaskAssignee'; import Select from 'shared/components/Select'; import { User, UserPlus, Checkmark } from 'shared/icons'; import { usePopup, Popup } from 'shared/components/PopupMenu'; import { RoleCode, useUpdateUserRoleMutation } from 'shared/generated/graphql'; import Input from 'shared/components/Input'; import Button from 'shared/components/Button'; import NOOP from 'shared/utils/noop'; import { mixin } from 'shared/utils/styles'; export const RoleCheckmark = styled(Checkmark)` padding-left: 4px; `; const permissions = [ { code: 'owner', name: 'Owner', description: 'Can view, create and edit team projects, and change settings for the team. Will have admin rights on all projects in this team. Can delete the team and all team projects.', }, { code: 'admin', name: 'Admin', description: 'Can view, create and edit team projects, and change settings for the team. Will have admin rights on all projects in this team.', }, { code: 'member', name: 'Member', description: 'Can view, create, and edit team projects, but not change settings.' }, ]; export const RoleName = styled.div` font-size: 14px; font-weight: 700; `; export const RoleDescription = styled.div` margin-top: 4px; font-size: 14px; `; export const MiniProfileActions = styled.ul` list-style-type: none; `; export const MiniProfileActionWrapper = styled.li``; export const MiniProfileActionItem = styled.span<{ disabled?: boolean }>` color: #c2c6dc; display: block; font-weight: 400; padding: 6px 12px; position: relative; text-decoration: none; ${props => props.disabled ? css` user-select: none; pointer-events: none; color: ${mixin.rgba(props.theme.colors.text.primary, 0.4)}; ` : css` cursor: pointer; &:hover { background: ${props.theme.colors.primary}; } `} `; export const Content = styled.div` padding: 0 12px 12px; `; export const CurrentPermission = styled.span` margin-left: 4px; color: ${props => mixin.rgba(props.theme.colors.text.secondary, 0.4)}; `; export const Separator = styled.div` height: 1px; border-top: 1px solid #414561; margin: 0.25rem !important; `; export const WarningText = styled.span` display: flex; color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.4)}; padding: 6px; `; export const DeleteDescription = styled.div` font-size: 14px; color: ${props => props.theme.colors.text.primary}; `; export const RemoveMemberButton = styled(Button)` margin-top: 16px; padding: 6px 12px; width: 100%; `; type TeamRoleManagerPopupProps = { user: User; users: Array; warning?: string | null; canChangeRole?: boolean; onChangeRole?: (roleCode: RoleCode) => void; updateUserPassword?: (user: TaskUser, password: string) => void; onDeleteUser?: (userID: string, newOwnerID: string | null) => void; }; const TeamRoleManagerPopup: React.FC = ({ warning, user, users, canChangeRole, onDeleteUser, updateUserPassword, onChangeRole, }) => { const { hidePopup, setTab } = usePopup(); const [userPass] = useState({ pass: '', passConfirm: '' }); const [deleteUser, setDeleteUser] = useState<{ label: string; value: string } | null>(null); const hasOwned = user.owned.projects.length !== 0 || user.owned.teams.length !== 0; return ( <> {user.role && ( { setTab(1); }} > Change permissions... {`(${user.role.name})`} )} { setTab(3); }} > Reset password... setTab(2)}>Remove from organzation... {warning && ( <> {warning} )} hidePopup()} tab={1}> {permissions .filter(p => (user.role && user.role.code === 'owner') || p.code !== 'owner') .map(perm => ( { if (onChangeRole && user.role && perm.code !== user.role.code) { switch (perm.code) { case 'owner': onChangeRole(RoleCode.Owner); break; case 'admin': onChangeRole(RoleCode.Admin); break; case 'member': onChangeRole(RoleCode.Member); break; default: break; } hidePopup(); } }} > {perm.name} {user.role && perm.code === user.role.code && } {perm.description} ))} {user.role && user.role.code === 'owner' && ( <> You can not change roles because there must be an owner. )} hidePopup()} tab={2}> Removing this user from the organzation will remove them from assigned tasks, projects, and teams. {hasOwned && ( <> {`The user is the owner of ${user.owned.projects.length} projects & ${user.owned.teams.length} teams.`} Choose a new user to take over ownership of the users teams & projects. setDeleteUser(v)} value={deleteUser} options={users.map(u => ({ label: u.fullName, value: u.id }))} /> )} { if (onDeleteUser) { if (!hasOwned || (hasOwned && deleteUser)) { onDeleteUser(user.id, deleteUser ? deleteUser.value : null); } } }} color="danger" > Delete user hidePopup()} tab={4}> Removing this user from the organzation will remove them from assigned tasks, projects, and teams. {`The user is the owner of ${user.owned.projects.length} projects & ${user.owned.teams.length} teams.`} ({ label: u.fullName, value: u.id }))} /> { // onDeleteUser(); }} color="danger" > Delete user hidePopup()} tab={3}> You can either set the users new password directly or send the user an email allowing them to reset their own password. setTab(4)} color="warning"> Set password... Send reset link hidePopup()} tab={4}> { if (userPass.pass === userPass.passConfirm && updateUserPassword) { updateUserPassword(user, userPass.pass); } }} color="danger" > Set password ); }; const UserSelect = styled(Select)` margin: 8px 0; padding: 8px 0; `; const NewUserPassInput = styled(Input)` margin: 8px 0; `; const InviteMemberButton = styled(Button)` padding: 7px 12px; `; const UserPassBar = styled.div` display: flex; padding-top: 8px; `; const UserPassConfirmButton = styled(Button)` width: 100%; padding: 7px 12px; `; const UserPassButton = styled(Button)` width: 50%; padding: 7px 12px; & ~ & { margin-left: 6px; } `; const MemberItemOptions = styled.div``; const MemberItemOption = styled(Button)` padding: 7px 9px; margin: 4px 0 4px 8px; float: left; min-width: 95px; `; const MemberList = styled.div` border-top: 1px solid ${props => props.theme.colors.border}; `; const MemberListItem = styled.div` display: flex; flex-flow: row wrap; justify-content: space-between; border-bottom: 1px solid ${props => props.theme.colors.border}; min-height: 40px; padding: 12px 0 12px 40px; position: relative; `; const MemberListItemDetails = styled.div` float: left; flex: 1 0 auto; padding-left: 8px; `; const InviteIcon = styled(UserPlus)` padding-right: 4px; `; const MemberProfile = styled(TaskAssignee)` position: absolute; top: 16px; left: 0; margin: 0; `; const MemberItemName = styled.p` color: ${props => props.theme.colors.text.secondary}; `; const MemberItemUsername = styled.p` color: ${props => props.theme.colors.text.primary}; `; const MemberListHeader = styled.div` display: flex; flex-direction: column; `; const ListTitle = styled.h3` font-size: 18px; color: ${props => props.theme.colors.text.secondary}; margin-bottom: 12px; `; const ListDesc = styled.span` font-size: 16px; color: ${props => props.theme.colors.text.primary}; `; const FilterSearch = styled(Input)` margin: 0; `; const ListActions = styled.div` display: flex; justify-content: space-between; align-items: center; margin-top: 8px; margin-bottom: 18px; `; const MemberListWrapper = styled.div` flex: 1 1; `; const Container = styled.div` padding: 2.2rem; display: flex; width: 100%; max-width: 1400px; position: relative; margin: 0 auto; `; const TabNav = styled.div` float: left; width: 220px; height: 100%; display: block; position: relative; `; const TabNavContent = styled.ul` display: block; width: auto; border-bottom: 0 !important; border-right: 1px solid rgba(0, 0, 0, 0.05); `; const TabNavItem = styled.li` padding: 0.35rem 0.3rem; height: 48px; display: block; position: relative; `; const TabNavItemButton = styled.button<{ active: boolean }>` cursor: pointer; display: flex; align-items: center; padding-top: 10px !important; padding-bottom: 10px !important; padding-left: 12px !important; padding-right: 8px !important; width: 100%; position: relative; color: ${props => (props.active ? `${props.theme.colors.secondary}` : props.theme.colors.text.primary)}; &:hover { color: ${props => `${props.theme.colors.primary}`}; } &:hover svg { fill: ${props => props.theme.colors.primary}; } `; const TabItemUser = styled(User)<{ active: boolean }>` fill: ${props => (props.active ? `${props.theme.colors.primary}` : props.theme.colors.text.primary)} stroke: ${props => (props.active ? `${props.theme.colors.primary}` : props.theme.colors.text.primary)} `; const TabNavItemSpan = styled.span` text-align: left; padding-left: 9px; font-size: 14px; `; const TabNavLine = styled.span<{ top: number }>` left: auto; right: 0; width: 2px; height: 48px; transform: scaleX(1); top: ${props => props.top}px; background: linear-gradient(30deg, ${props => props.theme.colors.primary}, ${props => props.theme.colors.primary}); box-shadow: 0 0 8px 0 ${props => props.theme.colors.primary}; display: block; position: absolute; transition: all 0.2s ease; `; const TabContentWrapper = styled.div` position: relative; display: block; overflow: hidden; width: 100%; margin-left: 1rem; `; const TabContent = styled.div` position: relative; width: 100%; display: block; padding: 0; padding: 1.5rem; background-color: #10163a; border-radius: 0.5rem; `; const items = [{ name: 'Members' }]; type NavItemProps = { active: boolean; name: string; tab: number; onClick: (tab: number, top: number) => void; }; const NavItem: React.FC = ({ active, name, tab, onClick }) => { const $item = useRef(null); return ( { if ($item && $item.current) { const pos = $item.current.getBoundingClientRect(); onClick(tab, pos.top); } }} > {name} ); }; type AdminProps = { initialTab: number; onAddUser: ($target: React.RefObject) => void; onDeleteUser: (userID: string, newOwnerID: string | null) => void; onInviteUser: ($target: React.RefObject) => void; users: Array; invitedUsers: Array; canInviteUser: boolean; onUpdateUserPassword: (user: TaskUser, password: string) => void; onDeleteInvitedUser: (invitedUserID: string) => void; }; const Admin: React.FC = ({ initialTab, onAddUser, onUpdateUserPassword, canInviteUser, onDeleteUser, onDeleteInvitedUser, onInviteUser, invitedUsers, users, }) => { const warning = 'You can’t leave because you are the only admin. To make another user an admin, click their avatar, select “Change permissions…”, and select “Admin”.'; const [currentTop, setTop] = useState(initialTab * 48); const [currentTab, setTab] = useState(initialTab); const { showPopup } = usePopup(); const $tabNav = useRef(null); const [updateUserRole] = useUpdateUserRoleMutation(); return ( {items.map((item, idx) => ( { if ($tabNav && $tabNav.current) { const pos = $tabNav.current.getBoundingClientRect(); setTab(tab); setTop(top - pos.top); } }} name={item.name} tab={idx} active={idx === currentTab} /> ))} {`Members (${users.length + invitedUsers.length})`} Organization admins can create / manage / delete all projects & teams. Members only have access to teams or projects they have been added to. {canInviteUser && ( { onAddUser($target); }} > New Member )} {users.map(member => { const projectTotal = member.owned.projects.length + member.member.projects.length; return ( {member.fullName} {`@${member.username}`} {`On ${projectTotal} projects`} { showPopup( $target, { onUpdateUserPassword(user, password); }} canChangeRole={(member.role && member.role.code !== 'owner') ?? false} onChangeRole={roleCode => { updateUserRole({ variables: { userID: member.id, roleCode } }); }} onDeleteUser={onDeleteUser} />, ); }} > Manage ); })} {invitedUsers.map(member => { return ( {member.email} Invited { showPopup( $target, { onDeleteInvitedUser(member.id); }} />, ); }} > Manage ); })} ); }; export default Admin;