chore: project cleanup and bugfixes
This commit is contained in:
@ -1,38 +1,294 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { User, Plus, Lock, Pencil, Trash } from 'shared/icons';
|
||||
import { AgGridReact } from 'ag-grid-react';
|
||||
import React, {useState, useRef} from 'react';
|
||||
import {UserPlus, Checkmark} from 'shared/icons';
|
||||
import styled, {css} from 'styled-components';
|
||||
import TaskAssignee from 'shared/components/TaskAssignee';
|
||||
import {User, Plus, Lock, Pencil, Trash} from 'shared/icons';
|
||||
import {usePopup, Popup} from 'shared/components/PopupMenu';
|
||||
import {RoleCode, useUpdateUserRoleMutation} from 'shared/generated/graphql';
|
||||
import {AgGridReact} from 'ag-grid-react';
|
||||
import Input from 'shared/components/Input';
|
||||
import Member from 'shared/components/Member';
|
||||
|
||||
import 'ag-grid-community/dist/styles/ag-grid.css';
|
||||
import 'ag-grid-community/dist/styles/ag-theme-material.css';
|
||||
import Button from 'shared/components/Button';
|
||||
|
||||
const NewUserButton = styled(Button)`
|
||||
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;
|
||||
margin-right: 12px;
|
||||
`;
|
||||
|
||||
const InviteUserButton = styled(Button)`
|
||||
padding: 6px 12px;
|
||||
margin-right: 8px;
|
||||
`;
|
||||
|
||||
const MemberActions = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const GridTable = styled.div`
|
||||
height: 620px;
|
||||
`;
|
||||
|
||||
const RootWrapper = styled.div`
|
||||
height: 100%;
|
||||
display: flex;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
|
||||
${props =>
|
||||
props.disabled
|
||||
? css`
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
color: rgba(${props.theme.colors.text.primary}, 0.4);
|
||||
`
|
||||
: css`
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background: rgb(115, 103, 240);
|
||||
}
|
||||
`}
|
||||
`;
|
||||
export const Content = styled.div`
|
||||
padding: 0 12px 12px;
|
||||
`;
|
||||
|
||||
export const CurrentPermission = styled.span`
|
||||
margin-left: 4px;
|
||||
color: rgba(${props => 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: rgba(${props => props.theme.colors.text.primary}, 0.4);
|
||||
padding: 6px;
|
||||
`;
|
||||
|
||||
export const DeleteDescription = styled.div`
|
||||
font-size: 14px;
|
||||
color: rgba(${props => props.theme.colors.text.primary});
|
||||
`;
|
||||
|
||||
export const RemoveMemberButton = styled(Button)`
|
||||
margin-top: 16px;
|
||||
padding: 6px 12px;
|
||||
width: 100%;
|
||||
`;
|
||||
type TeamRoleManagerPopupProps = {
|
||||
user: TaskUser;
|
||||
warning?: string | null;
|
||||
canChangeRole: boolean;
|
||||
onChangeRole: (roleCode: RoleCode) => void;
|
||||
onRemoveFromTeam?: () => void;
|
||||
};
|
||||
|
||||
const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
|
||||
warning,
|
||||
user,
|
||||
canChangeRole,
|
||||
onRemoveFromTeam,
|
||||
onChangeRole,
|
||||
}) => {
|
||||
const {hidePopup, setTab} = usePopup();
|
||||
return (
|
||||
<>
|
||||
<Popup title={null} tab={0}>
|
||||
<MiniProfileActions>
|
||||
<MiniProfileActionWrapper>
|
||||
{user.role && (
|
||||
<MiniProfileActionItem
|
||||
onClick={() => {
|
||||
setTab(1);
|
||||
}}
|
||||
>
|
||||
Change permissions...
|
||||
<CurrentPermission>{`(${user.role.name})`}</CurrentPermission>
|
||||
</MiniProfileActionItem>
|
||||
)}
|
||||
<MiniProfileActionItem onClick={() => {}}>Reset password...</MiniProfileActionItem>
|
||||
<MiniProfileActionItem onClick={() => {}}>Lock user...</MiniProfileActionItem>
|
||||
<MiniProfileActionItem onClick={() => {}}>Remove from organzation...</MiniProfileActionItem>
|
||||
</MiniProfileActionWrapper>
|
||||
</MiniProfileActions>
|
||||
{warning && (
|
||||
<>
|
||||
<Separator />
|
||||
<WarningText>{warning}</WarningText>
|
||||
</>
|
||||
)}
|
||||
</Popup>
|
||||
<Popup title="Change Permissions" onClose={() => hidePopup()} tab={1}>
|
||||
<MiniProfileActions>
|
||||
<MiniProfileActionWrapper>
|
||||
{permissions
|
||||
.filter(p => (user.role && user.role.code === 'owner') || p.code !== 'owner')
|
||||
.map(perm => (
|
||||
<MiniProfileActionItem
|
||||
disabled={user.role && perm.code !== user.role.code && !canChangeRole}
|
||||
key={perm.code}
|
||||
onClick={() => {
|
||||
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();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<RoleName>
|
||||
{perm.name}
|
||||
{user.role && perm.code === user.role.code && <RoleCheckmark width={12} height={12} />}
|
||||
</RoleName>
|
||||
<RoleDescription>{perm.description}</RoleDescription>
|
||||
</MiniProfileActionItem>
|
||||
))}
|
||||
</MiniProfileActionWrapper>
|
||||
{user.role && user.role.code === 'owner' && (
|
||||
<>
|
||||
<Separator />
|
||||
<WarningText>You can't change roles because there must be an owner.</WarningText>
|
||||
</>
|
||||
)}
|
||||
</MiniProfileActions>
|
||||
</Popup>
|
||||
<Popup title="Remove from Team?" onClose={() => hidePopup()} tab={2}>
|
||||
<Content>
|
||||
<DeleteDescription>
|
||||
The member will be removed from all cards on this project. They will receive a notification.
|
||||
</DeleteDescription>
|
||||
<RemoveMemberButton
|
||||
color="danger"
|
||||
onClick={() => {
|
||||
if (onRemoveFromTeam) {
|
||||
onRemoveFromTeam();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Remove Member
|
||||
</RemoveMemberButton>
|
||||
</Content>
|
||||
</Popup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const InviteMemberButton = styled(Button)`
|
||||
padding: 7px 12px;
|
||||
`;
|
||||
|
||||
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 rgba(${props => props.theme.colors.border});
|
||||
`;
|
||||
|
||||
const MemberListItem = styled.div`
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid rgba(${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: rgba(${props => props.theme.colors.text.secondary});
|
||||
`;
|
||||
|
||||
const MemberItemUsername = styled.p`
|
||||
color: rgba(${props => props.theme.colors.text.primary});
|
||||
`;
|
||||
|
||||
const MemberListHeader = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
`;
|
||||
const ListTitle = styled.h3`
|
||||
font-size: 18px;
|
||||
color: rgba(${props => props.theme.colors.text.secondary});
|
||||
margin-bottom: 12px;
|
||||
`;
|
||||
const ListDesc = styled.span`
|
||||
font-size: 16px;
|
||||
color: rgba(${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 Root = styled.div`
|
||||
@ -103,7 +359,7 @@ const ActionButtonWrapper = styled.div`
|
||||
display: inline-flex;
|
||||
`;
|
||||
|
||||
const ActionButton: React.FC<ActionButtonProps> = ({ onClick, children }) => {
|
||||
const ActionButton: React.FC<ActionButtonProps> = ({onClick, children}) => {
|
||||
const $wrapper = useRef<HTMLDivElement>(null);
|
||||
return (
|
||||
<ActionButtonWrapper onClick={() => onClick($wrapper)} ref={$wrapper}>
|
||||
@ -133,7 +389,7 @@ type ListTableProps = {
|
||||
onDeleteUser: ($target: React.RefObject<HTMLElement>, userID: string) => void;
|
||||
};
|
||||
|
||||
const ListTable: React.FC<ListTableProps> = ({ users, onDeleteUser }) => {
|
||||
const ListTable: React.FC<ListTableProps> = ({users, onDeleteUser}) => {
|
||||
const data = {
|
||||
defaultColDef: {
|
||||
resizable: true,
|
||||
@ -146,10 +402,10 @@ const ListTable: React.FC<ListTableProps> = ({ users, onDeleteUser }) => {
|
||||
headerCheckboxSelection: true,
|
||||
checkboxSelection: true,
|
||||
},
|
||||
{ minWidth: 210, headerName: 'Username', editable: true, field: 'username' },
|
||||
{ minWidth: 225, headerName: 'Email', field: 'email' },
|
||||
{ minWidth: 200, headerName: 'Name', editable: true, field: 'fullName' },
|
||||
{ minWidth: 200, headerName: 'Role', editable: true, field: 'roleName' },
|
||||
{minWidth: 210, headerName: 'Username', editable: true, field: 'username'},
|
||||
{minWidth: 225, headerName: 'Email', field: 'email'},
|
||||
{minWidth: 200, headerName: 'Name', editable: true, field: 'fullName'},
|
||||
{minWidth: 200, headerName: 'Role', editable: true, field: 'roleName'},
|
||||
{
|
||||
minWidth: 200,
|
||||
headerName: 'Actions',
|
||||
@ -168,12 +424,12 @@ const ListTable: React.FC<ListTableProps> = ({ users, onDeleteUser }) => {
|
||||
};
|
||||
return (
|
||||
<Root>
|
||||
<div className="ag-theme-material" style={{ height: '296px', width: '100%' }}>
|
||||
<div className="ag-theme-material" style={{height: '296px', width: '100%'}}>
|
||||
<AgGridReact
|
||||
rowSelection="multiple"
|
||||
defaultColDef={data.defaultColDef}
|
||||
columnDefs={data.columnDefs}
|
||||
rowData={users.map(u => ({ ...u, roleName: u.role.name }))}
|
||||
rowData={users.map(u => ({...u, roleName: u.role.name}))}
|
||||
frameworkComponents={data.frameworkComponents}
|
||||
onFirstDataRendered={params => {
|
||||
params.api.sizeColumnsToFit();
|
||||
@ -199,7 +455,9 @@ const Container = styled.div`
|
||||
padding: 2.2rem;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: 1400px;
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
`;
|
||||
|
||||
const TabNav = styled.div`
|
||||
@ -223,7 +481,7 @@ const TabNavItem = styled.li`
|
||||
display: block;
|
||||
position: relative;
|
||||
`;
|
||||
const TabNavItemButton = styled.button<{ active: boolean }>`
|
||||
const TabNavItemButton = styled.button<{active: boolean}>`
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -250,7 +508,7 @@ const TabNavItemSpan = styled.span`
|
||||
font-size: 14px;
|
||||
`;
|
||||
|
||||
const TabNavLine = styled.span<{ top: number }>`
|
||||
const TabNavLine = styled.span<{top: number}>`
|
||||
left: auto;
|
||||
right: 0;
|
||||
width: 2px;
|
||||
@ -270,6 +528,7 @@ const TabContentWrapper = styled.div`
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
margin-left: 1rem;
|
||||
`;
|
||||
|
||||
const TabContent = styled.div`
|
||||
@ -279,17 +538,10 @@ const TabContent = styled.div`
|
||||
padding: 0;
|
||||
padding: 1.5rem;
|
||||
background-color: #10163a;
|
||||
margin-left: 1rem !important;
|
||||
border-radius: 0.5rem;
|
||||
`;
|
||||
|
||||
const items = [
|
||||
{ name: 'Insights' },
|
||||
{ name: 'Members' },
|
||||
{ name: 'Teams' },
|
||||
{ name: 'Security' },
|
||||
{ name: 'Settings' },
|
||||
];
|
||||
const items = [{name: 'Members'}, {name: 'Settings'}];
|
||||
|
||||
type NavItemProps = {
|
||||
active: boolean;
|
||||
@ -297,7 +549,7 @@ type NavItemProps = {
|
||||
tab: number;
|
||||
onClick: (tab: number, top: number) => void;
|
||||
};
|
||||
const NavItem: React.FC<NavItemProps> = ({ active, name, tab, onClick }) => {
|
||||
const NavItem: React.FC<NavItemProps> = ({active, name, tab, onClick}) => {
|
||||
const $item = useRef<HTMLLIElement>(null);
|
||||
return (
|
||||
<TabNavItem
|
||||
@ -326,10 +578,15 @@ type AdminProps = {
|
||||
users: Array<User>;
|
||||
};
|
||||
|
||||
const Admin: React.FC<AdminProps> = ({ initialTab, onAddUser, onDeleteUser, onInviteUser, users }) => {
|
||||
const Admin: React.FC<AdminProps> = ({initialTab, onAddUser, onDeleteUser, onInviteUser, 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, hidePopup} = usePopup();
|
||||
const $tabNav = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [updateUserRole] = useUpdateUserRoleMutation()
|
||||
return (
|
||||
<Container>
|
||||
<TabNav ref={$tabNav}>
|
||||
@ -353,17 +610,64 @@ const Admin: React.FC<AdminProps> = ({ initialTab, onAddUser, onDeleteUser, onIn
|
||||
</TabNav>
|
||||
<TabContentWrapper>
|
||||
<TabContent>
|
||||
<MemberActions>
|
||||
<NewUserButton variant="outline" onClick={onAddUser}>
|
||||
<Plus color="rgba(115, 103, 240)" size={10} />
|
||||
<span style={{ paddingLeft: '5px' }}>Create member</span>
|
||||
</NewUserButton>
|
||||
<InviteUserButton variant="outline" onClick={onInviteUser}>
|
||||
<Plus color="rgba(115, 103, 240)" size={10} />
|
||||
<span style={{ paddingLeft: '5px' }}>Invite member</span>
|
||||
</InviteUserButton>
|
||||
</MemberActions>
|
||||
<ListTable onDeleteUser={onDeleteUser} users={users} />
|
||||
<MemberListWrapper>
|
||||
<MemberListHeader>
|
||||
<ListTitle>{`Users (${users.length})`}</ListTitle>
|
||||
<ListDesc>
|
||||
Team members can view and join all Team Visible boards and create new boards in the team.
|
||||
</ListDesc>
|
||||
<ListActions>
|
||||
<FilterSearch width="250px" variant="alternate" placeholder="Filter by name" />
|
||||
<InviteMemberButton
|
||||
onClick={$target => {
|
||||
onAddUser($target);
|
||||
}}
|
||||
>
|
||||
<InviteIcon width={16} height={16} />
|
||||
New Member
|
||||
</InviteMemberButton>
|
||||
</ListActions>
|
||||
</MemberListHeader>
|
||||
<MemberList>
|
||||
{users.map(member => (
|
||||
<MemberListItem>
|
||||
<MemberProfile showRoleIcons size={32} onMemberProfile={() => {}} member={member} />
|
||||
<MemberListItemDetails>
|
||||
<MemberItemName>{member.fullName}</MemberItemName>
|
||||
<MemberItemUsername>{`@${member.username}`}</MemberItemUsername>
|
||||
</MemberListItemDetails>
|
||||
<MemberItemOptions>
|
||||
<MemberItemOption variant="flat">On 6 projects</MemberItemOption>
|
||||
<MemberItemOption
|
||||
variant="outline"
|
||||
onClick={$target => {
|
||||
showPopup(
|
||||
$target,
|
||||
<TeamRoleManagerPopup
|
||||
user={member}
|
||||
warning={member.role && member.role.code === 'owner' ? warning : null}
|
||||
canChangeRole={member.role && member.role.code !== 'owner'}
|
||||
onChangeRole={roleCode => {
|
||||
updateUserRole({variables: {userID: member.id, roleCode}})
|
||||
}}
|
||||
onRemoveFromTeam={
|
||||
member.role && member.role.code === 'owner'
|
||||
? undefined
|
||||
: () => {
|
||||
hidePopup();
|
||||
}
|
||||
}
|
||||
/>,
|
||||
);
|
||||
}}
|
||||
>
|
||||
Manage
|
||||
</MemberItemOption>
|
||||
</MemberItemOptions>
|
||||
</MemberListItem>
|
||||
))}
|
||||
</MemberList>
|
||||
</MemberListWrapper>
|
||||
</TabContent>
|
||||
</TabContentWrapper>
|
||||
</Container>
|
||||
|
@ -1,9 +1,9 @@
|
||||
import styled, { css } from 'styled-components';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { mixin } from 'shared/utils/styles';
|
||||
import styled, {css} from 'styled-components';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {mixin} from 'shared/utils/styles';
|
||||
import TextareaAutosize from 'react-autosize-textarea';
|
||||
import { CheckCircle } from 'shared/icons';
|
||||
import { RefObject } from 'react';
|
||||
import {CheckCircle} from 'shared/icons';
|
||||
import {RefObject} from 'react';
|
||||
|
||||
export const ClockIcon = styled(FontAwesomeIcon)``;
|
||||
|
||||
@ -22,7 +22,7 @@ export const EditorTextarea = styled(TextareaAutosize)`
|
||||
min-height: 54px;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
line-height: 16px;
|
||||
line-height: 18px;
|
||||
color: rgba(${props => props.theme.colors.text.primary});
|
||||
&:focus {
|
||||
border: none;
|
||||
@ -57,7 +57,7 @@ export const DescriptionBadge = styled(ListCardBadge)`
|
||||
padding-right: 6px;
|
||||
`;
|
||||
|
||||
export const DueDateCardBadge = styled(ListCardBadge)<{ isPastDue: boolean }>`
|
||||
export const DueDateCardBadge = styled(ListCardBadge) <{isPastDue: boolean}>`
|
||||
font-size: 12px;
|
||||
${props =>
|
||||
props.isPastDue &&
|
||||
@ -76,7 +76,7 @@ export const ListCardBadgeText = styled.span`
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export const ListCardContainer = styled.div<{ isActive: boolean; editable: boolean }>`
|
||||
export const ListCardContainer = styled.div<{isActive: boolean; editable: boolean}>`
|
||||
max-width: 256px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 3px;
|
||||
@ -93,7 +93,7 @@ export const ListCardInnerContainer = styled.div`
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
export const ListCardDetails = styled.div<{ complete: boolean }>`
|
||||
export const ListCardDetails = styled.div<{complete: boolean}>`
|
||||
overflow: hidden;
|
||||
padding: 6px 8px 2px;
|
||||
position: relative;
|
||||
@ -147,7 +147,7 @@ export const CardTitle = styled.span`
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
word-wrap: break-word;
|
||||
line-height: 16px;
|
||||
line-height: 18px;
|
||||
font-size: 14px;
|
||||
color: rgba(${props => props.theme.colors.text.primary});
|
||||
|
||||
|
@ -5,7 +5,7 @@ import NormalizeStyles from 'App/NormalizeStyles';
|
||||
import { theme } from 'App/ThemeStyles';
|
||||
import produce from 'immer';
|
||||
import styled, { ThemeProvider } from 'styled-components';
|
||||
import Checklist from '.';
|
||||
import Checklist, { ChecklistItem } from '.';
|
||||
|
||||
export default {
|
||||
component: Checklist,
|
||||
@ -86,6 +86,8 @@ export const Default = () => {
|
||||
<ThemeProvider theme={theme}>
|
||||
<Container>
|
||||
<Checklist
|
||||
wrapperProps={{}}
|
||||
handleProps={{}}
|
||||
name={checklistName}
|
||||
checklistID="checklist-one"
|
||||
items={items}
|
||||
@ -130,7 +132,21 @@ export const Default = () => {
|
||||
);
|
||||
}}
|
||||
onToggleItem={onToggleItem}
|
||||
/>
|
||||
>
|
||||
{items.map((item, idx) => (
|
||||
<ChecklistItem
|
||||
key={item.id}
|
||||
wrapperProps={{}}
|
||||
handleProps={{}}
|
||||
itemID={item.id}
|
||||
name={item.name}
|
||||
complete={item.complete}
|
||||
onDeleteItem={() => {}}
|
||||
onChangeName={() => {}}
|
||||
onToggleItem={() => {}}
|
||||
/>
|
||||
))}
|
||||
</Checklist>
|
||||
</Container>
|
||||
</ThemeProvider>
|
||||
</>
|
||||
|
@ -1,6 +1,13 @@
|
||||
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 {
|
||||
isPositionChanged,
|
||||
getSortedDraggables,
|
||||
getNewDraggablePosition,
|
||||
getAfterDropDraggableList,
|
||||
} from 'shared/utils/draggables';
|
||||
import Button from 'shared/components/Button';
|
||||
import TextareaAutosize from 'react-autosize-textarea';
|
||||
import Control from 'react-select/src/components/Control';
|
||||
@ -84,7 +91,7 @@ const ChecklistProgressBarCurrent = styled.div<{ width: number }>`
|
||||
transition: width 0.14s ease-in, background 0.14s ease-in;
|
||||
`;
|
||||
|
||||
const ChecklistItems = styled.div`
|
||||
export const ChecklistItems = styled.div`
|
||||
min-height: 8px;
|
||||
`;
|
||||
|
||||
@ -199,7 +206,7 @@ const TrashButton = styled(Trash)`
|
||||
fill: rgba(${props => props.theme.colors.text.primary});
|
||||
`;
|
||||
|
||||
const ChecklistItemWrapper = styled.div`
|
||||
const ChecklistItemWrapper = styled.div<{ ref: any }>`
|
||||
user-select: none;
|
||||
clear: both;
|
||||
padding-left: 40px;
|
||||
@ -270,122 +277,121 @@ type ChecklistItemProps = {
|
||||
complete: boolean;
|
||||
name: string;
|
||||
onChangeName: (itemID: string, currentName: string) => void;
|
||||
wrapperProps: any;
|
||||
handleProps: any;
|
||||
onToggleItem: (itemID: string, complete: boolean) => void;
|
||||
onDeleteItem: (itemID: string) => void;
|
||||
};
|
||||
|
||||
const ChecklistItem: React.FC<ChecklistItemProps> = ({
|
||||
itemID,
|
||||
complete,
|
||||
name,
|
||||
onChangeName,
|
||||
onToggleItem,
|
||||
onDeleteItem,
|
||||
}) => {
|
||||
const $item = useRef<HTMLDivElement>(null);
|
||||
const $editor = useRef<HTMLTextAreaElement>(null);
|
||||
const [editting, setEditting] = useState(false);
|
||||
const [currentName, setCurrentName] = useState(name);
|
||||
useEffect(() => {
|
||||
if (editting && $editor && $editor.current) {
|
||||
$editor.current.focus();
|
||||
$editor.current.select();
|
||||
}
|
||||
}, [editting]);
|
||||
useOnOutsideClick($item, true, () => setEditting(false), null);
|
||||
return (
|
||||
<ChecklistItemWrapper ref={$item}>
|
||||
<ChecklistIcon
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onToggleItem(itemID, !complete);
|
||||
}}
|
||||
>
|
||||
{complete ? (
|
||||
<ChecklistItemCheckedIcon width={20} height={20} />
|
||||
) : (
|
||||
<ChecklistItemUncheckedIcon width={20} height={20} />
|
||||
)}
|
||||
</ChecklistIcon>
|
||||
{editting ? (
|
||||
<>
|
||||
<ChecklistNameEditorWrapper>
|
||||
<ChecklistNameEditor
|
||||
ref={$editor}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter') {
|
||||
onChangeName(itemID, currentName);
|
||||
setEditting(false);
|
||||
}
|
||||
}}
|
||||
onChange={e => {
|
||||
setCurrentName(e.currentTarget.value);
|
||||
}}
|
||||
value={currentName}
|
||||
/>
|
||||
</ChecklistNameEditorWrapper>
|
||||
<EditControls>
|
||||
<SaveButton
|
||||
onClick={() => {
|
||||
onChangeName(itemID, currentName);
|
||||
setEditting(false);
|
||||
}}
|
||||
variant="relief"
|
||||
>
|
||||
Save
|
||||
</SaveButton>
|
||||
<CancelButton
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
setEditting(false);
|
||||
}}
|
||||
>
|
||||
<Cross width={20} height={20} />
|
||||
</CancelButton>
|
||||
<Spacer />
|
||||
<EditableDeleteButton
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
setEditting(false);
|
||||
onDeleteItem(itemID);
|
||||
}}
|
||||
>
|
||||
<Trash width={16} height={16} />
|
||||
</EditableDeleteButton>
|
||||
</EditControls>
|
||||
</>
|
||||
) : (
|
||||
<ChecklistItemDetails
|
||||
onClick={() => {
|
||||
setEditting(true);
|
||||
export const ChecklistItem = React.forwardRef(
|
||||
(
|
||||
{ itemID, complete, name, wrapperProps, handleProps, onChangeName, onToggleItem, onDeleteItem }: ChecklistItemProps,
|
||||
$item,
|
||||
) => {
|
||||
const $editor = useRef<HTMLTextAreaElement>(null);
|
||||
const [editting, setEditting] = useState(false);
|
||||
const [currentName, setCurrentName] = useState(name);
|
||||
useEffect(() => {
|
||||
if (editting && $editor && $editor.current) {
|
||||
$editor.current.focus();
|
||||
$editor.current.select();
|
||||
}
|
||||
}, [editting]);
|
||||
// useOnOutsideClick($item, true, () => setEditting(false), null);
|
||||
return (
|
||||
<ChecklistItemWrapper ref={$item} {...wrapperProps} {...handleProps}>
|
||||
<ChecklistIcon
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onToggleItem(itemID, !complete);
|
||||
}}
|
||||
>
|
||||
<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(itemID);
|
||||
}}
|
||||
>
|
||||
<TrashButton width={14} height={14} />
|
||||
</ControlButton>
|
||||
</ChecklistControls>
|
||||
</ChecklistItemTextControls>
|
||||
</ChecklistItemRow>
|
||||
</ChecklistItemDetails>
|
||||
)}
|
||||
</ChecklistItemWrapper>
|
||||
);
|
||||
};
|
||||
{complete ? (
|
||||
<ChecklistItemCheckedIcon width={20} height={20} />
|
||||
) : (
|
||||
<ChecklistItemUncheckedIcon width={20} height={20} />
|
||||
)}
|
||||
</ChecklistIcon>
|
||||
{editting ? (
|
||||
<>
|
||||
<ChecklistNameEditorWrapper>
|
||||
<ChecklistNameEditor
|
||||
ref={$editor}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter') {
|
||||
onChangeName(itemID, currentName);
|
||||
setEditting(false);
|
||||
}
|
||||
}}
|
||||
onChange={e => {
|
||||
setCurrentName(e.currentTarget.value);
|
||||
}}
|
||||
value={currentName}
|
||||
/>
|
||||
</ChecklistNameEditorWrapper>
|
||||
<EditControls>
|
||||
<SaveButton
|
||||
onClick={() => {
|
||||
onChangeName(itemID, currentName);
|
||||
setEditting(false);
|
||||
}}
|
||||
variant="relief"
|
||||
>
|
||||
Save
|
||||
</SaveButton>
|
||||
<CancelButton
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
setEditting(false);
|
||||
}}
|
||||
>
|
||||
<Cross width={20} height={20} />
|
||||
</CancelButton>
|
||||
<Spacer />
|
||||
<EditableDeleteButton
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
setEditting(false);
|
||||
onDeleteItem(itemID);
|
||||
}}
|
||||
>
|
||||
<Trash width={16} height={16} />
|
||||
</EditableDeleteButton>
|
||||
</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(itemID);
|
||||
}}
|
||||
>
|
||||
<TrashButton width={14} height={14} />
|
||||
</ControlButton>
|
||||
</ChecklistControls>
|
||||
</ChecklistItemTextControls>
|
||||
</ChecklistItemRow>
|
||||
</ChecklistItemDetails>
|
||||
)}
|
||||
</ChecklistItemWrapper>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
type AddNewItemProps = {
|
||||
onAddItem: (name: string) => void;
|
||||
@ -503,94 +509,112 @@ type ChecklistProps = {
|
||||
checklistID: string;
|
||||
onDeleteChecklist: ($target: React.RefObject<HTMLElement>, checklistID: string) => void;
|
||||
name: string;
|
||||
children: React.ReactNode;
|
||||
onChangeName: (item: string) => void;
|
||||
onToggleItem: (taskID: string, complete: boolean) => void;
|
||||
onChangeItemName: (itemID: string, currentName: string) => void;
|
||||
wrapperProps: any;
|
||||
handleProps: any;
|
||||
onDeleteItem: (itemID: string) => void;
|
||||
onAddItem: (itemName: string) => void;
|
||||
items: Array<TaskChecklistItem>;
|
||||
};
|
||||
|
||||
const Checklist: React.FC<ChecklistProps> = ({
|
||||
checklistID,
|
||||
onDeleteChecklist,
|
||||
name,
|
||||
items,
|
||||
onToggleItem,
|
||||
onAddItem,
|
||||
onChangeItemName,
|
||||
onChangeName,
|
||||
onDeleteItem,
|
||||
}) => {
|
||||
const $name = useRef<HTMLTextAreaElement>(null);
|
||||
const complete = items.reduce((prev, item) => prev + (item.complete ? 1 : 0), 0);
|
||||
const percent = items.length === 0 ? 0 : Math.floor((complete / items.length) * 100);
|
||||
const [editting, setEditting] = useState(false);
|
||||
// useOnOutsideClick($name, true, () => setEditting(false), null);
|
||||
useEffect(() => {
|
||||
if (editting && $name && $name.current) {
|
||||
$name.current.focus();
|
||||
$name.current.select();
|
||||
}
|
||||
}, [editting]);
|
||||
return (
|
||||
<Wrapper>
|
||||
<WindowTitle>
|
||||
<WindowTitleIcon width={24} height={24} />
|
||||
{editting ? (
|
||||
<ChecklistTitleEditor
|
||||
ref={$name}
|
||||
name={name}
|
||||
onChangeName={currentName => {
|
||||
onChangeName(currentName);
|
||||
setEditting(false);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setEditting(false);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<WindowChecklistTitle>
|
||||
<WindowTitleText onClick={() => setEditting(true)}>{name}</WindowTitleText>
|
||||
<WindowOptions>
|
||||
<DeleteButton
|
||||
onClick={$target => {
|
||||
onDeleteChecklist($target, checklistID);
|
||||
}}
|
||||
color="danger"
|
||||
variant="outline"
|
||||
>
|
||||
Delete
|
||||
</DeleteButton>
|
||||
</WindowOptions>
|
||||
</WindowChecklistTitle>
|
||||
)}
|
||||
</WindowTitle>
|
||||
<ChecklistProgress>
|
||||
<ChecklistProgressPercent>{`${percent}%`}</ChecklistProgressPercent>
|
||||
<ChecklistProgressBar>
|
||||
<ChecklistProgressBarCurrent width={percent} />
|
||||
</ChecklistProgressBar>
|
||||
</ChecklistProgress>
|
||||
<ChecklistItems>
|
||||
{items
|
||||
.slice()
|
||||
.sort((a, b) => a.position - b.position)
|
||||
.map(item => (
|
||||
<ChecklistItem
|
||||
key={item.id}
|
||||
itemID={item.id}
|
||||
name={item.name}
|
||||
complete={item.complete}
|
||||
onDeleteItem={onDeleteItem}
|
||||
onChangeName={onChangeItemName}
|
||||
onToggleItem={onToggleItem}
|
||||
const Checklist = React.forwardRef(
|
||||
(
|
||||
{
|
||||
checklistID,
|
||||
children,
|
||||
onDeleteChecklist,
|
||||
name,
|
||||
items,
|
||||
wrapperProps,
|
||||
handleProps,
|
||||
onToggleItem,
|
||||
onAddItem,
|
||||
onChangeItemName,
|
||||
onChangeName,
|
||||
onDeleteItem,
|
||||
}: ChecklistProps,
|
||||
$container,
|
||||
) => {
|
||||
const $name = useRef<HTMLTextAreaElement>(null);
|
||||
const complete = items.reduce((prev, item) => prev + (item.complete ? 1 : 0), 0);
|
||||
const percent = items.length === 0 ? 0 : Math.floor((complete / items.length) * 100);
|
||||
const [editting, setEditting] = useState(false);
|
||||
// useOnOutsideClick($name, true, () => setEditting(false), null);
|
||||
useEffect(() => {
|
||||
if (editting && $name && $name.current) {
|
||||
$name.current.focus();
|
||||
$name.current.select();
|
||||
}
|
||||
}, [editting]);
|
||||
useEffect(() => {
|
||||
console.log($container);
|
||||
}, [$container]);
|
||||
return (
|
||||
<Wrapper ref={$container} {...wrapperProps}>
|
||||
<WindowTitle>
|
||||
<WindowTitleIcon width={24} height={24} />
|
||||
{editting ? (
|
||||
<ChecklistTitleEditor
|
||||
ref={$name}
|
||||
name={name}
|
||||
onChangeName={currentName => {
|
||||
onChangeName(currentName);
|
||||
setEditting(false);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setEditting(false);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ChecklistItems>
|
||||
<AddNewItem onAddItem={onAddItem} />
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
) : (
|
||||
<WindowChecklistTitle {...handleProps}>
|
||||
<WindowTitleText onClick={() => setEditting(true)}>{name}</WindowTitleText>
|
||||
<WindowOptions>
|
||||
<DeleteButton
|
||||
onClick={$target => {
|
||||
onDeleteChecklist($target, checklistID);
|
||||
}}
|
||||
color="danger"
|
||||
variant="outline"
|
||||
>
|
||||
Delete
|
||||
</DeleteButton>
|
||||
</WindowOptions>
|
||||
</WindowChecklistTitle>
|
||||
)}
|
||||
</WindowTitle>
|
||||
<ChecklistProgress>
|
||||
<ChecklistProgressPercent>{`${percent}%`}</ChecklistProgressPercent>
|
||||
<ChecklistProgressBar>
|
||||
<ChecklistProgressBarCurrent width={percent} />
|
||||
</ChecklistProgressBar>
|
||||
</ChecklistProgress>
|
||||
{children}
|
||||
<AddNewItem onAddItem={onAddItem} />
|
||||
</Wrapper>
|
||||
);
|
||||
},
|
||||
);
|
||||
/*
|
||||
<ChecklistItems>
|
||||
{items
|
||||
.slice()
|
||||
.sort((a, b) => a.position - b.position)
|
||||
.map((item, idx) => (
|
||||
<ChecklistItem
|
||||
index={idx}
|
||||
key={item.id}
|
||||
itemID={item.id}
|
||||
name={item.name}
|
||||
complete={item.complete}
|
||||
onDeleteItem={onDeleteItem}
|
||||
onChangeName={onChangeItemName}
|
||||
onToggleItem={onToggleItem}
|
||||
/>
|
||||
))}
|
||||
|
||||
</ChecklistItems>
|
||||
*/
|
||||
export default Checklist;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import styled, { css } from 'styled-components/macro';
|
||||
import React, {useState, useEffect} from 'react';
|
||||
import styled, {css} from 'styled-components/macro';
|
||||
|
||||
const InputWrapper = styled.div<{ width: string }>`
|
||||
const InputWrapper = styled.div<{width: string}>`
|
||||
position: relative;
|
||||
width: ${props => props.width};
|
||||
display: flex;
|
||||
@ -14,7 +14,7 @@ const InputWrapper = styled.div<{ width: string }>`
|
||||
margin-top: 24px;
|
||||
`;
|
||||
|
||||
const InputLabel = styled.span<{ width: string }>`
|
||||
const InputLabel = styled.span<{width: string}>`
|
||||
width: ${props => props.width};
|
||||
padding: 0.7rem !important;
|
||||
color: #c2c6dc;
|
||||
|
@ -76,9 +76,8 @@ export const HeaderTitle = styled.span`
|
||||
|
||||
export const Content = styled.div`
|
||||
max-height: 632px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
`;
|
||||
|
||||
export const LabelSearch = styled.input`
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
|
@ -63,7 +63,6 @@ export const TaskDetailsSidebar = styled.div`
|
||||
`;
|
||||
|
||||
export const TaskDetailsTitleWrapper = styled.div`
|
||||
height: 44px;
|
||||
width: 100%;
|
||||
margin: 0 0 0 -8px;
|
||||
display: inline-block;
|
||||
|
@ -75,6 +75,8 @@ export const Default = () => {
|
||||
onDeleteChecklist={action('delete checklist')}
|
||||
onOpenAddChecklistPopup={action(' open checklist')}
|
||||
onOpenDueDatePopop={action('open due date popup')}
|
||||
onChecklistDrop={action('on checklist drop')}
|
||||
onChecklistItemDrop={action('on checklist item drop')}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
|
@ -2,6 +2,14 @@ import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Bin, Cross, Plus } from 'shared/icons';
|
||||
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
|
||||
import {
|
||||
isPositionChanged,
|
||||
getSortedDraggables,
|
||||
getNewDraggablePosition,
|
||||
getAfterDropDraggableList,
|
||||
} from 'shared/utils/draggables';
|
||||
import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd';
|
||||
import TaskAssignee from 'shared/components/TaskAssignee';
|
||||
import moment from 'moment';
|
||||
|
||||
@ -46,7 +54,10 @@ import {
|
||||
MetaDetailTitle,
|
||||
MetaDetailContent,
|
||||
} from './Styles';
|
||||
import Checklist from '../Checklist';
|
||||
import Checklist, { ChecklistItem, ChecklistItems } from '../Checklist';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const ChecklistContainer = styled.div``;
|
||||
|
||||
type TaskContentProps = {
|
||||
onEditContent: () => void;
|
||||
@ -145,6 +156,8 @@ type TaskDetailsProps = {
|
||||
onChangeChecklistName: (checklistID: string, name: string) => void;
|
||||
onDeleteChecklist: ($target: React.RefObject<HTMLElement>, checklistID: string) => void;
|
||||
onCloseModal: () => void;
|
||||
onChecklistDrop: (checklist: TaskChecklist) => void;
|
||||
onChecklistItemDrop: (prevChecklistID: string, checklistID: string, checklistItem: TaskChecklistItem) => void;
|
||||
};
|
||||
|
||||
const TaskDetails: React.FC<TaskDetailsProps> = ({
|
||||
@ -153,6 +166,8 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
||||
onTaskNameChange,
|
||||
onOpenAddChecklistPopup,
|
||||
onChangeChecklistName,
|
||||
onChecklistDrop,
|
||||
onChecklistItemDrop,
|
||||
onToggleTaskComplete,
|
||||
onTaskDescriptionChange,
|
||||
onChangeItemName,
|
||||
@ -190,14 +205,91 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
||||
onOpenAddMemberPopup(task, $target);
|
||||
};
|
||||
const onAddChecklist = ($target: React.RefObject<HTMLElement>) => {
|
||||
onOpenAddChecklistPopup(task, $target)
|
||||
}
|
||||
onOpenAddChecklistPopup(task, $target);
|
||||
};
|
||||
const $dueDateLabel = useRef<HTMLDivElement>(null);
|
||||
const $addLabelRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const onAddLabel = ($target: React.RefObject<HTMLElement>) => {
|
||||
onOpenAddLabelPopup(task, $target);
|
||||
};
|
||||
|
||||
const onDragEnd = ({ draggableId, source, destination, type }: DropResult) => {
|
||||
if (typeof destination === 'undefined') return;
|
||||
if (!isPositionChanged(source, destination)) return;
|
||||
|
||||
const isChecklist = type === 'checklist';
|
||||
const isSameChecklist = destination.droppableId === source.droppableId;
|
||||
let droppedDraggable: DraggableElement | null = null;
|
||||
let beforeDropDraggables: Array<DraggableElement> | null = null;
|
||||
|
||||
if (!task.checklists) return;
|
||||
if (isChecklist) {
|
||||
const droppedGroup = task.checklists.find(taskGroup => taskGroup.id === draggableId);
|
||||
if (droppedGroup) {
|
||||
droppedDraggable = {
|
||||
id: draggableId,
|
||||
position: droppedGroup.position,
|
||||
};
|
||||
beforeDropDraggables = getSortedDraggables(
|
||||
task.checklists.map(checklist => {
|
||||
return { id: checklist.id, position: checklist.position };
|
||||
}),
|
||||
);
|
||||
if (droppedDraggable === null || beforeDropDraggables === null) {
|
||||
throw new Error('before drop draggables is null');
|
||||
}
|
||||
const afterDropDraggables = getAfterDropDraggableList(
|
||||
beforeDropDraggables,
|
||||
droppedDraggable,
|
||||
isChecklist,
|
||||
isSameChecklist,
|
||||
destination,
|
||||
);
|
||||
const newPosition = getNewDraggablePosition(afterDropDraggables, destination.index);
|
||||
console.log(droppedGroup);
|
||||
console.log(`positiion: ${newPosition}`);
|
||||
onChecklistDrop({ ...droppedGroup, position: newPosition });
|
||||
} else {
|
||||
throw { error: 'task group can not be found' };
|
||||
}
|
||||
} else {
|
||||
const targetChecklist = task.checklists.findIndex(
|
||||
checklist => checklist.items.findIndex(item => item.id === draggableId) !== -1,
|
||||
);
|
||||
const droppedChecklistItem = task.checklists[targetChecklist].items.find(item => item.id === draggableId);
|
||||
|
||||
if (droppedChecklistItem) {
|
||||
droppedDraggable = {
|
||||
id: draggableId,
|
||||
position: droppedChecklistItem.position,
|
||||
};
|
||||
beforeDropDraggables = getSortedDraggables(
|
||||
task.checklists[targetChecklist].items.map(item => {
|
||||
return { id: item.id, position: item.position };
|
||||
}),
|
||||
);
|
||||
if (droppedDraggable === null || beforeDropDraggables === null) {
|
||||
throw new Error('before drop draggables is null');
|
||||
}
|
||||
const afterDropDraggables = getAfterDropDraggableList(
|
||||
beforeDropDraggables,
|
||||
droppedDraggable,
|
||||
isChecklist,
|
||||
isSameChecklist,
|
||||
destination,
|
||||
);
|
||||
const newPosition = getNewDraggablePosition(afterDropDraggables, destination.index);
|
||||
const newItem = {
|
||||
...droppedChecklistItem,
|
||||
position: newPosition,
|
||||
};
|
||||
onChecklistItemDrop(droppedChecklistItem.taskChecklistID, destination.droppableId, newItem);
|
||||
console.log(newItem);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TaskActions>
|
||||
@ -289,33 +381,80 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
||||
) : (
|
||||
<TaskContent description={description} onEditContent={handleClick} />
|
||||
)}
|
||||
{task.checklists &&
|
||||
task.checklists
|
||||
.slice()
|
||||
.sort((a, b) => a.position - b.position)
|
||||
.map(checklist => (
|
||||
<Checklist
|
||||
key={checklist.id}
|
||||
name={checklist.name}
|
||||
checklistID={checklist.id}
|
||||
items={checklist.items}
|
||||
onDeleteChecklist={onDeleteChecklist}
|
||||
onChangeName={newName => onChangeChecklistName(checklist.id, newName)}
|
||||
onToggleItem={onToggleChecklistItem}
|
||||
onDeleteItem={onDeleteItem}
|
||||
onAddItem={n => {
|
||||
if (task.checklists) {
|
||||
let position = 65535;
|
||||
const [lastItem] = checklist.items.sort((a, b) => a.position - b.position).slice(-1);
|
||||
if (lastItem) {
|
||||
position = lastItem.position * 2 + 1;
|
||||
}
|
||||
onAddItem(checklist.id, n, position);
|
||||
}
|
||||
}}
|
||||
onChangeItemName={onChangeItemName}
|
||||
/>
|
||||
))}
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable direction="vertical" type="checklist" droppableId="root">
|
||||
{dropProvided => (
|
||||
<ChecklistContainer {...dropProvided.droppableProps} ref={dropProvided.innerRef}>
|
||||
{task.checklists &&
|
||||
task.checklists
|
||||
.slice()
|
||||
.sort((a, b) => a.position - b.position)
|
||||
.map((checklist, idx) => (
|
||||
<Draggable key={checklist.id} draggableId={checklist.id} index={idx}>
|
||||
{provided => (
|
||||
<Checklist
|
||||
ref={provided.innerRef}
|
||||
wrapperProps={provided.draggableProps}
|
||||
handleProps={provided.dragHandleProps}
|
||||
key={checklist.id}
|
||||
name={checklist.name}
|
||||
checklistID={checklist.id}
|
||||
items={checklist.items}
|
||||
onDeleteChecklist={onDeleteChecklist}
|
||||
onChangeName={newName => onChangeChecklistName(checklist.id, newName)}
|
||||
onToggleItem={onToggleChecklistItem}
|
||||
onDeleteItem={onDeleteItem}
|
||||
onAddItem={n => {
|
||||
if (task.checklists) {
|
||||
let position = 65535;
|
||||
const [lastItem] = checklist.items
|
||||
.sort((a, b) => a.position - b.position)
|
||||
.slice(-1);
|
||||
if (lastItem) {
|
||||
position = lastItem.position * 2 + 1;
|
||||
}
|
||||
onAddItem(checklist.id, n, position);
|
||||
}
|
||||
}}
|
||||
onChangeItemName={onChangeItemName}
|
||||
>
|
||||
<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}
|
||||
ref={itemDrop.innerRef}
|
||||
wrapperProps={itemDrop.draggableProps}
|
||||
handleProps={itemDrop.dragHandleProps}
|
||||
name={item.name}
|
||||
complete={item.complete}
|
||||
onDeleteItem={onDeleteItem}
|
||||
onChangeName={onChangeItemName}
|
||||
onToggleItem={() => {}}
|
||||
/>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
{checklistDrop.placeholder}
|
||||
</ChecklistItems>
|
||||
)}
|
||||
</Droppable>
|
||||
</Checklist>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
{dropProvided.placeholder}
|
||||
</ChecklistContainer>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
</TaskDetailsSection>
|
||||
</TaskDetailsContent>
|
||||
<TaskDetailsSidebar>
|
||||
|
Reference in New Issue
Block a user