Skip to content

Commit

Permalink
Fix tags issues not creating
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisvel committed Nov 17, 2024
1 parent 05cfbdd commit 0cd010b
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 93 deletions.
19 changes: 4 additions & 15 deletions app/frontend/components/Note/NoteModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,17 +98,11 @@ const NoteModal: React.FC<NoteModalProps> = ({ isOpen, onClose, note, onSave })

const handleTagsChange = useCallback((newTags: string[]) => {
setTags(newTags);

const updatedTags: Tag[] = newTags.map((name) => {
const existingTag = availableTags.find((tag) => tag.name === name);
return existingTag ? { id: existingTag.id, name } : { name };
});

setFormData((prev) => ({
...prev,
tags: updatedTags,
tags: newTags.map((name) => ({ name })),
}));
}, [availableTags]);
}, []);

const handleSubmit = async () => {
if (!formData.title.trim()) {
Expand All @@ -121,10 +115,10 @@ const NoteModal: React.FC<NoteModalProps> = ({ isOpen, onClose, note, onSave })

try {
if (formData.id && formData.id !== 0) {
await updateNote(formData.id, formData);
await updateNote(formData.id, { ...formData, tags });
showSuccessToast('Note updated successfully!');
} else {
await createNote(formData);
await createNote({ ...formData, tags });
showSuccessToast('Note created successfully!');
}
onSave(formData);
Expand Down Expand Up @@ -166,7 +160,6 @@ const NoteModal: React.FC<NoteModalProps> = ({ isOpen, onClose, note, onSave })
<form className="flex flex-col flex-1">
<fieldset className="flex flex-col flex-1">
<div className="p-4 space-y-3 flex-1 text-sm overflow-y-auto">
{/* Note Title */}
<div className="py-4">
<input
type="text"
Expand All @@ -180,7 +173,6 @@ const NoteModal: React.FC<NoteModalProps> = ({ isOpen, onClose, note, onSave })
/>
</div>

{/* Tags */}
<div className="pb-3">
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-2">
Tags
Expand All @@ -194,7 +186,6 @@ const NoteModal: React.FC<NoteModalProps> = ({ isOpen, onClose, note, onSave })
</div>
</div>

{/* Note Content */}
<div className="pb-3 flex-1">
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-2">
Content
Expand All @@ -210,11 +201,9 @@ const NoteModal: React.FC<NoteModalProps> = ({ isOpen, onClose, note, onSave })
></textarea>
</div>

{/* Error Message */}
{error && <div className="text-red-500">{error}</div>}
</div>

{/* Action Buttons */}
<div className="p-3 flex-shrink-0 border-t border-gray-200 dark:border-gray-700 flex justify-end space-x-2">
<button
type="button"
Expand Down
209 changes: 175 additions & 34 deletions app/frontend/components/Tag/TagInput.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React, { useState } from 'react';
import TaskTags from '../Task/TaskTags';
import React, { useState, useRef, useEffect } from 'react';
import { Tag } from '../../entities/Tag';

interface TagInputProps {
Expand All @@ -11,56 +10,198 @@ interface TagInputProps {
const TagInput: React.FC<TagInputProps> = ({ initialTags, onTagsChange, availableTags }) => {
const [inputValue, setInputValue] = useState('');
const [tags, setTags] = useState<string[]>(initialTags || []);
const [filteredTags, setFilteredTags] = useState<Tag[]>([]);
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [highlightedIndex, setHighlightedIndex] = useState<number>(-1);
const inputRef = useRef<HTMLInputElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const dropdownRef = useRef<HTMLDivElement>(null);

useEffect(() => {
const handler = setTimeout(() => {
if (inputValue.trim() === '') {
setFilteredTags([]);
setIsDropdownOpen(false);
return;
}

const filtered = availableTags.filter(tag =>
tag.name.toLowerCase().includes(inputValue.toLowerCase()) &&
!tags.includes(tag.name)
);
setFilteredTags(filtered);
setIsDropdownOpen(filtered.length > 0);
setHighlightedIndex(-1);
}, 300);

return () => {
clearTimeout(handler);
};
}, [inputValue, availableTags, tags]);

useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node) &&
containerRef.current &&
!containerRef.current.contains(event.target as Node)
) {
setIsDropdownOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);

const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value);
};

const handleKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
if ((event.key === 'Enter' || event.key === ',') && inputValue.trim()) {
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'ArrowDown') {
event.preventDefault();
setHighlightedIndex(prev => (prev < filteredTags.length - 1 ? prev + 1 : prev));
} else if (event.key === 'ArrowUp') {
event.preventDefault();
const trimmedValue = inputValue.trim();
if (!tags.includes(trimmedValue)) {
const updatedTags = [...tags, trimmedValue];
setTags(updatedTags);
onTagsChange(updatedTags);
setHighlightedIndex(prev => (prev > 0 ? prev - 1 : prev));
} else if (event.key === 'Enter') {
event.preventDefault();
if (highlightedIndex >= 0 && highlightedIndex < filteredTags.length) {
selectTag(filteredTags[highlightedIndex].name);
} else if (inputValue.trim()) {
addNewTag(inputValue.trim());
}
} else if (event.key === 'Escape') {
setIsDropdownOpen(false);
} else if (event.key === ',') {
if (inputValue.trim()) {
event.preventDefault();
addNewTag(inputValue.trim());
}
setInputValue('');
}
};

const removeTag = (tagId: string | number | undefined) => {
if (typeof tagId !== 'number') {
console.warn('Invalid tagId:', tagId);
const addNewTag = (tag: string) => {
if (tags.length >= 10) { // Example limit
return;
}
const updatedTags = tags.filter((_, index) => index !== tagId);

if (!tags.includes(tag)) {
const updatedTags = [...tags, tag];
setTags(updatedTags);
onTagsChange(updatedTags);
}
setInputValue('');
setIsDropdownOpen(false);
};

const selectTag = (tag: string) => {
if (!tags.includes(tag)) {
const updatedTags = [...tags, tag];
setTags(updatedTags);
onTagsChange(updatedTags);
}
setInputValue('');
setIsDropdownOpen(false);
};

const removeTag = (index: number) => {
const updatedTags = tags.filter((_, i) => i !== index);
setTags(updatedTags);
onTagsChange(updatedTags);
};

return (
<div className="space-y-2">
<TaskTags
tags={tags.map((tag, index) => ({ id: index, name: tag }))}
onTagRemove={removeTag}
className="flex flex-wrap gap-1"
/>

<input
type="text"
value={inputValue}
onChange={handleInputChange}
onKeyDown={handleKeyPress}
list="available-tags"
placeholder="Type to select an existing tag or add a new one"
className="w-full px-2 border border-gray-300 dark:border-gray-900 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 py-2 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
/>
<datalist id="available-tags">
{availableTags.map((tag, index) => (
<option key={index} value={tag.name} />
<div className="space-y-2 relative">
<div
ref={containerRef}
className="flex flex-wrap items-center border border-gray-300 dark:border-gray-900 bg-white dark:bg-gray-900 rounded-md p-2 h-10"
>
{tags.map((tag, index) => (
<span
key={index}
className="flex items-center bg-gray-200 text-gray-700 text-xs font-medium mr-2 px-2.5 py-0.5 rounded"
>
{tag}
<button
type="button"
onClick={() => removeTag(index)}
className="ml-1 text-gray-600 hover:text-gray-800 focus:outline-none"
aria-label={`Remove tag ${tag}`}
>
&times;
</button>
</span>
))}
</datalist>

<input
type="text"
ref={inputRef}
value={inputValue}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
placeholder="Type to add a tag"
className="flex-grow bg-transparent border-none outline-none text-sm text-gray-900 dark:text-gray-100"
onFocus={() => {
if (filteredTags.length > 0) setIsDropdownOpen(true);
}}
style={{ minWidth: '150px' }}
aria-haspopup="listbox"
aria-expanded={isDropdownOpen}
aria-controls="tag-suggestions"
/>
</div>

{isDropdownOpen && (
<div
ref={dropdownRef}
className="absolute z-10 mt-1 w-full bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-lg max-h-60 overflow-auto"
role="listbox"
id="tag-suggestions"
>
{filteredTags.map((tag, index) => (
<button
key={tag.id}
type="button"
onClick={() => selectTag(tag.name)}
className={`w-full text-left px-4 py-2 text-sm hover:bg-gray-200 dark:hover:bg-gray-700 ${
highlightedIndex === index ? 'bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-gray-100' : 'text-gray-700 dark:text-gray-300'
}`}
onMouseEnter={() => setHighlightedIndex(index)}
onMouseLeave={() => setHighlightedIndex(-1)}
role="option"
aria-selected={highlightedIndex === index}
>
{highlightedIndex === index ? (
<>
{inputValue.length > 0 && (
<span className="font-semibold">
{tag.name.substring(0, inputValue.length)}
</span>
)}
{tag.name.substring(inputValue.length)}
</>
) : (
tag.name
)}
</button>
))}
{/* Option to add a new tag if no matches */}
{filteredTags.length === 0 && inputValue.trim() !== '' && (
<button
type="button"
onClick={() => addNewTag(inputValue.trim())}
className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700"
role="option"
>
+ Create "{inputValue.trim()}"
</button>
)}
</div>
)}
</div>
);
};
Expand Down
47 changes: 23 additions & 24 deletions app/frontend/components/Task/TaskModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useToast } from "../Shared/ToastContext";
import TagInput from "../Tag/TagInput";
import { Project } from "../../entities/Project";
import { Tag } from "../../entities/Tag";
import useFetchTags from "../../hooks/useFetchTags";

interface TaskModalProps {
isOpen: boolean;
Expand All @@ -29,7 +30,6 @@ const TaskModal: React.FC<TaskModalProps> = ({
onCreateProject,
}) => {
const [formData, setFormData] = useState<Task>(task);
const [availableTags, setAvailableTags] = useState<Tag[]>([]);
const [tags, setTags] = useState<string[]>(
task.tags?.map((tag) => tag.name) || []
);
Expand All @@ -43,20 +43,13 @@ const TaskModal: React.FC<TaskModalProps> = ({

const { showSuccessToast, showErrorToast } = useToast();

const { tags: availableTags, isLoading, isError } = useFetchTags();

useEffect(() => {
setFormData(task);
setTags(task.tags?.map((tag) => tag.name) || []);
}, [task]);

useEffect(() => {
if (isOpen) {
fetch("/api/tags")
.then((response) => response.json())
.then((data) => setAvailableTags(data.map((tag: Tag) => tag.name)))
.catch((error) => console.error("Failed to fetch tags", error));
}
}, [isOpen]);

const handleChange = (
e: React.ChangeEvent<
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
Expand Down Expand Up @@ -138,20 +131,6 @@ const TaskModal: React.FC<TaskModalProps> = ({
}, 300);
};

const handleTagRemove = (tagId: string | number | undefined) => {
if (tagId === undefined) return;
const tagIndex = Number(tagId);
if (tagIndex >= 0 && tagIndex < tags.length) {
const updatedTags = tags.filter((_, index) => index !== tagIndex);
setTags(updatedTags);
setFormData((prev) => ({
...prev,
tags: updatedTags.map((name) => ({ name })),
}));
showSuccessToast("Tag removed successfully!");
}
};

useEffect(() => {
setFilteredProjects(projects);
}, [projects]);
Expand Down Expand Up @@ -189,6 +168,26 @@ const TaskModal: React.FC<TaskModalProps> = ({

if (!isOpen) return null;

if (isLoading) {
return (
<div className="fixed top-16 left-0 right-0 bottom-0 flex items-center justify-center bg-gray-900 bg-opacity-80 z-40">
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-lg">
Loading tags...
</div>
</div>
);
}

if (isError) {
return (
<div className="fixed top-16 left-0 right-0 bottom-0 flex items-center justify-center bg-gray-900 bg-opacity-80 z-40">
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-lg">
Error loading tags.
</div>
</div>
);
}

return (
<>
<div
Expand Down
Loading

0 comments on commit 0cd010b

Please sign in to comment.