diff --git a/client/src/components/ui/EditableTag/index.tsx b/client/src/components/ui/EditableTag/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8e076503d48c5d9f77c7c15044b2b377aafaab2c --- /dev/null +++ b/client/src/components/ui/EditableTag/index.tsx @@ -0,0 +1,43 @@ + +import { useEffect, useState } from 'react'; +import Tag from '../Tag'; + +import './tag.scss'; + +interface Props<Tag> { + label: Tag; + icon?: string; + color?: string; + possible: Tag[]; + onChange: (value: Tag) => any; +} + +export default function EditableTag<Tag extends string>({ label, icon, color, possible, onChange }: Props<Tag>) { + const [open, setOpen] = useState(false) + + useEffect(() => { + if (open) { + const onClick = () => setOpen(false); + document.addEventListener('click', onClick); + return () => { + document.removeEventListener('click', onClick); + } + } + }, [open]); + + return ( + <span className={'tag-wrapper' + (open ? ' open' : '')} onClick={() => setOpen(!open)}> + <Tag label={label} icon={icon} color={color} /> + <div className="tag-dropdown"> + { + possible.filter(item => item !== label).map(item => ( + <span className="tag-item" onClick={() => onChange(item)}> + {item} + </span> + )) + } + </div> + </span> + ); +} + diff --git a/client/src/components/ui/EditableTag/tag.scss b/client/src/components/ui/EditableTag/tag.scss new file mode 100644 index 0000000000000000000000000000000000000000..7550256df474ece437981e047c48118f9b07f323 --- /dev/null +++ b/client/src/components/ui/EditableTag/tag.scss @@ -0,0 +1,50 @@ + +@use 'styles/settings.scss' as s; + +.tag-wrapper { + display: inline-flex; + flex-flow: column; + align-items: flex-start; + cursor: pointer; + position: relative; + z-index: 2000; + + &.open { + .tag-dropdown { + opacity: 1; + transform: translateY(80%); + visibility: visible; + } + + .icon { + transform: rotate(180deg); + } + } + + .tag-dropdown { + position: absolute; + bottom: 0; + left: 0; + transform: translateY(75%); + background: s.$background-white; + z-index: 2000; + border-radius: 5px; + white-space: nowrap; + box-shadow: 0 0 5px rgba(s.$black, 0.05); + visibility: hidden; + opacity: 0; + + .tag-item { + padding: 10px 20px; + display: block; + color: s.$text; + border-radius: 5px; + min-width: 100px; + + &:hover { + box-shadow: 0 0 10px rgba(s.$black, 0.1); + } + } + } +} + diff --git a/client/src/components/ui/Tag/index.tsx b/client/src/components/ui/Tag/index.tsx index 9aa37e45ea6b74ea0a0d5af97718bba6e11001a6..65ad2e37ef0d4a045e1cdd2af6070390c2f596a3 100644 --- a/client/src/components/ui/Tag/index.tsx +++ b/client/src/components/ui/Tag/index.tsx @@ -17,7 +17,6 @@ export default function Tag({ label, icon, color }: Props) { )} {label} </span> - ); - } + diff --git a/client/src/index.scss b/client/src/index.scss index 56c028d318e99d0f2e6e2ca8e76ad873a767ae85..0a26121fbcf932c3e512acfcaab4bf60df49a956 100644 --- a/client/src/index.scss +++ b/client/src/index.scss @@ -75,7 +75,7 @@ * { transition: s.$transition; - transition-property: transform, opacity, color, background, box-shadow, stroke; + transition-property: transform, opacity, color, background, box-shadow, stroke, visibility; font-family: s.$body-font; margin: 0; padding: 0; diff --git a/client/src/pages/Tasks/TaskDetail/index.tsx b/client/src/pages/Tasks/TaskDetail/index.tsx index b3a8336fbf05819cd00219aeea70aab599314f39..220f81ad8282120143ba783eee60a5df7e3cd2fb 100644 --- a/client/src/pages/Tasks/TaskDetail/index.tsx +++ b/client/src/pages/Tasks/TaskDetail/index.tsx @@ -5,23 +5,23 @@ import { useHistory, useParams } from 'react-router'; import { getTeam } from 'adapters/team'; import { AssignedUser } from 'adapters/user'; -import { StatusColors } from 'adapters/common'; +import { StatusColors, Status } from 'adapters/common'; import { getLoggedInUser } from 'adapters/auth'; import { getProject, Project } from 'adapters/project'; import { getTask, getTaskAssignees, Task, updateTask } from 'adapters/task'; -import Tag from 'components/ui/Tag'; import LongText from 'components/ui/LongText'; import Tabs from 'components/navigation/Tabs'; import DetailGrid from 'components/layout/DetailGrid'; import LoadingScreen from 'components/ui/LoadingScreen'; import ButtonLink from 'components/navigation/ButtonLink'; +import AssignForm from 'components/forms/AssignForm'; +import EditableTag from 'components/ui/EditableTag'; import TaskAssignees from './TaskAssignees'; import TaskComments from './TaskComments'; import './task-detail.scss'; -import AssignForm from 'components/forms/AssignForm'; export interface Params { taskId: string; @@ -72,6 +72,19 @@ export default function TaskDetail() { } }, [taskId, userId, assignment]); + const onStatusChange = useCallback((status: Status) => { + if (task) { + updateTask(task.id, { + status: status, + }).then(() => { + setTask({ + ...task, + status: status + }); + }); + } + }, [task]); + if (task) { return ( <div className={'tasks-detail-page theme-' + task.color}> @@ -79,7 +92,12 @@ export default function TaskDetail() { arrow_back </Link> <div className="content-container"> - <Tag label={task.status} color={StatusColors.get(task.status)} /> + <EditableTag + label={task.status} + color={StatusColors.get(task.status)} + possible={Object.values(Status)} + onChange={onStatusChange} + /> <h1>{task.name}</h1> <div className="description-container"> <LongText text={task.text} />