feat: add task details
This commit is contained in:
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -25,7 +25,7 @@ const WindowTitle = styled.div`
|
||||
|
||||
const WindowTitleIcon = styled(CheckSquareOutline)`
|
||||
top: 10px;
|
||||
left: -40px;
|
||||
left: -32px;
|
||||
position: absolute;
|
||||
`;
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
`;
|
||||
|
@ -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,
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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});
|
||||
`;
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
90
frontend/src/shared/components/TaskDetails/onDragEnd.ts
Normal file
90
frontend/src/shared/components/TaskDetails/onDragEnd.ts
Normal 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;
|
Reference in New Issue
Block a user