feature: add checklist

This commit is contained in:
Jordan Knott
2020-06-18 18:12:15 -05:00
parent b6f0e8b6b2
commit 9d6c67f791
73 changed files with 4582 additions and 390 deletions

View File

@ -1,7 +1,9 @@
overwrite: true
schema:
- '../api/graph/schema.graphqls'
documents: 'src/shared/graphql/*.graphqls'
documents:
- 'src/shared/graphql/*.graphqls'
- 'src/shared/graphql/**/*.ts'
generates:
src/shared/generated/graphql.tsx:
plugins:

View File

@ -112,7 +112,7 @@ export default createGlobalStyle`
}
::-webkit-scrollbar {
width: 12px;
width: 10px;
}
::-webkit-scrollbar-track {

View File

@ -10,9 +10,12 @@ import Profile from 'Profile';
import styled from 'styled-components';
const MainContent = styled.div`
padding: 0 0 50px 80px;
padding: 0 0 0 80px;
background: #262c49;
height: 100%;
display: flex;
flex-direction: column;
flex-grow: 1;
`;
type RoutesProps = {
history: H.History;

View File

@ -26,23 +26,3 @@ const theme: DefaultTheme = {
};
export { theme };
export default createGlobalStyle`
:root {
--color-text: #c2c6dc;
--color-text-hover: #fff;
--color-primary: rgba(115, 103, 240);
--color-button-text: #c2c6dc;
--color-button-text-hover: #fff;
--color-button-background: rgba(115, 103, 240);
--color-background: #262c49;
--color-background-dark: #10163a;
--color-input-text: #c2c6dc;
--color-input-text-focus: #fff;
--color-icon: #c2c6dc;
--color-active-icon: rgba(115, 103, 240);
}
`;

View File

@ -5,7 +5,7 @@ import { setAccessToken } from 'shared/utils/accessToken';
import styled, { ThemeProvider } from 'styled-components';
import NormalizeStyles from './NormalizeStyles';
import BaseStyles from './BaseStyles';
import ThemeStyles, { theme } from './ThemeStyles';
import { theme } from './ThemeStyles';
import Routes from './Routes';
import { UserIDContext } from './context';
import Navbar from './Navbar';
@ -44,7 +44,6 @@ const App = () => {
<PopupProvider>
<NormalizeStyles />
<BaseStyles />
<ThemeStyles />
<Router history={history}>
{loading ? (
<div>loading</div>

View File

@ -9,10 +9,16 @@ import {
useUpdateTaskDueDateMutation,
useAssignTaskMutation,
useUnassignTaskMutation,
useSetTaskChecklistItemCompleteMutation,
useDeleteTaskChecklistItemMutation,
useUpdateTaskChecklistItemNameMutation,
useCreateTaskChecklistItemMutation,
FindTaskDocument,
} from 'shared/generated/graphql';
import UserIDContext from 'App/context';
import MiniProfile from 'shared/components/MiniProfile';
import DueDateManager from 'shared/components/DueDateManager';
import produce from 'immer';
type DetailsProps = {
taskID: string;
@ -43,6 +49,64 @@ const Details: React.FC<DetailsProps> = ({
const match = useRouteMatch();
const [currentMemberTask, setCurrentMemberTask] = useState('');
const [memberPopupData, setMemberPopupData] = useState(initialMemberPopupState);
const [setTaskChecklistItemComplete] = useSetTaskChecklistItemCompleteMutation();
const [updateTaskChecklistItemName] = useUpdateTaskChecklistItemNameMutation();
const [deleteTaskChecklistItem] = useDeleteTaskChecklistItemMutation({
update: (client, deleteData) => {
const cacheData: any = client.readQuery({
query: FindTaskDocument,
variables: { taskID },
});
console.log(deleteData);
const newData = produce(cacheData.findTask, (draftState: any) => {
const idx = draftState.checklists.findIndex(
(checklist: TaskChecklist) =>
checklist.id === deleteData.data.deleteTaskChecklistItem.taskChecklistItem.taskChecklistID,
);
console.log(`idx ${idx}`);
if (idx !== -1) {
draftState.checklists[idx].items = cacheData.findTask.checklists[idx].items.filter(
(item: any) => item.id !== deleteData.data.deleteTaskChecklistItem.taskChecklistItem.id,
);
}
});
client.writeQuery({
query: FindTaskDocument,
variables: { taskID },
data: {
findTask: newData,
},
});
},
});
const [createTaskChecklistItem] = useCreateTaskChecklistItemMutation({
update: (client, newTaskItem) => {
const cacheData: any = client.readQuery({
query: FindTaskDocument,
variables: { taskID },
});
console.log(cacheData);
console.log(newTaskItem);
const newData = produce(cacheData.findTask, (draftState: any) => {
const idx = draftState.checklists.findIndex(
(checklist: TaskChecklist) => checklist.id === newTaskItem.data.createTaskChecklistItem.taskChecklistID,
);
if (idx !== -1) {
draftState.checklists[idx].items = [
...cacheData.findTask.checklists[idx].items,
{ ...newTaskItem.data.createTaskChecklistItem },
];
}
});
client.writeQuery({
query: FindTaskDocument,
variables: { taskID },
data: {
findTask: newData,
},
});
},
});
const { loading, data, refetch } = useFindTaskQuery({ variables: { taskID } });
const [updateTaskDueDate] = useUpdateTaskDueDateMutation({
onCompleted: () => {
@ -83,7 +147,19 @@ const Details: React.FC<DetailsProps> = ({
onTaskNameChange={onTaskNameChange}
onTaskDescriptionChange={onTaskDescriptionChange}
onDeleteTask={onDeleteTask}
onChangeItemName={(itemID, itemName) => {
updateTaskChecklistItemName({ variables: { taskChecklistItemID: itemID, name: itemName } });
}}
onCloseModal={() => history.push(projectURL)}
onDeleteItem={itemID => {
deleteTaskChecklistItem({ variables: { taskChecklistItemID: itemID } });
}}
onToggleChecklistItem={(itemID, complete) => {
setTaskChecklistItemComplete({ variables: { taskChecklistItemID: itemID, complete } });
}}
onAddItem={(taskChecklistID, name, position) => {
createTaskChecklistItem({ variables: { taskChecklistID, name, position } });
}}
onMemberProfile={($targetRef, memberID) => {
const member = data.findTask.assigned.find(m => m.id === memberID);
const profileIcon = member ? member.profileIcon : null;
@ -124,7 +200,6 @@ const Details: React.FC<DetailsProps> = ({
onOpenDueDatePopop={(task, $targetRef) => {
showPopup(
$targetRef,
<Popup
title={'Change Due Date'}
tab={0}
@ -134,8 +209,11 @@ const Details: React.FC<DetailsProps> = ({
>
<DueDateManager
task={task}
onRemoveDueDate={t => {
updateTaskDueDate({ variables: { taskID: t.id, dueDate: null } });
hidePopup();
}}
onDueDateChange={(t, newDueDate) => {
console.log(`${newDueDate}`);
updateTaskDueDate({ variables: { taskID: t.id, dueDate: newDueDate } });
hidePopup();
}}

View File

@ -5,9 +5,11 @@ import { Bolt, ToggleOn, Tags } from 'shared/icons';
import { usePopup, Popup } from 'shared/components/PopupMenu';
import { useParams, Route, useRouteMatch, useHistory, RouteComponentProps } from 'react-router-dom';
import {
useSetTaskCompleteMutation,
useToggleTaskLabelMutation,
useUpdateProjectNameMutation,
useFindProjectQuery,
useUpdateTaskGroupNameMutation,
useUpdateTaskNameMutation,
useUpdateProjectLabelMutation,
useCreateTaskMutation,
@ -23,6 +25,7 @@ import {
FindProjectDocument,
useCreateProjectLabelMutation,
useUnassignTaskMutation,
useUpdateTaskDueDateMutation,
} from 'shared/generated/graphql';
import TaskAssignee from 'shared/components/TaskAssignee';
@ -40,6 +43,7 @@ import MiniProfile from 'shared/components/MiniProfile';
import Details from './Details';
import { useApolloClient } from '@apollo/react-hooks';
import UserIDContext from 'App/context';
import DueDateManager from 'shared/components/DueDateManager';
const getCacheData = (client: any, projectID: string) => {
const cacheData: any = client.readQuery({
@ -69,6 +73,7 @@ interface QuickCardEditorState {
isOpen: boolean;
left: number;
top: number;
width: number;
taskID: string | null;
taskGroupID: string | null;
}
@ -209,6 +214,7 @@ const initialQuickCardEditorState: QuickCardEditorState = {
isOpen: false,
top: 0,
left: 0,
width: 272,
};
const ProjectBar = styled.div`
@ -367,12 +373,36 @@ const Project = () => {
if (taskGroup) {
let position = 65535;
if (taskGroup.tasks.length !== 0) {
const [lastTask] = taskGroup.tasks.sort((a: any, b: any) => a.position - b.position).slice(-1);
const [lastTask] = taskGroup.tasks
.slice()
.sort((a: any, b: any) => a.position - b.position)
.slice(-1);
position = Math.ceil(lastTask.position) * 2 + 1;
}
console.log(`position ${position}`);
createTask({ variables: { taskGroupID, name, position } });
createTask({
variables: { taskGroupID, name, position },
optimisticResponse: {
__typename: 'Mutation',
createTask: {
__typename: 'Task',
id: '' + Math.round(Math.random() * -1000000),
name: name,
taskGroup: {
__typename: 'TaskGroup',
id: taskGroup.id,
name: taskGroup.name,
position: taskGroup.position,
},
position: position,
dueDate: null,
description: null,
labels: [],
assigned: [],
},
},
});
}
}
};
@ -410,6 +440,10 @@ const Project = () => {
const [assignTask] = useAssignTaskMutation();
const [unassignTask] = useUnassignTaskMutation();
const [updateTaskGroupName] = useUpdateTaskGroupNameMutation({});
const [updateTaskDueDate] = useUpdateTaskDueDateMutation();
const [updateProjectName] = useUpdateProjectNameMutation({
update: (client, newName) => {
const cacheData = getCacheData(client, projectID);
@ -421,6 +455,8 @@ const Project = () => {
},
});
const [setTaskComplete] = useSetTaskCompleteMutation();
const client = useApolloClient();
const { userID } = useContext(UserIDContext);
@ -441,6 +477,7 @@ const Project = () => {
);
}
if (data) {
console.log(data.findProject);
const onQuickEditorOpen = (e: ContextMenuEvent) => {
const taskGroup = data.findProject.taskGroups.find(t => t.id === e.taskGroupID);
const currentTask = taskGroup ? taskGroup.tasks.find(t => t.id === e.taskID) : null;
@ -448,6 +485,7 @@ const Project = () => {
setQuickCardEditor({
top: e.top,
left: e.left,
width: e.width,
isOpen: true,
taskID: currentTask.id,
taskGroupID: currentTask.taskGroup.id,
@ -567,7 +605,9 @@ const Project = () => {
</Popup>,
);
}}
onChangeTaskGroupName={(taskGroupID, name) => {}}
onChangeTaskGroupName={(taskGroupID, name) => {
updateTaskGroupName({ variables: { taskGroupID, name } });
}}
onQuickEditorOpen={onQuickEditorOpen}
onExtraMenuOpen={(taskGroupID: string, $targetRef: any) => {
showPopup(
@ -660,8 +700,37 @@ const Project = () => {
},
})
}
onOpenDueDatePopup={($targetRef, task) => {
showPopup(
$targetRef,
<Popup
title={'Change Due Date'}
tab={0}
onClose={() => {
hidePopup();
}}
>
<DueDateManager
task={task}
onRemoveDueDate={t => {
updateTaskDueDate({ variables: { taskID: t.id, dueDate: null } });
hidePopup();
}}
onDueDateChange={(t, newDueDate) => {
updateTaskDueDate({ variables: { taskID: t.id, dueDate: newDueDate } });
hidePopup();
}}
onCancel={() => {}}
/>
</Popup>,
);
}}
onToggleComplete={task => {
setTaskComplete({ variables: { taskID: task.id, complete: !task.complete } });
}}
top={quickCardEditor.top}
left={quickCardEditor.left}
width={quickCardEditor.width}
/>
)}
<Route

View File

@ -12,6 +12,7 @@ interface DraggableElement {
type ContextMenuEvent = {
left: number;
top: number;
width: number;
taskID: string;
taskGroupID: string;
};

19
web/src/projects.d.ts vendored
View File

@ -30,15 +30,34 @@ type TaskLabel = {
projectLabel: ProjectLabel;
};
type TaskChecklist = {
id: string;
position: number;
name: string;
items: Array<TaskChecklistItem>;
};
type TaskChecklistItem = {
id: string;
complete: boolean;
position: number;
name: string;
taskChecklistID: string;
assigned?: null | TaskUser;
dueDate?: null | string;
};
type Task = {
id: string;
taskGroup: InnerTaskGroup;
name: string;
position: number;
dueDate?: string;
complete?: boolean;
labels: TaskLabel[];
description?: string | null;
assigned?: Array<TaskUser>;
checklists?: Array<TaskChecklist> | null;
};
type Project = {

View File

@ -3,7 +3,15 @@ import TextareaAutosize from 'react-autosize-textarea/lib';
import { mixin } from 'shared/utils/styles';
import Button from 'shared/components/Button';
export const Container = styled.div``;
export const Container = styled.div`
width: 272px;
margin: 0 4px;
height: 100%;
box-sizing: border-box;
display: inline-block;
vertical-align: top;
white-space: nowrap;
`;
export const Wrapper = styled.div<{ editorOpen: boolean }>`
display: inline-block;

View File

@ -62,7 +62,7 @@ const NameEditor: React.FC<NameEditorProps> = ({ onSave, onCancel }) => {
Save
</AddListButton>
<CancelAdd onClick={() => onCancel()}>
<Cross color="#c2c6dc" />
<Cross width={16} height={16} />
</CancelAdd>
</ListAddControls>
</>

View File

@ -2,6 +2,7 @@ import styled, { css } from 'styled-components';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { mixin } from 'shared/utils/styles';
import TextareaAutosize from 'react-autosize-textarea';
import { CheckCircle } from 'shared/icons';
import { RefObject } from 'react';
export const ClockIcon = styled(FontAwesomeIcon)``;
@ -20,9 +21,9 @@ export const EditorTextarea = styled(TextareaAutosize)`
max-height: 162px;
min-height: 54px;
padding: 0;
font-size: 16px;
line-height: 20px;
color: rgba(${props => props.theme.colors.text.secondary});
font-size: 14px;
line-height: 16px;
color: rgba(${props => props.theme.colors.text.primary});
&:focus {
border: none;
outline: none;
@ -92,11 +93,13 @@ export const ListCardInnerContainer = styled.div`
height: 100%;
`;
export const ListCardDetails = styled.div`
export const ListCardDetails = styled.div<{ complete: boolean }>`
overflow: hidden;
padding: 6px 8px 2px;
position: relative;
z-index: 10;
${props => props.complete && 'opacity: 0.6;'}
`;
export const ListCardLabels = styled.div`
@ -140,15 +143,28 @@ export const ListCardOperation = styled.span`
export const CardTitle = styled.span`
clear: both;
display: block;
margin: 0 0 4px;
overflow: hidden;
text-decoration: none;
word-wrap: break-word;
line-height: 16px;
font-size: 14px;
color: rgba(${props => props.theme.colors.text.primary});
display: flex;
align-items: center;
`;
export const CardMembers = styled.div`
float: right;
margin: 0 -2px 4px 0;
`;
export const CompleteIcon = styled(CheckCircle)`
fill: rgba(${props => props.theme.colors.success});
margin-right: 4px;
`;
export const EditorContent = styled.div`
display: flex;
`;

View File

@ -5,6 +5,8 @@ import { faPencilAlt, faList } from '@fortawesome/free-solid-svg-icons';
import { faClock, faCheckSquare, faEye } from '@fortawesome/free-regular-svg-icons';
import {
EditorTextarea,
EditorContent,
CompleteIcon,
DescriptionBadge,
DueDateCardBadge,
ListCardBadges,
@ -35,6 +37,7 @@ type Props = {
title: string;
taskID: string;
taskGroupID: string;
complete?: boolean;
onContextMenu?: (e: ContextMenuEvent) => void;
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
description?: null | string;
@ -57,6 +60,7 @@ const Card = React.forwardRef(
onContextMenu,
taskID,
taskGroupID,
complete,
onClick,
labels,
title,
@ -101,6 +105,7 @@ const Card = React.forwardRef(
const pos = $innerCardRef.current.getBoundingClientRect();
if (onContextMenu) {
onContextMenu({
width: pos.width,
top: pos.top,
left: pos.left,
taskGroupID,
@ -140,7 +145,7 @@ const Card = React.forwardRef(
<FontAwesomeIcon onClick={onOperationClick} color="#c2c6dc" size="xs" icon={faPencilAlt} />
</ListCardOperation>
)}
<ListCardDetails>
<ListCardDetails complete={complete ?? false}>
<ListCardLabels>
{labels &&
labels.map(label => (
@ -150,22 +155,28 @@ const Card = React.forwardRef(
))}
</ListCardLabels>
{editable ? (
<EditorTextarea
onChange={e => {
setCardTitle(e.currentTarget.value);
if (onCardTitleChange) {
onCardTitleChange(e.currentTarget.value);
}
}}
onClick={e => {
e.stopPropagation();
}}
onKeyDown={handleKeyDown}
value={currentCardTitle}
ref={$editorRef}
/>
<EditorContent>
{complete && <CompleteIcon width={16} height={16} />}
<EditorTextarea
onChange={e => {
setCardTitle(e.currentTarget.value);
if (onCardTitleChange) {
onCardTitleChange(e.currentTarget.value);
}
}}
onClick={e => {
e.stopPropagation();
}}
onKeyDown={handleKeyDown}
value={currentCardTitle}
ref={$editorRef}
/>
</EditorContent>
) : (
<CardTitle>{title}</CardTitle>
<CardTitle>
{complete && <CompleteIcon width={16} height={16} />}
{title}
</CardTitle>
)}
<ListCardBadges>
{watched && (

View File

@ -0,0 +1,138 @@
import React, { useState } from 'react';
import { action } from '@storybook/addon-actions';
import BaseStyles from 'App/BaseStyles';
import NormalizeStyles from 'App/NormalizeStyles';
import { theme } from 'App/ThemeStyles';
import produce from 'immer';
import styled, { ThemeProvider } from 'styled-components';
import Checklist from '.';
export default {
component: Checklist,
title: 'Checklist',
parameters: {
backgrounds: [
{ name: 'gray', value: '#f8f8f8', default: true },
{ name: 'white', value: '#ffffff' },
],
},
};
const Container = styled.div`
width: 552px;
margin: 25px;
border: 1px solid rgba(${props => props.theme.colors.bg.primary});
`;
const defaultItems = [
{
id: '1',
position: 1,
taskChecklistID: '1',
complete: false,
name: 'Tasks',
assigned: null,
dueDate: null,
},
{
id: '2',
taskChecklistID: '1',
position: 2,
complete: false,
name: 'Projects',
assigned: null,
dueDate: null,
},
{
id: '3',
position: 3,
taskChecklistID: '1',
complete: false,
name: 'Teams',
assigned: null,
dueDate: null,
},
{
id: '4',
position: 4,
complete: false,
taskChecklistID: '1',
name: 'Organizations',
assigned: null,
dueDate: null,
},
];
export const Default = () => {
const [checklistName, setChecklistName] = useState('Checklist');
const [items, setItems] = useState(defaultItems);
const onToggleItem = (itemID: string, complete: boolean) => {
setItems(
produce(items, draftState => {
const idx = items.findIndex(item => item.id === itemID);
if (idx !== -1) {
draftState[idx] = {
...draftState[idx],
complete,
};
}
}),
);
};
return (
<>
<BaseStyles />
<NormalizeStyles />
<ThemeProvider theme={theme}>
<Container>
<Checklist
name={checklistName}
checklistID="checklist-one"
items={items}
onDeleteChecklist={action('delete checklist')}
onChangeName={currentName => {
setChecklistName(currentName);
}}
onAddItem={itemName => {
let position = 1;
const lastItem = items[-1];
if (lastItem) {
position = lastItem.position * 2 + 1;
}
setItems([
...items,
{
id: `${Math.random()}`,
name: itemName,
complete: false,
assigned: null,
dueDate: null,
position,
taskChecklistID: '1',
},
]);
}}
onDeleteItem={itemID => {
console.log(`itemID ${itemID}`);
setItems(items.filter(item => item.id !== itemID));
}}
onChangeItemName={(itemID, currentName) => {
setItems(
produce(items, draftState => {
const idx = items.findIndex(item => item.id === itemID);
if (idx !== -1) {
draftState[idx] = {
...draftState[idx],
name: currentName,
};
}
}),
);
}}
onToggleItem={onToggleItem}
/>
</Container>
</ThemeProvider>
</>
);
};

View File

@ -0,0 +1,596 @@
import React, { useState, useRef, useEffect } from 'react';
import styled from 'styled-components';
import { CheckSquare, Trash, Square, CheckSquareOutline, Clock, Cross, AccountPlus } from 'shared/icons';
import Button from 'shared/components/Button';
import TextareaAutosize from 'react-autosize-textarea';
import Control from 'react-select/src/components/Control';
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
const Wrapper = styled.div`
margin-bottom: 24px;
`;
const WindowTitle = styled.div`
padding: 8px 0;
position: relative;
margin: 0 0 4px 40px;
`;
const WindowTitleIcon = styled(CheckSquareOutline)`
top: 10px;
left: -40px;
position: absolute;
`;
const WindowChecklistTitle = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
flex-flow: row wrap;
`;
const WindowTitleText = styled.h3`
cursor: pointer;
color: rgba(${props => props.theme.colors.text.primary});
margin: 6px 0;
display: inline-block;
width: auto;
min-height: 18px;
font-size: 16px;
line-height: 20px;
min-width: 40px;
`;
const WindowOptions = styled.div`
margin: 0 2px 0 auto;
float: right;
`;
const DeleteButton = styled(Button)`
padding: 6px 12px;
`;
const ChecklistProgress = styled.div`
margin-bottom: 6px;
position: relative;
`;
const ChecklistProgressPercent = styled.span`
color: #5e6c84;
font-size: 11px;
line-height: 10px;
position: absolute;
left: 5px;
top: -1px;
text-align: center;
width: 32px;
`;
const ChecklistProgressBar = styled.div`
background: rgba(${props => props.theme.colors.bg.primary});
border-radius: 4px;
clear: both;
height: 8px;
margin: 0 0 0 40px;
overflow: hidden;
position: relative;
`;
const ChecklistProgressBarCurrent = styled.div<{ width: number }>`
width: ${props => props.width}%;
background: rgba(${props => (props.width === 100 ? props.theme.colors.success : props.theme.colors.primary)});
bottom: 0;
left: 0;
position: absolute;
top: 0;
transition: width 0.14s ease-in, background 0.14s ease-in;
`;
const ChecklistItems = styled.div`
min-height: 8px;
`;
const ChecklistItemUncheckedIcon = styled(Square)``;
const ChecklistIcon = styled.div`
cursor: pointer;
position: absolute;
left: 0;
top: 0;
margin: 10px;
text-align: center;
&:hover {
opacity: 0.8;
}
`;
const ChecklistItemCheckedIcon = styled(CheckSquare)`
fill: rgba(${props => props.theme.colors.primary});
`;
const ChecklistItemDetails = styled.div`
word-break: break-word;
word-wrap: break-word;
overflow-wrap: break-word;
`;
const ChecklistItemRow = styled.div`
cursor: pointer;
display: flex;
flex-direction: row;
`;
const ChecklistItemTextControls = styled.div`
padding: 6px 0;
width: 100%;
display: inline-flex;
`;
const ChecklistItemText = styled.span<{ complete: boolean }>`
color: ${props => (props.complete ? '#5e6c84' : `rgba(${props.theme.colors.text.primary})`)};
${props => props.complete && 'text-decoration: line-through;'}
line-height: 20px;
font-size: 16px;
min-height: 20px;
margin-bottom: 0;
align-self: center;
flex: 1;
`;
const ChecklistControls = styled.div`
display: inline-flex;
flex-direction: row;
float: right;
`;
const ControlButton = styled.div`
opacity: 0;
margin-left: 4px;
padding: 4px 6px;
border-radius: 6px;
background-color: rgba(${props => props.theme.colors.bg.primary}, 0.8);
&:hover {
background-color: rgba(${props => props.theme.colors.primary}, 1);
}
`;
const ChecklistNameEditorWrapper = styled.div`
display: block;
float: left;
padding-top: 6px;
padding-bottom: 8px;
z-index: 50;
width: 100%;
`;
export const ChecklistNameEditor = styled(TextareaAutosize)`
overflow: hidden;
overflow-wrap: break-word;
resize: none;
height: 54px;
width: 100%;
background: none;
border: none;
box-shadow: none;
max-height: 162px;
min-height: 54px;
padding: 8px 12px;
font-size: 16px;
line-height: 20px;
border: 1px solid rgba(${props => props.theme.colors.primary});
border-radius: 3px;
color: rgba(${props => props.theme.colors.text.primary});
border-color: rgba(${props => props.theme.colors.border});
background-color: rgba(${props => props.theme.colors.bg.primary}, 0.4);
&:focus {
border-color: rgba(${props => props.theme.colors.primary});
}
`;
const AssignUserButton = styled(AccountPlus)`
fill: rgba(${props => props.theme.colors.text.primary});
`;
const ClockButton = styled(Clock)`
fill: rgba(${props => props.theme.colors.text.primary});
`;
const TrashButton = styled(Trash)`
fill: rgba(${props => props.theme.colors.text.primary});
`;
const ChecklistItemWrapper = styled.div`
user-select: none;
clear: both;
padding-left: 40px;
position: relative;
border-radius: 6px;
transform-origin: left bottom;
transition-property: transform, opacity, height, padding, margin;
transition-duration: 0.14s;
transition-timing-function: ease-in;
&:hover {
background-color: rgba(${props => props.theme.colors.bg.primary}, 0.4);
}
&:hover ${ControlButton} {
opacity: 1;
}
`;
const EditControls = styled.div`
clear: both;
display: flex;
padding-bottom: 9px;
flex-direction: row;
`;
const SaveButton = styled(Button)`
margin-right: 4px;
padding: 6px 12px;
`;
const CancelButton = styled.div`
cursor: pointer;
margin: 5px;
& svg {
fill: rgba(${props => props.theme.colors.text.primary});
}
&:hover svg {
fill: rgba(${props => props.theme.colors.text.secondary});
}
`;
const Spacer = styled.div`
flex: 1;
`;
const EditableDeleteButton = styled.button`
cursor: pointer;
display: flex;
margin: 0 2px;
padding: 6px 8px;
border-radius: 3px;
&:hover {
background: rgba(${props => props.theme.colors.primary}, 0.8);
}
`;
const NewItemButton = styled(Button)`
padding: 6px 8px;
`;
const ChecklistNewItem = styled.div`
margin: 8px 0;
margin-left: 40px;
`;
type ChecklistItemProps = {
itemID: string;
complete: boolean;
name: string;
onChangeName: (itemID: string, currentName: string) => void;
onToggleItem: (itemID: string, complete: boolean) => void;
onDeleteItem: (itemID: string) => void;
};
const ChecklistItem: React.FC<ChecklistItemProps> = ({
itemID,
complete,
name,
onChangeName,
onToggleItem,
onDeleteItem,
}) => {
const $item = useRef<HTMLDivElement>(null);
const $editor = useRef<HTMLTextAreaElement>(null);
const [editting, setEditting] = useState(false);
const [currentName, setCurrentName] = useState(name);
useEffect(() => {
if (editting && $editor && $editor.current) {
$editor.current.focus();
$editor.current.select();
}
}, [editting]);
useOnOutsideClick($item, true, () => setEditting(false), null);
return (
<ChecklistItemWrapper ref={$item}>
<ChecklistIcon
onClick={e => {
e.stopPropagation();
onToggleItem(itemID, !complete);
}}
>
{complete ? (
<ChecklistItemCheckedIcon width={20} height={20} />
) : (
<ChecklistItemUncheckedIcon width={20} height={20} />
)}
</ChecklistIcon>
{editting ? (
<>
<ChecklistNameEditorWrapper>
<ChecklistNameEditor
ref={$editor}
onKeyDown={e => {
if (e.key === 'Enter') {
onChangeName(itemID, currentName);
setEditting(false);
}
}}
onChange={e => {
setCurrentName(e.currentTarget.value);
}}
value={currentName}
/>
</ChecklistNameEditorWrapper>
<EditControls>
<SaveButton
onClick={() => {
onChangeName(itemID, currentName);
setEditting(false);
}}
variant="relief"
>
Save
</SaveButton>
<CancelButton
onClick={e => {
e.stopPropagation();
setEditting(false);
}}
>
<Cross width={20} height={20} />
</CancelButton>
<Spacer />
<EditableDeleteButton
onClick={e => {
e.stopPropagation();
setEditting(false);
onDeleteItem(itemID);
}}
>
<Trash width={16} height={16} />
</EditableDeleteButton>
</EditControls>
</>
) : (
<ChecklistItemDetails
onClick={() => {
setEditting(true);
}}
>
<ChecklistItemRow>
<ChecklistItemTextControls>
<ChecklistItemText complete={complete}>{name}</ChecklistItemText>
<ChecklistControls>
<ControlButton>
<AssignUserButton width={14} height={14} />
</ControlButton>
<ControlButton>
<ClockButton width={14} height={14} />
</ControlButton>
<ControlButton
onClick={e => {
e.stopPropagation();
onDeleteItem(itemID);
}}
>
<TrashButton width={14} height={14} />
</ControlButton>
</ChecklistControls>
</ChecklistItemTextControls>
</ChecklistItemRow>
</ChecklistItemDetails>
)}
</ChecklistItemWrapper>
);
};
type AddNewItemProps = {
onAddItem: (name: string) => void;
};
const AddNewItem: React.FC<AddNewItemProps> = ({ onAddItem }) => {
const $editor = useRef<HTMLTextAreaElement>(null);
const $wrapper = useRef<HTMLDivElement>(null);
const [currentName, setCurrentName] = useState('');
const [editting, setEditting] = useState(false);
useEffect(() => {
if (editting && $editor && $editor.current) {
$editor.current.focus();
$editor.current.select();
}
}, [editting]);
useOnOutsideClick($wrapper, true, () => setEditting(false), null);
return (
<ChecklistNewItem ref={$wrapper}>
{editting ? (
<>
<ChecklistNameEditorWrapper>
<ChecklistNameEditor
ref={$editor}
onKeyDown={e => {
if (e.key === 'Enter') {
e.preventDefault();
onAddItem(currentName);
setCurrentName('');
}
}}
onChange={e => {
setCurrentName(e.currentTarget.value);
}}
value={currentName}
/>
</ChecklistNameEditorWrapper>
<EditControls>
<SaveButton
onClick={() => {
onAddItem(currentName);
setCurrentName('');
if (editting && $editor && $editor.current) {
$editor.current.focus();
$editor.current.select();
}
}}
variant="relief"
>
Save
</SaveButton>
<CancelButton
onClick={e => {
e.stopPropagation();
setEditting(false);
}}
>
<Cross width={20} height={20} />
</CancelButton>
</EditControls>
</>
) : (
<NewItemButton onClick={() => setEditting(true)}>Add an item</NewItemButton>
)}
</ChecklistNewItem>
);
};
type ChecklistTitleEditorProps = {
name: string;
onChangeName: (item: string) => void;
onCancel: () => void;
};
const ChecklistTitleEditor = React.forwardRef(
({ name, onChangeName, onCancel }: ChecklistTitleEditorProps, $name: any) => {
const [currentName, setCurrentName] = useState(name);
return (
<>
<ChecklistNameEditor
ref={$name}
value={currentName}
onChange={e => {
setCurrentName(e.currentTarget.value);
}}
onKeyDown={e => {
if (e.key === 'Enter') {
onChangeName(currentName);
}
}}
/>
<EditControls>
<SaveButton
onClick={() => {
onChangeName(currentName);
}}
variant="relief"
>
Save
</SaveButton>
<CancelButton
onClick={e => {
e.stopPropagation();
onCancel();
}}
>
<Cross width={20} height={20} />
</CancelButton>
</EditControls>
</>
);
},
);
type ChecklistProps = {
checklistID: string;
onDeleteChecklist: (checklistID: string) => void;
name: string;
onChangeName: (item: string) => void;
onToggleItem: (taskID: string, complete: boolean) => void;
onChangeItemName: (itemID: string, currentName: string) => void;
onDeleteItem: (itemID: string) => void;
onAddItem: (itemName: string) => void;
items: Array<TaskChecklistItem>;
};
const Checklist: React.FC<ChecklistProps> = ({
checklistID,
onDeleteChecklist,
name,
items,
onToggleItem,
onAddItem,
onChangeItemName,
onChangeName,
onDeleteItem,
}) => {
const $name = useRef<HTMLTextAreaElement>(null);
const complete = items.reduce((prev, item) => prev + (item.complete ? 1 : 0), 0);
const percent = items.length === 0 ? 0 : Math.floor((complete / items.length) * 100);
const [editting, setEditting] = useState(false);
// useOnOutsideClick($name, true, () => setEditting(false), null);
useEffect(() => {
if (editting && $name && $name.current) {
$name.current.focus();
$name.current.select();
}
}, [editting]);
return (
<Wrapper>
<WindowTitle>
<WindowTitleIcon width={24} height={24} />
{editting ? (
<ChecklistTitleEditor
ref={$name}
name={name}
onChangeName={currentName => {
onChangeName(currentName);
setEditting(false);
}}
onCancel={() => {
setEditting(false);
}}
/>
) : (
<WindowChecklistTitle>
<WindowTitleText onClick={() => setEditting(true)}>{name}</WindowTitleText>
<WindowOptions>
<DeleteButton
onClick={() => {
onDeleteChecklist(checklistID);
}}
color="danger"
variant="outline"
>
Delete
</DeleteButton>
</WindowOptions>
</WindowChecklistTitle>
)}
</WindowTitle>
<ChecklistProgress>
<ChecklistProgressPercent>{`${percent}%`}</ChecklistProgressPercent>
<ChecklistProgressBar>
<ChecklistProgressBarCurrent width={percent} />
</ChecklistProgressBar>
</ChecklistProgress>
<ChecklistItems>
{items
.slice()
.sort((a, b) => a.position - b.position)
.map(item => (
<ChecklistItem
key={item.id}
itemID={item.id}
name={item.name}
complete={item.complete}
onDeleteItem={onDeleteItem}
onChangeName={onChangeItemName}
onToggleItem={onToggleItem}
/>
))}
</ChecklistItems>
<AddNewItem onAddItem={onAddItem} />
</Wrapper>
);
};
export default Checklist;

View File

@ -64,6 +64,7 @@ export const Default = () => {
}}
onCancel={action('cancel')}
onDueDateChange={action('due date change')}
onRemoveDueDate={action('remove due date')}
/>
</Popup>
</PopupWrapper>

View File

@ -102,11 +102,15 @@ export const DueDatePickerWrapper = styled.div`
`;
export const ConfirmAddDueDate = styled(Button)`
float: left;
margin: 0 4px 0 0;
padding: 6px 12px;
`;
export const RemoveDueDate = styled(Button)`
padding: 6px 12px;
margin: 0 0 0 4px;
`;
export const CancelDueDate = styled.div`
display: flex;
align-items: center;
@ -126,4 +130,5 @@ export const ActionWrapper = styled.div`
padding-top: 8px;
width: 100%;
display: flex;
justify-content: space-between;
`;

View File

@ -5,7 +5,15 @@ import DatePicker from 'react-datepicker';
import { Cross } from 'shared/icons';
import _ from 'lodash';
import { Wrapper, ActionWrapper, DueDateInput, DueDatePickerWrapper, ConfirmAddDueDate, CancelDueDate } from './Styles';
import {
Wrapper,
ActionWrapper,
RemoveDueDate,
DueDateInput,
DueDatePickerWrapper,
ConfirmAddDueDate,
CancelDueDate,
} from './Styles';
import 'react-datepicker/dist/react-datepicker.css';
import { getYear, getMonth } from 'date-fns';
@ -14,6 +22,7 @@ import { useForm } from 'react-hook-form';
type DueDateManagerProps = {
task: Task;
onDueDateChange: (task: Task, newDueDate: Date) => void;
onRemoveDueDate: (task: Task) => void;
onCancel: () => void;
};
@ -109,7 +118,7 @@ const HeaderActions = styled.div`
}
`;
const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange, onCancel }) => {
const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange, onRemoveDueDate, onCancel }) => {
const now = moment();
const [textStartDate, setTextStartDate] = useState(now.format('YYYY-MM-DD'));
const [startDate, setStartDate] = useState(new Date());
@ -260,19 +269,18 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
/>
</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());
}}
>
<ConfirmAddDueDate type="submit" onClick={() => {}}>
Save
</ConfirmAddDueDate>
<CancelDueDate onClick={onCancel}>
<Cross size={16} color="#c2c6dc" />
</CancelDueDate>
<RemoveDueDate
variant="outline"
color="danger"
onClick={() => {
onRemoveDueDate(task);
}}
>
Remove
</RemoveDueDate>
</ActionWrapper>
</Form>
</Wrapper>

View File

@ -7,11 +7,41 @@ export const Container = styled.div`
overflow-x: auto;
overflow-y: hidden;
padding-bottom: 8px;
::-webkit-scrollbar {
height: 10px;
}
::-webkit-scrollbar-thumb {
background: #7367f0;
border-radius: 6px;
}
::-webkit-scrollbar-track {
background: #10163a;
border-radius: 6px;
}
`;
export const BoardContainer = styled.div`
position: relative;
overflow-y: auto;
outline: none;
flex-grow: 1;
`;
export const BoardWrapper = styled.div`
display: flex;
margin-top: 12px;
margin-left: 8px;
user-select: none;
white-space: nowrap;
margin-bottom: 8px;
overflow-x: auto;
overflow-y: hidden;
padding-bottom: 8px;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
`;
export default Container;

View File

@ -10,10 +10,10 @@ import {
getNewDraggablePosition,
getAfterDropDraggableList,
} from 'shared/utils/draggables';
import { Container, BoardWrapper } from './Styles';
import moment from 'moment';
import { Container, BoardContainer, BoardWrapper } from './Styles';
interface SimpleProps {
taskGroups: Array<TaskGroup>;
onTaskDrop: (task: Task, previousTaskGroupID: string) => void;
@ -120,104 +120,107 @@ const SimpleLists: React.FC<SimpleProps> = ({
const [currentComposer, setCurrentComposer] = useState('');
return (
<BoardWrapper>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable direction="horizontal" type="column" droppableId="root">
{provided => (
<Container {...provided.droppableProps} ref={provided.innerRef}>
{taskGroups
.slice()
.sort((a: any, b: any) => a.position - b.position)
.map((taskGroup: TaskGroup, index: number) => {
return (
<Draggable draggableId={taskGroup.id} key={taskGroup.id} index={index}>
{columnDragProvided => (
<Droppable type="tasks" droppableId={taskGroup.id}>
{(columnDropProvided, snapshot) => (
<List
name={taskGroup.name}
onOpenComposer={id => setCurrentComposer(id)}
isComposerOpen={currentComposer === taskGroup.id}
onSaveName={name => onChangeTaskGroupName(taskGroup.id, name)}
ref={columnDragProvided.innerRef}
wrapperProps={columnDragProvided.draggableProps}
headerProps={columnDragProvided.dragHandleProps}
onExtraMenuOpen={onExtraMenuOpen}
id={taskGroup.id}
key={taskGroup.id}
index={index}
>
<ListCards ref={columnDropProvided.innerRef} {...columnDropProvided.droppableProps}>
{taskGroup.tasks
.slice()
.sort((a: any, b: any) => a.position - b.position)
.map((task: Task, taskIndex: any) => {
return (
<Draggable key={task.id} draggableId={task.id} index={taskIndex}>
{taskProvided => {
return (
<Card
wrapperProps={{
...taskProvided.draggableProps,
...taskProvided.dragHandleProps,
}}
ref={taskProvided.innerRef}
taskID={task.id}
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={() => {
onTaskClick(task);
}}
onCardMemberClick={onCardMemberClick}
onContextMenu={onQuickEditorOpen}
/>
);
}}
</Draggable>
);
})}
{columnDropProvided.placeholder}
{currentComposer === taskGroup.id && (
<CardComposer
onClose={() => {
setCurrentComposer('');
}}
onCreateCard={name => {
onCreateTask(taskGroup.id, name);
}}
isOpen
/>
)}
</ListCards>
</List>
)}
</Droppable>
)}
</Draggable>
);
})}
{provided.placeholder}
</Container>
)}
</Droppable>
</DragDropContext>
<AddList
onSave={listName => {
onCreateTaskGroup(listName);
}}
/>
</BoardWrapper>
<BoardContainer>
<BoardWrapper>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable direction="horizontal" type="column" droppableId="root">
{provided => (
<Container {...provided.droppableProps} ref={provided.innerRef}>
{taskGroups
.slice()
.sort((a: any, b: any) => a.position - b.position)
.map((taskGroup: TaskGroup, index: number) => {
return (
<Draggable draggableId={taskGroup.id} key={taskGroup.id} index={index}>
{columnDragProvided => (
<Droppable type="tasks" droppableId={taskGroup.id}>
{(columnDropProvided, snapshot) => (
<List
name={taskGroup.name}
onOpenComposer={id => setCurrentComposer(id)}
isComposerOpen={currentComposer === taskGroup.id}
onSaveName={name => onChangeTaskGroupName(taskGroup.id, name)}
ref={columnDragProvided.innerRef}
wrapperProps={columnDragProvided.draggableProps}
headerProps={columnDragProvided.dragHandleProps}
onExtraMenuOpen={onExtraMenuOpen}
id={taskGroup.id}
key={taskGroup.id}
index={index}
>
<ListCards ref={columnDropProvided.innerRef} {...columnDropProvided.droppableProps}>
{taskGroup.tasks
.slice()
.sort((a: any, b: any) => a.position - b.position)
.map((task: Task, taskIndex: any) => {
return (
<Draggable key={task.id} draggableId={task.id} index={taskIndex}>
{taskProvided => {
return (
<Card
wrapperProps={{
...taskProvided.draggableProps,
...taskProvided.dragHandleProps,
}}
ref={taskProvided.innerRef}
taskID={task.id}
complete={task.complete ?? false}
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={() => {
onTaskClick(task);
}}
onCardMemberClick={onCardMemberClick}
onContextMenu={onQuickEditorOpen}
/>
);
}}
</Draggable>
);
})}
{columnDropProvided.placeholder}
{currentComposer === taskGroup.id && (
<CardComposer
onClose={() => {
setCurrentComposer('');
}}
onCreateCard={name => {
onCreateTask(taskGroup.id, name);
}}
isOpen
/>
)}
</ListCards>
</List>
)}
</Droppable>
)}
</Draggable>
);
})}
<AddList
onSave={listName => {
onCreateTaskGroup(listName);
}}
/>
{provided.placeholder}
</Container>
)}
</Droppable>
</DragDropContext>
</BoardWrapper>
</BoardContainer>
);
};

View File

@ -59,7 +59,7 @@ const MemberManager: React.FC<MemberManagerProps> = ({
<MemberName>{member.fullName}</MemberName>
{activeMembers.findIndex(m => m.id === member.id) !== -1 && (
<ActiveIconWrapper>
<Checkmark size={16} color="#42526e" />
<Checkmark width={16} height={16} />
</ActiveIconWrapper>
)}
</BoardMemberListItemContent>

View File

@ -232,7 +232,7 @@ const NewProject: React.FC<NewProjectProps> = ({ teams, onClose, onCreateProject
onClose();
}}
>
<Cross color="#c2c6dc" />
<Cross width={16} height={16} />
</HeaderRight>
</Header>
<Container>

View File

@ -44,7 +44,7 @@ const LabelManager = ({ labelColors, label, onLabelEdit, onLabelDelete }: Props)
setCurrentColor(labelColor);
}}
>
{currentColor && labelColor.id === currentColor.id && <Checkmark color="#fff" size={12} />}
{currentColor && labelColor.id === currentColor.id && <Checkmark width={12} height={12} />}
</LabelBox>
))}
</div>

View File

@ -72,7 +72,7 @@ const LabelManager: React.FC<Props> = ({ labels, taskLabels, onLabelToggle, onLa
{label.name}
{taskLabels && taskLabels.find(t => t.projectLabel.id === label.id) && (
<ActiveIcon>
<Checkmark color="#fff" />
<Checkmark width={16} height={16} />
</ActiveIcon>
)}
</CardLabel>

View File

@ -265,6 +265,7 @@ export const DueDateManagerPopup = () => {
{popupData.isOpen && (
<PopupMenu title="Due Date" top={popupData.top} onClose={() => setPopupData(initalState)} left={popupData.left}>
<DueDateManager
onRemoveDueDate={action('remove due date')}
task={{
id: '1',
taskGroup: { name: 'General', id: '1', position: 1 },

View File

@ -1,4 +1,4 @@
import React, { useRef, createContext, RefObject, useState, useContext } from 'react';
import React, { useRef, createContext, RefObject, useState, useContext, useEffect } from 'react';
import { Cross, AngleLeft } from 'shared/icons';
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import { createPortal } from 'react-dom';
@ -36,10 +36,19 @@ type PopupContainerProps = {
};
const PopupContainer: React.FC<PopupContainerProps> = ({ width, top, left, onClose, children, invert }) => {
const $containerRef = useRef();
const $containerRef = useRef<HTMLDivElement>(null);
const [currentTop, setCurrentTop] = useState(top);
useOnOutsideClick($containerRef, true, onClose, null);
useEffect(() => {
if ($containerRef && $containerRef.current) {
const bounding = $containerRef.current.getBoundingClientRect();
if (bounding.bottom > (window.innerHeight || document.documentElement.clientHeight)) {
setCurrentTop(44);
}
}
}, []);
return (
<Container width={width ?? 316} left={left} top={top} ref={$containerRef} invert={invert}>
<Container width={width ?? 316} left={left} top={currentTop} ref={$containerRef} invert={invert}>
{children}
</Container>
);
@ -91,11 +100,12 @@ export const PopupProvider: React.FC = ({ children }) => {
const show = (target: RefObject<HTMLElement>, content: JSX.Element, width?: number | string) => {
if (target && target.current) {
const bounds = target.current.getBoundingClientRect();
const top = bounds.top + bounds.height;
if (bounds.left + 304 + 30 > window.innerWidth) {
setState({
isOpen: true,
left: bounds.left + bounds.width,
top: bounds.top + bounds.height,
top,
invert: true,
currentTab: 0,
previousTab: 0,
@ -106,7 +116,7 @@ export const PopupProvider: React.FC = ({ children }) => {
setState({
isOpen: true,
left: bounds.left,
top: bounds.top + bounds.height,
top,
invert: false,
currentTab: 0,
previousTab: 0,
@ -176,7 +186,7 @@ type Props = {
};
const PopupMenu: React.FC<Props> = ({ width, title, top, left, onClose, noHeader, children, onPrevious }) => {
const $containerRef = useRef();
const $containerRef = useRef<HTMLDivElement>(null);
useOnOutsideClick($containerRef, true, onClose, null);
return (
@ -189,13 +199,13 @@ const PopupMenu: React.FC<Props> = ({ width, title, top, left, onClose, noHeader
)}
{noHeader ? (
<CloseButton onClick={() => onClose()}>
<Cross color="#c2c6dc" />
<Cross width={16} height={16} />
</CloseButton>
) : (
<Header>
<HeaderTitle>{title}</HeaderTitle>
<CloseButton onClick={() => onClose()}>
<Cross color="#c2c6dc" />
<Cross width={16} height={16} />
</CloseButton>
</Header>
)}
@ -230,7 +240,7 @@ export const Popup: React.FC<PopupProps> = ({ title, onClose, tab, children }) =
)}
{onClose && (
<CloseButton onClick={() => onClose()}>
<Cross color="#c2c6dc" />
<Cross width={16} height={16} />
</CloseButton>
)}
<Content>{children}</Content>

View File

@ -57,7 +57,9 @@ export const Default = () => {
onCloseEditor={() => setEditorOpen(false)}
onEditCard={action('edit card')}
onOpenLabelsPopup={action('open popup')}
onOpenDueDatePopup={action('open popup')}
onOpenMembersPopup={action('open popup')}
onToggleComplete={action('complete')}
onArchiveCard={action('archive card')}
top={top}
left={left}

View File

@ -14,9 +14,9 @@ export const Wrapper = styled.div<{ open: boolean }>`
visibility: ${props => (props.open ? 'show' : 'hidden')};
`;
export const Container = styled.div<{ top: number; left: number }>`
export const Container = styled.div<{ width: number; top: number; left: number }>`
position: absolute;
width: 256px;
width: ${props => props.width}px;
top: ${props => props.top}px;
left: ${props => props.left}px;
`;

View File

@ -14,12 +14,15 @@ type Props = {
task: Task;
onCloseEditor: () => void;
onEditCard: (taskGroupID: string, taskID: string, cardName: string) => void;
onToggleComplete: (task: Task) => void;
onOpenLabelsPopup: ($targetRef: React.RefObject<HTMLElement>, task: Task) => void;
onOpenMembersPopup: ($targetRef: React.RefObject<HTMLElement>, task: Task) => void;
onOpenDueDatePopup: ($targetRef: React.RefObject<HTMLElement>, task: Task) => void;
onArchiveCard: (taskGroupID: string, taskID: string) => void;
onCardMemberClick?: OnCardMemberClick;
top: number;
left: number;
width?: number;
};
const QuickCardEditor = ({
@ -27,14 +30,18 @@ const QuickCardEditor = ({
onCloseEditor,
onOpenLabelsPopup,
onOpenMembersPopup,
onOpenDueDatePopup,
onToggleComplete,
onCardMemberClick,
onArchiveCard,
onEditCard,
width = 272,
top,
left,
}: Props) => {
const [currentCardTitle, setCardTitle] = useState(task.name);
const $labelsRef: any = useRef();
const $dueDate: any = useRef();
const $membersRef: any = useRef();
const handleCloseEditor = (e: any) => {
@ -45,9 +52,9 @@ const QuickCardEditor = ({
return (
<Wrapper onClick={handleCloseEditor} open>
<CloseButton onClick={handleCloseEditor}>
<Cross size={16} color="#000" />
<Cross width={16} height={16} />
</CloseButton>
<Container left={left} top={top}>
<Container width={width} left={left} top={top}>
<Card
editable
onCardMemberClick={onCardMemberClick}
@ -56,6 +63,7 @@ const QuickCardEditor = ({
onEditCard(taskGroupID, taskID, name);
onCloseEditor();
}}
complete={task.complete ?? false}
members={task.assigned}
taskID={task.id}
taskGroupID={task.taskGroup.id}
@ -63,6 +71,14 @@ const QuickCardEditor = ({
/>
<SaveButton onClick={() => onEditCard(task.taskGroup.id, task.id, currentCardTitle)}>Save</SaveButton>
<EditorButtons>
<EditorButton
onClick={e => {
e.stopPropagation();
onToggleComplete(task);
}}
>
{task.complete ? 'Mark Incomplete' : 'Mark Complete'}
</EditorButton>
<EditorButton
ref={$membersRef}
onClick={e => {
@ -72,6 +88,15 @@ const QuickCardEditor = ({
>
Edit Assigned
</EditorButton>
<EditorButton
ref={$dueDate}
onClick={e => {
e.stopPropagation();
onOpenDueDatePopup($labelsRef, task);
}}
>
Edit Due Date
</EditorButton>
<EditorButton
ref={$labelsRef}
onClick={e => {

View File

@ -148,6 +148,20 @@ export const TaskDetailsMarkdown = styled.div`
cursor: pointer;
color: #c2c6dc;
h1 {
font-size: 24px;
font-weight: 600;
line-height: 28px;
margin: 0 0 12px;
}
h2 {
font-weight: 600;
font-size: 20px;
line-height: 24px;
margin: 16px 0 8px;
}
p {
margin: 0 0 8px;
}
@ -155,6 +169,15 @@ export const TaskDetailsMarkdown = styled.div`
strong {
font-weight: 700;
}
ul {
margin: 8px 0;
}
ul > li {
margin: 8px 8px 8px 24px;
list-style: disc;
}
`;
export const TaskDetailsControls = styled.div`

View File

@ -28,6 +28,8 @@ export const Default = () => {
renderContent={() => {
return (
<TaskDetails
onDeleteItem={action('delete item')}
onChangeItemName={action('change item name')}
task={{
id: '1',
taskGroup: { name: 'General', id: '1' },
@ -65,6 +67,8 @@ export const Default = () => {
onCloseModal={action('close modal')}
onMemberProfile={action('profile')}
onOpenAddMemberPopup={action('open add member popup')}
onAddItem={action('add item')}
onToggleChecklistItem={action('toggle checklist item')}
onOpenAddLabelPopup={action('open add label popup')}
onOpenDueDatePopop={action('open due date popup')}
/>

View File

@ -3,6 +3,7 @@ import { Bin, Cross, Plus } from 'shared/icons';
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import ReactMarkdown from 'react-markdown';
import TaskAssignee from 'shared/components/TaskAssignee';
import moment from 'moment';
import {
NoDueDateLabel,
@ -37,15 +38,33 @@ import {
TaskDetailAssignees,
TaskDetailsAddMemberIcon,
} from './Styles';
import convertDivElementRefToBounds from 'shared/utils/boundingRect';
import moment from 'moment';
import Checklist from '../Checklist';
type TaskContentProps = {
onEditContent: () => void;
description: string;
};
type TaskLabelProps = {
label: TaskLabel;
onClick: ($target: React.RefObject<HTMLElement>) => void;
};
const TaskLabelItem: React.FC<TaskLabelProps> = ({ label, onClick }) => {
const $label = useRef<HTMLDivElement>(null);
return (
<TaskDetailLabel
onClick={() => {
onClick($label);
}}
ref={$label}
color={label.projectLabel.labelColor.colorHex}
>
{label.projectLabel.name}
</TaskDetailLabel>
);
};
const TaskContent: React.FC<TaskContentProps> = ({ description, onEditContent }) => {
return description === '' ? (
<TaskDetailsAddDetailsButton onClick={onEditContent}>Add a more detailed description</TaskDetailsAddDetailsButton>
@ -105,6 +124,10 @@ type TaskDetailsProps = {
onTaskNameChange: (task: Task, newName: string) => void;
onTaskDescriptionChange: (task: Task, newDescription: string) => void;
onDeleteTask: (task: Task) => void;
onAddItem: (checklistID: string, name: string, position: number) => void;
onDeleteItem: (itemID: string) => void;
onChangeItemName: (itemID: string, itemName: string) => void;
onToggleChecklistItem: (itemID: string, complete: boolean) => 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;
@ -116,11 +139,15 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
task,
onTaskNameChange,
onTaskDescriptionChange,
onChangeItemName,
onDeleteItem,
onDeleteTask,
onCloseModal,
onOpenAddMemberPopup,
onOpenAddLabelPopup,
onOpenDueDatePopop,
onAddItem,
onToggleChecklistItem,
onMemberProfile,
}) => {
const [editorOpen, setEditorOpen] = useState(false);
@ -158,7 +185,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
<Bin size={20} color="#c2c6dc" />
</TaskAction>
<TaskAction onClick={onCloseModal}>
<Cross size={20} color="#c2c6dc" />
<Cross width={16} height={16} />
</TaskAction>
</TaskActions>
<TaskHeader>
@ -195,6 +222,33 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
) : (
<TaskContent description={description} onEditContent={handleClick} />
)}
{task.checklists &&
task.checklists
.slice()
.sort((a, b) => a.position - b.position)
.map(checklist => (
<Checklist
key={checklist.id}
name={checklist.name}
checklistID={checklist.id}
items={checklist.items}
onDeleteChecklist={() => {}}
onChangeName={() => {}}
onToggleItem={onToggleChecklistItem}
onDeleteItem={onDeleteItem}
onAddItem={n => {
if (task.checklists) {
let position = 1;
const lastChecklist = task.checklists.sort((a, b) => a.position - b.position)[-1];
if (lastChecklist) {
position = lastChecklist.position * 2 + 1;
}
onAddItem(checklist.id, n, position);
}
}}
onChangeItemName={onChangeItemName}
/>
))}
</TaskDetailsContent>
<TaskDetailsSidebar>
<TaskDetailSectionTitle>Assignees</TaskDetailSectionTitle>
@ -221,9 +275,13 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
<TaskDetailLabels>
{task.labels.map(label => {
return (
<TaskDetailLabel key={label.projectLabel.id} color={label.projectLabel.labelColor.colorHex}>
{label.projectLabel.name}
</TaskDetailLabel>
<TaskLabelItem
key={label.projectLabel.id}
label={label}
onClick={$target => {
onOpenAddLabelPopup(task, $target);
}}
/>
);
})}
<TaskDetailsAddLabel ref={$addLabelRef} onClick={onAddLabel}>

View File

@ -111,8 +111,10 @@ export type Task = {
position: Scalars['Float'];
description?: Maybe<Scalars['String']>;
dueDate?: Maybe<Scalars['Time']>;
complete: Scalars['Boolean'];
assigned: Array<ProjectMember>;
labels: Array<TaskLabel>;
checklists: Array<TaskChecklist>;
};
export type ProjectsFilter = {
@ -239,6 +241,30 @@ export type DeleteTaskGroupPayload = {
taskGroup: TaskGroup;
};
export type DeleteTaskChecklistItemPayload = {
__typename?: 'DeleteTaskChecklistItemPayload';
ok: Scalars['Boolean'];
taskChecklistItem: TaskChecklistItem;
};
export type TaskChecklistItem = {
__typename?: 'TaskChecklistItem';
id: Scalars['ID'];
name: Scalars['String'];
taskChecklistID: Scalars['UUID'];
complete: Scalars['Boolean'];
position: Scalars['Float'];
dueDate: Scalars['Time'];
};
export type TaskChecklist = {
__typename?: 'TaskChecklist';
id: Scalars['ID'];
name: Scalars['String'];
position: Scalars['Float'];
items: Array<TaskChecklistItem>;
};
export type AssignTaskInput = {
taskID: Scalars['UUID'];
userID: Scalars['UUID'];
@ -321,6 +347,37 @@ export type UpdateTaskDueDate = {
dueDate?: Maybe<Scalars['Time']>;
};
export type SetTaskComplete = {
taskID: Scalars['UUID'];
complete: Scalars['Boolean'];
};
export type CreateTaskChecklist = {
taskID: Scalars['UUID'];
name: Scalars['String'];
position: Scalars['Float'];
};
export type CreateTaskChecklistItem = {
taskChecklistID: Scalars['UUID'];
name: Scalars['String'];
position: Scalars['Float'];
};
export type SetTaskChecklistItemComplete = {
taskChecklistItemID: Scalars['UUID'];
complete: Scalars['Boolean'];
};
export type DeleteTaskChecklistItem = {
taskChecklistItemID: Scalars['UUID'];
};
export type UpdateTaskChecklistItemName = {
taskChecklistItemID: Scalars['UUID'];
name: Scalars['String'];
};
export type Mutation = {
__typename?: 'Mutation';
createRefreshToken: RefreshToken;
@ -341,10 +398,16 @@ export type Mutation = {
addTaskLabel: Task;
removeTaskLabel: Task;
toggleTaskLabel: ToggleTaskLabelPayload;
createTaskChecklist: TaskChecklist;
createTaskChecklistItem: TaskChecklistItem;
updateTaskChecklistItemName: TaskChecklistItem;
setTaskChecklistItemComplete: TaskChecklistItem;
deleteTaskChecklistItem: DeleteTaskChecklistItemPayload;
createTask: Task;
updateTaskDescription: Task;
updateTaskLocation: UpdateTaskLocationPayload;
updateTaskName: Task;
setTaskComplete: Task;
updateTaskDueDate: Task;
deleteTask: DeleteTaskPayload;
assignTask: Task;
@ -438,6 +501,31 @@ export type MutationToggleTaskLabelArgs = {
};
export type MutationCreateTaskChecklistArgs = {
input: CreateTaskChecklist;
};
export type MutationCreateTaskChecklistItemArgs = {
input: CreateTaskChecklistItem;
};
export type MutationUpdateTaskChecklistItemNameArgs = {
input: UpdateTaskChecklistItemName;
};
export type MutationSetTaskChecklistItemCompleteArgs = {
input: SetTaskChecklistItemComplete;
};
export type MutationDeleteTaskChecklistItemArgs = {
input: DeleteTaskChecklistItem;
};
export type MutationCreateTaskArgs = {
input: NewTask;
};
@ -458,6 +546,11 @@ export type MutationUpdateTaskNameArgs = {
};
export type MutationSetTaskCompleteArgs = {
input: SetTaskComplete;
};
export type MutationUpdateTaskDueDateArgs = {
input: UpdateTaskDueDate;
};
@ -564,7 +657,7 @@ export type CreateTaskMutation = (
{ __typename?: 'Mutation' }
& { createTask: (
{ __typename?: 'Task' }
& Pick<Task, 'id' | 'name' | 'position' | 'description'>
& Pick<Task, 'id' | 'name' | 'position' | 'description' | 'dueDate'>
& { taskGroup: (
{ __typename?: 'TaskGroup' }
& Pick<TaskGroup, 'id' | 'name' | 'position'>
@ -681,29 +774,7 @@ export type FindProjectQuery = (
& Pick<TaskGroup, 'id' | 'name' | 'position'>
& { tasks: Array<(
{ __typename?: 'Task' }
& Pick<Task, 'id' | 'name' | 'position' | 'description' | 'dueDate'>
& { taskGroup: (
{ __typename?: 'TaskGroup' }
& Pick<TaskGroup, 'id' | 'name' | 'position'>
), labels: Array<(
{ __typename?: 'TaskLabel' }
& Pick<TaskLabel, 'id' | 'assignedDate'>
& { projectLabel: (
{ __typename?: 'ProjectLabel' }
& Pick<ProjectLabel, 'id' | 'name' | 'createdDate'>
& { labelColor: (
{ __typename?: 'LabelColor' }
& Pick<LabelColor, 'id' | 'colorHex' | 'position' | 'name'>
) }
) }
)>, assigned: Array<(
{ __typename?: 'ProjectMember' }
& Pick<ProjectMember, 'id' | 'fullName'>
& { profileIcon: (
{ __typename?: 'ProfileIcon' }
& Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'>
) }
)> }
& TaskFieldsFragment
)> }
)> }
), labelColors: Array<(
@ -725,7 +796,14 @@ export type FindTaskQuery = (
& { taskGroup: (
{ __typename?: 'TaskGroup' }
& Pick<TaskGroup, 'id'>
), labels: Array<(
), checklists: Array<(
{ __typename?: 'TaskChecklist' }
& Pick<TaskChecklist, 'id' | 'name' | 'position'>
& { items: Array<(
{ __typename?: 'TaskChecklistItem' }
& Pick<TaskChecklistItem, 'id' | 'name' | 'taskChecklistID' | 'complete' | 'position'>
)> }
)>, labels: Array<(
{ __typename?: 'TaskLabel' }
& Pick<TaskLabel, 'id' | 'assignedDate'>
& { projectLabel: (
@ -747,6 +825,33 @@ export type FindTaskQuery = (
) }
);
export type TaskFieldsFragment = (
{ __typename?: 'Task' }
& Pick<Task, 'id' | 'name' | 'description' | 'dueDate' | 'complete' | 'position'>
& { taskGroup: (
{ __typename?: 'TaskGroup' }
& Pick<TaskGroup, 'id'>
), labels: Array<(
{ __typename?: 'TaskLabel' }
& Pick<TaskLabel, 'id' | 'assignedDate'>
& { projectLabel: (
{ __typename?: 'ProjectLabel' }
& Pick<ProjectLabel, 'id' | 'name' | 'createdDate'>
& { labelColor: (
{ __typename?: 'LabelColor' }
& Pick<LabelColor, 'id' | 'colorHex' | 'position' | 'name'>
) }
) }
)>, assigned: Array<(
{ __typename?: 'ProjectMember' }
& Pick<ProjectMember, 'id' | 'fullName'>
& { profileIcon: (
{ __typename?: 'ProfileIcon' }
& Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'>
) }
)> }
);
export type GetProjectsQueryVariables = {};
@ -780,6 +885,94 @@ export type MeQuery = (
) }
);
export type CreateTaskChecklistItemMutationVariables = {
taskChecklistID: Scalars['UUID'];
name: Scalars['String'];
position: Scalars['Float'];
};
export type CreateTaskChecklistItemMutation = (
{ __typename?: 'Mutation' }
& { createTaskChecklistItem: (
{ __typename?: 'TaskChecklistItem' }
& Pick<TaskChecklistItem, 'id' | 'name' | 'taskChecklistID' | 'position' | 'complete'>
) }
);
export type DeleteTaskChecklistItemMutationVariables = {
taskChecklistItemID: Scalars['UUID'];
};
export type DeleteTaskChecklistItemMutation = (
{ __typename?: 'Mutation' }
& { deleteTaskChecklistItem: (
{ __typename?: 'DeleteTaskChecklistItemPayload' }
& Pick<DeleteTaskChecklistItemPayload, 'ok'>
& { taskChecklistItem: (
{ __typename?: 'TaskChecklistItem' }
& Pick<TaskChecklistItem, 'id' | 'taskChecklistID'>
) }
) }
);
export type SetTaskChecklistItemCompleteMutationVariables = {
taskChecklistItemID: Scalars['UUID'];
complete: Scalars['Boolean'];
};
export type SetTaskChecklistItemCompleteMutation = (
{ __typename?: 'Mutation' }
& { setTaskChecklistItemComplete: (
{ __typename?: 'TaskChecklistItem' }
& Pick<TaskChecklistItem, 'id' | 'name' | 'taskChecklistID' | 'complete' | 'position'>
) }
);
export type SetTaskCompleteMutationVariables = {
taskID: Scalars['UUID'];
complete: Scalars['Boolean'];
};
export type SetTaskCompleteMutation = (
{ __typename?: 'Mutation' }
& { setTaskComplete: (
{ __typename?: 'Task' }
& TaskFieldsFragment
) }
);
export type UpdateTaskChecklistItemNameMutationVariables = {
taskChecklistItemID: Scalars['UUID'];
name: Scalars['String'];
};
export type UpdateTaskChecklistItemNameMutation = (
{ __typename?: 'Mutation' }
& { updateTaskChecklistItemName: (
{ __typename?: 'TaskChecklistItem' }
& Pick<TaskChecklistItem, 'id' | 'name'>
) }
);
export type UpdateTaskGroupNameMutationVariables = {
taskGroupID: Scalars['UUID'];
name: Scalars['String'];
};
export type UpdateTaskGroupNameMutation = (
{ __typename?: 'Mutation' }
& { updateTaskGroupName: (
{ __typename?: 'TaskGroup' }
& Pick<TaskGroup, 'id' | 'name'>
) }
);
export type ToggleTaskLabelMutationVariables = {
taskID: Scalars['UUID'];
projectLabelID: Scalars['UUID'];
@ -940,7 +1133,43 @@ export type UpdateTaskNameMutation = (
) }
);
export const TaskFieldsFragmentDoc = gql`
fragment TaskFields on Task {
id
name
description
dueDate
complete
position
taskGroup {
id
}
labels {
id
assignedDate
projectLabel {
id
name
createdDate
labelColor {
id
colorHex
position
name
}
}
}
assigned {
id
fullName
profileIcon {
url
initials
bgColor
}
}
}
`;
export const AssignTaskDocument = gql`
mutation assignTask($taskID: UUID!, $userID: UUID!) {
assignTask(input: {taskID: $taskID, userID: $userID}) {
@ -1103,6 +1332,7 @@ export const CreateTaskDocument = gql`
name
position
description
dueDate
taskGroup {
id
name
@ -1331,40 +1561,7 @@ export const FindProjectDocument = gql`
name
position
tasks {
id
name
position
description
dueDate
taskGroup {
id
name
position
}
labels {
id
assignedDate
projectLabel {
id
name
createdDate
labelColor {
id
colorHex
position
name
}
}
}
assigned {
id
fullName
profileIcon {
url
initials
bgColor
}
}
...TaskFields
}
}
}
@ -1375,7 +1572,7 @@ export const FindProjectDocument = gql`
name
}
}
`;
${TaskFieldsFragmentDoc}`;
/**
* __useFindProjectQuery__
@ -1413,6 +1610,18 @@ export const FindTaskDocument = gql`
taskGroup {
id
}
checklists {
id
name
position
items {
id
name
taskChecklistID
complete
position
}
}
labels {
id
assignedDate
@ -1546,6 +1755,218 @@ export function useMeLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptio
export type MeQueryHookResult = ReturnType<typeof useMeQuery>;
export type MeLazyQueryHookResult = ReturnType<typeof useMeLazyQuery>;
export type MeQueryResult = ApolloReactCommon.QueryResult<MeQuery, MeQueryVariables>;
export const CreateTaskChecklistItemDocument = gql`
mutation createTaskChecklistItem($taskChecklistID: UUID!, $name: String!, $position: Float!) {
createTaskChecklistItem(input: {taskChecklistID: $taskChecklistID, name: $name, position: $position}) {
id
name
taskChecklistID
position
complete
}
}
`;
export type CreateTaskChecklistItemMutationFn = ApolloReactCommon.MutationFunction<CreateTaskChecklistItemMutation, CreateTaskChecklistItemMutationVariables>;
/**
* __useCreateTaskChecklistItemMutation__
*
* To run a mutation, you first call `useCreateTaskChecklistItemMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useCreateTaskChecklistItemMutation` 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 [createTaskChecklistItemMutation, { data, loading, error }] = useCreateTaskChecklistItemMutation({
* variables: {
* taskChecklistID: // value for 'taskChecklistID'
* name: // value for 'name'
* position: // value for 'position'
* },
* });
*/
export function useCreateTaskChecklistItemMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<CreateTaskChecklistItemMutation, CreateTaskChecklistItemMutationVariables>) {
return ApolloReactHooks.useMutation<CreateTaskChecklistItemMutation, CreateTaskChecklistItemMutationVariables>(CreateTaskChecklistItemDocument, baseOptions);
}
export type CreateTaskChecklistItemMutationHookResult = ReturnType<typeof useCreateTaskChecklistItemMutation>;
export type CreateTaskChecklistItemMutationResult = ApolloReactCommon.MutationResult<CreateTaskChecklistItemMutation>;
export type CreateTaskChecklistItemMutationOptions = ApolloReactCommon.BaseMutationOptions<CreateTaskChecklistItemMutation, CreateTaskChecklistItemMutationVariables>;
export const DeleteTaskChecklistItemDocument = gql`
mutation deleteTaskChecklistItem($taskChecklistItemID: UUID!) {
deleteTaskChecklistItem(input: {taskChecklistItemID: $taskChecklistItemID}) {
ok
taskChecklistItem {
id
taskChecklistID
}
}
}
`;
export type DeleteTaskChecklistItemMutationFn = ApolloReactCommon.MutationFunction<DeleteTaskChecklistItemMutation, DeleteTaskChecklistItemMutationVariables>;
/**
* __useDeleteTaskChecklistItemMutation__
*
* To run a mutation, you first call `useDeleteTaskChecklistItemMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useDeleteTaskChecklistItemMutation` 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 [deleteTaskChecklistItemMutation, { data, loading, error }] = useDeleteTaskChecklistItemMutation({
* variables: {
* taskChecklistItemID: // value for 'taskChecklistItemID'
* },
* });
*/
export function useDeleteTaskChecklistItemMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<DeleteTaskChecklistItemMutation, DeleteTaskChecklistItemMutationVariables>) {
return ApolloReactHooks.useMutation<DeleteTaskChecklistItemMutation, DeleteTaskChecklistItemMutationVariables>(DeleteTaskChecklistItemDocument, baseOptions);
}
export type DeleteTaskChecklistItemMutationHookResult = ReturnType<typeof useDeleteTaskChecklistItemMutation>;
export type DeleteTaskChecklistItemMutationResult = ApolloReactCommon.MutationResult<DeleteTaskChecklistItemMutation>;
export type DeleteTaskChecklistItemMutationOptions = ApolloReactCommon.BaseMutationOptions<DeleteTaskChecklistItemMutation, DeleteTaskChecklistItemMutationVariables>;
export const SetTaskChecklistItemCompleteDocument = gql`
mutation setTaskChecklistItemComplete($taskChecklistItemID: UUID!, $complete: Boolean!) {
setTaskChecklistItemComplete(input: {taskChecklistItemID: $taskChecklistItemID, complete: $complete}) {
id
name
taskChecklistID
complete
position
}
}
`;
export type SetTaskChecklistItemCompleteMutationFn = ApolloReactCommon.MutationFunction<SetTaskChecklistItemCompleteMutation, SetTaskChecklistItemCompleteMutationVariables>;
/**
* __useSetTaskChecklistItemCompleteMutation__
*
* To run a mutation, you first call `useSetTaskChecklistItemCompleteMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useSetTaskChecklistItemCompleteMutation` 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 [setTaskChecklistItemCompleteMutation, { data, loading, error }] = useSetTaskChecklistItemCompleteMutation({
* variables: {
* taskChecklistItemID: // value for 'taskChecklistItemID'
* complete: // value for 'complete'
* },
* });
*/
export function useSetTaskChecklistItemCompleteMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<SetTaskChecklistItemCompleteMutation, SetTaskChecklistItemCompleteMutationVariables>) {
return ApolloReactHooks.useMutation<SetTaskChecklistItemCompleteMutation, SetTaskChecklistItemCompleteMutationVariables>(SetTaskChecklistItemCompleteDocument, baseOptions);
}
export type SetTaskChecklistItemCompleteMutationHookResult = ReturnType<typeof useSetTaskChecklistItemCompleteMutation>;
export type SetTaskChecklistItemCompleteMutationResult = ApolloReactCommon.MutationResult<SetTaskChecklistItemCompleteMutation>;
export type SetTaskChecklistItemCompleteMutationOptions = ApolloReactCommon.BaseMutationOptions<SetTaskChecklistItemCompleteMutation, SetTaskChecklistItemCompleteMutationVariables>;
export const SetTaskCompleteDocument = gql`
mutation setTaskComplete($taskID: UUID!, $complete: Boolean!) {
setTaskComplete(input: {taskID: $taskID, complete: $complete}) {
...TaskFields
}
}
${TaskFieldsFragmentDoc}`;
export type SetTaskCompleteMutationFn = ApolloReactCommon.MutationFunction<SetTaskCompleteMutation, SetTaskCompleteMutationVariables>;
/**
* __useSetTaskCompleteMutation__
*
* To run a mutation, you first call `useSetTaskCompleteMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useSetTaskCompleteMutation` 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 [setTaskCompleteMutation, { data, loading, error }] = useSetTaskCompleteMutation({
* variables: {
* taskID: // value for 'taskID'
* complete: // value for 'complete'
* },
* });
*/
export function useSetTaskCompleteMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<SetTaskCompleteMutation, SetTaskCompleteMutationVariables>) {
return ApolloReactHooks.useMutation<SetTaskCompleteMutation, SetTaskCompleteMutationVariables>(SetTaskCompleteDocument, baseOptions);
}
export type SetTaskCompleteMutationHookResult = ReturnType<typeof useSetTaskCompleteMutation>;
export type SetTaskCompleteMutationResult = ApolloReactCommon.MutationResult<SetTaskCompleteMutation>;
export type SetTaskCompleteMutationOptions = ApolloReactCommon.BaseMutationOptions<SetTaskCompleteMutation, SetTaskCompleteMutationVariables>;
export const UpdateTaskChecklistItemNameDocument = gql`
mutation updateTaskChecklistItemName($taskChecklistItemID: UUID!, $name: String!) {
updateTaskChecklistItemName(input: {taskChecklistItemID: $taskChecklistItemID, name: $name}) {
id
name
}
}
`;
export type UpdateTaskChecklistItemNameMutationFn = ApolloReactCommon.MutationFunction<UpdateTaskChecklistItemNameMutation, UpdateTaskChecklistItemNameMutationVariables>;
/**
* __useUpdateTaskChecklistItemNameMutation__
*
* To run a mutation, you first call `useUpdateTaskChecklistItemNameMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUpdateTaskChecklistItemNameMutation` 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 [updateTaskChecklistItemNameMutation, { data, loading, error }] = useUpdateTaskChecklistItemNameMutation({
* variables: {
* taskChecklistItemID: // value for 'taskChecklistItemID'
* name: // value for 'name'
* },
* });
*/
export function useUpdateTaskChecklistItemNameMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<UpdateTaskChecklistItemNameMutation, UpdateTaskChecklistItemNameMutationVariables>) {
return ApolloReactHooks.useMutation<UpdateTaskChecklistItemNameMutation, UpdateTaskChecklistItemNameMutationVariables>(UpdateTaskChecklistItemNameDocument, baseOptions);
}
export type UpdateTaskChecklistItemNameMutationHookResult = ReturnType<typeof useUpdateTaskChecklistItemNameMutation>;
export type UpdateTaskChecklistItemNameMutationResult = ApolloReactCommon.MutationResult<UpdateTaskChecklistItemNameMutation>;
export type UpdateTaskChecklistItemNameMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateTaskChecklistItemNameMutation, UpdateTaskChecklistItemNameMutationVariables>;
export const UpdateTaskGroupNameDocument = gql`
mutation updateTaskGroupName($taskGroupID: UUID!, $name: String!) {
updateTaskGroupName(input: {taskGroupID: $taskGroupID, name: $name}) {
id
name
}
}
`;
export type UpdateTaskGroupNameMutationFn = ApolloReactCommon.MutationFunction<UpdateTaskGroupNameMutation, UpdateTaskGroupNameMutationVariables>;
/**
* __useUpdateTaskGroupNameMutation__
*
* To run a mutation, you first call `useUpdateTaskGroupNameMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUpdateTaskGroupNameMutation` 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 [updateTaskGroupNameMutation, { data, loading, error }] = useUpdateTaskGroupNameMutation({
* variables: {
* taskGroupID: // value for 'taskGroupID'
* name: // value for 'name'
* },
* });
*/
export function useUpdateTaskGroupNameMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<UpdateTaskGroupNameMutation, UpdateTaskGroupNameMutationVariables>) {
return ApolloReactHooks.useMutation<UpdateTaskGroupNameMutation, UpdateTaskGroupNameMutationVariables>(UpdateTaskGroupNameDocument, baseOptions);
}
export type UpdateTaskGroupNameMutationHookResult = ReturnType<typeof useUpdateTaskGroupNameMutation>;
export type UpdateTaskGroupNameMutationResult = ApolloReactCommon.MutationResult<UpdateTaskGroupNameMutation>;
export type UpdateTaskGroupNameMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateTaskGroupNameMutation, UpdateTaskGroupNameMutationVariables>;
export const ToggleTaskLabelDocument = gql`
mutation toggleTaskLabel($taskID: UUID!, $projectLabelID: UUID!) {
toggleTaskLabel(input: {taskID: $taskID, projectLabelID: $projectLabelID}) {

View File

@ -4,6 +4,7 @@ mutation createTask($taskGroupID: String!, $name: String!, $position: Float!) {
name
position
description
dueDate
taskGroup {
id
name

View File

@ -1,72 +0,0 @@
query findProject($projectId: String!) {
findProject(input: { projectId: $projectId }) {
name
members {
id
fullName
profileIcon {
url
initials
bgColor
}
}
labels {
id
createdDate
name
labelColor {
id
name
colorHex
position
}
}
taskGroups {
id
name
position
tasks {
id
name
position
description
dueDate
taskGroup {
id
name
position
}
labels {
id
assignedDate
projectLabel {
id
name
createdDate
labelColor {
id
colorHex
position
name
}
}
}
assigned {
id
fullName
profileIcon {
url
initials
bgColor
}
}
}
}
}
labelColors {
id
position
colorHex
name
}
}

View File

@ -0,0 +1,45 @@
import gql from 'graphql-tag';
import TASK_FRAGMENT from './fragments/task';
const FIND_PROJECT_QUERY = gql`
query findProject($projectId: String!) {
findProject(input: { projectId: $projectId }) {
name
members {
id
fullName
profileIcon {
url
initials
bgColor
}
}
labels {
id
createdDate
name
labelColor {
id
name
colorHex
position
}
}
taskGroups {
id
name
position
tasks {
...TaskFields
}
}
}
labelColors {
id
position
colorHex
name
}
${TASK_FRAGMENT}
}
`;

View File

@ -8,6 +8,18 @@ query findTask($taskID: UUID!) {
taskGroup {
id
}
checklists {
id
name
position
items {
id
name
taskChecklistID
complete
position
}
}
labels {
id
assignedDate

View File

@ -0,0 +1,41 @@
import gql from 'graphql-tag';
const TASK_FRAGMENT = gql`
fragment TaskFields on Task {
id
name
description
dueDate
complete
position
taskGroup {
id
}
labels {
id
assignedDate
projectLabel {
id
name
createdDate
labelColor {
id
colorHex
position
name
}
}
}
assigned {
id
fullName
profileIcon {
url
initials
bgColor
}
}
}
`;
export default TASK_FRAGMENT;

View File

@ -0,0 +1,13 @@
import gql from 'graphql-tag';
const CREATE_TASK_CHECKLIST_ITEM = gql`
mutation createTaskChecklistItem($taskChecklistID: UUID!, $name: String!, $position: Float!) {
createTaskChecklistItem(input: { taskChecklistID: $taskChecklistID, name: $name, position: $position }) {
id
name
taskChecklistID
position
complete
}
}
`;

View File

@ -0,0 +1,13 @@
import gql from 'graphql-tag';
const DELETE_TASK_CHECKLIST_ITEM = gql`
mutation deleteTaskChecklistItem($taskChecklistItemID: UUID!) {
deleteTaskChecklistItem(input: { taskChecklistItemID: $taskChecklistItemID }) {
ok
taskChecklistItem {
id
taskChecklistID
}
}
}
`;

View File

@ -0,0 +1,13 @@
import gql from 'graphql-tag';
const SET_TASK_CHECKLIST_ITEM_COMPLETE = gql`
mutation setTaskChecklistItemComplete($taskChecklistItemID: UUID!, $complete: Boolean!) {
setTaskChecklistItemComplete(input: { taskChecklistItemID: $taskChecklistItemID, complete: $complete }) {
id
name
taskChecklistID
complete
position
}
}
`;

View File

@ -0,0 +1,11 @@
import gql from 'graphql-tag';
import TASK_FRAGMENT from '../fragments/task';
const UPDATE_TASK_GROUP_NAME_MUTATION = gql`
mutation setTaskComplete($taskID: UUID!, $complete: Boolean!) {
setTaskComplete(input: { taskID: $taskID, complete: $complete }) {
...TaskFields
}
${TASK_FRAGMENT}
}
`;

View File

@ -0,0 +1,10 @@
import gql from 'graphql-tag';
const UPDATE_TASK_CHECKLIST_ITEM_NAME = gql`
mutation updateTaskChecklistItemName($taskChecklistItemID: UUID!, $name: String!) {
updateTaskChecklistItemName(input: { taskChecklistItemID: $taskChecklistItemID, name: $name }) {
id
name
}
}
`;

View File

@ -0,0 +1,12 @@
import gql from 'graphql-tag';
import TASK_FRAGMENT from '../fragments/task';
const UPDATE_TASK_GROUP_NAME_MUTATION = gql`
mutation updateTaskGroupName($taskGroupID: UUID!, $name: String!) {
updateTaskGroupName(input:{taskGroupID:$taskGroupID, name:$name}) {
id
name
}
${TASK_FRAGMENT}
}
`;

View File

@ -0,0 +1,12 @@
import React from 'react';
import Icon, { IconProps } from './Icon';
const AccountPlus: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
return (
<Icon width={width} height={height} className={className} viewBox="0 0 640 512">
<path d="M224 256c70.7 0 128-57.3 128-128S294.7 0 224 0 96 57.3 96 128s57.3 128 128 128zm89.6 32h-16.7c-22.2 10.2-46.9 16-72.9 16s-50.6-5.8-72.9-16h-16.7C60.2 288 0 348.2 0 422.4V464c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48v-41.6c0-74.2-60.2-134.4-134.4-134.4zm323-128.4l-27.8-28.1c-4.6-4.7-12.1-4.7-16.8-.1l-104.8 104-45.5-45.8c-4.6-4.7-12.1-4.7-16.8-.1l-28.1 27.9c-4.7 4.6-4.7 12.1-.1 16.8l81.7 82.3c4.6 4.7 12.1 4.7 16.8.1l141.3-140.2c4.6-4.7 4.7-12.2.1-16.8z" />
</Icon>
);
};
export default AccountPlus;

View File

@ -0,0 +1,12 @@
import React from 'react';
import Icon, { IconProps } from './Icon';
const CheckCircle: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
return (
<Icon width={width} height={height} className={className} viewBox="0 0 512 512">
<path d="M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z" />
</Icon>
);
};
export default CheckCircle;

View File

@ -0,0 +1,12 @@
import React from 'react';
import Icon, { IconProps } from './Icon';
const CheckSquare: React.FC<IconProps> = ({ width = '16px', height = '16px', onClick, className }) => {
return (
<Icon width={width} onClick={onClick} height={height} className={className} viewBox="0 0 448 512">
<path d="M400 480H48c-26.51 0-48-21.49-48-48V80c0-26.51 21.49-48 48-48h352c26.51 0 48 21.49 48 48v352c0 26.51-21.49 48-48 48zm-204.686-98.059l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.248-16.379-6.249-22.628 0L184 302.745l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.25 16.379 6.25 22.628.001z" />
</Icon>
);
};
export default CheckSquare;

View File

@ -0,0 +1,12 @@
import React from 'react';
import Icon, { IconProps } from './Icon';
const CheckSquareOutline: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
return (
<Icon width={width} height={height} className={className} viewBox="0 0 448 512">
<path d="M400 32H48C21.49 32 0 53.49 0 80v352c0 26.51 21.49 48 48 48h352c26.51 0 48-21.49 48-48V80c0-26.51-21.49-48-48-48zm0 400H48V80h352v352zm-35.864-241.724L191.547 361.48c-4.705 4.667-12.303 4.637-16.97-.068l-90.781-91.516c-4.667-4.705-4.637-12.303.069-16.971l22.719-22.536c4.705-4.667 12.303-4.637 16.97.069l59.792 60.277 141.352-140.216c4.705-4.667 12.303-4.637 16.97.068l22.536 22.718c4.667 4.706 4.637 12.304-.068 16.971z" />
</Icon>
);
};
export default CheckSquareOutline;

View File

@ -1,21 +1,12 @@
import React from 'react';
import Icon, { IconProps } from './Icon';
type Props = {
size: number | string;
color: string;
};
const Checkmark = ({ size, color }: Props) => {
const Checkmark: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
return (
<svg fill={color} xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 16 16">
<Icon width={width} height={height} className={className} viewBox="0 0 16 16">
<path d="M13.5 2l-7.5 7.5-3.5-3.5-2.5 2.5 6 6 10-10z" />
</svg>
</Icon>
);
};
Checkmark.defaultProps = {
size: 16,
color: '#000',
};
export default Checkmark;

View File

@ -0,0 +1,12 @@
import React from 'react';
import Icon, { IconProps } from './Icon';
const Clock: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
return (
<Icon width={width} height={height} className={className} viewBox="0 0 512 512">
<path d="M256,8C119,8,8,119,8,256S119,504,256,504,504,393,504,256,393,8,256,8Zm92.49,313h0l-20,25a16,16,0,0,1-22.49,2.5h0l-67-49.72a40,40,0,0,1-15-31.23V112a16,16,0,0,1,16-16h32a16,16,0,0,1,16,16V256l58,42.5A16,16,0,0,1,348.49,321Z" />
</Icon>
);
};
export default Clock;

View File

@ -1,21 +1,12 @@
import React from 'react';
import Icon, { IconProps } from './Icon';
type Props = {
size: number | string;
color: string;
};
const Cross = ({ size, color }: Props) => {
const Cross: React.FC<IconProps> = ({ width = '16px', height = '16px', className, onClick }) => {
return (
<svg fill={color} width={size} height={size} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512">
<Icon width={width} height={height} onClick={onClick} className={className} viewBox="0 0 352 512">
<path d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z" />
</svg>
</Icon>
);
};
Cross.defaultProps = {
size: 16,
color: '#000',
};
export default Cross;

View File

@ -4,6 +4,8 @@ import styled from 'styled-components/macro';
export type IconProps = {
width: number | string;
height: number | string;
className?: string;
onClick?: () => void;
};
type Props = {
@ -11,15 +13,27 @@ type Props = {
height: number | string;
viewBox: string;
className?: string;
onClick?: () => void;
};
const Svg = styled.svg`
fill: rgba(${props => props.theme.colors.text.primary});
`;
const Icon: React.FC<Props> = ({ width, height, viewBox, className, children }) => {
const Icon: React.FC<Props> = ({ width, height, viewBox, className, onClick, children }) => {
return (
<Svg className={className} width={width} height={height} xmlns="http://www.w3.org/2000/svg" viewBox={viewBox}>
<Svg
onClick={e => {
if (onClick) {
onClick();
}
}}
className={className}
width={width}
height={height}
xmlns="http://www.w3.org/2000/svg"
viewBox={viewBox}
>
{children}
</Svg>
);

View File

@ -0,0 +1,12 @@
import React from 'react';
import Icon, { IconProps } from './Icon';
const Square: React.FC<IconProps> = ({ width = '16px', height = '16px', className, onClick }) => {
return (
<Icon onClick={onClick} width={width} height={height} className={className} viewBox="0 0 448 512">
<path d="M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zm-6 400H54c-3.3 0-6-2.7-6-6V86c0-3.3 2.7-6 6-6h340c3.3 0 6 2.7 6 6v340c0 3.3-2.7 6-6 6z" />
</Icon>
);
};
export default Square;

View File

@ -0,0 +1,12 @@
import React from 'react';
import Icon, { IconProps } from './Icon';
const Trash: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
return (
<Icon width={width} height={height} className={className} viewBox="0 0 448 512">
<path d="M432 32H312l-9.4-18.7A24 24 0 0 0 281.1 0H166.8a23.72 23.72 0 0 0-21.4 13.3L136 32H16A16 16 0 0 0 0 48v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16zM53.2 467a48 48 0 0 0 47.9 45h245.8a48 48 0 0 0 47.9-45L416 128H32z" />
</Icon>
);
};
export default Trash;

View File

@ -1,6 +1,13 @@
import Cross from './Cross';
import Cog from './Cog';
import Trash from './Trash';
import CheckCircle from './CheckCircle';
import Clock from './Clock';
import AccountPlus from './AccountPlus';
import CheckSquare from './CheckSquare';
import ArrowLeft from './ArrowLeft';
import CheckSquareOutline from './CheckSquareOutline';
import Square from './Square';
import Bolt from './Bolt';
import Plus from './Plus';
import Bell from './Bell';
@ -42,8 +49,15 @@ export {
Citadel,
Checkmark,
User,
Trash,
Users,
Lock,
ArrowLeft,
CheckCircle,
AccountPlus,
Clock,
CheckSquareOutline,
CheckSquare,
Square,
ToggleOn,
};