diff --git a/client/run-from-docker.sh b/client/run-from-docker.sh index ce96274..938b18f 100644 --- a/client/run-from-docker.sh +++ b/client/run-from-docker.sh @@ -26,7 +26,7 @@ else fi echo "3/3 - Tests started..." - npm run test + npm run test:no-watch if [ $? -eq 1 ]; then echo "Issues when running test. Please review.." exit 1 diff --git a/client/src/views/NoteAdd/index.tsx b/client/src/views/NoteAdd/index.tsx index 020bda6..a982b3d 100644 --- a/client/src/views/NoteAdd/index.tsx +++ b/client/src/views/NoteAdd/index.tsx @@ -1,11 +1,14 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { Card, Col, Container, Form, + InputGroup, + ListGroup, Row } from 'react-bootstrap'; +import { Hash } from 'react-bootstrap-icons'; import { useNavigate, useParams } from 'react-router'; import { useTranslation } from 'react-i18next'; import { NoteResponse } from '../../types/NoteResponse'; @@ -32,11 +35,24 @@ function NoteAdd(): React.ReactNode { const [noteContent, setNoteContent] = useState(''); const [noteUrl, setNoteUrl] = useState(''); const [noteTag, setNoteTag] = useState(''); + const [tags, setTags] = useState([]); + const [showTagDropdown, setShowTagDropdown] = useState(false); const [action, setAction] = useState('add'); const [showPreviewMd, setShowPreviewMd] = useState(false); const { i18n, t } = useTranslation(); const params = useParams(); const navigate = useNavigate(); + const tagContainerRef = useRef(null); + + const loadTags = async (): Promise => { + try { + const response: string[] = await api.getJSON(`${ApiConfig.homeUrl}/tasks/tags`); + setTags(response); + } + catch (e) { + handleError(e); + } + }; /** * Handles errors by setting the error message and form invalid state. @@ -222,8 +238,20 @@ function NoteAdd(): React.ReactNode { const handleCloseModal = (): void => setShowPreviewMd(false); useEffect(() => { + loadTags(); checkEditUrl(); checkCloneUrl(); + + const handleClickOutside = (event: MouseEvent): void => { + if (tagContainerRef.current && !tagContainerRef.current.contains(event.target as Node)) { + setShowTagDropdown(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; }, []); return ( @@ -284,19 +312,57 @@ function NoteAdd(): React.ReactNode { /> - {/* Tag */} - ) => { - setNoteTag(e.target.value); - }} - /> + {/* Tag with suggestion dropdown */} + + Tag + + + + + ) => { + setNoteTag(e.target.value); + setShowTagDropdown(true); + }} + onFocus={() => setShowTagDropdown(true)} + autoComplete="off" + /> + + {showTagDropdown && tags.filter(t => t.toLowerCase().includes(noteTag.toLowerCase())).length > 0 && ( + + {tags + .filter(t => t.toLowerCase().includes(noteTag.toLowerCase())) + .map(t => ( + { + e.preventDefault(); + setNoteTag(t); + setShowTagDropdown(false); + }} + > + # + {t} + + ))} + + )} + diff --git a/client/src/views/TaskAdd/index.tsx b/client/src/views/TaskAdd/index.tsx index 34f620f..0b522f8 100644 --- a/client/src/views/TaskAdd/index.tsx +++ b/client/src/views/TaskAdd/index.tsx @@ -1,11 +1,14 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { Card, Col, Container, Form, + InputGroup, + ListGroup, Row } from 'react-bootstrap'; +import { Hash } from 'react-bootstrap-icons'; import { useNavigate, useParams } from 'react-router'; import TaskNoteRequest from '../../types/TaskNoteRequest'; import { TaskResponse } from '../../types/TaskResponse'; @@ -35,9 +38,22 @@ function TaskAdd(): React.ReactNode { const [dueDate, setDueDate] = useState(null); const [highPriority, setHighPriority] = useState(false); const [tag, setTag] = useState(''); + const [tags, setTags] = useState([]); + const [showTagDropdown, setShowTagDropdown] = useState(false); const { i18n, t } = useTranslation(); const params = useParams(); const navigate = useNavigate(); + const tagContainerRef = useRef(null); + + const loadTags = async (): Promise => { + try { + const response: string[] = await api.getJSON(`${ApiConfig.homeUrl}/tasks/tags`); + setTags(response); + } + catch (e) { + handleError(e); + } + }; /** * Handles errors by setting the error message and form invalid state. @@ -190,7 +206,19 @@ function TaskAdd(): React.ReactNode { }; useEffect(() => { + loadTags(); checkEditUrl(); + + const handleClickOutside = (event: MouseEvent): void => { + if (tagContainerRef.current && !tagContainerRef.current.contains(event.target as Node)) { + setShowTagDropdown(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; }, []); return ( @@ -267,19 +295,57 @@ function TaskAdd(): React.ReactNode { /> - {/* Tag */} - ) => { - setTag(e.target.value); - }} - /> + {/* Tag with suggestion dropdown */} + + Tag + + + + + ) => { + setTag(e.target.value); + setShowTagDropdown(true); + }} + onFocus={() => setShowTagDropdown(true)} + autoComplete="off" + /> + + {showTagDropdown && tags.filter(t => t.toLowerCase().includes(tag.toLowerCase())).length > 0 && ( + + {tags + .filter(t => t.toLowerCase().includes(tag.toLowerCase())) + .map(t => ( + { + e.preventDefault(); + setTag(t); + setShowTagDropdown(false); + }} + > + # + {t} + + ))} + + )} +