feat: add task details

This commit is contained in:
Jordan Knott
2020-09-02 20:25:28 -05:00
parent a9a1576f46
commit 771d598c04
28 changed files with 1470 additions and 683 deletions

View File

@ -98,7 +98,7 @@ const AddList: React.FC<AddListProps> = ({ onSave }) => {
) : (
<Placeholder>
<AddIconWrapper>
<Plus size={12} color="#c2c6dc" />
<Plus width={12} height={12} />
</AddIconWrapper>
Add another list
</Placeholder>

View File

@ -1,7 +1,7 @@
import React, { useRef } from 'react';
import styled, { css } from 'styled-components/macro';
const Text = styled.span<{ fontSize: string; justifyTextContent: string }>`
const Text = styled.span<{ fontSize: string; justifyTextContent: string; hasIcon?: boolean }>`
position: relative;
display: flex;
align-items: center;
@ -9,6 +9,11 @@ const Text = styled.span<{ fontSize: string; justifyTextContent: string }>`
transition: all 0.2s ease;
font-size: ${props => props.fontSize};
color: rgba(${props => props.theme.colors.text.secondary});
${props =>
props.hasIcon &&
css`
padding-left: 4px;
`}
`;
const Base = styled.button<{ color: string; disabled: boolean }>`
@ -18,6 +23,8 @@ const Base = styled.button<{ color: string; disabled: boolean }>`
cursor: pointer;
padding: 0.75rem 2rem;
border-radius: ${props => props.theme.borderRadius.alternate};
display: flex;
align-items: center;
${props =>
props.disabled &&
@ -34,16 +41,28 @@ const Filled = styled(Base)`
box-shadow: 0 8px 25px -8px rgba(${props => props.theme.colors[props.color]});
}
`;
const Outline = styled(Base)`
const Outline = styled(Base)<{ invert: boolean }>`
border: 1px solid rgba(${props => props.theme.colors[props.color]});
background: transparent;
& ${Text} {
color: rgba(${props => props.theme.colors[props.color]});
}
&:hover {
background: rgba(${props => props.theme.colors[props.color]}, 0.08);
}
${props =>
props.invert
? css`
background: rgba(${props.theme.colors[props.color]});
& ${Text} {
color: rgba(${props.theme.colors.text.secondary});
}
&:hover {
background: rgba(${props.theme.colors[props.color]}, 0.8);
}
`
: css`
& ${Text} {
color: rgba(${props.theme.colors[props.color]});
}
&:hover {
background: rgba(${props.theme.colors[props.color]}, 0.08);
}
`}
`;
const Flat = styled(Base)`
@ -110,6 +129,8 @@ type ButtonProps = {
color?: 'primary' | 'danger' | 'success' | 'warning' | 'dark';
disabled?: boolean;
type?: 'button' | 'submit';
icon?: JSX.Element;
invert?: boolean;
className?: string;
onClick?: ($target: React.RefObject<HTMLButtonElement>) => void;
justifyTextContent?: string;
@ -118,10 +139,12 @@ type ButtonProps = {
const Button: React.FC<ButtonProps> = ({
disabled = false,
fontSize = '14px',
invert = false,
color = 'primary',
variant = 'filled',
type = 'button',
justifyTextContent = 'center',
icon,
onClick,
className,
children,
@ -136,7 +159,8 @@ const Button: React.FC<ButtonProps> = ({
case 'filled':
return (
<Filled ref={$button} type={type} onClick={handleClick} className={className} disabled={disabled} color={color}>
<Text justifyTextContent={justifyTextContent} fontSize={fontSize}>
{icon && icon}
<Text hasIcon={typeof icon !== 'undefined'} justifyTextContent={justifyTextContent} fontSize={fontSize}>
{children}
</Text>
</Filled>
@ -145,6 +169,7 @@ const Button: React.FC<ButtonProps> = ({
return (
<Outline
ref={$button}
invert={invert}
type={type}
onClick={handleClick}
className={className}

View File

@ -25,7 +25,7 @@ const WindowTitle = styled.div`
const WindowTitleIcon = styled(CheckSquareOutline)`
top: 10px;
left: -40px;
left: -32px;
position: absolute;
`;

View File

@ -102,7 +102,7 @@ const List = React.forwardRef(
{children && children}
<AddCardContainer hidden={isComposerOpen}>
<AddCardButton onClick={() => onOpenComposer(id)}>
<Plus size={12} color="#c2c6dc" />
<Plus width={12} height={12} />
<AddCardButtonText>Add another card</AddCardButtonText>
</AddCardButton>
</AddCardContainer>

View File

@ -4,10 +4,8 @@ export const Container = styled.div`
flex: 1;
user-select: none;
white-space: nowrap;
margin-bottom: 8px;
overflow-x: auto;
overflow-y: hidden;
padding-bottom: 8px;
::-webkit-scrollbar {
height: 10px;
@ -35,10 +33,9 @@ export const BoardWrapper = styled.div`
user-select: none;
white-space: nowrap;
margin-bottom: 8px;
overflow-x: auto;
overflow-y: hidden;
padding-bottom: 8px;
padding-bottom: 4px;
position: absolute;
top: 0;
right: 0;

View File

@ -44,6 +44,7 @@ type MemberProps = {
showName?: boolean;
className?: string;
showCheckmark?: boolean;
size?: number;
};
const CardMemberWrapper = styled.div<{ ref: any }>`
@ -63,6 +64,7 @@ const Member: React.FC<MemberProps> = ({
showName,
showCheckmark = false,
className,
size = 28,
}) => {
const $targetRef = useRef<HTMLDivElement>();
return (
@ -77,7 +79,7 @@ const Member: React.FC<MemberProps> = ({
}
}}
>
<TaskAssignee onMemberProfile={NOOP} size={28} member={member} />
<TaskAssignee onMemberProfile={NOOP} size={32} member={member} />
{showName && <CardMemberName>{member.fullName}</CardMemberName>}
{showCheckmark && <CardCheckmark width={12} height={12} />}
</CardMemberWrapper>

View File

@ -15,18 +15,20 @@ export const ScrollOverlay = styled.div`
export const ClickableOverlay = styled.div`
min-height: 100%;
background: rgba(0, 0, 0, 0.4);
display: flex;
justify-content: center;
`;
export const StyledModal = styled.div<{ width: number }>`
display: inline-block;
export const StyledModal = styled.div<{ width: number; height: number }>`
position: relative;
margin: 48px 0 80px;
width: 100%;
width: ${props => props.width}px;
height: ${props => props.height}px;
left: 0;
right: 0;
top: 48px;
bottom: 16px;
margin: auto;
background: #262c49;
max-width: ${props => props.width}px;
vertical-align: middle;
border-radius: 3px;
border-radius: 6px;
${mixin.boxShadowMedium}
`;

View File

@ -1,9 +1,10 @@
import React, { useRef } from 'react';
import React, { useRef, useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import useOnEscapeKeyDown from 'shared/hooks/onEscapeKeyDown';
import useWindowSize from 'shared/hooks/useWindowSize';
import styled from 'styled-components';
import { Cross } from 'shared/icons';
import { ScrollOverlay, ClickableOverlay, StyledModal } from './Styles';
const $root: HTMLElement = document.getElementById('root')!; // eslint-disable-line @typescript-eslint/no-non-null-assertion
@ -14,21 +15,50 @@ type ModalProps = {
renderContent: () => JSX.Element;
};
const Modal: React.FC<ModalProps> = ({ width, onClose: tellParentToClose, renderContent }) => {
function getAdjustedHeight(height: number) {
if (height >= 900) {
return height - 150;
}
if (height >= 800) {
return height - 125;
}
return height - 70;
}
const CloseIcon = styled(Cross)`
position: absolute;
top: 16px;
right: -32px;
cursor: pointer;
fill: rgba(${props => props.theme.colors.text.primary});
&:hover {
fill: rgba(${props => props.theme.colors.text.secondary});
}
`;
const InnerModal: React.FC<ModalProps> = ({ width, onClose: tellParentToClose, renderContent }) => {
const $modalRef = useRef<HTMLDivElement>(null);
const $clickableOverlayRef = useRef<HTMLDivElement>(null);
const [_width, height] = useWindowSize();
useOnOutsideClick($modalRef, true, tellParentToClose, $clickableOverlayRef);
useOnEscapeKeyDown(true, tellParentToClose);
return ReactDOM.createPortal(
return (
<ScrollOverlay>
<ClickableOverlay ref={$clickableOverlayRef}>
<StyledModal width={width} ref={$modalRef}>
<StyledModal width={width} height={getAdjustedHeight(height)} ref={$modalRef}>
{renderContent()}
<CloseIcon onClick={() => tellParentToClose()} width={20} height={20} />
</StyledModal>
</ClickableOverlay>
</ScrollOverlay>,
</ScrollOverlay>
);
};
const Modal: React.FC<ModalProps> = ({ width, onClose: tellParentToClose, renderContent }) => {
return ReactDOM.createPortal(
<InnerModal width={width} onClose={tellParentToClose} renderContent={renderContent} />,
$root,
);
};

View File

@ -13,7 +13,7 @@ export const AddProjectItem: React.FC<AddProjectItemProps> = ({ onAddProject })
onAddProject();
}}
>
<Plus size={20} color="#c2c6dc" />
<Plus width={12} height={12} />
<AddProjectLabel>New Project</AddProjectLabel>
</AddProjectWrapper>
);

View File

@ -1,288 +1,365 @@
import styled from 'styled-components';
import TextareaAutosize from 'react-autosize-textarea/lib';
import styled, { css } from 'styled-components';
import TextareaAutosize from 'react-autosize-textarea';
import { mixin } from 'shared/utils/styles';
import Button from 'shared/components/Button';
import TaskAssignee from 'shared/components/TaskAssignee';
import { User, Trash, Paperclip } from 'shared/icons';
import Member from 'shared/components/Member';
export const TaskHeader = styled.div`
padding: 21px 30px 0px;
margin-right: 70px;
export const Container = styled.div`
display: flex;
height: 100%;
`;
export const LeftSidebar = styled.div`
display: flex;
flex-direction: column;
width: 300px;
background: #222740;
`;
export const MarkCompleteButton = styled.button<{ invert: boolean }>`
padding: 4px 8px;
position: relative;
border: none;
cursor: pointer;
border-radius: ${props => props.theme.borderRadius.alternate};
display: flex;
align-items: center;
background: transparent;
& span {
margin-left: 4px;
}
${props =>
props.invert
? css`
background: rgba(${props.theme.colors.success});
& svg {
fill: rgba(${props.theme.colors.text.secondary});
}
& span {
color: rgba(${props.theme.colors.text.secondary});
}
&:hover {
background: rgba(${props.theme.colors.success}, 0.8);
}
`
: css`
background: none;
border: 1px solid rgba(${props.theme.colors.text.secondary});
& svg {
fill: rgba(${props.theme.colors.text.secondary});
}
& span {
color: rgba(${props.theme.colors.text.secondary});
}
&:hover {
background: rgba(${props.theme.colors.success}, 0.08);
border: 1px solid rgba(${props.theme.colors.success});
}
&:hover svg {
fill: rgba(${props.theme.colors.success});
}
&:hover span {
color: rgba(${props.theme.colors.success});
}
`}
`;
export const LeftSidebarContent = styled.div`
padding-top: 32px;
display: flex;
flex-direction: column;
`;
export const TaskMeta = styled.div`
position: relative;
export const LeftSidebarSection = styled.div`
display: flex;
flex-direction: column;
padding-left: 32px;
padding-right: 32px;
padding-bottom: 16px;
border-bottom: 1px solid #414561;
`;
export const SidebarTitle = styled.div`
font-size: 12px;
min-height: 24px;
margin-left: 8px;
color: rgba(${props => props.theme.colors.text.primary}, 0.75);
padding-top: 4px;
letter-spacing: 0.5px;
text-transform: uppercase;
`;
export const SidebarButton = styled.div`
font-size: 14px;
color: rgba(${props => props.theme.colors.text.primary});
min-height: 32px;
width: 100%;
padding: 9px 8px 7px 8px;
border-color: transparent;
border-radius: 6px;
border-width: 1px;
border-style: solid;
display: inline-block;
outline: 0;
cursor: pointer;
font-size: 14px;
&:hover {
border-color: #414561;
}
`;
export const SidebarButtonText = styled.span`
min-height: 16px;
flex: 1 1 auto;
display: flex;
align-items: center;
border-radius: 4px;
justify-content: flex-start;
`;
export const TaskGroupLabel = styled.span`
color: #c2c6dc;
font-size: 14px;
`;
export const TaskGroupLabelName = styled.span`
color: #c2c6dc;
text-decoration: underline;
font-size: 14px;
`;
export const TaskActions = styled.div`
position: absolute;
top: 0;
right: 0;
padding: 21px 18px 0px;
export const ContentContainer = styled.div`
display: flex;
align-items: center;
`;
export const TaskAction = styled.button`
display: inline-flex;
align-items: center;
justify-content: center;
height: 32px;
cursor: pointer;
padding: 0px 9px;
`;
export const TaskDetailsWrapper = styled.div`
display: flex;
padding: 0px 16px 60px;
`;
export const TaskDetailsContent = styled.div`
flex-direction: column;
flex: 1;
padding-right: 8px;
padding-top: 32px;
overflow: auto;
`;
export const TaskDetailsSidebar = styled.div`
width: 168px;
padding-left: 8px;
export const HeaderContainer = styled.div`
flex: 0 0 auto;
padding: 0px 32px 0px 24px;
`;
export const HeaderInnerContainer = styled.div`
display: flex;
justify-content: space-between;
margin: 0 0 0 0;
padding: 0 0 0 4px;
`;
export const HeaderLeft = styled.div`
display: flex;
justify-content: flex-start;
`;
export const TaskDetailsTitleWrapper = styled.div`
width: 100%;
margin: 0 0 0 -8px;
margin: 8px 0 4px 0;
display: inline-block;
`;
export const TaskDetailsTitle = styled(TextareaAutosize)`
line-height: 1.28;
resize: none;
box-shadow: transparent 0px 0px 0px 1px;
font-size: 24px;
font-weight: 700;
padding: 4px;
background: #262c49;
padding: 9px 8px 7px 8px;
border-color: transparent;
border-radius: 6px;
border-width: 1px;
border-style: solid;
border-color: transparent;
border-image: initial;
transition: background 0.1s ease 0s;
overflow-y: hidden;
width: 100%;
color: #c2c6dc;
&:focus {
box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px;
background: ${mixin.darken('#262c49', 0.15)};
}
`;
display: inline-block;
outline: 0;
font-size: 24px;
font-weight: 700;
background: none;
export const TaskDetailsLabel = styled.div`
padding: 24px 0px 12px;
font-size: 15px;
font-weight: 600;
color: #c2c6dc;
`;
export const TaskDetailsAddDetailsButton = styled.div`
background: ${mixin.darken('#262c49', 0.15)};
box-shadow: none;
border: none;
border-radius: 3px;
display: block;
min-height: 56px;
padding: 8px 12px;
text-decoration: none;
font-size: 14px;
cursor: pointer;
color: #c2c6dc;
&:hover {
background: ${mixin.darken('#262c49', 0.25)};
box-shadow: none;
border: none;
border-color: #414561;
}
`;
export const TaskDetailsEditorWrapper = styled.div`
display: block;
padding-bottom: 9px;
z-index: 50;
width: 100%;
`;
export const TaskDetailsEditor = styled(TextareaAutosize)`
width: 100%;
min-height: 108px;
color: #c2c6dc;
background: #262c49;
box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px;
border-radius: 3px;
line-height: 20px;
padding: 8px 12px;
outline: none;
border: none;
&:focus {
box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px;
background: ${mixin.darken('#262c49', 0.05)};
border-color: rgba(${props => props.theme.colors.primary});
}
`;
export const TaskDetailsMarkdown = styled.div`
width: 100%;
export const DueDateTitle = styled.div`
font-size: 12px;
min-height: 24px;
margin-left: 8px;
color: rgba(${props => props.theme.colors.text.primary}, 0.75);
padding-top: 8px;
letter-spacing: 0.5px;
text-transform: uppercase;
`;
export const AssignedUsersSection = styled.div`
padding-left: 32px;
padding-right: 32px;
padding-top: 24px;
padding-bottom: 24px;
border-bottom: 1px solid #414561;
display: flex;
flex-direction: column;
`;
export const AssignUserIcon = styled.div`
cursor: pointer;
color: #c2c6dc;
h1 {
font-size: 24px;
font-weight: 600;
line-height: 28px;
margin: 0 0 12px;
height: 32px;
width: 32px;
position: relative;
border-radius: 50%;
border: 1px dashed #414561;
margin-right: 8px;
display: flex;
flex-shrink: 0;
justify-content: center;
align-items: center;
&:hover {
border: 1px solid rgba(${props => props.theme.colors.text.secondary}, 0.75);
}
h2 {
font-weight: 600;
font-size: 20px;
line-height: 24px;
margin: 16px 0 8px;
&:hover svg {
fill: rgba(${props => props.theme.colors.text.secondary}, 0.75);
}
`;
p {
margin: 0 0 8px;
export const AssignUsersButton = styled.div`
cursor: pointer;
display: inline-flex;
flex: 1 1 auto;
height: 40px;
padding: 4px 16px 4px 8px;
margin-left: -1px;
border-radius: 6px;
align-items: center;
border: 1px solid transparent;
&:hover {
border: 1px solid ${mixin.darken('#414561', 0.15)};
}
&:hover ${AssignUserIcon} {
border: 1px solid #414561;
}
`;
strong {
font-weight: 700;
export const AssignUserLabel = styled.span`
flex: 1 1 auto;
line-height: 15px;
color: rgba(${props => props.theme.colors.text.primary}, 0.75);
`;
export const ExtraActionsSection = styled.div`
padding-left: 32px;
padding-right: 32px;
padding-top: 24px;
display: flex;
flex-direction: column;
`;
export const ActionButtonsTitle = styled.h3`
color: rgba(${props => props.theme.colors.text.primary});
font-size: 12px;
font-weight: 500;
letter-spacing: 0.04em;
`;
export const ActionButton = styled(Button)`
margin-top: 8px;
margin-left: -10px;
padding: 8px 16px;
background: rgba(${props => props.theme.colors.bg.primary}, 0.5);
text-align: left;
transition: transform 0.2s ease;
& span {
justify-content: flex-start;
}
&:hover {
box-shadow: none;
transform: translateX(4px);
background: rgba(${props => props.theme.colors.bg.primary}, 0.75);
}
`;
export const HeaderRight = styled.div`
display: flex;
justify-content: center;
align-items: center;
`;
export const HeaderActionIcon = styled.div`
padding: 4px 4px 4px 4px;
margin: 0 4px 0 4px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
svg {
fill: rgba(${props => props.theme.colors.text.primary}, 0.75);
}
&:hover svg {
fill: rgba(${props => props.theme.colors.primary});
}
`;
export const EditorContainer = styled.div`
margin-left: 32px;
margin-right: 32px;
padding: 9px 8px 7px 8px;
border-color: transparent;
border-radius: 6px;
border-width: 1px;
border-style: solid;
outline: 0;
background: none;
ul {
margin: 8px 0;
list-style-type: disc;
}
ul > li {
margin: 8px 8px 8px 24px;
list-style: disc;
}
p a {
color: rgba(${props => props.theme.colors.primary});
ul.checkbox_list input[type='checkbox'] {
border-radius: 6px;
border-width: 1px;
border-style: solid;
border-color: #414561;
}
`;
export const TaskDetailsControls = styled.div`
clear: both;
margin-top: 8px;
export const InnerContentContainer = styled.div`
overflow: auto;
position: relative;
`;
export const DescriptionContainer = styled.div`
position: relative;
display: flex;
flex-direction: column;
`;
export const ConfirmSave = styled(Button)`
padding: 6px 12px;
border-radius: 3px;
margin-right: 6px;
export const DescriptionActionButton = styled(Button)`
padding: 8px 16px;
`;
export const CancelEdit = styled.div`
export const Labels = styled.div`
display: flex;
align-items: center;
justify-content: center;
height: 32px;
width: 32px;
cursor: pointer;
padding-left: 9px;
margin: 0 0 8px 0;
`;
export const TaskDetailSectionTitle = styled.div`
text-transform: uppercase;
color: #c2c6dc;
font-size: 12.5px;
font-weight: 600;
margin: 24px 0px 5px;
`;
export const TaskDetailAssignees = styled.div`
display: flex;
flex-wrap: wrap;
align-items: center;
`;
export const TaskDetailAssignee = styled.div`
&:hover {
opacity: 0.8;
}
margin-right: 4px;
`;
export const ProfileIcon = styled.div`
width: 32px;
height: 32px;
border-radius: 9999px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-weight: 700;
background: rgb(115, 103, 240);
cursor: pointer;
`;
export const TaskDetailsAddMemberIcon = styled.div`
float: left;
height: 32px;
width: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 100%;
background: ${mixin.darken('#262c49', 0.15)};
cursor: pointer;
&:hover {
opacity: 0.8;
}
`;
export const TaskDetailLabels = styled.div`
display: flex;
align-items: center;
flex-wrap: wrap;
`;
export const TaskDetailLabel = styled.div<{ color: string }>`
&:hover {
opacity: 0.8;
}
background-color: ${props => props.color};
color: #fff;
cursor: pointer;
display: flex;
align-items: center;
border-radius: 3px;
box-sizing: border-box;
export const MetaDetail = styled.div`
display: block;
float: left;
font-weight: 600;
height: 32px;
line-height: 32px;
margin: 0 4px 4px 0;
min-width: 40px;
padding: 0 12px;
width: auto;
margin: 0 16px 8px 0;
max-width: 100%;
`;
export const MetaDetailTitle = styled.h3`
color: rgba(${props => props.theme.colors.text.primary});
font-size: 12px;
font-weight: 500;
letter-spacing: 0.04em;
margin-top: 16px;
text-transform: uppercase;
display: block;
line-height: 20px;
margin: 0 8px 4px 0;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
`;
export const MetaDetailContent = styled.div`
display: flex;
`;
export const TaskDetailsAddLabel = styled.div`
border-radius: 3px;
background: ${mixin.darken('#262c49', 0.15)};
@ -307,90 +384,204 @@ export const TaskDetailsAddLabelIcon = styled.div`
}
`;
export const NoDueDateLabel = styled.span`
color: rgb(137, 147, 164);
font-size: 14px;
cursor: pointer;
export const ChecklistSection = styled.div`
margin-top: 8px;
padding: 0 24px;
`;
export const UnassignedLabel = styled.div`
color: rgb(137, 147, 164);
font-size: 14px;
export const TaskDetailLabel = styled.div<{ color: string }>`
&:hover {
opacity: 0.8;
}
background-color: ${props => props.color};
color: #fff;
cursor: pointer;
display: flex;
align-items: center;
border-radius: 3px;
box-sizing: border-box;
display: block;
float: left;
font-weight: 600;
height: 32px;
line-height: 32px;
margin: 0 4px 0 0;
min-width: 40px;
padding: 0 12px;
width: auto;
`;
export const ActionButtons = styled.div`
export const MemberList = styled.div`
display: flex;
align-items: center;
padding: 4px 16px 4px 8px;
margin-left: -1px;
`;
export const TaskMember = styled(TaskAssignee)`
margin-right: 4px;
`;
export const ActionButtonIcon = styled.div``;
export const EditorActions = styled.div`
display: flex;
align-items: center;
margin-left: 32px;
margin-right: 32px;
padding: 9px 8px 7px 8px;
`;
export const CancelIcon = styled.div`
width: 32px;
height: 32p;
display: flex;
align-items: center;
justify-content: center;
margin-left: 4px;
`;
export const TabBarSection = styled.div`
margin-top: 2px;
padding-left: 23px;
display: flex;
text-transform: uppercase;
min-height: 35px;
border-bottom: 1px solid #414561;
`;
export const TabBarItem = styled.div`
box-shadow: inset 0 -2px rgba(216, 93, 216);
padding: 12px 7px 14px 7px;
margin-bottom: -1px;
margin-right: 36px;
color: rgba(${props => props.theme.colors.text.primary});
`;
export const CommentContainer = styled.div`
flex: 0 0 auto;
margin-top: auto;
padding: 15px 26px;
background: #1f243e;
`;
export const CommentInnerWrapper = styled.div`
display: flex;
position: relative;
`;
export const CommentEditorContainer = styled.div`
flex: 1;
border-radius: 6px;
border: 1px solid #414561;
display: flex;
flex-direction: column;
`;
export const ActionButtonsTitle = styled.h3`
color: rgba(${props => props.theme.colors.text.primary});
font-size: 12px;
font-weight: 500;
letter-spacing: 0.04em;
export const CommentProfile = styled(TaskAssignee)`
margin-right: 8px;
position: relative;
top: 0;
padding-top: 3px;
align-items: normal;
`;
export const ActionButton = styled(Button)`
margin-top: 8px;
padding: 6px 12px;
background: rgba(${props => props.theme.colors.bg.primary}, 0.4);
text-align: left;
&:hover {
box-shadow: none;
background: rgba(${props => props.theme.colors.bg.primary}, 0.6);
export const CommentTextArea = styled(TextareaAutosize)`
width: 100%;
line-height: 28px;
padding: 4px 6px;
border-radius: 6px;
color: rgba(${props => props.theme.colors.text.primary});
background: #1f243e;
border: none;
transition: max-height 200ms, height 200ms, min-height 200ms;
min-height: 36px;
max-height: 36px;
&:not(:focus) {
height: 36px;
}
&:focus {
min-height: 80px;
max-height: none;
line-height: 20px;
}
`;
export const MetaDetails = styled.div`
margin-top: 8px;
display: flex;
export const CommentEditorActions = styled.div<{ visible: boolean }>`
display: ${props => (props.visible ? 'flex' : 'none')};
align-items: center;
padding: 5px 5px 5px 9px;
border-top: 1px solid #414561;
`;
export const TaskDueDateButton = styled(Button)`
export const CommentEditorActionIcon = styled.div`
width: 32px;
height: 32px;
padding: 6px 12px;
background: rgba(${props => props.theme.colors.bg.primary}, 0.4);
&:hover {
box-shadow: none;
background: rgba(${props => props.theme.colors.bg.primary}, 0.6);
}
`;
export const MetaDetail = styled.div`
display: block;
float: left;
margin: 0 16px 8px 0;
max-width: 100%;
`;
export const TaskDetailsSection = styled.div`
display: block;
`;
export const MetaDetailTitle = styled.h3`
color: rgba(${props => props.theme.colors.text.primary});
font-size: 12px;
font-weight: 500;
letter-spacing: 0.04em;
margin-top: 16px;
text-transform: uppercase;
display: block;
line-height: 20px;
margin: 0 8px 4px 0;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
`;
export const TaskMember = styled(TaskAssignee)``;
export const MetaDetailContent = styled.div`
display: flex;
& ${TaskMember} {
margin-right: 4px;
}
align-items: center;
justify-content: center;
`;
export const CommentEditorSaveButton = styled(Button)`
margin-left: auto;
padding: 8px 16px;
`;
export const ActivitySection = styled.div`
overflow-x: hidden;
padding: 8px 26px;
`;
export const ActivityItem = styled.div`
padding: 8px 0;
position: relative;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
`;
export const ActivityItemHeader = styled.div`
display: flex;
`;
export const ActivityItemHeaderUser = styled(TaskAssignee)`
margin-right: 4px;
`;
export const ActivityItemHeaderTitle = styled.div`
margin-left: 4px;
line-height: 18px;
display: flex;
align-items: center;
`;
export const ActivityItemHeaderTitleName = styled.span`
color: rgba(${props => props.theme.colors.text.primary});
font-weight: 500;
`;
export const ActivityItemTimestamp = styled.span<{ margin: number }>`
font-size: 12px;
color: rgba(${props => props.theme.colors.text.primary}, 0.65);
margin-left: ${props => props.margin}px;
`;
export const ActivityItemDetails = styled.div`
margin-left: 32px;
`;
export const ActivityItemComment = styled.div`
display: inline-flex;
border-radius: 3px;
${mixin.boxShadowCard}
position: relative;
color: rgba(${props => props.theme.colors.text.primary});
padding: 8px 12px;
margin: 4px 0;
background-color: ${mixin.darken('#262c49', 0.1)};
`;
export const ActivityItemLog = styled.span`
margin-left: 2px;
color: rgba(${props => props.theme.colors.text.primary});
`;

View File

@ -1,70 +1,76 @@
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 React, { useState, useRef } from 'react';
import {
Plus,
User,
Trash,
Paperclip,
Clone,
Share,
Tags,
Checkmark,
CheckSquareOutline,
At,
Smile,
} from 'shared/icons';
import Editor from 'rich-markdown-editor';
import dark from 'shared/utils/editorTheme';
import styled from 'styled-components';
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 { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
import moment from 'moment';
import Task from 'shared/icons/Task';
import {
TaskMember,
NoDueDateLabel,
TaskDueDateButton,
UnassignedLabel,
TaskGroupLabel,
TaskGroupLabelName,
TaskDetailsSection,
TaskActions,
TaskDetailsAddLabel,
TaskDetailLabel,
CommentContainer,
MetaDetailContent,
TaskDetailsAddLabelIcon,
TaskAction,
TaskMeta,
ActionButtons,
ActionButton,
ActionButtonsTitle,
TaskHeader,
ProfileIcon,
TaskDetailsContent,
TaskDetailsWrapper,
TaskDetailsSidebar,
AssignUserIcon,
AssignUserLabel,
AssignUsersButton,
AssignedUsersSection,
DueDateTitle,
Container,
LeftSidebar,
ContentContainer,
LeftSidebarContent,
LeftSidebarSection,
SidebarTitle,
SidebarButton,
SidebarButtonText,
MarkCompleteButton,
HeaderContainer,
HeaderLeft,
HeaderInnerContainer,
TaskDetailsTitleWrapper,
TaskDetailsTitle,
TaskDetailsLabel,
TaskDetailsAddDetailsButton,
TaskDetailsEditor,
TaskDetailsEditorWrapper,
TaskDetailsMarkdown,
TaskDetailsControls,
ConfirmSave,
CancelEdit,
TaskDetailSectionTitle,
TaskDetailLabel,
TaskDetailLabels,
TaskDetailAssignee,
TaskDetailAssignees,
TaskDetailsAddMemberIcon,
MetaDetails,
MetaDetail,
MetaDetailTitle,
MetaDetailContent,
ExtraActionsSection,
HeaderRight,
HeaderActionIcon,
EditorContainer,
InnerContentContainer,
DescriptionContainer,
Labels,
ChecklistSection,
MemberList,
TaskMember,
TabBarSection,
TabBarItem,
CommentTextArea,
CommentEditorContainer,
CommentEditorActions,
CommentEditorActionIcon,
CommentEditorSaveButton,
CommentProfile,
CommentInnerWrapper,
ActivitySection,
} from './Styles';
import Checklist, { ChecklistItem, ChecklistItems } from '../Checklist';
import onDragEnd from './onDragEnd';
const ChecklistContainer = styled.div``;
type TaskContentProps = {
onEditContent: () => void;
description: string;
};
type TaskLabelProps = {
label: TaskLabel;
onClick: ($target: React.RefObject<HTMLElement>) => void;
@ -85,62 +91,15 @@ const TaskLabelItem: React.FC<TaskLabelProps> = ({ label, onClick }) => {
);
};
const TaskContent: React.FC<TaskContentProps> = ({ description, onEditContent }) => {
return description === '' ? (
<TaskDetailsAddDetailsButton onClick={onEditContent}>Add a more detailed description</TaskDetailsAddDetailsButton>
) : (
<TaskDetailsMarkdown onClick={onEditContent}>
<ReactMarkdown source={description} />
</TaskDetailsMarkdown>
);
};
type DetailsEditorProps = {
description: string;
onTaskDescriptionChange: (newDescription: string) => void;
onCancel: () => void;
};
const DetailsEditor: React.FC<DetailsEditorProps> = ({
description: initialDescription,
onTaskDescriptionChange,
onCancel,
}) => {
const [description, setDescription] = useState(initialDescription);
const $editorWrapperRef = useRef<HTMLDivElement>(null);
const $editorRef = useRef<HTMLTextAreaElement>(null);
const handleOutsideClick = () => {
onTaskDescriptionChange(description);
};
useEffect(() => {
if ($editorRef && $editorRef.current) {
$editorRef.current.focus();
$editorRef.current.select();
}
}, []);
useOnOutsideClick($editorWrapperRef, true, handleOutsideClick, null);
return (
<TaskDetailsEditorWrapper ref={$editorWrapperRef}>
<TaskDetailsEditor
ref={$editorRef}
value={description}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setDescription(e.currentTarget.value)}
/>
<TaskDetailsControls>
<ConfirmSave variant="relief" onClick={handleOutsideClick}>
Save
</ConfirmSave>
<CancelEdit onClick={onCancel}>
<Plus size={16} color="#c2c6dc" />
</CancelEdit>
</TaskDetailsControls>
</TaskDetailsEditorWrapper>
);
};
type TaskDetailsProps = {
task: Task;
me?: TaskUser | null;
onTaskNameChange: (task: Task, newName: string) => void;
onTaskDescriptionChange: (task: Task, newDescription: string) => void;
onDeleteTask: (task: Task) => void;
@ -162,6 +121,7 @@ type TaskDetailsProps = {
};
const TaskDetails: React.FC<TaskDetailsProps> = ({
me,
task,
onDeleteChecklist,
onTaskNameChange,
@ -182,209 +142,198 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
onToggleChecklistItem,
onMemberProfile,
}) => {
const [editorOpen, setEditorOpen] = useState(false);
const [description, setDescription] = useState(task.description ?? '');
const [taskName, setTaskName] = useState(task.name);
const handleClick = () => {
setEditorOpen(!editorOpen);
};
const handleDeleteTask = () => {
onDeleteTask(task);
};
const $title = useRef<HTMLTextAreaElement>(null);
const onKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
onTaskNameChange(task, taskName);
if ($title && $title.current) {
$title.current.blur();
const [editTaskDescription, setEditTaskDescription] = useState(() => {
if (task.description) {
if (task.description.trim() === '' || task.description.trim() === '\\') {
return true;
}
return false;
}
};
const $unassignedRef = useRef<HTMLDivElement>(null);
const $addMemberRef = useRef<HTMLDivElement>(null);
const onUnassignedClick = () => {
onOpenAddMemberPopup(task, $unassignedRef);
};
const onAddMember = ($target: React.RefObject<HTMLElement>) => {
onOpenAddMemberPopup(task, $target);
};
const onAddChecklist = ($target: React.RefObject<HTMLElement>) => {
onOpenAddChecklistPopup(task, $target);
};
const $dueDateLabel = useRef<HTMLDivElement>(null);
const $addLabelRef = useRef<HTMLDivElement>(null);
return true;
});
const [saveTimeout, setSaveTimeout] = useState<any>(null);
const [showCommentActions, setShowCommentActions] = useState(false);
const taskDescriptionRef = useRef(task.description ?? '');
const $noMemberBtn = useRef<HTMLDivElement>(null);
const $addMemberBtn = useRef<HTMLDivElement>(null);
const $dueDateBtn = useRef<HTMLDivElement>(null);
const onAddLabel = ($target: React.RefObject<HTMLElement>) => {
onOpenAddLabelPopup(task, $target);
const saveDescription = () => {
onTaskDescriptionChange(task, taskDescriptionRef.current);
};
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);
onChecklistDrop({ ...droppedGroup, position: newPosition });
} else {
throw new 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);
}
}
};
return (
<>
<TaskActions>
<TaskAction onClick={handleDeleteTask}>
<Bin size={20} color="#c2c6dc" />
</TaskAction>
<TaskAction onClick={onCloseModal}>
<Cross width={16} height={16} />
</TaskAction>
</TaskActions>
<TaskHeader>
<TaskDetailsTitleWrapper>
<TaskDetailsTitle
ref={$title}
value={taskName}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setTaskName(e.currentTarget.value)}
onKeyDown={onKeyDown}
/>
</TaskDetailsTitleWrapper>
<TaskMeta>
{task.taskGroup.name && (
<TaskGroupLabel>
{`in list ${(<TaskGroupLabelName>{task.taskGroup.name}</TaskGroupLabelName>)}`}
</TaskGroupLabel>
)}
</TaskMeta>
</TaskHeader>
<TaskDetailsWrapper>
<TaskDetailsContent>
<MetaDetails>
{task.assigned && task.assigned.length !== 0 && (
<MetaDetail>
<MetaDetailTitle>MEMBERS</MetaDetailTitle>
<MetaDetailContent>
{task.assigned &&
task.assigned.map(member => (
<TaskMember key={member.id} size={32} member={member} onMemberProfile={onMemberProfile} />
))}
<TaskDetailsAddMemberIcon ref={$addMemberRef} onClick={() => onAddMember($addMemberRef)}>
<Plus size={16} color="#c2c6dc" />
</TaskDetailsAddMemberIcon>
</MetaDetailContent>
</MetaDetail>
)}
{task.labels.length !== 0 && (
<MetaDetail>
<MetaDetailTitle>LABELS</MetaDetailTitle>
<MetaDetailContent>
{task.labels.map(label => {
return (
<TaskLabelItem
key={label.projectLabel.id}
label={label}
onClick={$target => {
onOpenAddLabelPopup(task, $target);
}}
/>
);
})}
<TaskDetailsAddLabelIcon ref={$addLabelRef} onClick={() => onAddLabel($addLabelRef)}>
<Plus size={16} color="#c2c6dc" />
</TaskDetailsAddLabelIcon>
</MetaDetailContent>
</MetaDetail>
)}
{task.dueDate && (
<MetaDetail>
<MetaDetailTitle>DUE DATE</MetaDetailTitle>
<MetaDetailContent>
<TaskDueDateButton>{moment(task.dueDate).format('MMM D [at] h:mm A')}</TaskDueDateButton>
</MetaDetailContent>
</MetaDetail>
)}
</MetaDetails>
<TaskDetailsSection>
<TaskDetailsLabel>Description</TaskDetailsLabel>
{editorOpen ? (
<DetailsEditor
description={description}
onTaskDescriptionChange={newDescription => {
setEditorOpen(false);
setDescription(newDescription);
onTaskDescriptionChange(task, newDescription);
<Container>
<LeftSidebar>
<LeftSidebarContent>
<LeftSidebarSection>
<SidebarTitle>TASK GROUP</SidebarTitle>
<SidebarButton>
<SidebarButtonText>Release 0.1.0</SidebarButtonText>
</SidebarButton>
<DueDateTitle>DUE DATE</DueDateTitle>
<SidebarButton
ref={$dueDateBtn}
onClick={() => {
onOpenDueDatePopop(task, $dueDateBtn);
}}
>
{task.dueDate ? (
<SidebarButtonText>{moment(task.dueDate).format('MMM D [at] h:mm A')}</SidebarButtonText>
) : (
<SidebarButtonText>No due date</SidebarButtonText>
)}
</SidebarButton>
</LeftSidebarSection>
<AssignedUsersSection>
<DueDateTitle>MEMBERS</DueDateTitle>
{task.assigned && task.assigned.length !== 0 ? (
<MemberList>
{task.assigned.map(m => (
<TaskMember
key={m.id}
member={m}
size={32}
onMemberProfile={$target => {
onMemberProfile($target, m.id);
}}
/>
))}
<AssignUserIcon
ref={$addMemberBtn}
onClick={() => {
onOpenAddMemberPopup(task, $addMemberBtn);
}}
>
<Plus width={16} height={16} />
</AssignUserIcon>
</MemberList>
) : (
<AssignUsersButton
ref={$noMemberBtn}
onClick={() => {
onOpenAddMemberPopup(task, $noMemberBtn);
}}
onCancel={() => {
setEditorOpen(false);
>
<AssignUserIcon>
<User width={16} height={16} />
</AssignUserIcon>
<AssignUserLabel>No members</AssignUserLabel>
</AssignUsersButton>
)}
</AssignedUsersSection>
<ExtraActionsSection>
<DueDateTitle>ACTIONS</DueDateTitle>
<ActionButton
onClick={$target => {
onOpenAddLabelPopup(task, $target);
}}
icon={<Tags width={12} height={12} />}
>
Labels
</ActionButton>
<ActionButton
onClick={$target => {
onOpenAddChecklistPopup(task, $target);
}}
icon={<CheckSquareOutline width={12} height={12} />}
>
Checklist
</ActionButton>
<ActionButton>Cover</ActionButton>
</ExtraActionsSection>
</LeftSidebarContent>
</LeftSidebar>
<ContentContainer>
<HeaderContainer>
<HeaderInnerContainer>
<HeaderLeft>
<MarkCompleteButton
invert={task.complete ?? false}
onClick={() => {
onToggleTaskComplete(task);
}}
>
<Checkmark width={8} height={8} />
<span>{task.complete ? 'Completed' : 'Mark complete'}</span>
</MarkCompleteButton>
</HeaderLeft>
<HeaderRight>
<HeaderActionIcon>
<Paperclip width={16} height={16} />
</HeaderActionIcon>
<HeaderActionIcon>
<Clone width={16} height={16} />
</HeaderActionIcon>
<HeaderActionIcon>
<Share width={16} height={16} />
</HeaderActionIcon>
<HeaderActionIcon onClick={() => onDeleteTask(task)}>
<Trash width={16} height={16} />
</HeaderActionIcon>
</HeaderRight>
</HeaderInnerContainer>
<TaskDetailsTitleWrapper>
<TaskDetailsTitle
value={taskName}
onChange={e => {
setTaskName(e.currentTarget.value);
}}
onBlur={() => {
if (taskName !== task.name) {
onTaskNameChange(task, taskName);
}
}}
/>
</TaskDetailsTitleWrapper>
<Labels>
{task.labels.length !== 0 && (
<MetaDetailContent>
{task.labels.map(label => {
return (
<TaskLabelItem
key={label.projectLabel.id}
label={label}
onClick={$target => {
onOpenAddLabelPopup(task, $target);
}}
/>
);
})}
<TaskDetailsAddLabelIcon>
<Plus width={12} height={12} />
</TaskDetailsAddLabelIcon>
</MetaDetailContent>
)}
</Labels>
</HeaderContainer>
<InnerContentContainer>
<DescriptionContainer>
<EditorContainer
onClick={e => {
if (!editTaskDescription) {
setEditTaskDescription(true);
}
}}
>
<Editor
defaultValue={task.description ?? ''}
theme={dark}
readOnly={!editTaskDescription}
autoFocus
onChange={value => {
setSaveTimeout(() => {
clearTimeout(saveTimeout);
return setTimeout(saveDescription, 2000);
});
const text = value();
taskDescriptionRef.current = text;
}}
/>
) : (
<TaskContent description={description} onEditContent={handleClick} />
)}
<DragDropContext onDragEnd={onDragEnd}>
</EditorContainer>
</DescriptionContainer>
<ChecklistSection>
<DragDropContext onDragEnd={result => onDragEnd(result, task, onChecklistDrop, onChecklistItemDrop)}>
<Droppable direction="vertical" type="checklist" droppableId="root">
{dropProvided => (
<ChecklistContainer {...dropProvided.droppableProps} ref={dropProvided.innerRef}>
@ -463,32 +412,54 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
)}
</Droppable>
</DragDropContext>
</TaskDetailsSection>
</TaskDetailsContent>
<TaskDetailsSidebar>
<ActionButtons>
<ActionButtonsTitle>ADD TO CARD</ActionButtonsTitle>
<ActionButton justifyTextContent="flex-start" onClick={() => onToggleTaskComplete(task)}>
{task.complete ? 'Mark Incomplete' : 'Mark Complete'}
</ActionButton>
<ActionButton justifyTextContent="flex-start" onClick={$target => onAddMember($target)}>
Members
</ActionButton>
<ActionButton justifyTextContent="flex-start" onClick={$target => onAddLabel($target)}>
Labels
</ActionButton>
<ActionButton justifyTextContent="flex-start" onClick={$target => onAddChecklist($target)}>
Checklist
</ActionButton>
<ActionButton justifyTextContent="flex-start" onClick={$target => onOpenDueDatePopop(task, $target)}>
Due Date
</ActionButton>
<ActionButton justifyTextContent="flex-start">Attachment</ActionButton>
<ActionButton justifyTextContent="flex-start">Cover</ActionButton>
</ActionButtons>
</TaskDetailsSidebar>
</TaskDetailsWrapper>
</>
</ChecklistSection>
<TabBarSection>
<TabBarItem>Activity</TabBarItem>
</TabBarSection>
<ActivitySection />
</InnerContentContainer>
<CommentContainer>
{me && (
<CommentInnerWrapper>
<CommentProfile
member={me}
size={32}
onMemberProfile={$target => {
onMemberProfile($target, me.id);
}}
/>
<CommentEditorContainer>
<CommentTextArea
disabled
placeholder="Write a comment..."
onFocus={() => {
setShowCommentActions(true);
}}
onBlur={() => {
setShowCommentActions(false);
}}
/>
<CommentEditorActions visible={showCommentActions}>
<CommentEditorActionIcon>
<Paperclip width={12} height={12} />
</CommentEditorActionIcon>
<CommentEditorActionIcon>
<At width={12} height={12} />
</CommentEditorActionIcon>
<CommentEditorActionIcon>
<Smile width={12} height={12} />
</CommentEditorActionIcon>
<CommentEditorActionIcon>
<Task width={12} height={12} />
</CommentEditorActionIcon>
<CommentEditorSaveButton>Save</CommentEditorSaveButton>
</CommentEditorActions>
</CommentEditorContainer>
</CommentInnerWrapper>
)}
</CommentContainer>
</ContentContainer>
</Container>
);
};

View File

@ -0,0 +1,90 @@
import {
getSortedDraggables,
isPositionChanged,
getNewDraggablePosition,
getAfterDropDraggableList,
} from 'shared/utils/draggables';
import { DropResult } from 'react-beautiful-dnd';
type OnChecklistDropFn = (checklist: TaskChecklist) => void;
type OnChecklistItemDropFn = (prevChecklistID: string, checklistID: string, checklistItem: TaskChecklistItem) => void;
const onDragEnd = (
{ draggableId, source, destination, type }: DropResult,
task: Task,
onChecklistDrop: OnChecklistDropFn,
onChecklistItemDrop: OnChecklistItemDropFn,
) => {
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);
onChecklistDrop({ ...droppedGroup, position: newPosition });
} else {
throw new 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);
}
}
};
export default onDragEnd;