diff --git a/web/src/Projects/Project/index.tsx b/web/src/Projects/Project/index.tsx index f0f0fa9..efc6182 100644 --- a/web/src/Projects/Project/index.tsx +++ b/web/src/Projects/Project/index.tsx @@ -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(); 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, - taskGroupID: taskGroup.taskGroupID, + 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 } }); + }} /> @@ -208,7 +246,7 @@ const Project = () => { setQuickCardEditor(initialQuickCardEditorState)} onEditCard={(_listId: string, cardId: string, cardName: string) => { @@ -221,6 +259,16 @@ const Project = () => { left={quickCardEditor.left} /> )} + {popupData.isOpen && ( + setPopupData(initialPopupState)} + left={popupData.left} + > + + + )} ); } diff --git a/web/src/citadel.d.ts b/web/src/citadel.d.ts index e84a5da..ffde3c4 100644 --- a/web/src/citadel.d.ts +++ b/web/src/citadel.d.ts @@ -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; +}; diff --git a/web/src/shared/components/AddList/AddList.stories.tsx b/web/src/shared/components/AddList/AddList.stories.tsx new file mode 100644 index 0000000..4b795c6 --- /dev/null +++ b/web/src/shared/components/AddList/AddList.stories.tsx @@ -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 ; +}; + diff --git a/web/src/shared/components/AddList/Styles.ts b/web/src/shared/components/AddList/Styles.ts new file mode 100644 index 0000000..7ff1472 --- /dev/null +++ b/web/src/shared/components/AddList/Styles.ts @@ -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; +`; diff --git a/web/src/shared/components/AddList/index.tsx b/web/src/shared/components/AddList/index.tsx new file mode 100644 index 0000000..8a143e9 --- /dev/null +++ b/web/src/shared/components/AddList/index.tsx @@ -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 = ({ onSave, onCancel }) => { + const $editorRef = useRef(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 ( + <> + + ) => setListName(e.currentTarget.value)} + /> + + + { + onSave(listName); + setListName(''); + if ($editorRef && $editorRef.current) { + $editorRef.current.focus(); + } + }} + > + Save + + onCancel()}> + + + + + ); +}; + +type AddListProps = { + onSave: (listName: string) => void; +}; + +const AddList: React.FC = ({ onSave }) => { + const [editorOpen, setEditorOpen] = useState(false); + const $wrapperRef = useRef(null); + const onOutsideClick = () => { + setEditorOpen(false); + }; + useOnOutsideClick($wrapperRef, editorOpen, onOutsideClick, null); + + return ( + { + if (!editorOpen) { + setEditorOpen(true); + } + }} + > + {editorOpen ? ( + setEditorOpen(false)} onSave={onSave} /> + ) : ( + + + + + Add another list + + )} + + ); +}; + +export default AddList; diff --git a/web/src/shared/components/CardComposer/index.tsx b/web/src/shared/components/CardComposer/index.tsx index 6407b07..faffeef 100644 --- a/web/src/shared/components/CardComposer/index.tsx +++ b/web/src/shared/components/CardComposer/index.tsx @@ -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) => { 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) => { - Add Card + Add Card diff --git a/web/src/shared/components/List/List.stories.tsx b/web/src/shared/components/List/List.stories.tsx index 8cbfed1..deb0d76 100644 --- a/web/src/shared/components/List/List.stories.tsx +++ b/web/src/shared/components/List/List.stories.tsx @@ -59,6 +59,7 @@ export const Default = () => { onSaveName={action('on save name')} onOpenComposer={action('on open composer')} tasks={[]} + onExtraMenuOpen={action('extra menu open')} > { onSaveName={action('on save name')} onOpenComposer={action('on open composer')} tasks={[]} + onExtraMenuOpen={action('extra menu open')} > { onSaveName={action('on save name')} onOpenComposer={action('on open composer')} tasks={[]} + onExtraMenuOpen={action('extra menu open')} > { onSaveName={action('on save name')} onOpenComposer={action('on open composer')} tasks={[]} + onExtraMenuOpen={action('extra menu open')} > 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(); + const $listNameRef = useRef(null); + const $extraActionsRef = useRef(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 = () => { - $listNameRef.current.blur(); + if ($listNameRef && $listNameRef.current) { + $listNameRef.current.blur(); + } }; const onChange = (event: React.FormEvent): void => { setListName(event.currentTarget.value); @@ -56,7 +70,28 @@ const List = React.forwardRef( const onKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { e.preventDefault(); - $listNameRef.current.blur(); + 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} /> + + + {children && children} diff --git a/web/src/shared/components/ListActions/ListActions.stories.tsx b/web/src/shared/components/ListActions/ListActions.stories.tsx new file mode 100644 index 0000000..9fb88fd --- /dev/null +++ b/web/src/shared/components/ListActions/ListActions.stories.tsx @@ -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 ; +}; diff --git a/web/src/shared/components/ListActions/Styles.ts b/web/src/shared/components/ListActions/Styles.ts new file mode 100644 index 0000000..0130120 --- /dev/null +++ b/web/src/shared/components/ListActions/Styles.ts @@ -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%; +`; diff --git a/web/src/shared/components/ListActions/index.tsx b/web/src/shared/components/ListActions/index.tsx new file mode 100644 index 0000000..6cce235 --- /dev/null +++ b/web/src/shared/components/ListActions/index.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { ListActionsWrapper, ListActionItemWrapper, ListActionItem, ListSeparator } from './Styles'; + +type Props = { + taskGroupID: string; +}; +const LabelManager = ({ taskGroupID }: Props) => { + return ( + <> + + + Add card... + + + Copy List... + + + Move card... + + + Watch + + + + + + Sort By... + + + + + + Move All Cards in This List... + + + Archive All Cards in This List... + + + + + + Archive This List + + + + ); +}; +export default LabelManager; diff --git a/web/src/shared/components/Lists/Lists.stories.tsx b/web/src/shared/components/Lists/Lists.stories.tsx index a61bff6..2c06b3b 100644 --- a/web/src/shared/components/Lists/Lists.stories.tsx +++ b/web/src/shared/components/Lists/Lists.stories.tsx @@ -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')} /> ); }; diff --git a/web/src/shared/components/Lists/index.tsx b/web/src/shared/components/Lists/index.tsx index 9aa3fdb..cb76c69 100644 --- a/web/src/shared/components/Lists/index.tsx +++ b/web/src/shared/components/Lists/index.tsx @@ -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 {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 ( @@ -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)} > {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} + )} diff --git a/web/src/shared/components/PopupMenu/LabelManager.tsx b/web/src/shared/components/PopupMenu/LabelManager.tsx index a568615..2f07e0d 100644 --- a/web/src/shared/components/PopupMenu/LabelManager.tsx +++ b/web/src/shared/components/PopupMenu/LabelManager.tsx @@ -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 = ({ labels, onLabelToggle, onLabelEdit }) => { const [currentLabel, setCurrentLabel] = useState(''); return ( <> diff --git a/web/src/shared/components/PopupMenu/PopupMenu.stories.tsx b/web/src/shared/components/PopupMenu/PopupMenu.stories.tsx index 5c61750..4a24782 100644 --- a/web/src/shared/components/PopupMenu/PopupMenu.stories.tsx +++ b/web/src/shared/components/PopupMenu/PopupMenu.stories.tsx @@ -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 && ( - setPopupOpen(false)} - left={10} - onLabelEdit={action('label edit')} - onLabelToggle={action('label toggle')} - labels={labelData} - /> + setPopupOpen(false)} left={10}> + + )} + + ); +}; diff --git a/web/src/shared/components/PopupMenu/index.tsx b/web/src/shared/components/PopupMenu/index.tsx index 7953770..8761c21 100644 --- a/web/src/shared/components/PopupMenu/index.tsx +++ b/web/src/shared/components/PopupMenu/index.tsx @@ -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 = ({ 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, - - {menuType === MenuTypes.LABEL_MANAGER && ( - - )} - {menuType === MenuTypes.LABEL_EDITOR && ( - - )} - + {children} ); }; diff --git a/web/src/shared/components/QuickCardEditor/QuickCardEditor.stories.tsx b/web/src/shared/components/QuickCardEditor/QuickCardEditor.stories.tsx index 50c17ea..8b0bd75 100644 --- a/web/src/shared/components/QuickCardEditor/QuickCardEditor.stories.tsx +++ b/web/src/shared/components/QuickCardEditor/QuickCardEditor.stories.tsx @@ -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)} > { setDescription(desc)} + onDeleteTask={action('delete task')} + onCloseModal={action('close modal')} /> ); }} diff --git a/web/src/shared/components/TaskDetails/index.tsx b/web/src/shared/components/TaskDetails/index.tsx index 0e43368..8470e67 100644 --- a/web/src/shared/components/TaskDetails/index.tsx +++ b/web/src/shared/components/TaskDetails/index.tsx @@ -68,7 +68,7 @@ const DetailsEditor: React.FC = ({ onChange={(e: React.ChangeEvent) => setDescription(e.currentTarget.value)} /> - Save + Save @@ -80,22 +80,27 @@ const DetailsEditor: React.FC = ({ type TaskDetailsProps = { task: Task; onTaskDescriptionChange: (task: Task, newDescription: string) => void; + onDeleteTask: (task: Task) => void; + onCloseModal: () => void; }; -const TaskDetails: React.FC = ({ task, onTaskDescriptionChange }) => { +const TaskDetails: React.FC = ({ task, onTaskDescriptionChange, onDeleteTask, onCloseModal }) => { const [editorOpen, setEditorOpen] = useState(false); const handleClick = () => { setEditorOpen(!editorOpen); }; + const handleDeleteTask = () => { + onDeleteTask(task); + }; return ( <> - + - + diff --git a/web/src/shared/constants/menuTypes.ts b/web/src/shared/constants/menuTypes.ts index 352b6bf..74275f0 100644 --- a/web/src/shared/constants/menuTypes.ts +++ b/web/src/shared/constants/menuTypes.ts @@ -1,6 +1,7 @@ const MenuTypes = { LABEL_MANAGER: 1, LABEL_EDITOR: 2, + LIST_ACTIONS: 3, }; export default MenuTypes; diff --git a/web/src/shared/generated/graphql.tsx b/web/src/shared/generated/graphql.tsx index c177acc..5884d35 100644 --- a/web/src/shared/generated/graphql.tsx +++ b/web/src/shared/generated/graphql.tsx @@ -13,8 +13,10 @@ export type Scalars = { UUID: string; }; + + export type RefreshToken = { - __typename?: 'RefreshToken'; + __typename?: 'RefreshToken'; tokenId: Scalars['ID']; userId: Scalars['UUID']; expiresAt: Scalars['Time']; @@ -22,7 +24,7 @@ export type RefreshToken = { }; export type UserAccount = { - __typename?: 'UserAccount'; + __typename?: 'UserAccount'; userID: Scalars['ID']; email: Scalars['String']; createdAt: Scalars['Time']; @@ -31,7 +33,7 @@ export type UserAccount = { }; export type Organization = { - __typename?: 'Organization'; + __typename?: 'Organization'; organizationID: Scalars['ID']; createdAt: Scalars['Time']; name: Scalars['String']; @@ -39,7 +41,7 @@ export type Organization = { }; export type Team = { - __typename?: 'Team'; + __typename?: 'Team'; teamID: Scalars['ID']; createdAt: Scalars['Time']; name: Scalars['String']; @@ -47,7 +49,7 @@ export type Team = { }; export type Project = { - __typename?: 'Project'; + __typename?: 'Project'; projectID: Scalars['ID']; teamID: Scalars['String']; createdAt: Scalars['Time']; @@ -56,7 +58,7 @@ export type Project = { }; export type TaskGroup = { - __typename?: 'TaskGroup'; + __typename?: 'TaskGroup'; taskGroupID: Scalars['ID']; projectID: Scalars['String']; createdAt: Scalars['Time']; @@ -66,9 +68,9 @@ export type TaskGroup = { }; export type Task = { - __typename?: 'Task'; + __typename?: 'Task'; taskID: Scalars['ID']; - taskGroupID: Scalars['String']; + taskGroup: TaskGroup; createdAt: Scalars['Time']; name: Scalars['String']; position: Scalars['Float']; @@ -87,7 +89,7 @@ export type FindProject = { }; export type Query = { - __typename?: 'Query'; + __typename?: 'Query'; organizations: Array; users: Array; findUser: UserAccount; @@ -97,14 +99,17 @@ export type Query = { taskGroups: Array; }; + export type QueryFindUserArgs = { input: FindUser; }; + export type QueryFindProjectArgs = { input: FindProject; }; + export type QueryProjectsArgs = { input?: Maybe; }; @@ -161,7 +166,7 @@ export type DeleteTaskInput = { }; export type DeleteTaskPayload = { - __typename?: 'DeleteTaskPayload'; + __typename?: 'DeleteTaskPayload'; taskID: Scalars['String']; }; @@ -171,7 +176,7 @@ export type UpdateTaskName = { }; export type Mutation = { - __typename?: 'Mutation'; + __typename?: 'Mutation'; createRefreshToken: RefreshToken; createUserAccount: UserAccount; createOrganization: Organization; @@ -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; + +export type CreateTaskMutation = ( + { __typename?: 'Mutation' } + & { createTask: ( + { __typename?: 'Task' } + & Pick + & { taskGroup: ( + { __typename?: 'TaskGroup' } + & Pick + ) } + ) } +); + +export type CreateTaskGroupMutationVariables = { + projectID: Scalars['String']; + name: Scalars['String']; + position: Scalars['Float']; }; + +export type CreateTaskGroupMutation = ( + { __typename?: 'Mutation' } + & { createTaskGroup: ( + { __typename?: 'TaskGroup' } + & Pick + ) } +); + export type DeleteTaskMutationVariables = { taskID: Scalars['String']; }; -export type DeleteTaskMutation = { __typename?: 'Mutation' } & { - deleteTask: { __typename?: 'DeleteTaskPayload' } & Pick; -}; + +export type DeleteTaskMutation = ( + { __typename?: 'Mutation' } + & { deleteTask: ( + { __typename?: 'DeleteTaskPayload' } + & Pick + ) } +); export type FindProjectQueryVariables = { projectId: Scalars['String']; }; -export type FindProjectQuery = { __typename?: 'Query' } & { - findProject: { __typename?: 'Project' } & Pick & { - taskGroups: Array< - { __typename?: 'TaskGroup' } & Pick & { - tasks: Array<{ __typename?: 'Task' } & Pick>; - } - >; - }; -}; + +export type FindProjectQuery = ( + { __typename?: 'Query' } + & { findProject: ( + { __typename?: 'Project' } + & Pick + & { taskGroups: Array<( + { __typename?: 'TaskGroup' } + & Pick + & { tasks: Array<( + { __typename?: 'Task' } + & Pick + )> } + )> } + ) } +); export type GetProjectsQueryVariables = {}; -export type GetProjectsQuery = { __typename?: 'Query' } & { - organizations: Array< - { __typename?: 'Organization' } & Pick & { - teams: Array< - { __typename?: 'Team' } & Pick & { - projects: Array<{ __typename?: 'Project' } & Pick>; - } - >; - } - >; -}; + +export type GetProjectsQuery = ( + { __typename?: 'Query' } + & { organizations: Array<( + { __typename?: 'Organization' } + & Pick + & { teams: Array<( + { __typename?: 'Team' } + & Pick + & { projects: Array<( + { __typename?: 'Project' } + & Pick + )> } + )> } + )> } +); export type UpdateTaskLocationMutationVariables = { taskID: Scalars['String']; @@ -281,29 +338,42 @@ export type UpdateTaskLocationMutationVariables = { position: Scalars['Float']; }; -export type UpdateTaskLocationMutation = { __typename?: 'Mutation' } & { - updateTaskLocation: { __typename?: 'Task' } & Pick; -}; + +export type UpdateTaskLocationMutation = ( + { __typename?: 'Mutation' } + & { updateTaskLocation: ( + { __typename?: 'Task' } + & Pick + ) } +); export type UpdateTaskNameMutationVariables = { taskID: Scalars['String']; name: Scalars['String']; }; -export type UpdateTaskNameMutation = { __typename?: 'Mutation' } & { - updateTaskName: { __typename?: 'Task' } & Pick; -}; + +export type UpdateTaskNameMutation = ( + { __typename?: 'Mutation' } + & { updateTaskName: ( + { __typename?: 'Task' } + & Pick + ) } +); + export const CreateTaskDocument = gql` - mutation createTask($taskGroupID: String!, $name: String!, $position: Float!) { - createTask(input: { taskGroupID: $taskGroupID, name: $name, position: $position }) { - taskID + mutation createTask($taskGroupID: String!, $name: String!, $position: Float!) { + createTask(input: {taskGroupID: $taskGroupID, name: $name, position: $position}) { + taskID + taskGroup { taskGroupID - name - position } + name + position } -`; +} + `; export type CreateTaskMutationFn = ApolloReactCommon.MutationFunction; /** @@ -325,24 +395,55 @@ export type CreateTaskMutationFn = ApolloReactCommon.MutationFunction, -) { - return ApolloReactHooks.useMutation(CreateTaskDocument, baseOptions); -} +export function useCreateTaskMutation(baseOptions?: ApolloReactHooks.MutationHookOptions) { + return ApolloReactHooks.useMutation(CreateTaskDocument, baseOptions); + } export type CreateTaskMutationHookResult = ReturnType; export type CreateTaskMutationResult = ApolloReactCommon.MutationResult; -export type CreateTaskMutationOptions = ApolloReactCommon.BaseMutationOptions< - CreateTaskMutation, - CreateTaskMutationVariables ->; -export const DeleteTaskDocument = gql` - mutation deleteTask($taskID: String!) { - deleteTask(input: { taskID: $taskID }) { - taskID - } +export type CreateTaskMutationOptions = ApolloReactCommon.BaseMutationOptions; +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; + +/** + * __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) { + return ApolloReactHooks.useMutation(CreateTaskGroupDocument, baseOptions); + } +export type CreateTaskGroupMutationHookResult = ReturnType; +export type CreateTaskGroupMutationResult = ApolloReactCommon.MutationResult; +export type CreateTaskGroupMutationOptions = ApolloReactCommon.BaseMutationOptions; +export const DeleteTaskDocument = gql` + mutation deleteTask($taskID: String!) { + deleteTask(input: {taskID: $taskID}) { + taskID + } +} + `; export type DeleteTaskMutationFn = ApolloReactCommon.MutationFunction; /** @@ -362,34 +463,29 @@ export type DeleteTaskMutationFn = ApolloReactCommon.MutationFunction, -) { - return ApolloReactHooks.useMutation(DeleteTaskDocument, baseOptions); -} +export function useDeleteTaskMutation(baseOptions?: ApolloReactHooks.MutationHookOptions) { + return ApolloReactHooks.useMutation(DeleteTaskDocument, baseOptions); + } export type DeleteTaskMutationHookResult = ReturnType; export type DeleteTaskMutationResult = ApolloReactCommon.MutationResult; -export type DeleteTaskMutationOptions = ApolloReactCommon.BaseMutationOptions< - DeleteTaskMutation, - DeleteTaskMutationVariables ->; +export type DeleteTaskMutationOptions = ApolloReactCommon.BaseMutationOptions; export const FindProjectDocument = gql` - query findProject($projectId: String!) { - findProject(input: { projectId: $projectId }) { + query findProject($projectId: String!) { + findProject(input: {projectId: $projectId}) { + name + taskGroups { + taskGroupID name - taskGroups { - taskGroupID + position + tasks { + taskID name position - tasks { - taskID - name - position - } } } } -`; +} + `; /** * __useFindProjectQuery__ @@ -407,33 +503,29 @@ export const FindProjectDocument = gql` * }, * }); */ -export function useFindProjectQuery( - baseOptions?: ApolloReactHooks.QueryHookOptions, -) { - return ApolloReactHooks.useQuery(FindProjectDocument, baseOptions); -} -export function useFindProjectLazyQuery( - baseOptions?: ApolloReactHooks.LazyQueryHookOptions, -) { - return ApolloReactHooks.useLazyQuery(FindProjectDocument, baseOptions); -} +export function useFindProjectQuery(baseOptions?: ApolloReactHooks.QueryHookOptions) { + return ApolloReactHooks.useQuery(FindProjectDocument, baseOptions); + } +export function useFindProjectLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions) { + return ApolloReactHooks.useLazyQuery(FindProjectDocument, baseOptions); + } export type FindProjectQueryHookResult = ReturnType; export type FindProjectLazyQueryHookResult = ReturnType; export type FindProjectQueryResult = ApolloReactCommon.QueryResult; export const GetProjectsDocument = gql` - query getProjects { - organizations { + query getProjects { + organizations { + name + teams { name - teams { + projects { name - projects { - name - projectID - } + projectID } } } -`; +} + `; /** * __useGetProjectsQuery__ @@ -450,33 +542,26 @@ export const GetProjectsDocument = gql` * }, * }); */ -export function useGetProjectsQuery( - baseOptions?: ApolloReactHooks.QueryHookOptions, -) { - return ApolloReactHooks.useQuery(GetProjectsDocument, baseOptions); -} -export function useGetProjectsLazyQuery( - baseOptions?: ApolloReactHooks.LazyQueryHookOptions, -) { - return ApolloReactHooks.useLazyQuery(GetProjectsDocument, baseOptions); -} +export function useGetProjectsQuery(baseOptions?: ApolloReactHooks.QueryHookOptions) { + return ApolloReactHooks.useQuery(GetProjectsDocument, baseOptions); + } +export function useGetProjectsLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions) { + return ApolloReactHooks.useLazyQuery(GetProjectsDocument, baseOptions); + } export type GetProjectsQueryHookResult = ReturnType; export type GetProjectsLazyQueryHookResult = ReturnType; export type GetProjectsQueryResult = ApolloReactCommon.QueryResult; export const UpdateTaskLocationDocument = gql` - mutation updateTaskLocation($taskID: String!, $taskGroupID: String!, $position: Float!) { - updateTaskLocation(input: { taskID: $taskID, taskGroupID: $taskGroupID, position: $position }) { - taskID - createdAt - name - position - } + mutation updateTaskLocation($taskID: String!, $taskGroupID: String!, $position: Float!) { + updateTaskLocation(input: {taskID: $taskID, taskGroupID: $taskGroupID, position: $position}) { + taskID + createdAt + name + position } -`; -export type UpdateTaskLocationMutationFn = ApolloReactCommon.MutationFunction< - UpdateTaskLocationMutation, - UpdateTaskLocationMutationVariables ->; +} + `; +export type UpdateTaskLocationMutationFn = ApolloReactCommon.MutationFunction; /** * __useUpdateTaskLocationMutation__ @@ -497,33 +582,22 @@ export type UpdateTaskLocationMutationFn = ApolloReactCommon.MutationFunction< * }, * }); */ -export function useUpdateTaskLocationMutation( - baseOptions?: ApolloReactHooks.MutationHookOptions, -) { - return ApolloReactHooks.useMutation( - UpdateTaskLocationDocument, - baseOptions, - ); -} +export function useUpdateTaskLocationMutation(baseOptions?: ApolloReactHooks.MutationHookOptions) { + return ApolloReactHooks.useMutation(UpdateTaskLocationDocument, baseOptions); + } export type UpdateTaskLocationMutationHookResult = ReturnType; export type UpdateTaskLocationMutationResult = ApolloReactCommon.MutationResult; -export type UpdateTaskLocationMutationOptions = ApolloReactCommon.BaseMutationOptions< - UpdateTaskLocationMutation, - UpdateTaskLocationMutationVariables ->; +export type UpdateTaskLocationMutationOptions = ApolloReactCommon.BaseMutationOptions; export const UpdateTaskNameDocument = gql` - mutation updateTaskName($taskID: String!, $name: String!) { - updateTaskName(input: { taskID: $taskID, name: $name }) { - taskID - name - position - } + mutation updateTaskName($taskID: String!, $name: String!) { + updateTaskName(input: {taskID: $taskID, name: $name}) { + taskID + name + position } -`; -export type UpdateTaskNameMutationFn = ApolloReactCommon.MutationFunction< - UpdateTaskNameMutation, - UpdateTaskNameMutationVariables ->; +} + `; +export type UpdateTaskNameMutationFn = ApolloReactCommon.MutationFunction; /** * __useUpdateTaskNameMutation__ @@ -543,17 +617,9 @@ export type UpdateTaskNameMutationFn = ApolloReactCommon.MutationFunction< * }, * }); */ -export function useUpdateTaskNameMutation( - baseOptions?: ApolloReactHooks.MutationHookOptions, -) { - return ApolloReactHooks.useMutation( - UpdateTaskNameDocument, - baseOptions, - ); -} +export function useUpdateTaskNameMutation(baseOptions?: ApolloReactHooks.MutationHookOptions) { + return ApolloReactHooks.useMutation(UpdateTaskNameDocument, baseOptions); + } export type UpdateTaskNameMutationHookResult = ReturnType; export type UpdateTaskNameMutationResult = ApolloReactCommon.MutationResult; -export type UpdateTaskNameMutationOptions = ApolloReactCommon.BaseMutationOptions< - UpdateTaskNameMutation, - UpdateTaskNameMutationVariables ->; +export type UpdateTaskNameMutationOptions = ApolloReactCommon.BaseMutationOptions; \ No newline at end of file diff --git a/web/src/shared/graphql/createTask.graphqls b/web/src/shared/graphql/createTask.graphqls index 4220a61..6a3c40e 100644 --- a/web/src/shared/graphql/createTask.graphqls +++ b/web/src/shared/graphql/createTask.graphqls @@ -1,7 +1,9 @@ mutation createTask($taskGroupID: String!, $name: String!, $position: Float!) { createTask(input: { taskGroupID: $taskGroupID, name: $name, position: $position }) { taskID - taskGroupID + taskGroup { + taskGroupID + } name position } diff --git a/web/src/shared/graphql/createTaskGroup.graphqls b/web/src/shared/graphql/createTaskGroup.graphqls new file mode 100644 index 0000000..015081d --- /dev/null +++ b/web/src/shared/graphql/createTaskGroup.graphqls @@ -0,0 +1,9 @@ +mutation createTaskGroup( $projectID: String!, $name: String!, $position: Float! ) { + createTaskGroup( + input: { projectID: $projectID, name: $name, position: $position } + ) { + taskGroupID + name + position + } +} diff --git a/web/src/shared/icons/Ellipsis.tsx b/web/src/shared/icons/Ellipsis.tsx new file mode 100644 index 0000000..3051344 --- /dev/null +++ b/web/src/shared/icons/Ellipsis.tsx @@ -0,0 +1,21 @@ +import React from 'react'; + +type Props = { + size: number | string; + color: string; +}; + +const Ellipsis = ({ size, color }: Props) => { + return ( + + + + ); +}; + +Ellipsis.defaultProps = { + size: 16, + color: '#000', +}; + +export default Ellipsis; diff --git a/web/src/shared/icons/Plus.tsx b/web/src/shared/icons/Plus.tsx new file mode 100644 index 0000000..66006c6 --- /dev/null +++ b/web/src/shared/icons/Plus.tsx @@ -0,0 +1,21 @@ +import React from 'react'; + +type Props = { + size: number | string; + color: string; +}; + +const Plus = ({ size, color }: Props) => { + return ( + + + + ); +}; + +Plus.defaultProps = { + size: 16, + color: '#000', +}; + +export default Plus; diff --git a/web/src/shared/icons/index.ts b/web/src/shared/icons/index.ts index b0c006c..88287cf 100644 --- a/web/src/shared/icons/index.ts +++ b/web/src/shared/icons/index.ts @@ -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 };