feature: fix user admin related bugs

This commit is contained in:
Jordan Knott 2020-07-17 21:55:38 -05:00
parent 68fa7aef94
commit e5bfe9b9ab
24 changed files with 373 additions and 162 deletions

View File

@ -214,18 +214,9 @@ const AdminRoute = () => {
console.log(password); console.log(password);
hidePopup(); hidePopup();
}} }}
onDeleteUser={($target, userID) => { onDeleteUser={(userID, newOwnerID) => {
showPopup( deleteUser({ variables: { userID, newOwnerID } });
$target, hidePopup();
<Popup tab={0} title="Delete user?" onClose={() => hidePopup()}>
<DeleteUserPopup
onDeleteUser={() => {
deleteUser({ variables: { userID } });
hidePopup();
}}
/>
</Popup>,
);
}} }}
onAddUser={$target => { onAddUser={$target => {
showPopup( showPopup(

View File

@ -481,9 +481,9 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({
activeMembers={task.assigned ?? []} activeMembers={task.assigned ?? []}
onMemberChange={(member, isActive) => { onMemberChange={(member, isActive) => {
if (isActive) { if (isActive) {
assignTask({ variables: { taskID: task.id, userID: userID ?? '' } }); assignTask({ variables: { taskID: task.id, userID: member.id } });
} else { } else {
unassignTask({ variables: { taskID: task.id, userID: userID ?? '' } }); unassignTask({ variables: { taskID: task.id, userID: member.id } });
} }
}} }}
/> />

View File

@ -335,7 +335,6 @@ const Details: React.FC<DetailsProps> = ({
onChecklistItemDrop={(prevChecklistID, checklistID, checklistItem) => { onChecklistItemDrop={(prevChecklistID, checklistID, checklistItem) => {
updateTaskChecklistItemLocation({ updateTaskChecklistItemLocation({
variables: { checklistID, checklistItemID: checklistItem.id, position: checklistItem.position }, variables: { checklistID, checklistItemID: checklistItem.id, position: checklistItem.position },
optimisticResponse: { optimisticResponse: {
__typename: 'Mutation', __typename: 'Mutation',
updateTaskChecklistItemLocation: { updateTaskChecklistItemLocation: {

View File

@ -104,24 +104,28 @@ export const RemoveMemberButton = styled(Button)`
width: 100%; width: 100%;
`; `;
type TeamRoleManagerPopupProps = { type TeamRoleManagerPopupProps = {
user: TaskUser; user: User;
users: Array<User>;
warning?: string | null; warning?: string | null;
canChangeRole: boolean; canChangeRole: boolean;
onChangeRole: (roleCode: RoleCode) => void; onChangeRole: (roleCode: RoleCode) => void;
updateUserPassword?: (user: TaskUser, password: string) => void; updateUserPassword?: (user: TaskUser, password: string) => void;
onRemoveFromTeam?: () => void; onDeleteUser?: (userID: string, newOwnerID: string | null) => void;
}; };
const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
warning, warning,
user, user,
users,
canChangeRole, canChangeRole,
onRemoveFromTeam, onDeleteUser,
updateUserPassword, updateUserPassword,
onChangeRole, onChangeRole,
}) => { }) => {
const { hidePopup, setTab } = usePopup(); const { hidePopup, setTab } = usePopup();
const [userPass, setUserPass] = useState({ pass: '', passConfirm: '' }); const [userPass, setUserPass] = 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 ( return (
<> <>
<Popup title={null} tab={0}> <Popup title={null} tab={0}>
@ -144,7 +148,7 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
> >
Reset password... Reset password...
</MiniProfileActionItem> </MiniProfileActionItem>
<MiniProfileActionItem onClick={() => setTab(5)}>Remove from organzation...</MiniProfileActionItem> <MiniProfileActionItem onClick={() => setTab(2)}>Remove from organzation...</MiniProfileActionItem>
</MiniProfileActionWrapper> </MiniProfileActionWrapper>
</MiniProfileActions> </MiniProfileActions>
{warning && ( {warning && (
@ -198,21 +202,55 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
)} )}
</MiniProfileActions> </MiniProfileActions>
</Popup> </Popup>
<Popup title="Remove from Team?" onClose={() => hidePopup()} tab={2}> <Popup title="Remove from Organization?" onClose={() => hidePopup()} tab={2}>
<Content> <Content>
<DeleteDescription> <DeleteDescription>
The member will be removed from all cards on this project. They will receive a notification. Removing this user from the organzation will remove them from assigned tasks, projects, and teams.
</DeleteDescription> </DeleteDescription>
<RemoveMemberButton {hasOwned && (
color="danger" <>
<DeleteDescription>{`The user is the owner of ${user.owned.projects.length} projects & ${user.owned.teams.length} teams.`}</DeleteDescription>
<DeleteDescription>
Choose a new user to take over ownership of this user's teams & projects.
</DeleteDescription>
<UserSelect
onChange={v => setDeleteUser(v)}
value={deleteUser}
options={users.map(u => ({ label: u.fullName, value: u.id }))}
/>
</>
)}
<UserPassConfirmButton
disabled={!(!hasOwned || (hasOwned && deleteUser))}
onClick={() => { onClick={() => {
if (onRemoveFromTeam) { if (onDeleteUser) {
onRemoveFromTeam(); console.log(`${!hasOwned} || (${hasOwned} && ${deleteUser})`);
if (!hasOwned || (hasOwned && deleteUser)) {
onDeleteUser(user.id, deleteUser ? deleteUser.value : null);
}
} }
}} }}
color="danger"
> >
Remove Member Delete user
</RemoveMemberButton> </UserPassConfirmButton>
</Content>
</Popup>
<Popup title="Really remove from Team?" onClose={() => hidePopup()} tab={4}>
<Content>
<DeleteDescription>
Removing this user from the organzation will remove them from assigned tasks, projects, and teams.
</DeleteDescription>
<DeleteDescription>{`The user is the owner of ${user.owned.projects.length} projects & ${user.owned.teams.length} teams.`}</DeleteDescription>
<UserSelect onChange={() => {}} value={null} options={users.map(u => ({ label: u.fullName, value: u.id }))} />
<UserPassConfirmButton
onClick={() => {
// onDeleteUser();
}}
color="danger"
>
Delete user
</UserPassConfirmButton>
</Content> </Content>
</Popup> </Popup>
<Popup title="Reset password?" onClose={() => hidePopup()} tab={3}> <Popup title="Reset password?" onClose={() => hidePopup()} tab={3}>
@ -253,18 +291,6 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
</UserPassConfirmButton> </UserPassConfirmButton>
</Content> </Content>
</Popup> </Popup>
<Popup title="Remove user" onClose={() => hidePopup()} tab={5}>
<Content>
<DeleteDescription>
Removing this user from the organzation will remove them from assigned tasks, projects, and teams.
</DeleteDescription>
<DeleteDescription>The user is the owner of 3 projects & 2 teams.</DeleteDescription>
<UserSelect onChange={() => {}} value={null} options={[{ label: 'Jordan Knott', value: 'jordanknott' }]} />
<UserPassConfirmButton onClick={() => {}} color="danger">
Set password
</UserPassConfirmButton>
</Content>
</Popup>
</> </>
); );
}; };
@ -653,7 +679,7 @@ const NavItem: React.FC<NavItemProps> = ({ active, name, tab, onClick }) => {
type AdminProps = { type AdminProps = {
initialTab: number; initialTab: number;
onAddUser: ($target: React.RefObject<HTMLElement>) => void; onAddUser: ($target: React.RefObject<HTMLElement>) => void;
onDeleteUser: ($target: React.RefObject<HTMLElement>, userID: string) => void; onDeleteUser: (userID: string, newOwnerID: string | null) => void;
onInviteUser: ($target: React.RefObject<HTMLElement>) => void; onInviteUser: ($target: React.RefObject<HTMLElement>) => void;
users: Array<User>; users: Array<User>;
onUpdateUserPassword: (user: TaskUser, password: string) => void; onUpdateUserPassword: (user: TaskUser, password: string) => void;
@ -700,9 +726,10 @@ const Admin: React.FC<AdminProps> = ({
<TabContent> <TabContent>
<MemberListWrapper> <MemberListWrapper>
<MemberListHeader> <MemberListHeader>
<ListTitle>{`Users (${users.length})`}</ListTitle> <ListTitle>{`Members (${users.length})`}</ListTitle>
<ListDesc> <ListDesc>
Team members can view and join all Team Visible boards and create new boards in the team. Organization admins can create / manage / delete all projects & teams. Members only have access to teams
or projects they have been added to.
</ListDesc> </ListDesc>
<ListActions> <ListActions>
<FilterSearch width="250px" variant="alternate" placeholder="Filter by name" /> <FilterSearch width="250px" variant="alternate" placeholder="Filter by name" />
@ -735,6 +762,7 @@ const Admin: React.FC<AdminProps> = ({
$target, $target,
<TeamRoleManagerPopup <TeamRoleManagerPopup
user={member} user={member}
users={users}
warning={member.role && member.role.code === 'owner' ? warning : null} warning={member.role && member.role.code === 'owner' ? warning : null}
updateUserPassword={(user, password) => { updateUserPassword={(user, password) => {
onUpdateUserPassword(user, password); onUpdateUserPassword(user, password);
@ -743,13 +771,7 @@ const Admin: React.FC<AdminProps> = ({
onChangeRole={roleCode => { onChangeRole={roleCode => {
updateUserRole({ variables: { userID: member.id, roleCode } }); updateUserRole({ variables: { userID: member.id, roleCode } });
}} }}
onRemoveFromTeam={ onDeleteUser={onDeleteUser}
member.role && member.role.code === 'owner'
? undefined
: () => {
hidePopup();
}
}
/>, />,
); );
}} }}

View File

@ -1,7 +1,7 @@
import React, {useState, useRef, useEffect} from 'react'; import React, { useState, useRef, useEffect } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import {CheckSquare, Trash, Square, CheckSquareOutline, Clock, Cross, AccountPlus} from 'shared/icons'; import { CheckSquare, Trash, Square, CheckSquareOutline, Clock, Cross, AccountPlus } from 'shared/icons';
import {DragDropContext, Droppable, Draggable, DropResult} from 'react-beautiful-dnd'; import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd';
import { import {
isPositionChanged, isPositionChanged,
getSortedDraggables, getSortedDraggables,
@ -81,7 +81,7 @@ const ChecklistProgressBar = styled.div`
overflow: hidden; overflow: hidden;
position: relative; position: relative;
`; `;
const ChecklistProgressBarCurrent = styled.div<{width: number}>` const ChecklistProgressBarCurrent = styled.div<{ width: number }>`
width: ${props => props.width}%; width: ${props => props.width}%;
background: rgba(${props => (props.width === 100 ? props.theme.colors.success : props.theme.colors.primary)}); background: rgba(${props => (props.width === 100 ? props.theme.colors.success : props.theme.colors.primary)});
bottom: 0; bottom: 0;
@ -132,7 +132,7 @@ const ChecklistItemTextControls = styled.div`
align-items: center; align-items: center;
`; `;
const ChecklistItemText = styled.span<{complete: boolean}>` const ChecklistItemText = styled.span<{ complete: boolean }>`
color: ${props => (props.complete ? '#5e6c84' : `rgba(${props.theme.colors.text.primary})`)}; color: ${props => (props.complete ? '#5e6c84' : `rgba(${props.theme.colors.text.primary})`)};
${props => props.complete && 'text-decoration: line-through;'} ${props => props.complete && 'text-decoration: line-through;'}
line-height: 20px; line-height: 20px;
@ -156,11 +156,11 @@ const ControlButton = styled.div`
padding: 4px 6px; padding: 4px 6px;
border-radius: 6px; border-radius: 6px;
background-color: rgba(${props => props.theme.colors.bg.primary}, 0.8); background-color: rgba(${props => props.theme.colors.bg.primary}, 0.8);
display: flex; display: flex;
width: 32px; width: 32px;
height: 32px; height: 32px;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
&:hover { &:hover {
background-color: rgba(${props => props.theme.colors.primary}, 1); background-color: rgba(${props => props.theme.colors.primary}, 1);
} }
@ -212,16 +212,13 @@ const TrashButton = styled(Trash)`
fill: rgba(${props => props.theme.colors.text.primary}); fill: rgba(${props => props.theme.colors.text.primary});
`; `;
const ChecklistItemWrapper = styled.div<{ref: any}>` const ChecklistItemWrapper = styled.div<{ ref: any }>`
user-select: none; user-select: none;
clear: both; clear: both;
padding-left: 40px; padding-left: 40px;
position: relative; position: relative;
border-radius: 6px; border-radius: 6px;
transform-origin: left bottom;
transition-property: transform, opacity, height, padding, margin;
transition-duration: 0.14s;
transition-timing-function: ease-in;
& ${ControlButton}:last-child { & ${ControlButton}:last-child {
margin-right: 4px; margin-right: 4px;
} }
@ -295,7 +292,17 @@ type ChecklistItemProps = {
export const ChecklistItem = React.forwardRef( export const ChecklistItem = React.forwardRef(
( (
{itemID, checklistID, complete, name, wrapperProps, handleProps, onChangeName, onToggleItem, onDeleteItem}: ChecklistItemProps, {
itemID,
checklistID,
complete,
name,
wrapperProps,
handleProps,
onChangeName,
onToggleItem,
onDeleteItem,
}: ChecklistItemProps,
$item, $item,
) => { ) => {
const $editor = useRef<HTMLTextAreaElement>(null); const $editor = useRef<HTMLTextAreaElement>(null);
@ -319,8 +326,8 @@ export const ChecklistItem = React.forwardRef(
{complete ? ( {complete ? (
<ChecklistItemCheckedIcon width={20} height={20} /> <ChecklistItemCheckedIcon width={20} height={20} />
) : ( ) : (
<ChecklistItemUncheckedIcon width={20} height={20} /> <ChecklistItemUncheckedIcon width={20} height={20} />
)} )}
</ChecklistIcon> </ChecklistIcon>
{editting ? ( {editting ? (
<> <>
@ -370,34 +377,34 @@ export const ChecklistItem = React.forwardRef(
</EditControls> </EditControls>
</> </>
) : ( ) : (
<ChecklistItemDetails <ChecklistItemDetails
onClick={() => { onClick={() => {
setEditting(true); setEditting(true);
}} }}
> >
<ChecklistItemRow> <ChecklistItemRow>
<ChecklistItemTextControls> <ChecklistItemTextControls>
<ChecklistItemText complete={complete}>{name}</ChecklistItemText> <ChecklistItemText complete={complete}>{name}</ChecklistItemText>
<ChecklistControls> <ChecklistControls>
<ControlButton> <ControlButton>
<AssignUserButton width={14} height={14} /> <AssignUserButton width={14} height={14} />
</ControlButton> </ControlButton>
<ControlButton> <ControlButton>
<ClockButton width={14} height={14} /> <ClockButton width={14} height={14} />
</ControlButton> </ControlButton>
<ControlButton <ControlButton
onClick={e => { onClick={e => {
e.stopPropagation(); e.stopPropagation();
onDeleteItem(checklistID, itemID); onDeleteItem(checklistID, itemID);
}} }}
> >
<TrashButton width={14} height={14} /> <TrashButton width={14} height={14} />
</ControlButton> </ControlButton>
</ChecklistControls> </ChecklistControls>
</ChecklistItemTextControls> </ChecklistItemTextControls>
</ChecklistItemRow> </ChecklistItemRow>
</ChecklistItemDetails> </ChecklistItemDetails>
)} )}
</ChecklistItemWrapper> </ChecklistItemWrapper>
); );
}, },
@ -407,7 +414,7 @@ type AddNewItemProps = {
onAddItem: (name: string) => void; onAddItem: (name: string) => void;
}; };
const AddNewItem: React.FC<AddNewItemProps> = ({onAddItem}) => { const AddNewItem: React.FC<AddNewItemProps> = ({ onAddItem }) => {
const $editor = useRef<HTMLTextAreaElement>(null); const $editor = useRef<HTMLTextAreaElement>(null);
const $wrapper = useRef<HTMLDivElement>(null); const $wrapper = useRef<HTMLDivElement>(null);
const [currentName, setCurrentName] = useState(''); const [currentName, setCurrentName] = useState('');
@ -464,8 +471,8 @@ const AddNewItem: React.FC<AddNewItemProps> = ({onAddItem}) => {
</EditControls> </EditControls>
</> </>
) : ( ) : (
<NewItemButton onClick={() => setEditting(true)}>Add an item</NewItemButton> <NewItemButton onClick={() => setEditting(true)}>Add an item</NewItemButton>
)} )}
</ChecklistNewItem> </ChecklistNewItem>
); );
}; };
@ -477,7 +484,7 @@ type ChecklistTitleEditorProps = {
}; };
const ChecklistTitleEditor = React.forwardRef( const ChecklistTitleEditor = React.forwardRef(
({name, onChangeName, onCancel}: ChecklistTitleEditorProps, $name: any) => { ({ name, onChangeName, onCancel }: ChecklistTitleEditorProps, $name: any) => {
const [currentName, setCurrentName] = useState(name); const [currentName, setCurrentName] = useState(name);
return ( return (
<> <>
@ -579,21 +586,21 @@ const Checklist = React.forwardRef(
}} }}
/> />
) : ( ) : (
<WindowChecklistTitle {...handleProps}> <WindowChecklistTitle {...handleProps}>
<WindowTitleText onClick={() => setEditting(true)}>{name}</WindowTitleText> <WindowTitleText onClick={() => setEditting(true)}>{name}</WindowTitleText>
<WindowOptions> <WindowOptions>
<DeleteButton <DeleteButton
onClick={$target => { onClick={$target => {
onDeleteChecklist($target, checklistID); onDeleteChecklist($target, checklistID);
}} }}
color="danger" color="danger"
variant="outline" variant="outline"
> >
Delete Delete
</DeleteButton> </DeleteButton>
</WindowOptions> </WindowOptions>
</WindowChecklistTitle> </WindowChecklistTitle>
)} )}
</WindowTitle> </WindowTitle>
<ChecklistProgress> <ChecklistProgress>
<ChecklistProgressPercent>{`${percent}%`}</ChecklistProgressPercent> <ChecklistProgressPercent>{`${percent}%`}</ChecklistProgressPercent>

View File

@ -1,6 +1,14 @@
import React, { useRef } from 'react'; import React, { useRef } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import TaskAssignee from 'shared/components/TaskAssignee';
import { Checkmark } from 'shared/icons';
const CardCheckmark = styled(Checkmark)`
position: absolute;
top: 0;
right: 0;
margin: 11px;
`;
const CardMember = styled.div<{ bgColor: string }>` const CardMember = styled.div<{ bgColor: string }>`
height: 28px; height: 28px;
width: 28px; width: 28px;
@ -34,6 +42,7 @@ type MemberProps = {
member: TaskUser; member: TaskUser;
showName?: boolean; showName?: boolean;
className?: string; className?: string;
showCheckmark?: boolean;
}; };
const CardMemberWrapper = styled.div<{ ref: any }>` const CardMemberWrapper = styled.div<{ ref: any }>`
@ -46,7 +55,14 @@ const CardMemberName = styled.span`
padding-left: 8px; padding-left: 8px;
`; `;
const Member: React.FC<MemberProps> = ({ onCardMemberClick, taskID, member, showName, className }) => { const Member: React.FC<MemberProps> = ({
onCardMemberClick,
taskID,
member,
showName,
showCheckmark = false,
className,
}) => {
const $targetRef = useRef<HTMLDivElement>(); const $targetRef = useRef<HTMLDivElement>();
return ( return (
<CardMemberWrapper <CardMemberWrapper
@ -60,10 +76,9 @@ const Member: React.FC<MemberProps> = ({ onCardMemberClick, taskID, member, show
} }
}} }}
> >
<CardMember bgColor={member.profileIcon.bgColor ?? '#7367F0'}> <TaskAssignee onMemberProfile={() => {}} size={28} member={member} />
<CardMemberInitials>{member.profileIcon.initials}</CardMemberInitials>
</CardMember>
{showName && <CardMemberName>{member.fullName}</CardMemberName>} {showName && <CardMemberName>{member.fullName}</CardMemberName>}
{showCheckmark && <CardCheckmark width={12} height={12} />}
</CardMemberWrapper> </CardMemberWrapper>
); );
}; };

View File

@ -1,6 +1,7 @@
import styled from 'styled-components'; import styled from 'styled-components';
import TextareaAutosize from 'react-autosize-textarea/lib'; import TextareaAutosize from 'react-autosize-textarea/lib';
import { mixin } from 'shared/utils/styles'; import { mixin } from 'shared/utils/styles';
import Member from '../Member';
export const MemberManagerWrapper = styled.div``; export const MemberManagerWrapper = styled.div``;
@ -48,7 +49,7 @@ export const BoardMembersList = styled.ul`
export const BoardMembersListItem = styled.li``; export const BoardMembersListItem = styled.li``;
export const BoardMemberListItemContent = styled.div` export const BoardMemberListItemContent = styled(Member)`
background-color: rgba(9, 30, 66, 0.04); background-color: rgba(9, 30, 66, 0.04);
padding-right: 28px; padding-right: 28px;
border-radius: 3px; border-radius: 3px;
@ -64,6 +65,11 @@ export const BoardMemberListItemContent = styled.div`
padding: 4px; padding: 4px;
margin-bottom: 2px; margin-bottom: 2px;
color: #c2c6dc; color: #c2c6dc;
&:hover {
background-color: rgba(${props => props.theme.colors.primary});
color: rgba(${props => props.theme.colors.text.secondary});
}
`; `;
export const ProfileIcon = styled.div` export const ProfileIcon = styled.div`

View File

@ -13,6 +13,7 @@ import {
ActiveIconWrapper, ActiveIconWrapper,
} from './Styles'; } from './Styles';
import { Checkmark } from 'shared/icons'; import { Checkmark } from 'shared/icons';
import Member from 'shared/components/Member';
type MemberManagerProps = { type MemberManagerProps = {
availableMembers: Array<TaskUser>; availableMembers: Array<TaskUser>;
@ -45,7 +46,10 @@ const MemberManager: React.FC<MemberManagerProps> = ({
return ( return (
<BoardMembersListItem key={member.id}> <BoardMembersListItem key={member.id}>
<BoardMemberListItemContent <BoardMemberListItemContent
onClick={() => { member={member}
showName
showCheckmark={activeMembers.findIndex(m => m.id === member.id) !== -1}
onCardMemberClick={() => {
const isActive = activeMembers.findIndex(m => m.id === member.id) !== -1; const isActive = activeMembers.findIndex(m => m.id === member.id) !== -1;
if (isActive) { if (isActive) {
setActiveMembers(activeMembers.filter(m => m.id !== member.id)); setActiveMembers(activeMembers.filter(m => m.id !== member.id));
@ -54,15 +58,7 @@ const MemberManager: React.FC<MemberManagerProps> = ({
} }
onMemberChange(member, !isActive); onMemberChange(member, !isActive);
}} }}
> />
<ProfileIcon>JK</ProfileIcon>
<MemberName>{member.fullName}</MemberName>
{activeMembers.findIndex(m => m.id === member.id) !== -1 && (
<ActiveIconWrapper>
<Checkmark width={16} height={16} />
</ActiveIconWrapper>
)}
</BoardMemberListItemContent>
</BoardMembersListItem> </BoardMembersListItem>
); );
})} })}

View File

@ -74,7 +74,13 @@ export const EditorButton = styled.div`
margin: 0 0 4px 8px; margin: 0 0 4px 8px;
padding: 6px 12px 6px 8px; padding: 6px 12px 6px 8px;
text-decoration: none; text-decoration: none;
transition: transform 85ms ease-in; transition: all 85ms ease-in;
&:hover {
transform: translateX(5px);
background: rgba(0, 0, 0, 1);
color: #fff;
}
`; `;
export const CloseButton = styled.div` export const CloseButton = styled.div`

View File

@ -420,33 +420,35 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
> >
<Droppable direction="vertical" type="checklistItem" droppableId={checklist.id}> <Droppable direction="vertical" type="checklistItem" droppableId={checklist.id}>
{checklistDrop => ( {checklistDrop => (
<ChecklistItems ref={checklistDrop.innerRef} {...checklistDrop.droppableProps}> <>
{checklist.items <ChecklistItems ref={checklistDrop.innerRef} {...checklistDrop.droppableProps}>
.slice() {checklist.items
.sort((a, b) => a.position - b.position) .slice()
.map((item, itemIdx) => ( .sort((a, b) => a.position - b.position)
<Draggable key={item.id} draggableId={item.id} index={itemIdx}> .map((item, itemIdx) => (
{itemDrop => ( <Draggable key={item.id} draggableId={item.id} index={itemIdx}>
<ChecklistItem {itemDrop => (
key={item.id} <ChecklistItem
itemID={item.id} key={item.id}
checklistID={item.taskChecklistID} itemID={item.id}
ref={itemDrop.innerRef} checklistID={item.taskChecklistID}
wrapperProps={itemDrop.draggableProps} ref={itemDrop.innerRef}
handleProps={itemDrop.dragHandleProps} wrapperProps={itemDrop.draggableProps}
name={item.name} handleProps={itemDrop.dragHandleProps}
complete={item.complete} name={item.name}
onDeleteItem={onDeleteItem} complete={item.complete}
onChangeName={onChangeItemName} onDeleteItem={onDeleteItem}
onToggleItem={(itemID, complete) => onChangeName={onChangeItemName}
onToggleChecklistItem(item.id, complete) onToggleItem={(itemID, complete) =>
} onToggleChecklistItem(item.id, complete)
/> }
)} />
</Draggable> )}
))} </Draggable>
))}
</ChecklistItems>
{checklistDrop.placeholder} {checklistDrop.placeholder}
</ChecklistItems> </>
)} )}
</Droppable> </Droppable>
</Checklist> </Checklist>

View File

@ -923,6 +923,7 @@ export type LogoutUser = {
export type DeleteUserAccount = { export type DeleteUserAccount = {
userID: Scalars['UUID']; userID: Scalars['UUID'];
newOwnerID?: Maybe<Scalars['UUID']>;
}; };
export type DeleteUserAccountPayload = { export type DeleteUserAccountPayload = {
@ -1886,12 +1887,31 @@ export type CreateUserAccountMutation = (
), role: ( ), role: (
{ __typename?: 'Role' } { __typename?: 'Role' }
& Pick<Role, 'code' | 'name'> & Pick<Role, 'code' | 'name'>
), owned: (
{ __typename?: 'OwnedList' }
& { teams: Array<(
{ __typename?: 'Team' }
& Pick<Team, 'id' | 'name'>
)>, projects: Array<(
{ __typename?: 'Project' }
& Pick<Project, 'id' | 'name'>
)> }
), member: (
{ __typename?: 'MemberList' }
& { teams: Array<(
{ __typename?: 'Team' }
& Pick<Team, 'id' | 'name'>
)>, projects: Array<(
{ __typename?: 'Project' }
& Pick<Project, 'id' | 'name'>
)> }
) } ) }
) } ) }
); );
export type DeleteUserAccountMutationVariables = { export type DeleteUserAccountMutationVariables = {
userID: Scalars['UUID']; userID: Scalars['UUID'];
newOwnerID?: Maybe<Scalars['UUID']>;
}; };
@ -3851,6 +3871,26 @@ export const CreateUserAccountDocument = gql`
code code
name name
} }
owned {
teams {
id
name
}
projects {
id
name
}
}
member {
teams {
id
name
}
projects {
id
name
}
}
} }
} }
`; `;
@ -3885,8 +3925,8 @@ export type CreateUserAccountMutationHookResult = ReturnType<typeof useCreateUse
export type CreateUserAccountMutationResult = ApolloReactCommon.MutationResult<CreateUserAccountMutation>; export type CreateUserAccountMutationResult = ApolloReactCommon.MutationResult<CreateUserAccountMutation>;
export type CreateUserAccountMutationOptions = ApolloReactCommon.BaseMutationOptions<CreateUserAccountMutation, CreateUserAccountMutationVariables>; export type CreateUserAccountMutationOptions = ApolloReactCommon.BaseMutationOptions<CreateUserAccountMutation, CreateUserAccountMutationVariables>;
export const DeleteUserAccountDocument = gql` export const DeleteUserAccountDocument = gql`
mutation deleteUserAccount($userID: UUID!) { mutation deleteUserAccount($userID: UUID!, $newOwnerID: UUID) {
deleteUserAccount(input: {userID: $userID}) { deleteUserAccount(input: {userID: $userID, newOwnerID: $newOwnerID}) {
ok ok
userAccount { userAccount {
id id
@ -3910,6 +3950,7 @@ export type DeleteUserAccountMutationFn = ApolloReactCommon.MutationFunction<Del
* const [deleteUserAccountMutation, { data, loading, error }] = useDeleteUserAccountMutation({ * const [deleteUserAccountMutation, { data, loading, error }] = useDeleteUserAccountMutation({
* variables: { * variables: {
* userID: // value for 'userID' * userID: // value for 'userID'
* newOwnerID: // value for 'newOwnerID'
* }, * },
* }); * });
*/ */

View File

@ -33,6 +33,26 @@ export const CREATE_USER_MUTATION = gql`
code code
name name
} }
owned {
teams {
id
name
}
projects {
id
name
}
}
member {
teams {
id
name
}
projects {
id
name
}
}
} }
} }
`; `;

View File

@ -1,8 +1,8 @@
import gql from 'graphql-tag'; import gql from 'graphql-tag';
export const DELETE_USER_MUTATION = gql` export const DELETE_USER_MUTATION = gql`
mutation deleteUserAccount($userID: UUID!) { mutation deleteUserAccount($userID: UUID!, $newOwnerID: UUID) {
deleteUserAccount(input: { userID: $userID }) { deleteUserAccount(input: { userID: $userID, newOwnerID: $newOwnerID }) {
ok ok
userAccount { userAccount {
id id

View File

@ -3,8 +3,8 @@ import Icon, { IconProps } from './Icon';
const Checkmark: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => { const Checkmark: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
return ( return (
<Icon width={width} height={height} className={className} viewBox="0 0 16 16"> <Icon width={width} height={height} className={className} viewBox="0 0 512 512">
<path d="M13.5 2l-7.5 7.5-3.5-3.5-2.5 2.5 6 6 10-10z" /> <path d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z" />
</Icon> </Icon>
); );
}; };

View File

@ -384,3 +384,35 @@ func (q *Queries) UpdateProjectNameByID(ctx context.Context, arg UpdateProjectNa
) )
return i, err return i, err
} }
const updateProjectOwnerByOwnerID = `-- name: UpdateProjectOwnerByOwnerID :many
UPDATE project SET owner = $2 WHERE owner = $1 RETURNING project_id
`
type UpdateProjectOwnerByOwnerIDParams struct {
Owner uuid.UUID `json:"owner"`
Owner_2 uuid.UUID `json:"owner_2"`
}
func (q *Queries) UpdateProjectOwnerByOwnerID(ctx context.Context, arg UpdateProjectOwnerByOwnerIDParams) ([]uuid.UUID, error) {
rows, err := q.db.QueryContext(ctx, updateProjectOwnerByOwnerID, arg.Owner, arg.Owner_2)
if err != nil {
return nil, err
}
defer rows.Close()
var items []uuid.UUID
for rows.Next() {
var project_id uuid.UUID
if err := rows.Scan(&project_id); err != nil {
return nil, err
}
items = append(items, project_id)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

View File

@ -95,6 +95,7 @@ type Querier interface {
UpdateProjectLabelName(ctx context.Context, arg UpdateProjectLabelNameParams) (ProjectLabel, error) UpdateProjectLabelName(ctx context.Context, arg UpdateProjectLabelNameParams) (ProjectLabel, error)
UpdateProjectMemberRole(ctx context.Context, arg UpdateProjectMemberRoleParams) (ProjectMember, error) UpdateProjectMemberRole(ctx context.Context, arg UpdateProjectMemberRoleParams) (ProjectMember, error)
UpdateProjectNameByID(ctx context.Context, arg UpdateProjectNameByIDParams) (Project, error) UpdateProjectNameByID(ctx context.Context, arg UpdateProjectNameByIDParams) (Project, error)
UpdateProjectOwnerByOwnerID(ctx context.Context, arg UpdateProjectOwnerByOwnerIDParams) ([]uuid.UUID, error)
UpdateTaskChecklistItemLocation(ctx context.Context, arg UpdateTaskChecklistItemLocationParams) (TaskChecklistItem, error) UpdateTaskChecklistItemLocation(ctx context.Context, arg UpdateTaskChecklistItemLocationParams) (TaskChecklistItem, error)
UpdateTaskChecklistItemName(ctx context.Context, arg UpdateTaskChecklistItemNameParams) (TaskChecklistItem, error) UpdateTaskChecklistItemName(ctx context.Context, arg UpdateTaskChecklistItemNameParams) (TaskChecklistItem, error)
UpdateTaskChecklistName(ctx context.Context, arg UpdateTaskChecklistNameParams) (TaskChecklist, error) UpdateTaskChecklistName(ctx context.Context, arg UpdateTaskChecklistNameParams) (TaskChecklist, error)
@ -105,6 +106,7 @@ type Querier interface {
UpdateTaskLocation(ctx context.Context, arg UpdateTaskLocationParams) (Task, error) UpdateTaskLocation(ctx context.Context, arg UpdateTaskLocationParams) (Task, error)
UpdateTaskName(ctx context.Context, arg UpdateTaskNameParams) (Task, error) UpdateTaskName(ctx context.Context, arg UpdateTaskNameParams) (Task, error)
UpdateTeamMemberRole(ctx context.Context, arg UpdateTeamMemberRoleParams) (TeamMember, error) UpdateTeamMemberRole(ctx context.Context, arg UpdateTeamMemberRoleParams) (TeamMember, error)
UpdateTeamOwnerByOwnerID(ctx context.Context, arg UpdateTeamOwnerByOwnerIDParams) ([]uuid.UUID, error)
UpdateUserAccountProfileAvatarURL(ctx context.Context, arg UpdateUserAccountProfileAvatarURLParams) (UserAccount, error) UpdateUserAccountProfileAvatarURL(ctx context.Context, arg UpdateUserAccountProfileAvatarURLParams) (UserAccount, error)
UpdateUserRole(ctx context.Context, arg UpdateUserRoleParams) (UserAccount, error) UpdateUserRole(ctx context.Context, arg UpdateUserRoleParams) (UserAccount, error)
} }

View File

@ -45,3 +45,6 @@ SELECT * FROM project WHERE owner = $1;
-- name: GetMemberProjectIDsForUserID :many -- name: GetMemberProjectIDsForUserID :many
SELECT project_id FROM project_member WHERE user_id = $1; SELECT project_id FROM project_member WHERE user_id = $1;
-- name: UpdateProjectOwnerByOwnerID :many
UPDATE project SET owner = $2 WHERE owner = $1 RETURNING project_id;

View File

@ -21,3 +21,6 @@ SELECT * FROM team WHERE owner = $1;
-- name: GetMemberTeamIDsForUserID :many -- name: GetMemberTeamIDsForUserID :many
SELECT team_id FROM team_member WHERE user_id = $1; SELECT team_id FROM team_member WHERE user_id = $1;
-- name: UpdateTeamOwnerByOwnerID :many
UPDATE team SET owner = $2 WHERE owner = $1 RETURNING team_id;

View File

@ -212,3 +212,35 @@ func (q *Queries) SetTeamOwner(ctx context.Context, arg SetTeamOwnerParams) (Tea
) )
return i, err return i, err
} }
const updateTeamOwnerByOwnerID = `-- name: UpdateTeamOwnerByOwnerID :many
UPDATE team SET owner = $2 WHERE owner = $1 RETURNING team_id
`
type UpdateTeamOwnerByOwnerIDParams struct {
Owner uuid.UUID `json:"owner"`
Owner_2 uuid.UUID `json:"owner_2"`
}
func (q *Queries) UpdateTeamOwnerByOwnerID(ctx context.Context, arg UpdateTeamOwnerByOwnerIDParams) ([]uuid.UUID, error) {
rows, err := q.db.QueryContext(ctx, updateTeamOwnerByOwnerID, arg.Owner, arg.Owner_2)
if err != nil {
return nil, err
}
defer rows.Close()
var items []uuid.UUID
for rows.Next() {
var team_id uuid.UUID
if err := rows.Scan(&team_id); err != nil {
return nil, err
}
items = append(items, team_id)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

View File

@ -2859,6 +2859,7 @@ input LogoutUser {
input DeleteUserAccount { input DeleteUserAccount {
userID: UUID! userID: UUID!
newOwnerID: UUID
} }
type DeleteUserAccountPayload { type DeleteUserAccountPayload {
@ -12139,6 +12140,12 @@ func (ec *executionContext) unmarshalInputDeleteUserAccount(ctx context.Context,
if err != nil { if err != nil {
return it, err return it, err
} }
case "newOwnerID":
var err error
it.NewOwnerID, err = ec.unmarshalOUUID2ᚖgithubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, v)
if err != nil {
return it, err
}
} }
} }

View File

@ -142,7 +142,8 @@ type DeleteTeamPayload struct {
} }
type DeleteUserAccount struct { type DeleteUserAccount struct {
UserID uuid.UUID `json:"userID"` UserID uuid.UUID `json:"userID"`
NewOwnerID *uuid.UUID `json:"newOwnerID"`
} }
type DeleteUserAccountPayload struct { type DeleteUserAccountPayload struct {

View File

@ -625,6 +625,7 @@ input LogoutUser {
input DeleteUserAccount { input DeleteUserAccount {
userID: UUID! userID: UUID!
newOwnerID: UUID
} }
type DeleteUserAccountPayload { type DeleteUserAccountPayload {

View File

@ -756,6 +756,30 @@ func (r *mutationResolver) DeleteUserAccount(ctx context.Context, input DeleteUs
return &DeleteUserAccountPayload{Ok: false}, err return &DeleteUserAccountPayload{Ok: false}, err
} }
var newOwnerID uuid.UUID
if input.NewOwnerID == nil {
sysUser, err := r.Repository.GetUserAccountByUsername(ctx, "system")
if err != nil {
return &DeleteUserAccountPayload{Ok: false}, err
}
newOwnerID = sysUser.UserID
} else {
newOwnerID = *input.NewOwnerID
}
projectIDs, err := r.Repository.UpdateProjectOwnerByOwnerID(ctx, db.UpdateProjectOwnerByOwnerIDParams{Owner: user.UserID, Owner_2: newOwnerID})
if err != sql.ErrNoRows && err != nil {
return &DeleteUserAccountPayload{Ok: false}, err
}
for _, projectID := range projectIDs {
r.Repository.DeleteProjectMember(ctx, db.DeleteProjectMemberParams{UserID: newOwnerID, ProjectID: projectID})
}
teamIDs, err := r.Repository.UpdateTeamOwnerByOwnerID(ctx, db.UpdateTeamOwnerByOwnerIDParams{Owner: user.UserID, Owner_2: newOwnerID})
if err != sql.ErrNoRows && err != nil {
return &DeleteUserAccountPayload{Ok: false}, err
}
for _, teamID := range teamIDs {
r.Repository.DeleteTeamMember(ctx, db.DeleteTeamMemberParams{UserID: newOwnerID, TeamID: teamID})
}
err = r.Repository.DeleteUserAccountByID(ctx, input.UserID) err = r.Repository.DeleteUserAccountByID(ctx, input.UserID)
if err != nil { if err != nil {
return &DeleteUserAccountPayload{Ok: false}, err return &DeleteUserAccountPayload{Ok: false}, err

View File

@ -47,6 +47,7 @@ input LogoutUser {
input DeleteUserAccount { input DeleteUserAccount {
userID: UUID! userID: UUID!
newOwnerID: UUID
} }
type DeleteUserAccountPayload { type DeleteUserAccountPayload {