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);
hidePopup();
}}
onDeleteUser={($target, userID) => {
showPopup(
$target,
<Popup tab={0} title="Delete user?" onClose={() => hidePopup()}>
<DeleteUserPopup
onDeleteUser={() => {
deleteUser({ variables: { userID } });
hidePopup();
}}
/>
</Popup>,
);
onDeleteUser={(userID, newOwnerID) => {
deleteUser({ variables: { userID, newOwnerID } });
hidePopup();
}}
onAddUser={$target => {
showPopup(

View File

@ -481,9 +481,9 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({
activeMembers={task.assigned ?? []}
onMemberChange={(member, isActive) => {
if (isActive) {
assignTask({ variables: { taskID: task.id, userID: userID ?? '' } });
assignTask({ variables: { taskID: task.id, userID: member.id } });
} 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) => {
updateTaskChecklistItemLocation({
variables: { checklistID, checklistItemID: checklistItem.id, position: checklistItem.position },
optimisticResponse: {
__typename: 'Mutation',
updateTaskChecklistItemLocation: {

View File

@ -104,24 +104,28 @@ export const RemoveMemberButton = styled(Button)`
width: 100%;
`;
type TeamRoleManagerPopupProps = {
user: TaskUser;
user: User;
users: Array<User>;
warning?: string | null;
canChangeRole: boolean;
onChangeRole: (roleCode: RoleCode) => void;
updateUserPassword?: (user: TaskUser, password: string) => void;
onRemoveFromTeam?: () => void;
onDeleteUser?: (userID: string, newOwnerID: string | null) => void;
};
const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
warning,
user,
users,
canChangeRole,
onRemoveFromTeam,
onDeleteUser,
updateUserPassword,
onChangeRole,
}) => {
const { hidePopup, setTab } = usePopup();
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 (
<>
<Popup title={null} tab={0}>
@ -144,7 +148,7 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
>
Reset password...
</MiniProfileActionItem>
<MiniProfileActionItem onClick={() => setTab(5)}>Remove from organzation...</MiniProfileActionItem>
<MiniProfileActionItem onClick={() => setTab(2)}>Remove from organzation...</MiniProfileActionItem>
</MiniProfileActionWrapper>
</MiniProfileActions>
{warning && (
@ -198,21 +202,55 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
)}
</MiniProfileActions>
</Popup>
<Popup title="Remove from Team?" onClose={() => hidePopup()} tab={2}>
<Popup title="Remove from Organization?" onClose={() => hidePopup()} tab={2}>
<Content>
<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>
<RemoveMemberButton
color="danger"
{hasOwned && (
<>
<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={() => {
if (onRemoveFromTeam) {
onRemoveFromTeam();
if (onDeleteUser) {
console.log(`${!hasOwned} || (${hasOwned} && ${deleteUser})`);
if (!hasOwned || (hasOwned && deleteUser)) {
onDeleteUser(user.id, deleteUser ? deleteUser.value : null);
}
}
}}
color="danger"
>
Remove Member
</RemoveMemberButton>
Delete user
</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>
</Popup>
<Popup title="Reset password?" onClose={() => hidePopup()} tab={3}>
@ -253,18 +291,6 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
</UserPassConfirmButton>
</Content>
</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 = {
initialTab: number;
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;
users: Array<User>;
onUpdateUserPassword: (user: TaskUser, password: string) => void;
@ -700,9 +726,10 @@ const Admin: React.FC<AdminProps> = ({
<TabContent>
<MemberListWrapper>
<MemberListHeader>
<ListTitle>{`Users (${users.length})`}</ListTitle>
<ListTitle>{`Members (${users.length})`}</ListTitle>
<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>
<ListActions>
<FilterSearch width="250px" variant="alternate" placeholder="Filter by name" />
@ -735,6 +762,7 @@ const Admin: React.FC<AdminProps> = ({
$target,
<TeamRoleManagerPopup
user={member}
users={users}
warning={member.role && member.role.code === 'owner' ? warning : null}
updateUserPassword={(user, password) => {
onUpdateUserPassword(user, password);
@ -743,13 +771,7 @@ const Admin: React.FC<AdminProps> = ({
onChangeRole={roleCode => {
updateUserRole({ variables: { userID: member.id, roleCode } });
}}
onRemoveFromTeam={
member.role && member.role.code === 'owner'
? undefined
: () => {
hidePopup();
}
}
onDeleteUser={onDeleteUser}
/>,
);
}}

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

View File

@ -1,6 +1,14 @@
import React, { useRef } from 'react';
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 }>`
height: 28px;
width: 28px;
@ -34,6 +42,7 @@ type MemberProps = {
member: TaskUser;
showName?: boolean;
className?: string;
showCheckmark?: boolean;
};
const CardMemberWrapper = styled.div<{ ref: any }>`
@ -46,7 +55,14 @@ const CardMemberName = styled.span`
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>();
return (
<CardMemberWrapper
@ -60,10 +76,9 @@ const Member: React.FC<MemberProps> = ({ onCardMemberClick, taskID, member, show
}
}}
>
<CardMember bgColor={member.profileIcon.bgColor ?? '#7367F0'}>
<CardMemberInitials>{member.profileIcon.initials}</CardMemberInitials>
</CardMember>
<TaskAssignee onMemberProfile={() => {}} size={28} member={member} />
{showName && <CardMemberName>{member.fullName}</CardMemberName>}
{showCheckmark && <CardCheckmark width={12} height={12} />}
</CardMemberWrapper>
);
};

View File

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

View File

@ -13,6 +13,7 @@ import {
ActiveIconWrapper,
} from './Styles';
import { Checkmark } from 'shared/icons';
import Member from 'shared/components/Member';
type MemberManagerProps = {
availableMembers: Array<TaskUser>;
@ -45,7 +46,10 @@ const MemberManager: React.FC<MemberManagerProps> = ({
return (
<BoardMembersListItem key={member.id}>
<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;
if (isActive) {
setActiveMembers(activeMembers.filter(m => m.id !== member.id));
@ -54,15 +58,7 @@ const MemberManager: React.FC<MemberManagerProps> = ({
}
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>
);
})}

View File

@ -74,7 +74,13 @@ export const EditorButton = styled.div`
margin: 0 0 4px 8px;
padding: 6px 12px 6px 8px;
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`

View File

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

View File

@ -923,6 +923,7 @@ export type LogoutUser = {
export type DeleteUserAccount = {
userID: Scalars['UUID'];
newOwnerID?: Maybe<Scalars['UUID']>;
};
export type DeleteUserAccountPayload = {
@ -1886,12 +1887,31 @@ export type CreateUserAccountMutation = (
), role: (
{ __typename?: 'Role' }
& 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 = {
userID: Scalars['UUID'];
newOwnerID?: Maybe<Scalars['UUID']>;
};
@ -3851,6 +3871,26 @@ export const CreateUserAccountDocument = gql`
code
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 CreateUserAccountMutationOptions = ApolloReactCommon.BaseMutationOptions<CreateUserAccountMutation, CreateUserAccountMutationVariables>;
export const DeleteUserAccountDocument = gql`
mutation deleteUserAccount($userID: UUID!) {
deleteUserAccount(input: {userID: $userID}) {
mutation deleteUserAccount($userID: UUID!, $newOwnerID: UUID) {
deleteUserAccount(input: {userID: $userID, newOwnerID: $newOwnerID}) {
ok
userAccount {
id
@ -3910,6 +3950,7 @@ export type DeleteUserAccountMutationFn = ApolloReactCommon.MutationFunction<Del
* const [deleteUserAccountMutation, { data, loading, error }] = useDeleteUserAccountMutation({
* variables: {
* userID: // value for 'userID'
* newOwnerID: // value for 'newOwnerID'
* },
* });
*/

View File

@ -33,6 +33,26 @@ export const CREATE_USER_MUTATION = gql`
code
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';
export const DELETE_USER_MUTATION = gql`
mutation deleteUserAccount($userID: UUID!) {
deleteUserAccount(input: { userID: $userID }) {
mutation deleteUserAccount($userID: UUID!, $newOwnerID: UUID) {
deleteUserAccount(input: { userID: $userID, newOwnerID: $newOwnerID }) {
ok
userAccount {
id

View File

@ -3,8 +3,8 @@ import Icon, { IconProps } from './Icon';
const Checkmark: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
return (
<Icon width={width} height={height} className={className} viewBox="0 0 16 16">
<path d="M13.5 2l-7.5 7.5-3.5-3.5-2.5 2.5 6 6 10-10z" />
<Icon width={width} height={height} className={className} viewBox="0 0 512 512">
<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>
);
};

View File

@ -384,3 +384,35 @@ func (q *Queries) UpdateProjectNameByID(ctx context.Context, arg UpdateProjectNa
)
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)
UpdateProjectMemberRole(ctx context.Context, arg UpdateProjectMemberRoleParams) (ProjectMember, 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)
UpdateTaskChecklistItemName(ctx context.Context, arg UpdateTaskChecklistItemNameParams) (TaskChecklistItem, error)
UpdateTaskChecklistName(ctx context.Context, arg UpdateTaskChecklistNameParams) (TaskChecklist, error)
@ -105,6 +106,7 @@ type Querier interface {
UpdateTaskLocation(ctx context.Context, arg UpdateTaskLocationParams) (Task, error)
UpdateTaskName(ctx context.Context, arg UpdateTaskNameParams) (Task, 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)
UpdateUserRole(ctx context.Context, arg UpdateUserRoleParams) (UserAccount, error)
}

View File

@ -45,3 +45,6 @@ SELECT * FROM project WHERE owner = $1;
-- name: GetMemberProjectIDsForUserID :many
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
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
}
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 {
userID: UUID!
newOwnerID: UUID
}
type DeleteUserAccountPayload {
@ -12139,6 +12140,12 @@ func (ec *executionContext) unmarshalInputDeleteUserAccount(ctx context.Context,
if err != nil {
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 {
UserID uuid.UUID `json:"userID"`
UserID uuid.UUID `json:"userID"`
NewOwnerID *uuid.UUID `json:"newOwnerID"`
}
type DeleteUserAccountPayload struct {

View File

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

View File

@ -756,6 +756,30 @@ func (r *mutationResolver) DeleteUserAccount(ctx context.Context, input DeleteUs
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)
if err != nil {
return &DeleteUserAccountPayload{Ok: false}, err

View File

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