feat: Add tag suggestion dropdown for Notes and Tasks (#12)
* feat: add tag suggestion dropdown for NoteAdd and TaskAdd views Implement a tag suggestion dropdown in both NoteAdd and TaskAdd components. When the tag input is focused, available tags are fetched from the API (GET /rest/home/tasks/tags) and displayed in a filterable dropdown list. Users can select an existing tag or type a custom one. Co-authored-by: Ricardo Campos <RMCampos@users.noreply.github.com> * chore: fix lint issue --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Ricardo Campos <RMCampos@users.noreply.github.com>
This commit is contained in:
co-authored by
claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Ricardo Campos
parent
ed6738381a
commit
1eab863fca
@@ -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
|
||||
|
||||
@@ -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<string>('');
|
||||
const [noteUrl, setNoteUrl] = useState<string>('');
|
||||
const [noteTag, setNoteTag] = useState<string>('');
|
||||
const [tags, setTags] = useState<string[]>([]);
|
||||
const [showTagDropdown, setShowTagDropdown] = useState<boolean>(false);
|
||||
const [action, setAction] = useState<NoteAction>('add');
|
||||
const [showPreviewMd, setShowPreviewMd] = useState<boolean>(false);
|
||||
const { i18n, t } = useTranslation();
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
const tagContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const loadTags = async (): Promise<void> => {
|
||||
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 {
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={12} xl={3}>
|
||||
{/* Tag */}
|
||||
<FormInput
|
||||
labelText="Tag"
|
||||
iconName="Hash"
|
||||
required={false}
|
||||
type="text"
|
||||
name="tag"
|
||||
placeholder="my-tag (Optional)"
|
||||
value={noteTag}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setNoteTag(e.target.value);
|
||||
}}
|
||||
/>
|
||||
{/* Tag with suggestion dropdown */}
|
||||
<Form.Group className="mb-3" ref={tagContainerRef} style={{ position: 'relative' }}>
|
||||
<Form.Label>Tag</Form.Label>
|
||||
<InputGroup className="mb-3">
|
||||
<InputGroup.Text>
|
||||
<Hash />
|
||||
</InputGroup.Text>
|
||||
<Form.Control
|
||||
type="text"
|
||||
name="tag"
|
||||
placeholder="my-tag (Optional)"
|
||||
value={noteTag}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setNoteTag(e.target.value);
|
||||
setShowTagDropdown(true);
|
||||
}}
|
||||
onFocus={() => setShowTagDropdown(true)}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</InputGroup>
|
||||
{showTagDropdown && tags.filter(t => t.toLowerCase().includes(noteTag.toLowerCase())).length > 0 && (
|
||||
<ListGroup
|
||||
style={{
|
||||
position: 'absolute',
|
||||
zIndex: 1000,
|
||||
width: '100%',
|
||||
maxHeight: '200px',
|
||||
overflowY: 'auto',
|
||||
top: '100%',
|
||||
left: 0
|
||||
}}
|
||||
>
|
||||
{tags
|
||||
.filter(t => t.toLowerCase().includes(noteTag.toLowerCase()))
|
||||
.map(t => (
|
||||
<ListGroup.Item
|
||||
key={t}
|
||||
action
|
||||
onMouseDown={(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
setNoteTag(t);
|
||||
setShowTagDropdown(false);
|
||||
}}
|
||||
>
|
||||
#
|
||||
{t}
|
||||
</ListGroup.Item>
|
||||
))}
|
||||
</ListGroup>
|
||||
)}
|
||||
</Form.Group>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
|
||||
@@ -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<Date | null>(null);
|
||||
const [highPriority, setHighPriority] = useState<boolean>(false);
|
||||
const [tag, setTag] = useState<string>('');
|
||||
const [tags, setTags] = useState<string[]>([]);
|
||||
const [showTagDropdown, setShowTagDropdown] = useState<boolean>(false);
|
||||
const { i18n, t } = useTranslation();
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
const tagContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const loadTags = async (): Promise<void> => {
|
||||
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 {
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={12} sm={6} xxl={3}>
|
||||
{/* Tag */}
|
||||
<FormInput
|
||||
labelText="Tag"
|
||||
iconName="Hash"
|
||||
required={false}
|
||||
type="text"
|
||||
name="tag"
|
||||
placeholder="my-tag (Optional)"
|
||||
value={tag}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTag(e.target.value);
|
||||
}}
|
||||
/>
|
||||
{/* Tag with suggestion dropdown */}
|
||||
<Form.Group className="mb-3" ref={tagContainerRef} style={{ position: 'relative' }}>
|
||||
<Form.Label>Tag</Form.Label>
|
||||
<InputGroup className="mb-3">
|
||||
<InputGroup.Text>
|
||||
<Hash />
|
||||
</InputGroup.Text>
|
||||
<Form.Control
|
||||
type="text"
|
||||
name="tag"
|
||||
placeholder="my-tag (Optional)"
|
||||
value={tag}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTag(e.target.value);
|
||||
setShowTagDropdown(true);
|
||||
}}
|
||||
onFocus={() => setShowTagDropdown(true)}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</InputGroup>
|
||||
{showTagDropdown && tags.filter(t => t.toLowerCase().includes(tag.toLowerCase())).length > 0 && (
|
||||
<ListGroup
|
||||
style={{
|
||||
position: 'absolute',
|
||||
zIndex: 1000,
|
||||
width: '100%',
|
||||
maxHeight: '200px',
|
||||
overflowY: 'auto',
|
||||
top: '100%',
|
||||
left: 0
|
||||
}}
|
||||
>
|
||||
{tags
|
||||
.filter(t => t.toLowerCase().includes(tag.toLowerCase()))
|
||||
.map(t => (
|
||||
<ListGroup.Item
|
||||
key={t}
|
||||
action
|
||||
onMouseDown={(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
setTag(t);
|
||||
setShowTagDropdown(false);
|
||||
}}
|
||||
>
|
||||
#
|
||||
{t}
|
||||
</ListGroup.Item>
|
||||
))}
|
||||
</ListGroup>
|
||||
)}
|
||||
</Form.Group>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user