feature(Project): add ability to create new task groups
This commit is contained in:
parent
e022e7914a
commit
bd73717deb
@ -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
22
web/src/citadel.d.ts
vendored
@ -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;
|
||||
};
|
||||
|
19
web/src/shared/components/AddList/AddList.stories.tsx
Normal file
19
web/src/shared/components/AddList/AddList.stories.tsx
Normal 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')} />;
|
||||
};
|
||||
|
98
web/src/shared/components/AddList/Styles.ts
Normal file
98
web/src/shared/components/AddList/Styles.ts
Normal 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;
|
||||
`;
|
105
web/src/shared/components/AddList/index.tsx
Normal file
105
web/src/shared/components/AddList/index.tsx
Normal 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;
|
@ -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 />
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
`;
|
||||
|
@ -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>
|
||||
|
@ -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" />;
|
||||
};
|
35
web/src/shared/components/ListActions/Styles.ts
Normal file
35
web/src/shared/components/ListActions/Styles.ts
Normal 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%;
|
||||
`;
|
48
web/src/shared/components/ListActions/index.tsx
Normal file
48
web/src/shared/components/ListActions/index.tsx
Normal 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;
|
@ -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')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -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 (
|
||||
<>
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -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')}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
|
@ -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>
|
||||
|
@ -1,6 +1,7 @@
|
||||
const MenuTypes = {
|
||||
LABEL_MANAGER: 1,
|
||||
LABEL_EDITOR: 2,
|
||||
LIST_ACTIONS: 3,
|
||||
};
|
||||
|
||||
export default MenuTypes;
|
||||
|
@ -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>;
|
@ -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
|
||||
}
|
||||
|
9
web/src/shared/graphql/createTaskGroup.graphqls
Normal file
9
web/src/shared/graphql/createTaskGroup.graphqls
Normal file
@ -0,0 +1,9 @@
|
||||
mutation createTaskGroup( $projectID: String!, $name: String!, $position: Float! ) {
|
||||
createTaskGroup(
|
||||
input: { projectID: $projectID, name: $name, position: $position }
|
||||
) {
|
||||
taskGroupID
|
||||
name
|
||||
position
|
||||
}
|
||||
}
|
21
web/src/shared/icons/Ellipsis.tsx
Normal file
21
web/src/shared/icons/Ellipsis.tsx
Normal 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;
|
21
web/src/shared/icons/Plus.tsx
Normal file
21
web/src/shared/icons/Plus.tsx
Normal 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;
|
@ -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 };
|
||||
|
Loading…
Reference in New Issue
Block a user