feature: add due dates to tasks

This commit is contained in:
Jordan Knott
2020-06-15 17:36:59 -05:00
parent a12e9c1e50
commit b6f0e8b6b2
32 changed files with 816 additions and 222 deletions

View File

@ -111,5 +111,19 @@ export default createGlobalStyle`
resize: none;
}
::-webkit-scrollbar {
width: 12px;
}
::-webkit-scrollbar-track {
background: #262c49;
border-radius: 20px;
}
::-webkit-scrollbar-thumb {
background: #7367f0;
border-radius: 20px;
}
${mixin.placeholderColor(color.textLight)}
`;

View File

@ -17,6 +17,7 @@ const theme: DefaultTheme = {
primary: '194, 198, 220',
secondary: '255, 255, 255',
},
border: '65, 69, 97',
bg: {
primary: '16, 22, 58',
secondary: '38, 44, 73',

View File

@ -4,9 +4,15 @@ import TaskDetails from 'shared/components/TaskDetails';
import PopupMenu, { Popup, usePopup } from 'shared/components/PopupMenu';
import MemberManager from 'shared/components/MemberManager';
import { useRouteMatch, useHistory } from 'react-router';
import { useFindTaskQuery, useAssignTaskMutation, useUnassignTaskMutation } from 'shared/generated/graphql';
import {
useFindTaskQuery,
useUpdateTaskDueDateMutation,
useAssignTaskMutation,
useUnassignTaskMutation,
} from 'shared/generated/graphql';
import UserIDContext from 'App/context';
import MiniProfile from 'shared/components/MiniProfile';
import DueDateManager from 'shared/components/DueDateManager';
type DetailsProps = {
taskID: string;
@ -32,12 +38,18 @@ const Details: React.FC<DetailsProps> = ({
refreshCache,
}) => {
const { userID } = useContext(UserIDContext);
const { showPopup } = usePopup();
const { showPopup, hidePopup } = usePopup();
const history = useHistory();
const match = useRouteMatch();
const [currentMemberTask, setCurrentMemberTask] = useState('');
const [memberPopupData, setMemberPopupData] = useState(initialMemberPopupState);
const { loading, data, refetch } = useFindTaskQuery({ variables: { taskID } });
const [updateTaskDueDate] = useUpdateTaskDueDateMutation({
onCompleted: () => {
refetch();
refreshCache();
},
});
const [assignTask] = useAssignTaskMutation({
onCompleted: () => {
refetch();
@ -56,6 +68,7 @@ const Details: React.FC<DetailsProps> = ({
if (!data) {
return <div>loading</div>;
}
console.log(data.findTask);
return (
<>
<Modal
@ -108,6 +121,29 @@ const Details: React.FC<DetailsProps> = ({
);
}}
onOpenAddLabelPopup={onOpenAddLabelPopup}
onOpenDueDatePopop={(task, $targetRef) => {
showPopup(
$targetRef,
<Popup
title={'Change Due Date'}
tab={0}
onClose={() => {
hidePopup();
}}
>
<DueDateManager
task={task}
onDueDateChange={(t, newDueDate) => {
console.log(`${newDueDate}`);
updateTaskDueDate({ variables: { taskID: t.id, dueDate: newDueDate } });
hidePopup();
}}
onCancel={() => {}}
/>
</Popup>,
);
}}
/>
);
}}

View File

@ -229,14 +229,14 @@ const ProjectAction = styled.div`
display: flex;
align-items: center;
font-size: 15px;
color: var(--color-text);
color: rgba(${props => props.theme.colors.text.primary});
&:not(:last-child) {
margin-right: 16px;
}
&:hover {
color: var(--color-text-hover);
color: rgba(${props => props.theme.colors.text.secondary});
}
`;
@ -490,15 +490,15 @@ const Project = () => {
);
}}
>
<Tags size={13} color="var(--color-icon)" />
<Tags width={13} height={13} />
<ProjectActionText>Labels</ProjectActionText>
</ProjectAction>
<ProjectAction>
<ToggleOn size={13} color="var(--color-icon)" />
<ToggleOn width={13} height={13} />
<ProjectActionText>Fields</ProjectActionText>
</ProjectAction>
<ProjectAction>
<Bolt size={13} color="var(--color-icon)" />
<Bolt width={13} height={13} />
<ProjectActionText>Rules</ProjectActionText>
</ProjectAction>
</ProjectActions>

View File

@ -32,8 +32,8 @@ type LoginFormData = {
};
type DueDateFormData = {
endDate: Date;
endTime: string | null;
endDate: string;
endTime: string;
};
type LoginProps = {

View File

@ -35,6 +35,7 @@ type Task = {
taskGroup: InnerTaskGroup;
name: string;
position: number;
dueDate?: string;
labels: TaskLabel[];
description?: string | null;
assigned?: Array<TaskUser>;

View File

@ -107,6 +107,7 @@ type ButtonProps = {
variant?: 'filled' | 'outline' | 'flat' | 'lineDown' | 'gradient' | 'relief';
color?: 'primary' | 'danger' | 'success' | 'warning' | 'dark';
disabled?: boolean;
type?: 'button' | 'submit';
className?: string;
onClick?: () => void;
};
@ -116,6 +117,7 @@ const Button: React.FC<ButtonProps> = ({
fontSize = '14px',
color = 'primary',
variant = 'filled',
type = 'button',
onClick,
className,
children,
@ -128,38 +130,38 @@ const Button: React.FC<ButtonProps> = ({
switch (variant) {
case 'filled':
return (
<Filled onClick={handleClick} className={className} disabled={disabled} color={color}>
<Filled type={type} onClick={handleClick} className={className} disabled={disabled} color={color}>
<Text fontSize={fontSize}>{children}</Text>
</Filled>
);
case 'outline':
return (
<Outline onClick={handleClick} className={className} disabled={disabled} color={color}>
<Outline type={type} onClick={handleClick} className={className} disabled={disabled} color={color}>
<Text fontSize={fontSize}>{children}</Text>
</Outline>
);
case 'flat':
return (
<Flat onClick={handleClick} className={className} disabled={disabled} color={color}>
<Flat type={type} onClick={handleClick} className={className} disabled={disabled} color={color}>
<Text fontSize={fontSize}>{children}</Text>
</Flat>
);
case 'lineDown':
return (
<LineDown onClick={handleClick} className={className} disabled={disabled} color={color}>
<LineDown type={type} onClick={handleClick} className={className} disabled={disabled} color={color}>
<Text fontSize={fontSize}>{children}</Text>
<LineX color={color} />
</LineDown>
);
case 'gradient':
return (
<Gradient onClick={handleClick} className={className} disabled={disabled} color={color}>
<Gradient type={type} onClick={handleClick} className={className} disabled={disabled} color={color}>
<Text fontSize={fontSize}>{children}</Text>
</Gradient>
);
case 'relief':
return (
<Relief onClick={handleClick} className={className} disabled={disabled} color={color}>
<Relief type={type} onClick={handleClick} className={className} disabled={disabled} color={color}>
<Text fontSize={fontSize}>{children}</Text>
</Relief>
);

View File

@ -22,7 +22,7 @@ export const EditorTextarea = styled(TextareaAutosize)`
padding: 0;
font-size: 16px;
line-height: 20px;
color: var(--color-input-text-focus);
color: rgba(${props => props.theme.colors.text.secondary});
&:focus {
border: none;
outline: none;
@ -84,7 +84,7 @@ export const ListCardContainer = styled.div<{ isActive: boolean; editable: boole
position: relative;
background-color: ${props =>
props.isActive && !props.editable ? mixin.darken('#262c49', 0.1) : 'var(--color-background)'};
props.isActive && !props.editable ? mixin.darken('#262c49', 0.1) : `rgba(${props.theme.colors.bg.secondary})`};
`;
export const ListCardInnerContainer = styled.div`
@ -145,7 +145,7 @@ export const CardTitle = styled.span`
overflow: hidden;
text-decoration: none;
word-wrap: break-word;
color: var(--color-text);
color: rgba(${props => props.theme.colors.text.primary});
`;
export const CardMembers = styled.div`

View File

@ -1,12 +1,16 @@
import React, { useRef } from 'react';
import { action } from '@storybook/addon-actions';
import DueDateManager from '.';
import BaseStyles from 'App/BaseStyles';
import NormalizeStyles from 'App/NormalizeStyles';
import { theme } from 'App/ThemeStyles';
import styled, { ThemeProvider } from 'styled-components';
import { Popup } from '../PopupMenu';
import styled from 'styled-components';
import DueDateManager from '.';
const PopupWrapper = styled.div`
width: 300px;
width: 310px;
`;
export default {
component: DueDateManager,
title: 'DueDateManager',
@ -20,44 +24,50 @@ export default {
export const Default = () => {
return (
<PopupWrapper>
<Popup title={null} tab={0}>
<DueDateManager
task={{
id: '1',
taskGroup: { name: 'General', id: '1', position: 1 },
name: 'Hello, world',
position: 1,
labels: [
{
id: 'soft-skills',
assignedDate: new Date().toString(),
projectLabel: {
createdDate: new Date().toString(),
id: 'label-soft-skills',
name: 'Soft Skills',
labelColor: {
id: '1',
name: 'white',
colorHex: '#fff',
position: 1,
},
},
},
],
description: 'hello!',
assigned: [
{
<>
<NormalizeStyles />
<BaseStyles />
<ThemeProvider theme={theme}>
<PopupWrapper>
<Popup title={null} tab={0}>
<DueDateManager
task={{
id: '1',
profileIcon: { url: null, initials: null, bgColor: null },
fullName: 'Jordan Knott',
},
],
}}
onCancel={action('cancel')}
onDueDateChange={action('due date change')}
/>
</Popup>
</PopupWrapper>
taskGroup: { name: 'General', id: '1', position: 1 },
name: 'Hello, world',
position: 1,
labels: [
{
id: 'soft-skills',
assignedDate: new Date().toString(),
projectLabel: {
createdDate: new Date().toString(),
id: 'label-soft-skills',
name: 'Soft Skills',
labelColor: {
id: '1',
name: 'white',
colorHex: '#fff',
position: 1,
},
},
},
],
description: 'hello!',
assigned: [
{
id: '1',
profileIcon: { url: null, initials: null, bgColor: null },
fullName: 'Jordan Knott',
},
],
}}
onCancel={action('cancel')}
onDueDateChange={action('due date change')}
/>
</Popup>
</PopupWrapper>
</ThemeProvider>
</>
);
};

View File

@ -1,5 +1,7 @@
import styled from 'styled-components';
import Button from 'shared/components/Button';
import { mixin } from 'shared/utils/styles';
import Input from 'shared/components/Input';
export const Wrapper = styled.div`
display: flex
@ -8,7 +10,37 @@ display: flex
background: #262c49;
font-family: 'Droid Sans', sans-serif;
border: none;
}
& .react-datepicker__triangle {
display: none;
}
& .react-datepicker-popper {
z-index: 10000;
margin-top: 0;
}
& .react-datepicker-time__header {
color: rgba(${props => props.theme.colors.text.primary});
}
& .react-datepicker__time-list-item {
color: rgba(${props => props.theme.colors.text.primary});
}
& .react-datepicker__time-container .react-datepicker__time
.react-datepicker__time-box ul.react-datepicker__time-list
li.react-datepicker__time-list-item:hover {
color: rgba(${props => props.theme.colors.text.secondary});
background: rgba(${props => props.theme.colors.bg.secondary});
}
& .react-datepicker__time-container .react-datepicker__time {
background: rgba(${props => props.theme.colors.bg.primary});
}
& .react-datepicker--time-only {
background: rgba(${props => props.theme.colors.bg.primary});
border: 1px solid rgba(${props => props.theme.colors.border});
}
& .react-datepicker * {
box-sizing: content-box;
}
& .react-datepicker__day-name {
color: #c2c6dc;
@ -56,6 +88,9 @@ display: flex
background: none;
border: none;
}
& .react-datepicker__header--time {
border-bottom: 1px solid rgba(${props => props.theme.colors.border});
}
`;
@ -66,21 +101,10 @@ export const DueDatePickerWrapper = styled.div`
justify-content: center;
`;
export const ConfirmAddDueDate = styled.div`
background-color: #5aac44;
box-shadow: none;
border: none;
color: #fff;
export const ConfirmAddDueDate = styled(Button)`
float: left;
margin: 0 4px 0 0;
cursor: pointer;
display: inline-block;
font-weight: 400;
line-height: 20px;
padding: 6px 12px;
text-align: center;
border-radius: 3px;
font-size: 14px;
`;
export const CancelDueDate = styled.div`
@ -92,6 +116,12 @@ export const CancelDueDate = styled.div`
cursor: pointer;
`;
export const DueDateInput = styled(Input)`
margin-top: 15px;
margin-bottom: 5px;
padding-right: 10px;
`;
export const ActionWrapper = styled.div`
padding-top: 8px;
width: 100%;

View File

@ -1,11 +1,11 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, forwardRef } from 'react';
import moment from 'moment';
import styled from 'styled-components';
import DatePicker from 'react-datepicker';
import { Cross } from 'shared/icons';
import _ from 'lodash';
import { Wrapper, ActionWrapper, DueDatePickerWrapper, ConfirmAddDueDate, CancelDueDate } from './Styles';
import { Wrapper, ActionWrapper, DueDateInput, DueDatePickerWrapper, ConfirmAddDueDate, CancelDueDate } from './Styles';
import 'react-datepicker/dist/react-datepicker.css';
import { getYear, getMonth } from 'date-fns';
@ -17,6 +17,14 @@ type DueDateManagerProps = {
onCancel: () => void;
};
const Form = styled.form`
padding-top: 25px;
`;
const FormField = styled.div`
width: 50%;
display: inline-block;
`;
const HeaderSelectLabel = styled.div`
display: inline-block;
position: relative;
@ -104,12 +112,17 @@ const HeaderActions = styled.div`
const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange, onCancel }) => {
const now = moment();
const [textStartDate, setTextStartDate] = useState(now.format('YYYY-MM-DD'));
const [startDate, setStartDate] = useState(new Date());
useEffect(() => {
setTextStartDate(moment(startDate).format('YYYY-MM-DD'));
}, [startDate]);
const [textEndTime, setTextEndTime] = useState(now.format('h:mm A'));
const [endTime, setEndTime] = useState(now.toDate());
useEffect(() => {
setTextEndTime(moment(endTime).format('h:mm A'));
}, [endTime]);
const years = _.range(2010, getYear(new Date()) + 10, 1);
const months = [
'January',
@ -125,85 +138,143 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
'November',
'December',
];
const { register, handleSubmit, errors, setError, formState } = useForm<DueDateFormData>();
const { register, handleSubmit, errors, setValue, setError, formState } = useForm<DueDateFormData>();
const saveDueDate = (data: any) => {
console.log(data);
const newDate = moment(`${data.endDate} ${data.endTime}`, 'YYYY-MM-DD h:mm A');
if (newDate.isValid()) {
onDueDateChange(task, newDate.toDate());
}
};
console.log(errors);
register({ name: 'endTime' }, { required: 'End time is required' });
useEffect(() => {
setValue('endTime', now.format('h:mm A'));
}, []);
const CustomTimeInput = forwardRef(({ value, onClick }: any, $ref: any) => {
return (
<DueDateInput
id="endTime"
name="endTime"
ref={$ref}
onChange={e => {
console.log(`onCahnge ${e.currentTarget.value}`);
setTextEndTime(e.currentTarget.value);
setValue('endTime', e.currentTarget.value);
}}
width="100%"
variant="alternate"
label="Date"
onClick={onClick}
value={value}
/>
);
});
console.log(`textStartDate ${textStartDate}`);
return (
<Wrapper>
<form>
<input
type="text"
id="endDate"
name="endDate"
onChange={e => {
setTextStartDate(e.currentTarget.value);
}}
value={textStartDate}
ref={register({
required: 'End due date is required.',
validate: value => {
const isValid = moment(value, 'YYYY-MM-DD').isValid();
console.log(`${value} - ${isValid}`);
return isValid;
},
})}
/>
</form>
<DueDatePickerWrapper>
<DatePicker
useWeekdaysShort
renderCustomHeader={({
date,
changeYear,
changeMonth,
decreaseMonth,
increaseMonth,
prevMonthButtonDisabled,
nextMonthButtonDisabled,
}) => (
<HeaderActions>
<HeaderButton onClick={decreaseMonth} disabled={prevMonthButtonDisabled}>
Prev
</HeaderButton>
<HeaderSelectLabel>
{months[date.getMonth()]}
<HeaderSelect value={getYear(date)} onChange={({ target: { value } }) => changeYear(parseInt(value))}>
{years.map(option => (
<option key={option} value={option}>
{option}
</option>
))}
</HeaderSelect>
</HeaderSelectLabel>
<HeaderSelectLabel>
{date.getFullYear()}
<HeaderSelect
value={months[getMonth(date)]}
onChange={({ target: { value } }) => changeMonth(months.indexOf(value))}
>
{months.map(option => (
<option key={option} value={option}>
{option}
</option>
))}
</HeaderSelect>
</HeaderSelectLabel>
<Form onSubmit={handleSubmit(saveDueDate)}>
<FormField>
<DueDateInput
id="endDate"
name="endDate"
width="100%"
variant="alternate"
label="Date"
onChange={e => {
setTextStartDate(e.currentTarget.value);
}}
value={textStartDate}
ref={register({
required: 'End date is required.',
})}
/>
</FormField>
<FormField>
<DatePicker
selected={endTime}
onChange={date => {
const changedDate = moment(date ?? new Date());
console.log(`changed ${date}`);
setEndTime(changedDate.toDate());
setValue('endTime', changedDate.format('h:mm A'));
}}
showTimeSelect
showTimeSelectOnly
timeIntervals={15}
timeCaption="Time"
dateFormat="h:mm aa"
customInput={<CustomTimeInput />}
/>
</FormField>
<DueDatePickerWrapper>
<DatePicker
useWeekdaysShort
renderCustomHeader={({
date,
changeYear,
changeMonth,
decreaseMonth,
increaseMonth,
prevMonthButtonDisabled,
nextMonthButtonDisabled,
}) => (
<HeaderActions>
<HeaderButton onClick={decreaseMonth} disabled={prevMonthButtonDisabled}>
Prev
</HeaderButton>
<HeaderSelectLabel>
{months[date.getMonth()]}
<HeaderSelect value={getYear(date)} onChange={({ target: { value } }) => changeYear(parseInt(value))}>
{years.map(option => (
<option key={option} value={option}>
{option}
</option>
))}
</HeaderSelect>
</HeaderSelectLabel>
<HeaderSelectLabel>
{date.getFullYear()}
<HeaderSelect
value={months[getMonth(date)]}
onChange={({ target: { value } }) => changeMonth(months.indexOf(value))}
>
{months.map(option => (
<option key={option} value={option}>
{option}
</option>
))}
</HeaderSelect>
</HeaderSelectLabel>
<HeaderButton onClick={increaseMonth} disabled={nextMonthButtonDisabled}>
Next
</HeaderButton>
</HeaderActions>
)}
selected={startDate}
inline
onChange={date => setStartDate(date ?? new Date())}
/>
</DueDatePickerWrapper>
<ActionWrapper>
<ConfirmAddDueDate onClick={() => onDueDateChange(task, startDate)}>Save</ConfirmAddDueDate>
<CancelDueDate onClick={onCancel}>
<Cross size={16} color="#c2c6dc" />
</CancelDueDate>
</ActionWrapper>
<HeaderButton onClick={increaseMonth} disabled={nextMonthButtonDisabled}>
Next
</HeaderButton>
</HeaderActions>
)}
selected={startDate}
inline
onChange={date => {
setStartDate(date ?? new Date());
}}
/>
</DueDatePickerWrapper>
<ActionWrapper>
<ConfirmAddDueDate
type="submit"
onClick={() => {
// const newDate = moment(startDate).format('YYYY-MM-DD');
// const newTime = moment(endTime).format('h:mm A');
// onDueDateChange(task, moment(`${newDate} ${newTime}`, 'YYYY-MM-DD h:mm A').toDate());
}}
>
Save
</ConfirmAddDueDate>
<CancelDueDate onClick={onCancel}>
<Cross size={16} color="#c2c6dc" />
</CancelDueDate>
</ActionWrapper>
</Form>
</Wrapper>
);
};

View File

@ -1,5 +1,5 @@
import React from 'react';
import styled from 'styled-components/macro';
import React, { useState, useEffect } from 'react';
import styled, { css } from 'styled-components/macro';
const InputWrapper = styled.div<{ width: string }>`
position: relative;
@ -32,7 +32,13 @@ const InputLabel = styled.span<{ width: string }>`
}
`;
const InputInput = styled.input<{ hasIcon: boolean; width: string; focusBg: string; borderColor: string }>`
const InputInput = styled.input<{
hasValue: boolean;
hasIcon: boolean;
width: string;
focusBg: string;
borderColor: string;
}>`
width: ${props => props.width};
font-size: 14px;
border: 1px solid rgba(0, 0, 0, 0.2);
@ -54,6 +60,14 @@ const InputInput = styled.input<{ hasIcon: boolean; width: string; focusBg: stri
color: rgba(115, 103, 240);
transform: translate(-3px, -90%);
}
${props =>
props.hasValue &&
css`
& ~ ${InputLabel} {
color: rgba(115, 103, 240);
transform: translate(-3px, -90%);
}
`}
`;
const Icon = styled.div`
@ -68,24 +82,66 @@ type InputProps = {
width?: string;
placeholder?: string;
icon?: JSX.Element;
id?: string;
name?: string;
className?: string;
value?: string;
onClick?: (e: React.MouseEvent<HTMLInputElement>) => void;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
};
const Input: React.FC<InputProps> = ({ width = 'auto', variant = 'normal', label, placeholder, icon }) => {
const borderColor = variant === 'normal' ? 'rgba(0, 0, 0, 0.2)' : '#414561';
const focusBg = variant === 'normal' ? 'rgba(38, 44, 73, )' : 'rgba(16, 22, 58, 1)';
return (
<InputWrapper width={width}>
<InputInput
hasIcon={typeof icon !== 'undefined'}
width={width}
placeholder={placeholder}
focusBg={focusBg}
borderColor={borderColor}
/>
{label && <InputLabel width={width}>{label}</InputLabel>}
<Icon>{icon && icon}</Icon>
</InputWrapper>
);
};
const Input = React.forwardRef(
(
{
width = 'auto',
variant = 'normal',
label,
placeholder,
icon,
name,
onChange,
className,
onClick,
value: initialValue,
id,
}: InputProps,
$ref: any,
) => {
const [value, setValue] = useState(initialValue ?? '');
useEffect(() => {
if (initialValue) {
setValue(initialValue);
}
}, [initialValue]);
const borderColor = variant === 'normal' ? 'rgba(0, 0, 0, 0.2)' : '#414561';
const focusBg = variant === 'normal' ? 'rgba(38, 44, 73, )' : 'rgba(16, 22, 58, 1)';
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.currentTarget.value);
if (onChange) {
onChange(e);
}
};
return (
<InputWrapper className={className} width={width}>
<InputInput
hasValue={value !== ''}
ref={$ref}
id={id}
name={name}
onClick={onClick}
onChange={handleChange}
value={value}
hasIcon={typeof icon !== 'undefined'}
width={width}
placeholder={placeholder}
focusBg={focusBg}
borderColor={borderColor}
/>
{label && <InputLabel width={width}>{label}</InputLabel>}
<Icon>{icon && icon}</Icon>
</InputWrapper>
);
},
);
export default Input;

View File

@ -12,6 +12,7 @@ import {
} from 'shared/utils/draggables';
import { Container, BoardWrapper } from './Styles';
import moment from 'moment';
interface SimpleProps {
taskGroups: Array<TaskGroup>;
@ -165,6 +166,14 @@ const SimpleLists: React.FC<SimpleProps> = ({
taskGroupID={taskGroup.id}
description=""
labels={task.labels.map(label => label.projectLabel)}
dueDate={
task.dueDate
? {
isPastDue: false,
formattedDate: moment(task.dueDate).format('MMM D, YYYY'),
}
: undefined
}
title={task.name}
members={task.assigned}
onClick={() => {

View File

@ -66,6 +66,7 @@ export const Default = () => {
onMemberProfile={action('profile')}
onOpenAddMemberPopup={action('open add member popup')}
onOpenAddLabelPopup={action('open add label popup')}
onOpenDueDatePopop={action('open due date popup')}
/>
);
}}

View File

@ -39,6 +39,7 @@ import {
} from './Styles';
import convertDivElementRefToBounds from 'shared/utils/boundingRect';
import moment from 'moment';
type TaskContentProps = {
onEditContent: () => void;
@ -106,6 +107,7 @@ type TaskDetailsProps = {
onDeleteTask: (task: Task) => void;
onOpenAddMemberPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
onOpenAddLabelPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
onOpenDueDatePopop: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
onMemberProfile: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void;
onCloseModal: () => void;
};
@ -118,6 +120,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
onCloseModal,
onOpenAddMemberPopup,
onOpenAddLabelPopup,
onOpenDueDatePopop,
onMemberProfile,
}) => {
const [editorOpen, setEditorOpen] = useState(false);
@ -143,6 +146,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
const onAddMember = () => {
onOpenAddMemberPopup(task, $addMemberRef);
};
const $dueDateLabel = useRef<HTMLDivElement>(null);
const $addLabelRef = useRef<HTMLDivElement>(null);
const onAddLabel = () => {
onOpenAddLabelPopup(task, $addLabelRef);
@ -229,7 +233,15 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
</TaskDetailsAddLabel>
</TaskDetailLabels>
<TaskDetailSectionTitle>Due Date</TaskDetailSectionTitle>
<NoDueDateLabel>No due date</NoDueDateLabel>
{task.dueDate ? (
<NoDueDateLabel ref={$dueDateLabel} onClick={() => onOpenDueDatePopop(task, $dueDateLabel)}>
{moment(task.dueDate).format('MMM D [at] h:mm A')}
</NoDueDateLabel>
) : (
<NoDueDateLabel ref={$dueDateLabel} onClick={() => onOpenDueDatePopop(task, $dueDateLabel)}>
No due date
</NoDueDateLabel>
)}
</TaskDetailsSidebar>
</TaskDetailsWrapper>
</>

View File

@ -110,6 +110,7 @@ export type Task = {
name: Scalars['String'];
position: Scalars['Float'];
description?: Maybe<Scalars['String']>;
dueDate?: Maybe<Scalars['Time']>;
assigned: Array<ProjectMember>;
labels: Array<TaskLabel>;
};
@ -310,6 +311,16 @@ export type UpdateTaskLocationPayload = {
task: Task;
};
export type UpdateTaskGroupName = {
taskGroupID: Scalars['UUID'];
name: Scalars['String'];
};
export type UpdateTaskDueDate = {
taskID: Scalars['UUID'];
dueDate?: Maybe<Scalars['Time']>;
};
export type Mutation = {
__typename?: 'Mutation';
createRefreshToken: RefreshToken;
@ -325,6 +336,7 @@ export type Mutation = {
updateProjectLabelColor: ProjectLabel;
createTaskGroup: TaskGroup;
updateTaskGroupLocation: TaskGroup;
updateTaskGroupName: TaskGroup;
deleteTaskGroup: DeleteTaskGroupPayload;
addTaskLabel: Task;
removeTaskLabel: Task;
@ -333,6 +345,7 @@ export type Mutation = {
updateTaskDescription: Task;
updateTaskLocation: UpdateTaskLocationPayload;
updateTaskName: Task;
updateTaskDueDate: Task;
deleteTask: DeleteTaskPayload;
assignTask: Task;
unassignTask: Task;
@ -400,6 +413,11 @@ export type MutationUpdateTaskGroupLocationArgs = {
};
export type MutationUpdateTaskGroupNameArgs = {
input: UpdateTaskGroupName;
};
export type MutationDeleteTaskGroupArgs = {
input: DeleteTaskGroupInput;
};
@ -440,6 +458,11 @@ export type MutationUpdateTaskNameArgs = {
};
export type MutationUpdateTaskDueDateArgs = {
input: UpdateTaskDueDate;
};
export type MutationDeleteTaskArgs = {
input: DeleteTaskInput;
};
@ -658,7 +681,7 @@ export type FindProjectQuery = (
& Pick<TaskGroup, 'id' | 'name' | 'position'>
& { tasks: Array<(
{ __typename?: 'Task' }
& Pick<Task, 'id' | 'name' | 'position' | 'description'>
& Pick<Task, 'id' | 'name' | 'position' | 'description' | 'dueDate'>
& { taskGroup: (
{ __typename?: 'TaskGroup' }
& Pick<TaskGroup, 'id' | 'name' | 'position'>
@ -698,7 +721,7 @@ export type FindTaskQuery = (
{ __typename?: 'Query' }
& { findTask: (
{ __typename?: 'Task' }
& Pick<Task, 'id' | 'name' | 'description' | 'position'>
& Pick<Task, 'id' | 'name' | 'description' | 'dueDate' | 'position'>
& { taskGroup: (
{ __typename?: 'TaskGroup' }
& Pick<TaskGroup, 'id'>
@ -852,6 +875,20 @@ export type UpdateTaskDescriptionMutation = (
) }
);
export type UpdateTaskDueDateMutationVariables = {
taskID: Scalars['UUID'];
dueDate?: Maybe<Scalars['Time']>;
};
export type UpdateTaskDueDateMutation = (
{ __typename?: 'Mutation' }
& { updateTaskDueDate: (
{ __typename?: 'Task' }
& Pick<Task, 'id' | 'dueDate'>
) }
);
export type UpdateTaskGroupLocationMutationVariables = {
taskGroupID: Scalars['UUID'];
position: Scalars['Float'];
@ -1298,6 +1335,7 @@ export const FindProjectDocument = gql`
name
position
description
dueDate
taskGroup {
id
name
@ -1370,6 +1408,7 @@ export const FindTaskDocument = gql`
id
name
description
dueDate
position
taskGroup {
id
@ -1705,6 +1744,40 @@ export function useUpdateTaskDescriptionMutation(baseOptions?: ApolloReactHooks.
export type UpdateTaskDescriptionMutationHookResult = ReturnType<typeof useUpdateTaskDescriptionMutation>;
export type UpdateTaskDescriptionMutationResult = ApolloReactCommon.MutationResult<UpdateTaskDescriptionMutation>;
export type UpdateTaskDescriptionMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateTaskDescriptionMutation, UpdateTaskDescriptionMutationVariables>;
export const UpdateTaskDueDateDocument = gql`
mutation updateTaskDueDate($taskID: UUID!, $dueDate: Time) {
updateTaskDueDate(input: {taskID: $taskID, dueDate: $dueDate}) {
id
dueDate
}
}
`;
export type UpdateTaskDueDateMutationFn = ApolloReactCommon.MutationFunction<UpdateTaskDueDateMutation, UpdateTaskDueDateMutationVariables>;
/**
* __useUpdateTaskDueDateMutation__
*
* To run a mutation, you first call `useUpdateTaskDueDateMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUpdateTaskDueDateMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [updateTaskDueDateMutation, { data, loading, error }] = useUpdateTaskDueDateMutation({
* variables: {
* taskID: // value for 'taskID'
* dueDate: // value for 'dueDate'
* },
* });
*/
export function useUpdateTaskDueDateMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<UpdateTaskDueDateMutation, UpdateTaskDueDateMutationVariables>) {
return ApolloReactHooks.useMutation<UpdateTaskDueDateMutation, UpdateTaskDueDateMutationVariables>(UpdateTaskDueDateDocument, baseOptions);
}
export type UpdateTaskDueDateMutationHookResult = ReturnType<typeof useUpdateTaskDueDateMutation>;
export type UpdateTaskDueDateMutationResult = ApolloReactCommon.MutationResult<UpdateTaskDueDateMutation>;
export type UpdateTaskDueDateMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateTaskDueDateMutation, UpdateTaskDueDateMutationVariables>;
export const UpdateTaskGroupLocationDocument = gql`
mutation updateTaskGroupLocation($taskGroupID: UUID!, $position: Float!) {
updateTaskGroupLocation(input: {taskGroupID: $taskGroupID, position: $position}) {

View File

@ -30,6 +30,7 @@ query findProject($projectId: String!) {
name
position
description
dueDate
taskGroup {
id
name

View File

@ -3,6 +3,7 @@ query findTask($taskID: UUID!) {
id
name
description
dueDate
position
taskGroup {
id

View File

@ -0,0 +1,12 @@
mutation updateTaskDueDate($taskID: UUID!, $dueDate: Time) {
updateTaskDueDate (
input: {
taskID: $taskID
dueDate: $dueDate
}
) {
id
dueDate
}
}

View File

@ -1,24 +1,12 @@
import React from 'react';
import Icon, { IconProps } from './Icon';
type Props = {
size: number | string;
color: string;
};
const Bolt = ({ size, color }: Props) => {
const Bolt: React.FC<IconProps> = ({ width = '16px', height = '16px' }) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" width={size} height={size}>
<path
fill={color}
d="M296 160H180.6l42.6-129.8C227.2 15 215.7 0 200 0H56C44 0 33.8 8.9 32.2 20.8l-32 240C-1.7 275.2 9.5 288 24 288h118.7L96.6 482.5c-3.6 15.2 8 29.5 23.3 29.5 8.4 0 16.4-4.4 20.8-12l176-304c9.3-15.9-2.2-36-20.7-36z"
/>
</svg>
<Icon width={width} height={height} viewBox="0 0 320 512">
<path d="M296 160H180.6l42.6-129.8C227.2 15 215.7 0 200 0H56C44 0 33.8 8.9 32.2 20.8l-32 240C-1.7 275.2 9.5 288 24 288h118.7L96.6 482.5c-3.6 15.2 8 29.5 23.3 29.5 8.4 0 16.4-4.4 20.8-12l176-304c9.3-15.9-2.2-36-20.7-36z" />
</Icon>
);
};
Bolt.defaultProps = {
size: 16,
color: '#000',
};
export default Bolt;

View File

@ -0,0 +1,28 @@
import React from 'react';
import styled from 'styled-components/macro';
export type IconProps = {
width: number | string;
height: number | string;
};
type Props = {
width: number | string;
height: number | string;
viewBox: string;
className?: string;
};
const Svg = styled.svg`
fill: rgba(${props => props.theme.colors.text.primary});
`;
const Icon: React.FC<Props> = ({ width, height, viewBox, className, children }) => {
return (
<Svg className={className} width={width} height={height} xmlns="http://www.w3.org/2000/svg" viewBox={viewBox}>
{children}
</Svg>
);
};
export default Icon;

View File

@ -1,24 +1,12 @@
import React from 'react';
import Icon, { IconProps } from './Icon';
type Props = {
size: number | string;
color: string;
};
const Tags = ({ size, color }: Props) => {
const Tags: React.FC<IconProps> = ({ width = '16px', height = '16px' }) => {
return (
<svg width={size} height={size} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512">
<path
fill={color}
d="M497.941 225.941L286.059 14.059A48 48 0 0 0 252.118 0H48C21.49 0 0 21.49 0 48v204.118a48 48 0 0 0 14.059 33.941l211.882 211.882c18.744 18.745 49.136 18.746 67.882 0l204.118-204.118c18.745-18.745 18.745-49.137 0-67.882zM112 160c-26.51 0-48-21.49-48-48s21.49-48 48-48 48 21.49 48 48-21.49 48-48 48zm513.941 133.823L421.823 497.941c-18.745 18.745-49.137 18.745-67.882 0l-.36-.36L527.64 323.522c16.999-16.999 26.36-39.6 26.36-63.64s-9.362-46.641-26.36-63.64L331.397 0h48.721a48 48 0 0 1 33.941 14.059l211.882 211.882c18.745 18.745 18.745 49.137 0 67.882z"
/>
</svg>
<Icon width={width} height={height} viewBox="0 0 640 512">
<path d="M497.941 225.941L286.059 14.059A48 48 0 0 0 252.118 0H48C21.49 0 0 21.49 0 48v204.118a48 48 0 0 0 14.059 33.941l211.882 211.882c18.744 18.745 49.136 18.746 67.882 0l204.118-204.118c18.745-18.745 18.745-49.137 0-67.882zM112 160c-26.51 0-48-21.49-48-48s21.49-48 48-48 48 21.49 48 48-21.49 48-48 48zm513.941 133.823L421.823 497.941c-18.745 18.745-49.137 18.745-67.882 0l-.36-.36L527.64 323.522c16.999-16.999 26.36-39.6 26.36-63.64s-9.362-46.641-26.36-63.64L331.397 0h48.721a48 48 0 0 1 33.941 14.059l211.882 211.882c18.745 18.745 18.745 49.137 0 67.882z" />
</Icon>
);
};
Tags.defaultProps = {
size: 16,
color: '#000',
};
export default Tags;

View File

@ -1,24 +1,13 @@
import React from 'react';
type Props = {
size: number | string;
color: string;
};
import Icon, { IconProps } from './Icon';
const ToggleOn = ({ size, color }: Props) => {
const ToggleOn: React.FC<IconProps> = ({ width = '16px', height = '16px' }) => {
return (
<svg width={size} height={size} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
<path
fill={color}
d="M384 64H192C86 64 0 150 0 256s86 192 192 192h192c106 0 192-86 192-192S490 64 384 64zm0 320c-70.8 0-128-57.3-128-128 0-70.8 57.3-128 128-128 70.8 0 128 57.3 128 128 0 70.8-57.3 128-128 128z"
/>
</svg>
<Icon width={width} height={height} viewBox="0 0 576 512">
<path d="M384 64H192C86 64 0 150 0 256s86 192 192 192h192c106 0 192-86 192-192S490 64 384 64zm0 320c-70.8 0-128-57.3-128-128 0-70.8 57.3-128 128-128 70.8 0 128 57.3 128 128 0 70.8-57.3 128-128 128z" />
</Icon>
);
};
ToggleOn.defaultProps = {
size: 16,
color: '#000',
};
export default ToggleOn;