feature(Project): add ability to create new task groups

This commit is contained in:
Jordan Knott 2020-04-10 21:22:58 -05:00
parent e022e7914a
commit bd73717deb
27 changed files with 847 additions and 248 deletions

View File

@ -7,12 +7,15 @@ import {
useCreateTaskMutation,
useDeleteTaskMutation,
useUpdateTaskLocationMutation,
useCreateTaskGroupMutation,
} from 'shared/generated/graphql';
import Navbar from 'App/Navbar';
import TopNavbar from 'App/TopNavbar';
import Lists from 'shared/components/Lists';
import QuickCardEditor from 'shared/components/QuickCardEditor';
import PopupMenu from 'shared/components/PopupMenu';
import ListActions from 'shared/components/ListActions';
interface ColumnState {
[key: string]: TaskGroup;
@ -65,13 +68,32 @@ interface ProjectParams {
}
const initialState: State = { tasks: {}, columns: {} };
const initialPopupState = { left: 0, top: 0, isOpen: false, taskGroupID: '' };
const initialQuickCardEditorState: QuickCardEditorState = { isOpen: false, top: 0, left: 0 };
const Project = () => {
const { projectId } = useParams<ProjectParams>();
const [listsData, setListsData] = useState(initialState);
const [popupData, setPopupData] = useState(initialPopupState);
const [quickCardEditor, setQuickCardEditor] = useState(initialQuickCardEditorState);
const [updateTaskLocation] = useUpdateTaskLocationMutation();
const [createTaskGroup] = useCreateTaskGroupMutation({
onCompleted: newTaskGroupData => {
const newListsData = {
...listsData,
columns: {
...listsData.columns,
[newTaskGroupData.createTaskGroup.taskGroupID]: {
taskGroupID: newTaskGroupData.createTaskGroup.taskGroupID,
name: newTaskGroupData.createTaskGroup.name,
position: newTaskGroupData.createTaskGroup.position,
tasks: [],
},
},
};
setListsData(newListsData);
},
});
const [createTask] = useCreateTaskMutation({
onCompleted: newTaskData => {
const newListsData = {
@ -79,7 +101,9 @@ const Project = () => {
tasks: {
...listsData.tasks,
[newTaskData.createTask.taskID]: {
taskGroupID: newTaskData.createTask.taskGroupID,
taskGroup: {
taskGroupID: newTaskData.createTask.taskGroup.taskGroupID,
},
taskID: newTaskData.createTask.taskID,
name: newTaskData.createTask.name,
position: newTaskData.createTask.position,
@ -119,17 +143,19 @@ const Project = () => {
variables: { projectId },
onCompleted: newData => {
const newListsData: State = { tasks: {}, columns: {} };
newData.findProject.taskGroups.forEach((taskGroup: TaskGroup) => {
newData.findProject.taskGroups.forEach(taskGroup => {
newListsData.columns[taskGroup.taskGroupID] = {
taskGroupID: taskGroup.taskGroupID,
name: taskGroup.name,
position: taskGroup.position,
tasks: [],
};
taskGroup.tasks.forEach((task: Task) => {
taskGroup.tasks.forEach(task => {
newListsData.tasks[task.taskID] = {
taskID: task.taskID,
taskGroup: {
taskGroupID: taskGroup.taskGroupID,
},
name: task.name,
position: task.position,
labels: [],
@ -163,7 +189,9 @@ const Project = () => {
setListsData(newState);
};
const onCardCreate = (taskGroupID: string, name: string) => {
const taskGroupTasks = Object.values(listsData.tasks).filter((task: Task) => task.taskGroupID === taskGroupID);
const taskGroupTasks = Object.values(listsData.tasks).filter(
(task: Task) => task.taskGroup.taskGroupID === taskGroupID,
);
let position = 65535;
if (taskGroupTasks.length !== 0) {
const [lastTask] = taskGroupTasks.sort((a: any, b: any) => a.position - b.position).slice(-1);
@ -201,6 +229,16 @@ const Project = () => {
{...listsData}
onCardDrop={onCardDrop}
onListDrop={onListDrop}
onCreateList={listName => {
const [lastColumn] = Object.values(listsData.columns)
.sort((a, b) => a.position - b.position)
.slice(-1);
let position = 65535;
if (lastColumn) {
position = lastColumn.position * 2 + 1;
}
createTaskGroup({ variables: { projectID: projectId, name: listName, position } });
}}
/>
</Board>
</MainContent>
@ -208,7 +246,7 @@ const Project = () => {
<QuickCardEditor
isOpen
taskID={quickCardEditor.task ? quickCardEditor.task.taskID : ''}
taskGroupID={quickCardEditor.task ? quickCardEditor.task.taskGroupID : ''}
taskGroupID={quickCardEditor.task ? quickCardEditor.task.taskGroup.taskGroupID : ''}
cardTitle={quickCardEditor.task ? quickCardEditor.task.name : ''}
onCloseEditor={() => setQuickCardEditor(initialQuickCardEditorState)}
onEditCard={(_listId: string, cardId: string, cardName: string) => {
@ -221,6 +259,16 @@ const Project = () => {
left={quickCardEditor.left}
/>
)}
{popupData.isOpen && (
<PopupMenu
title="List Actions"
top={popupData.top}
onClose={() => setPopupData(initialPopupState)}
left={popupData.left}
>
<ListActions taskGroupID={popupData.taskGroupID} />
</PopupMenu>
)}
</>
);
}

22
web/src/citadel.d.ts vendored
View File

@ -5,9 +5,15 @@ type ContextMenuEvent = {
taskGroupID: string;
};
type InnerTaskGroup = {
taskGroupID: string;
name?: string;
position?: number;
};
type Task = {
taskID: string;
taskGroupID: string;
taskGroup: InnerTaskGroup;
name: string;
position: number;
labels: Label[];
@ -18,7 +24,7 @@ type TaskGroup = {
taskGroupID: string;
name: string;
position: number;
tasks: RemoteTask[];
tasks: Task[];
};
type Project = {
@ -62,3 +68,15 @@ type LoginProps = {
setError: (field: string, eType: string, message: string) => void,
) => void;
};
type ElementPosition = {
top: number;
left: number;
right: number;
bottom: number;
};
type ElementSize = {
width: number;
height: number;
};

View File

@ -0,0 +1,19 @@
import React from 'react';
import { action } from '@storybook/addon-actions';
import AddList from '.';
export default {
component: AddList,
title: 'AddList',
parameters: {
backgrounds: [
{ name: 'gray', value: '#262c49', default: true },
{ name: 'white', value: '#ffffff' },
],
},
};
export const Default = () => {
return <AddList onSave={action('on save')} />;
};

View File

@ -0,0 +1,98 @@
import styled, { css } from 'styled-components';
import TextareaAutosize from 'react-autosize-textarea/lib';
export const Wrapper = styled.div<{ editorOpen: boolean }>`
display: inline-block;
background-color: hsla(0, 0%, 100%, 0.24);
cursor: pointer;
border-radius: 3px;
height: auto;
min-height: 32px;
padding: 4px;
transition: background 85ms ease-in, opacity 40ms ease-in, border-color 85ms ease-in;
width: 272px;
margin: 0 4px;
margin-right: 8px;
${props =>
!props.editorOpen &&
css`
&:hover {
background-color: hsla(0, 0%, 100%, 0.32);
}
`}
${props =>
props.editorOpen &&
css`
background-color: #ebecf0;
border-radius: 3px;
height: auto;
min-height: 32px;
padding: 4px;
transition: background 85ms ease-in, opacity 40ms ease-in, border-color 85ms ease-in;
`}
`;
export const Placeholder = styled.span`
color: #c2c6dc;
display: flex;
align-items: center;
padding: 6px 8px;
transition: color 85ms ease-in;
`;
export const AddIconWrapper = styled.div`
color: #fff;
margin-right: 6px;
`;
export const ListNameEditorWrapper = styled.div`
display: flex;
`;
export const ListNameEditor = styled(TextareaAutosize)`
background: #fff;
border: none;
box-shadow: inset 0 0 0 2px #0079bf;
display: block;
margin: 0;
transition: margin 85ms ease-in, background 85ms ease-in;
width: 100%;
line-height: 20px;
padding: 8px 12px;
font-size: 14px;
outline: none;
`;
export const ListAddControls = styled.div`
height: 32px;
transition: margin 85ms ease-in, height 85ms ease-in;
overflow: hidden;
margin: 4px 0 0;
`;
export const AddListButton = styled.button`
background-color: #5aac44;
box-shadow: none;
border: none;
color: #fff;
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 CancelAdd = styled.div`
display: flex;
align-items: center;
justify-content: center;
height: 32px;
width: 32px;
cursor: pointer;
`;

View File

@ -0,0 +1,105 @@
import React, { useState, useRef, useEffect } from 'react';
import { Plus, Cross } from 'shared/icons';
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import {
Wrapper,
Placeholder,
AddIconWrapper,
ListNameEditor,
ListAddControls,
CancelAdd,
AddListButton,
ListNameEditorWrapper,
} from './Styles';
type NameEditorProps = {
onSave: (listName: string) => void;
onCancel: () => void;
};
const NameEditor: React.FC<NameEditorProps> = ({ onSave, onCancel }) => {
const $editorRef = useRef<HTMLTextAreaElement>(null);
const [listName, setListName] = useState('');
useEffect(() => {
if ($editorRef && $editorRef.current) {
$editorRef.current.focus();
}
});
const onKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
onSave(listName);
setListName('');
if ($editorRef && $editorRef.current) {
$editorRef.current.focus();
}
}
};
return (
<>
<ListNameEditorWrapper>
<ListNameEditor
ref={$editorRef}
onKeyDown={onKeyDown}
value={listName}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setListName(e.currentTarget.value)}
/>
</ListNameEditorWrapper>
<ListAddControls>
<AddListButton
onClick={() => {
onSave(listName);
setListName('');
if ($editorRef && $editorRef.current) {
$editorRef.current.focus();
}
}}
>
Save
</AddListButton>
<CancelAdd onClick={() => onCancel()}>
<Cross />
</CancelAdd>
</ListAddControls>
</>
);
};
type AddListProps = {
onSave: (listName: string) => void;
};
const AddList: React.FC<AddListProps> = ({ onSave }) => {
const [editorOpen, setEditorOpen] = useState(false);
const $wrapperRef = useRef<HTMLDivElement>(null);
const onOutsideClick = () => {
setEditorOpen(false);
};
useOnOutsideClick($wrapperRef, editorOpen, onOutsideClick, null);
return (
<Wrapper
ref={$wrapperRef}
editorOpen={editorOpen}
onClick={() => {
if (!editorOpen) {
setEditorOpen(true);
}
}}
>
{editorOpen ? (
<NameEditor onCancel={() => setEditorOpen(false)} onSave={onSave} />
) : (
<Placeholder>
<AddIconWrapper>
<Plus size={12} color="#c2c6dc" />
</AddIconWrapper>
Add another list
</Placeholder>
)}
</Wrapper>
);
};
export default AddList;

View File

@ -25,20 +25,17 @@ type Props = {
const CardComposer = ({ isOpen, onCreateCard, onClose }: Props) => {
const [cardName, setCardName] = useState('');
const $cardEditor: any = useRef(null);
const onClick = () => {
const handleCreateCard = () => {
onCreateCard(cardName);
setCardName('');
if ($cardEditor && $cardEditor.current) {
$cardEditor.current.focus();
}
};
const onKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter') {
e.preventDefault();
onCreateCard(cardName);
}
};
const onBlur = () => {
if (cardName === '') {
onClose();
} else {
onCreateCard(cardName);
handleCreateCard();
}
};
useOnEscapeKeyDown(isOpen, onClose);
@ -63,7 +60,7 @@ const CardComposer = ({ isOpen, onCreateCard, onClose }: Props) => {
</ListCard>
<ComposerControls>
<ComposerControlsSaveSection>
<AddCardButton onClick={onClick}>Add Card</AddCardButton>
<AddCardButton onClick={handleCreateCard}>Add Card</AddCardButton>
<CancelIcon onClick={onClose} icon={faTimes} color="#42526e" />
</ComposerControlsSaveSection>
<ComposerControlsActionsSection />

View File

@ -59,6 +59,7 @@ export const Default = () => {
onSaveName={action('on save name')}
onOpenComposer={action('on open composer')}
tasks={[]}
onExtraMenuOpen={action('extra menu open')}
>
<ListCards>
<CardComposer
@ -84,6 +85,7 @@ export const WithCardComposer = () => {
onSaveName={action('on save name')}
onOpenComposer={action('on open composer')}
tasks={[]}
onExtraMenuOpen={action('extra menu open')}
>
<ListCards>
<CardComposer
@ -110,6 +112,7 @@ export const WithCard = () => {
onSaveName={action('on save name')}
onOpenComposer={action('on open composer')}
tasks={[]}
onExtraMenuOpen={action('extra menu open')}
>
<ListCards>
<Card
@ -148,6 +151,7 @@ export const WithCardAndComposer = () => {
onSaveName={action('on save name')}
onOpenComposer={action('on open composer')}
tasks={[]}
onExtraMenuOpen={action('extra menu open')}
>
<ListCards>
<Card

View File

@ -117,3 +117,12 @@ export const ListCards = styled.div`
overflow-y: auto;
overflow-x: hidden;
`;
export const ListExtraMenuButtonWrapper = styled.div`
cursor: pointer;
position: absolute;
right: 4px;
top: 4px;
z-index: 1;
padding: 6px;
`;

View File

@ -1,7 +1,6 @@
import React, { useState, useRef } from 'react';
import useOnEscapeKeyDown from 'shared/hooks/onEscapeKeyDown';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPlus } from '@fortawesome/free-solid-svg-icons';
import { Plus, Ellipsis } from 'shared/icons';
import {
Container,
@ -13,6 +12,7 @@ import {
AddCardButton,
AddCardButtonText,
ListCards,
ListExtraMenuButtonWrapper,
} from './Styles';
type Props = {
@ -26,20 +26,32 @@ type Props = {
wrapperProps?: any;
headerProps?: any;
index?: number;
onExtraMenuOpen: (taskGroupID: string, pos: ElementPosition, size: ElementSize) => void;
};
const List = React.forwardRef(
(
{ id, name, onSaveName, isComposerOpen, onOpenComposer, children, wrapperProps, headerProps }: Props,
{
id,
name,
onSaveName,
isComposerOpen,
onOpenComposer,
children,
wrapperProps,
headerProps,
onExtraMenuOpen,
}: Props,
$wrapperRef: any,
) => {
const [listName, setListName] = useState(name);
const [isEditingTitle, setEditingTitle] = useState(false);
const $listNameRef: any = useRef<HTMLTextAreaElement>();
const $listNameRef = useRef<HTMLTextAreaElement>(null);
const $extraActionsRef = useRef<HTMLDivElement>(null);
const onClick = () => {
setEditingTitle(true);
if ($listNameRef) {
if ($listNameRef && $listNameRef.current) {
$listNameRef.current.select();
}
};
@ -48,7 +60,9 @@ const List = React.forwardRef(
onSaveName(listName);
};
const onEscape = () => {
if ($listNameRef && $listNameRef.current) {
$listNameRef.current.blur();
}
};
const onChange = (event: React.FormEvent<HTMLTextAreaElement>): void => {
setListName(event.currentTarget.value);
@ -56,8 +70,29 @@ const List = React.forwardRef(
const onKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
if ($listNameRef && $listNameRef.current) {
$listNameRef.current.blur();
}
}
};
const handleExtraMenuOpen = () => {
if ($extraActionsRef && $extraActionsRef.current) {
const pos = $extraActionsRef.current.getBoundingClientRect();
onExtraMenuOpen(
id,
{
top: pos.top,
left: pos.left,
right: pos.right,
bottom: pos.bottom,
},
{
width: pos.width,
height: pos.height,
},
);
}
};
useOnEscapeKeyDown(isEditingTitle, onEscape);
@ -74,11 +109,14 @@ const List = React.forwardRef(
spellCheck={false}
value={listName}
/>
<ListExtraMenuButtonWrapper ref={$extraActionsRef} onClick={handleExtraMenuOpen}>
<Ellipsis size={16} color="#c2c6dc" />
</ListExtraMenuButtonWrapper>
</Header>
{children && children}
<AddCardContainer hidden={isComposerOpen}>
<AddCardButton onClick={() => onOpenComposer(id)}>
<FontAwesomeIcon icon={faPlus} size="xs" color="#42526e" />
<Plus size={12} color="#42526e" />
<AddCardButtonText>Add another card</AddCardButtonText>
</AddCardButton>
</AddCardContainer>

View File

@ -0,0 +1,17 @@
import React from 'react';
import ListActions from '.';
export default {
component: ListActions,
title: 'ListActions',
parameters: {
backgrounds: [
{ name: 'white', value: '#ffffff', default: true },
{ name: 'gray', value: '#f8f8f8' },
],
},
};
export const Default = () => {
return <ListActions taskGroupID="1" />;
};

View File

@ -0,0 +1,35 @@
import styled from 'styled-components';
export const ListActionsWrapper = styled.ul`
list-style-type: none;
margin: 0;
padding: 0;
`;
export const ListActionItemWrapper = styled.li`
margin: 0;
padding: 0;
`;
export const ListActionItem = styled.span`
cursor: pointer;
display: block;
font-size: 14px;
color: #172b4d;
font-weight: 400;
padding: 6px 12px;
position: relative;
margin: 0 -12px;
text-decoration: none;
&:hover {
background-color: rgba(9, 30, 66, 0.04);
}
`;
export const ListSeparator = styled.hr`
background-color: rgba(9, 30, 66, 0.13);
border: 0;
height: 1px;
margin: 8px 0;
padding: 0;
width: 100%;
`;

View File

@ -0,0 +1,48 @@
import React from 'react';
import { ListActionsWrapper, ListActionItemWrapper, ListActionItem, ListSeparator } from './Styles';
type Props = {
taskGroupID: string;
};
const LabelManager = ({ taskGroupID }: Props) => {
return (
<>
<ListActionsWrapper>
<ListActionItemWrapper>
<ListActionItem>Add card...</ListActionItem>
</ListActionItemWrapper>
<ListActionItemWrapper>
<ListActionItem>Copy List...</ListActionItem>
</ListActionItemWrapper>
<ListActionItemWrapper>
<ListActionItem>Move card...</ListActionItem>
</ListActionItemWrapper>
<ListActionItemWrapper>
<ListActionItem>Watch</ListActionItem>
</ListActionItemWrapper>
</ListActionsWrapper>
<ListSeparator />
<ListActionsWrapper>
<ListActionItemWrapper>
<ListActionItem>Sort By...</ListActionItem>
</ListActionItemWrapper>
</ListActionsWrapper>
<ListSeparator />
<ListActionsWrapper>
<ListActionItemWrapper>
<ListActionItem>Move All Cards in This List...</ListActionItem>
</ListActionItemWrapper>
<ListActionItemWrapper>
<ListActionItem>Archive All Cards in This List...</ListActionItem>
</ListActionItemWrapper>
</ListActionsWrapper>
<ListSeparator />
<ListActionsWrapper>
<ListActionItemWrapper>
<ListActionItem>Archive This List</ListActionItem>
</ListActionItemWrapper>
</ListActionsWrapper>
</>
);
};
export default LabelManager;

View File

@ -7,8 +7,8 @@ export default {
title: 'Lists',
parameters: {
backgrounds: [
{ name: 'white', value: '#ffffff', default: true },
{ name: 'gray', value: '#f8f8f8' },
{ name: 'gray', value: '#262c49', default: true },
{ name: 'white', value: '#ffffff' },
],
},
};
@ -33,28 +33,28 @@ const initialListsData = {
tasks: {
'task-1': {
taskID: 'task-1',
taskGroupID: 'column-1',
taskGroup: { taskGroupID: 'column-1' },
name: 'Create roadmap',
position: 2,
labels: [],
},
'task-2': {
taskID: 'task-2',
taskGroupID: 'column-1',
taskGroup: { taskGroupID: 'column-1' },
position: 1,
name: 'Create authentication',
labels: [],
},
'task-3': {
taskID: 'task-3',
taskGroupID: 'column-1',
taskGroup: { taskGroupID: 'column-1' },
position: 3,
name: 'Create login',
labels: [],
},
'task-4': {
taskID: 'task-4',
taskGroupID: 'column-1',
taskGroup: { taskGroupID: 'column-1' },
position: 4,
name: 'Create plugins',
labels: [],
@ -93,6 +93,29 @@ export const Default = () => {
onCardDrop={onCardDrop}
onListDrop={onListDrop}
onCardCreate={action('card create')}
onCreateList={listName => {
const [lastColumn] = Object.values(listsData.columns)
.sort((a, b) => a.position - b.position)
.slice(-1);
let position = 1;
if (lastColumn) {
position = lastColumn.position + 1;
}
const taskGroupID = Math.random().toString();
const newListsData = {
...listsData,
columns: {
...listsData.columns,
[taskGroupID]: {
taskGroupID,
name: listName,
position,
tasks: [],
},
},
};
setListsData(newListsData);
}}
/>
);
};
@ -121,28 +144,28 @@ const initialListsDataLarge = {
tasks: {
'task-1': {
taskID: 'task-1',
taskGroupID: 'column-1',
taskGroup: { taskGroupID: 'column-1' },
name: 'Create roadmap',
position: 2,
labels: [],
},
'task-2': {
taskID: 'task-2',
taskGroupID: 'column-1',
taskGroup: { taskGroupID: 'column-1' },
position: 1,
name: 'Create authentication',
labels: [],
},
'task-3': {
taskID: 'task-3',
taskGroupID: 'column-1',
taskGroup: { taskGroupID: 'column-1' },
position: 3,
name: 'Create login',
labels: [],
},
'task-4': {
taskID: 'task-4',
taskGroupID: 'column-1',
taskGroup: { taskGroupID: 'column-1' },
position: 4,
name: 'Create plugins',
labels: [],
@ -179,6 +202,7 @@ export const ListsWithManyList = () => {
onCardCreate={action('card create')}
onCardDrop={onCardDrop}
onListDrop={onListDrop}
onCreateList={action('create list')}
/>
);
};

View File

@ -3,6 +3,7 @@ import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautif
import List, { ListCards } from 'shared/components/List';
import Card from 'shared/components/Card';
import CardComposer from 'shared/components/CardComposer';
import AddList from 'shared/components/AddList';
import {
isPositionChanged,
getSortedDraggables,
@ -26,9 +27,10 @@ type Props = {
onListDrop: any;
onCardCreate: (taskGroupID: string, name: string) => void;
onQuickEditorOpen: (e: ContextMenuEvent) => void;
onCreateList: (listName: string) => void;
};
const Lists = ({ columns, tasks, onCardDrop, onListDrop, onCardCreate, onQuickEditorOpen }: Props) => {
const Lists = ({ columns, tasks, onCardDrop, onListDrop, onCardCreate, onQuickEditorOpen, onCreateList }: Props) => {
const onDragEnd = ({ draggableId, source, destination, type }: DropResult) => {
if (typeof destination === 'undefined') return;
if (!isPositionChanged(source, destination)) return;
@ -74,7 +76,7 @@ const Lists = ({ columns, tasks, onCardDrop, onListDrop, onCardCreate, onQuickEd
<Container {...provided.droppableProps} ref={provided.innerRef}>
{orderedColumns.map((column: TaskGroup, index: number) => {
const columnCards = getSortedDraggables(
Object.values(tasks).filter((t: any) => t.taskGroupID === column.taskGroupID),
Object.values(tasks).filter((t: Task) => t.taskGroup.taskGroupID === column.taskGroupID),
);
return (
<Draggable draggableId={column.taskGroupID} key={column.taskGroupID} index={index}>
@ -91,6 +93,7 @@ const Lists = ({ columns, tasks, onCardDrop, onListDrop, onCardCreate, onQuickEd
ref={columnDragProvided.innerRef}
wrapperProps={columnDragProvided.draggableProps}
headerProps={columnDragProvided.dragHandleProps}
onExtraMenuOpen={(taskGroupID, pos, size) => console.log(taskGroupID, pos, size)}
>
<Droppable type="tasks" droppableId={column.taskGroupID}>
{columnDropProvided => (
@ -127,7 +130,6 @@ const Lists = ({ columns, tasks, onCardDrop, onListDrop, onCardCreate, onQuickEd
setCurrentComposer('');
}}
onCreateCard={name => {
setCurrentComposer('');
onCardCreate(column.taskGroupID, name);
}}
isOpen
@ -142,6 +144,7 @@ const Lists = ({ columns, tasks, onCardDrop, onListDrop, onCardCreate, onQuickEd
);
})}
{provided.placeholder}
<AddList onSave={onCreateList} />
</Container>
)}
</Droppable>

View File

@ -8,7 +8,7 @@ type Props = {
onLabelToggle: (labelId: string) => void;
onLabelEdit: (labelId: string, labelName: string, color: string) => void;
};
const LabelManager = ({ labels, onLabelToggle, onLabelEdit }: Props) => {
const LabelManager: React.FC<Props> = ({ labels, onLabelToggle, onLabelEdit }) => {
const [currentLabel, setCurrentLabel] = useState('');
return (
<>

View File

@ -1,7 +1,10 @@
import React, { useState } from 'react';
import React, { useState, useRef } from 'react';
import { action } from '@storybook/addon-actions';
import LabelColors from 'shared/constants/labelColors';
import MenuTypes from 'shared/constants/menuTypes';
import LabelManager from 'shared/components/PopupMenu/LabelManager';
import LabelEditor from 'shared/components/PopupMenu/LabelEditor';
import ListActions from 'shared/components/ListActions';
import PopupMenu from '.';
export default {
@ -34,16 +37,9 @@ export const LabelsPopup = () => {
return (
<>
{isPopupOpen && (
<PopupMenu
title="Label"
menuType={MenuTypes.LABEL_MANAGER}
top={10}
onClose={() => setPopupOpen(false)}
left={10}
onLabelEdit={action('label edit')}
onLabelToggle={action('label toggle')}
labels={labelData}
/>
<PopupMenu title="Label" top={10} onClose={() => setPopupOpen(false)} left={10}>
<LabelManager labels={labelData} onLabelToggle={action('label toggle')} onLabelEdit={action('label edit')} />
</PopupMenu>
)}
<button type="submit" onClick={() => setPopupOpen(true)}>
Open
@ -57,16 +53,9 @@ export const LabelsLabelEditor = () => {
return (
<>
{isPopupOpen && (
<PopupMenu
title="Change Label"
menuType={MenuTypes.LABEL_EDITOR}
top={10}
onClose={() => setPopupOpen(false)}
left={10}
onLabelEdit={action('label edit')}
onLabelToggle={action('label toggle')}
labels={labelData}
/>
<PopupMenu title="Change Label" top={10} onClose={() => setPopupOpen(false)} left={10}>
<LabelEditor label={labelData[0]} onLabelEdit={action('label edit')} />
</PopupMenu>
)}
<button type="submit" onClick={() => setPopupOpen(true)}>
Open
@ -74,3 +63,39 @@ export const LabelsLabelEditor = () => {
</>
);
};
const initalState = { left: 0, top: 0, isOpen: false };
export const ListActionsPopup = () => {
const $buttonRef = useRef<HTMLButtonElement>(null);
const [popupData, setPopupData] = useState(initalState);
return (
<>
{popupData.isOpen && (
<PopupMenu
title="List Actions"
top={popupData.top}
onClose={() => setPopupData(initalState)}
left={popupData.left}
>
<ListActions taskGroupID="1" />
</PopupMenu>
)}
<button
ref={$buttonRef}
type="submit"
onClick={() => {
if ($buttonRef && $buttonRef.current) {
const pos = $buttonRef.current.getBoundingClientRect();
setPopupData({
isOpen: true,
left: pos.left,
top: pos.top + pos.height + 10,
});
}
}}
>
Open
</button>
</>
);
};

View File

@ -1,25 +1,16 @@
import React, { useRef } from 'react';
import { Cross } from 'shared/icons';
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import MenuTypes from 'shared/constants/menuTypes';
import LabelColors from 'shared/constants/labelColors';
import LabelManager from './LabelManager';
import LabelEditor from './LabelEditor';
import { Container, Header, HeaderTitle, Content, Label, CloseButton } from './Styles';
import { Container, Header, HeaderTitle, Content, CloseButton } from './Styles';
type Props = {
title: string;
top: number;
left: number;
menuType: number;
labels?: Label[];
onClose: () => void;
onLabelToggle: (labelId: string) => void;
onLabelEdit: (labelId: string, labelName: string, color: string) => void;
};
const PopupMenu = ({ title, menuType, labels, top, left, onClose, onLabelToggle, onLabelEdit }: Props) => {
const PopupMenu: React.FC<Props> = ({ title, top, left, onClose, children }) => {
const $containerRef = useRef();
useOnOutsideClick($containerRef, true, onClose, null);
@ -31,17 +22,7 @@ const PopupMenu = ({ title, menuType, labels, top, left, onClose, onLabelToggle,
<Cross />
</CloseButton>
</Header>
<Content>
{menuType === MenuTypes.LABEL_MANAGER && (
<LabelManager onLabelEdit={onLabelEdit} onLabelToggle={onLabelToggle} labels={labels} />
)}
{menuType === MenuTypes.LABEL_EDITOR && (
<LabelEditor
onLabelEdit={onLabelEdit}
label={{ active: false, color: LabelColors.GREEN, name: 'General', labelId: 'general' }}
/>
)}
</Content>
<Content>{children}</Content>
</Container>
);
};

View File

@ -61,6 +61,7 @@ export const Default = () => {
onSaveName={action('on save name')}
onOpenComposer={action('on open composer')}
tasks={[]}
onExtraMenuOpen={(taskGroupID, pos, size) => console.log(taskGroupID, pos, size)}
>
<ListCards>
<Card

View File

@ -30,13 +30,15 @@ export const Default = () => {
<TaskDetails
task={{
taskID: '1',
taskGroupID: '1',
taskGroup: { taskGroupID: '1' },
name: 'Hello, world',
position: 1,
labels: [],
description,
}}
onTaskDescriptionChange={(_task, desc) => setDescription(desc)}
onDeleteTask={action('delete task')}
onCloseModal={action('close modal')}
/>
);
}}

View File

@ -68,7 +68,7 @@ const DetailsEditor: React.FC<DetailsEditorProps> = ({
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setDescription(e.currentTarget.value)}
/>
<TaskDetailsControls>
<ConfirmSave>Save</ConfirmSave>
<ConfirmSave onClick={handleOutsideClick}>Save</ConfirmSave>
<CancelEdit onClick={onCancel}>
<Cross size={16} />
</CancelEdit>
@ -80,22 +80,27 @@ const DetailsEditor: React.FC<DetailsEditorProps> = ({
type TaskDetailsProps = {
task: Task;
onTaskDescriptionChange: (task: Task, newDescription: string) => void;
onDeleteTask: (task: Task) => void;
onCloseModal: () => void;
};
const TaskDetails: React.FC<TaskDetailsProps> = ({ task, onTaskDescriptionChange }) => {
const TaskDetails: React.FC<TaskDetailsProps> = ({ task, onTaskDescriptionChange, onDeleteTask, onCloseModal }) => {
const [editorOpen, setEditorOpen] = useState(false);
const handleClick = () => {
setEditorOpen(!editorOpen);
};
const handleDeleteTask = () => {
onDeleteTask(task);
};
return (
<>
<TaskHeader>
<TaskMeta />
<TaskActions>
<TaskAction>
<TaskAction onClick={handleDeleteTask}>
<Bin size={20} />
</TaskAction>
<TaskAction>
<TaskAction onClick={onCloseModal}>
<Cross size={20} />
</TaskAction>
</TaskActions>

View File

@ -1,6 +1,7 @@
const MenuTypes = {
LABEL_MANAGER: 1,
LABEL_EDITOR: 2,
LIST_ACTIONS: 3,
};
export default MenuTypes;

View File

@ -13,6 +13,8 @@ export type Scalars = {
UUID: string;
};
export type RefreshToken = {
__typename?: 'RefreshToken';
tokenId: Scalars['ID'];
@ -68,7 +70,7 @@ export type TaskGroup = {
export type Task = {
__typename?: 'Task';
taskID: Scalars['ID'];
taskGroupID: Scalars['String'];
taskGroup: TaskGroup;
createdAt: Scalars['Time'];
name: Scalars['String'];
position: Scalars['Float'];
@ -97,14 +99,17 @@ export type Query = {
taskGroups: Array<TaskGroup>;
};
export type QueryFindUserArgs = {
input: FindUser;
};
export type QueryFindProjectArgs = {
input: FindProject;
};
export type QueryProjectsArgs = {
input?: Maybe<ProjectsFilter>;
};
@ -185,46 +190,57 @@ export type Mutation = {
deleteTask: DeleteTaskPayload;
};
export type MutationCreateRefreshTokenArgs = {
input: NewRefreshToken;
};
export type MutationCreateUserAccountArgs = {
input: NewUserAccount;
};
export type MutationCreateOrganizationArgs = {
input: NewOrganization;
};
export type MutationCreateTeamArgs = {
input: NewTeam;
};
export type MutationCreateProjectArgs = {
input: NewProject;
};
export type MutationCreateTaskGroupArgs = {
input: NewTaskGroup;
};
export type MutationCreateTaskArgs = {
input: NewTask;
};
export type MutationUpdateTaskLocationArgs = {
input: NewTaskLocation;
};
export type MutationLogoutUserArgs = {
input: LogoutUser;
};
export type MutationUpdateTaskNameArgs = {
input: UpdateTaskName;
};
export type MutationDeleteTaskArgs = {
input: DeleteTaskInput;
};
@ -235,45 +251,86 @@ export type CreateTaskMutationVariables = {
position: Scalars['Float'];
};
export type CreateTaskMutation = { __typename?: 'Mutation' } & {
createTask: { __typename?: 'Task' } & Pick<Task, 'taskID' | 'taskGroupID' | 'name' | 'position'>;
export type CreateTaskMutation = (
{ __typename?: 'Mutation' }
& { createTask: (
{ __typename?: 'Task' }
& Pick<Task, 'taskID' | 'name' | 'position'>
& { taskGroup: (
{ __typename?: 'TaskGroup' }
& Pick<TaskGroup, 'taskGroupID'>
) }
) }
);
export type CreateTaskGroupMutationVariables = {
projectID: Scalars['String'];
name: Scalars['String'];
position: Scalars['Float'];
};
export type CreateTaskGroupMutation = (
{ __typename?: 'Mutation' }
& { createTaskGroup: (
{ __typename?: 'TaskGroup' }
& Pick<TaskGroup, 'taskGroupID' | 'name' | 'position'>
) }
);
export type DeleteTaskMutationVariables = {
taskID: Scalars['String'];
};
export type DeleteTaskMutation = { __typename?: 'Mutation' } & {
deleteTask: { __typename?: 'DeleteTaskPayload' } & Pick<DeleteTaskPayload, 'taskID'>;
};
export type DeleteTaskMutation = (
{ __typename?: 'Mutation' }
& { deleteTask: (
{ __typename?: 'DeleteTaskPayload' }
& Pick<DeleteTaskPayload, 'taskID'>
) }
);
export type FindProjectQueryVariables = {
projectId: Scalars['String'];
};
export type FindProjectQuery = { __typename?: 'Query' } & {
findProject: { __typename?: 'Project' } & Pick<Project, 'name'> & {
taskGroups: Array<
{ __typename?: 'TaskGroup' } & Pick<TaskGroup, 'taskGroupID' | 'name' | 'position'> & {
tasks: Array<{ __typename?: 'Task' } & Pick<Task, 'taskID' | 'name' | 'position'>>;
}
>;
};
};
export type FindProjectQuery = (
{ __typename?: 'Query' }
& { findProject: (
{ __typename?: 'Project' }
& Pick<Project, 'name'>
& { taskGroups: Array<(
{ __typename?: 'TaskGroup' }
& Pick<TaskGroup, 'taskGroupID' | 'name' | 'position'>
& { tasks: Array<(
{ __typename?: 'Task' }
& Pick<Task, 'taskID' | 'name' | 'position'>
)> }
)> }
) }
);
export type GetProjectsQueryVariables = {};
export type GetProjectsQuery = { __typename?: 'Query' } & {
organizations: Array<
{ __typename?: 'Organization' } & Pick<Organization, 'name'> & {
teams: Array<
{ __typename?: 'Team' } & Pick<Team, 'name'> & {
projects: Array<{ __typename?: 'Project' } & Pick<Project, 'name' | 'projectID'>>;
}
>;
}
>;
};
export type GetProjectsQuery = (
{ __typename?: 'Query' }
& { organizations: Array<(
{ __typename?: 'Organization' }
& Pick<Organization, 'name'>
& { teams: Array<(
{ __typename?: 'Team' }
& Pick<Team, 'name'>
& { projects: Array<(
{ __typename?: 'Project' }
& Pick<Project, 'name' | 'projectID'>
)> }
)> }
)> }
);
export type UpdateTaskLocationMutationVariables = {
taskID: Scalars['String'];
@ -281,24 +338,37 @@ export type UpdateTaskLocationMutationVariables = {
position: Scalars['Float'];
};
export type UpdateTaskLocationMutation = { __typename?: 'Mutation' } & {
updateTaskLocation: { __typename?: 'Task' } & Pick<Task, 'taskID' | 'createdAt' | 'name' | 'position'>;
};
export type UpdateTaskLocationMutation = (
{ __typename?: 'Mutation' }
& { updateTaskLocation: (
{ __typename?: 'Task' }
& Pick<Task, 'taskID' | 'createdAt' | 'name' | 'position'>
) }
);
export type UpdateTaskNameMutationVariables = {
taskID: Scalars['String'];
name: Scalars['String'];
};
export type UpdateTaskNameMutation = { __typename?: 'Mutation' } & {
updateTaskName: { __typename?: 'Task' } & Pick<Task, 'taskID' | 'name' | 'position'>;
};
export type UpdateTaskNameMutation = (
{ __typename?: 'Mutation' }
& { updateTaskName: (
{ __typename?: 'Task' }
& Pick<Task, 'taskID' | 'name' | 'position'>
) }
);
export const CreateTaskDocument = gql`
mutation createTask($taskGroupID: String!, $name: String!, $position: Float!) {
createTask(input: {taskGroupID: $taskGroupID, name: $name, position: $position}) {
taskID
taskGroup {
taskGroupID
}
name
position
}
@ -325,17 +395,48 @@ export type CreateTaskMutationFn = ApolloReactCommon.MutationFunction<CreateTask
* },
* });
*/
export function useCreateTaskMutation(
baseOptions?: ApolloReactHooks.MutationHookOptions<CreateTaskMutation, CreateTaskMutationVariables>,
) {
export function useCreateTaskMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<CreateTaskMutation, CreateTaskMutationVariables>) {
return ApolloReactHooks.useMutation<CreateTaskMutation, CreateTaskMutationVariables>(CreateTaskDocument, baseOptions);
}
export type CreateTaskMutationHookResult = ReturnType<typeof useCreateTaskMutation>;
export type CreateTaskMutationResult = ApolloReactCommon.MutationResult<CreateTaskMutation>;
export type CreateTaskMutationOptions = ApolloReactCommon.BaseMutationOptions<
CreateTaskMutation,
CreateTaskMutationVariables
>;
export type CreateTaskMutationOptions = ApolloReactCommon.BaseMutationOptions<CreateTaskMutation, CreateTaskMutationVariables>;
export const CreateTaskGroupDocument = gql`
mutation createTaskGroup($projectID: String!, $name: String!, $position: Float!) {
createTaskGroup(input: {projectID: $projectID, name: $name, position: $position}) {
taskGroupID
name
position
}
}
`;
export type CreateTaskGroupMutationFn = ApolloReactCommon.MutationFunction<CreateTaskGroupMutation, CreateTaskGroupMutationVariables>;
/**
* __useCreateTaskGroupMutation__
*
* To run a mutation, you first call `useCreateTaskGroupMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useCreateTaskGroupMutation` 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 [createTaskGroupMutation, { data, loading, error }] = useCreateTaskGroupMutation({
* variables: {
* projectID: // value for 'projectID'
* name: // value for 'name'
* position: // value for 'position'
* },
* });
*/
export function useCreateTaskGroupMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<CreateTaskGroupMutation, CreateTaskGroupMutationVariables>) {
return ApolloReactHooks.useMutation<CreateTaskGroupMutation, CreateTaskGroupMutationVariables>(CreateTaskGroupDocument, baseOptions);
}
export type CreateTaskGroupMutationHookResult = ReturnType<typeof useCreateTaskGroupMutation>;
export type CreateTaskGroupMutationResult = ApolloReactCommon.MutationResult<CreateTaskGroupMutation>;
export type CreateTaskGroupMutationOptions = ApolloReactCommon.BaseMutationOptions<CreateTaskGroupMutation, CreateTaskGroupMutationVariables>;
export const DeleteTaskDocument = gql`
mutation deleteTask($taskID: String!) {
deleteTask(input: {taskID: $taskID}) {
@ -362,17 +463,12 @@ export type DeleteTaskMutationFn = ApolloReactCommon.MutationFunction<DeleteTask
* },
* });
*/
export function useDeleteTaskMutation(
baseOptions?: ApolloReactHooks.MutationHookOptions<DeleteTaskMutation, DeleteTaskMutationVariables>,
) {
export function useDeleteTaskMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<DeleteTaskMutation, DeleteTaskMutationVariables>) {
return ApolloReactHooks.useMutation<DeleteTaskMutation, DeleteTaskMutationVariables>(DeleteTaskDocument, baseOptions);
}
export type DeleteTaskMutationHookResult = ReturnType<typeof useDeleteTaskMutation>;
export type DeleteTaskMutationResult = ApolloReactCommon.MutationResult<DeleteTaskMutation>;
export type DeleteTaskMutationOptions = ApolloReactCommon.BaseMutationOptions<
DeleteTaskMutation,
DeleteTaskMutationVariables
>;
export type DeleteTaskMutationOptions = ApolloReactCommon.BaseMutationOptions<DeleteTaskMutation, DeleteTaskMutationVariables>;
export const FindProjectDocument = gql`
query findProject($projectId: String!) {
findProject(input: {projectId: $projectId}) {
@ -407,14 +503,10 @@ export const FindProjectDocument = gql`
* },
* });
*/
export function useFindProjectQuery(
baseOptions?: ApolloReactHooks.QueryHookOptions<FindProjectQuery, FindProjectQueryVariables>,
) {
export function useFindProjectQuery(baseOptions?: ApolloReactHooks.QueryHookOptions<FindProjectQuery, FindProjectQueryVariables>) {
return ApolloReactHooks.useQuery<FindProjectQuery, FindProjectQueryVariables>(FindProjectDocument, baseOptions);
}
export function useFindProjectLazyQuery(
baseOptions?: ApolloReactHooks.LazyQueryHookOptions<FindProjectQuery, FindProjectQueryVariables>,
) {
export function useFindProjectLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions<FindProjectQuery, FindProjectQueryVariables>) {
return ApolloReactHooks.useLazyQuery<FindProjectQuery, FindProjectQueryVariables>(FindProjectDocument, baseOptions);
}
export type FindProjectQueryHookResult = ReturnType<typeof useFindProjectQuery>;
@ -450,14 +542,10 @@ export const GetProjectsDocument = gql`
* },
* });
*/
export function useGetProjectsQuery(
baseOptions?: ApolloReactHooks.QueryHookOptions<GetProjectsQuery, GetProjectsQueryVariables>,
) {
export function useGetProjectsQuery(baseOptions?: ApolloReactHooks.QueryHookOptions<GetProjectsQuery, GetProjectsQueryVariables>) {
return ApolloReactHooks.useQuery<GetProjectsQuery, GetProjectsQueryVariables>(GetProjectsDocument, baseOptions);
}
export function useGetProjectsLazyQuery(
baseOptions?: ApolloReactHooks.LazyQueryHookOptions<GetProjectsQuery, GetProjectsQueryVariables>,
) {
export function useGetProjectsLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions<GetProjectsQuery, GetProjectsQueryVariables>) {
return ApolloReactHooks.useLazyQuery<GetProjectsQuery, GetProjectsQueryVariables>(GetProjectsDocument, baseOptions);
}
export type GetProjectsQueryHookResult = ReturnType<typeof useGetProjectsQuery>;
@ -473,10 +561,7 @@ export const UpdateTaskLocationDocument = gql`
}
}
`;
export type UpdateTaskLocationMutationFn = ApolloReactCommon.MutationFunction<
UpdateTaskLocationMutation,
UpdateTaskLocationMutationVariables
>;
export type UpdateTaskLocationMutationFn = ApolloReactCommon.MutationFunction<UpdateTaskLocationMutation, UpdateTaskLocationMutationVariables>;
/**
* __useUpdateTaskLocationMutation__
@ -497,20 +582,12 @@ export type UpdateTaskLocationMutationFn = ApolloReactCommon.MutationFunction<
* },
* });
*/
export function useUpdateTaskLocationMutation(
baseOptions?: ApolloReactHooks.MutationHookOptions<UpdateTaskLocationMutation, UpdateTaskLocationMutationVariables>,
) {
return ApolloReactHooks.useMutation<UpdateTaskLocationMutation, UpdateTaskLocationMutationVariables>(
UpdateTaskLocationDocument,
baseOptions,
);
export function useUpdateTaskLocationMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<UpdateTaskLocationMutation, UpdateTaskLocationMutationVariables>) {
return ApolloReactHooks.useMutation<UpdateTaskLocationMutation, UpdateTaskLocationMutationVariables>(UpdateTaskLocationDocument, baseOptions);
}
export type UpdateTaskLocationMutationHookResult = ReturnType<typeof useUpdateTaskLocationMutation>;
export type UpdateTaskLocationMutationResult = ApolloReactCommon.MutationResult<UpdateTaskLocationMutation>;
export type UpdateTaskLocationMutationOptions = ApolloReactCommon.BaseMutationOptions<
UpdateTaskLocationMutation,
UpdateTaskLocationMutationVariables
>;
export type UpdateTaskLocationMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateTaskLocationMutation, UpdateTaskLocationMutationVariables>;
export const UpdateTaskNameDocument = gql`
mutation updateTaskName($taskID: String!, $name: String!) {
updateTaskName(input: {taskID: $taskID, name: $name}) {
@ -520,10 +597,7 @@ export const UpdateTaskNameDocument = gql`
}
}
`;
export type UpdateTaskNameMutationFn = ApolloReactCommon.MutationFunction<
UpdateTaskNameMutation,
UpdateTaskNameMutationVariables
>;
export type UpdateTaskNameMutationFn = ApolloReactCommon.MutationFunction<UpdateTaskNameMutation, UpdateTaskNameMutationVariables>;
/**
* __useUpdateTaskNameMutation__
@ -543,17 +617,9 @@ export type UpdateTaskNameMutationFn = ApolloReactCommon.MutationFunction<
* },
* });
*/
export function useUpdateTaskNameMutation(
baseOptions?: ApolloReactHooks.MutationHookOptions<UpdateTaskNameMutation, UpdateTaskNameMutationVariables>,
) {
return ApolloReactHooks.useMutation<UpdateTaskNameMutation, UpdateTaskNameMutationVariables>(
UpdateTaskNameDocument,
baseOptions,
);
export function useUpdateTaskNameMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<UpdateTaskNameMutation, UpdateTaskNameMutationVariables>) {
return ApolloReactHooks.useMutation<UpdateTaskNameMutation, UpdateTaskNameMutationVariables>(UpdateTaskNameDocument, baseOptions);
}
export type UpdateTaskNameMutationHookResult = ReturnType<typeof useUpdateTaskNameMutation>;
export type UpdateTaskNameMutationResult = ApolloReactCommon.MutationResult<UpdateTaskNameMutation>;
export type UpdateTaskNameMutationOptions = ApolloReactCommon.BaseMutationOptions<
UpdateTaskNameMutation,
UpdateTaskNameMutationVariables
>;
export type UpdateTaskNameMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateTaskNameMutation, UpdateTaskNameMutationVariables>;

View File

@ -1,7 +1,9 @@
mutation createTask($taskGroupID: String!, $name: String!, $position: Float!) {
createTask(input: { taskGroupID: $taskGroupID, name: $name, position: $position }) {
taskID
taskGroup {
taskGroupID
}
name
position
}

View File

@ -0,0 +1,9 @@
mutation createTaskGroup( $projectID: String!, $name: String!, $position: Float! ) {
createTaskGroup(
input: { projectID: $projectID, name: $name, position: $position }
) {
taskGroupID
name
position
}
}

View File

@ -0,0 +1,21 @@
import React from 'react';
type Props = {
size: number | string;
color: string;
};
const Ellipsis = ({ size, color }: Props) => {
return (
<svg fill={color} xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 512 512">
<path d="M328 256c0 39.8-32.2 72-72 72s-72-32.2-72-72 32.2-72 72-72 72 32.2 72 72zm104-72c-39.8 0-72 32.2-72 72s32.2 72 72 72 72-32.2 72-72-32.2-72-72-72zm-352 0c-39.8 0-72 32.2-72 72s32.2 72 72 72 72-32.2 72-72-32.2-72-72-72z" />
</svg>
);
};
Ellipsis.defaultProps = {
size: 16,
color: '#000',
};
export default Ellipsis;

View File

@ -0,0 +1,21 @@
import React from 'react';
type Props = {
size: number | string;
color: string;
};
const Plus = ({ size, color }: Props) => {
return (
<svg fill={color} xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 16 16">
<path d="M15.5 6h-5.5v-5.5c0-0.276-0.224-0.5-0.5-0.5h-3c-0.276 0-0.5 0.224-0.5 0.5v5.5h-5.5c-0.276 0-0.5 0.224-0.5 0.5v3c0 0.276 0.224 0.5 0.5 0.5h5.5v5.5c0 0.276 0.224 0.5 0.5 0.5h3c0.276 0 0.5-0.224 0.5-0.5v-5.5h5.5c0.276 0 0.5-0.224 0.5-0.5v-3c0-0.276-0.224-0.5-0.5-0.5z" />
</svg>
);
};
Plus.defaultProps = {
size: 16,
color: '#000',
};
export default Plus;

View File

@ -1,4 +1,5 @@
import Cross from './Cross';
import Plus from './Plus';
import Bell from './Bell';
import Bin from './Bin';
import Pencil from './Pencil';
@ -11,5 +12,6 @@ import Home from './Home';
import Stack from './Stack';
import Question from './Question';
import Exit from './Exit';
import Ellipsis from './Ellipsis';
export { Cross, Bell, Bin, Exit, Pencil, Stack, Question, Home, Citadel, Checkmark, User, Users, Lock };
export { Cross, Plus, Bell, Ellipsis, Bin, Exit, Pencil, Stack, Question, Home, Citadel, Checkmark, User, Users, Lock };