From cf8b1c9709a46d1021613d88a37068a6c8291926 Mon Sep 17 00:00:00 2001 From: Chris Veleris Date: Tue, 22 Jul 2025 10:51:26 +0300 Subject: [PATCH] Fix an issue with create-from buttons in inbox --- frontend/components/Inbox/InboxItemDetail.tsx | 108 +++++++------ frontend/components/Inbox/InboxItems.tsx | 149 ++++++++++++------ frontend/components/Note/NoteModal.tsx | 21 +-- frontend/components/Project/ProjectModal.tsx | 41 +++-- frontend/components/Task/TaskModal.tsx | 15 +- 5 files changed, 196 insertions(+), 138 deletions(-) diff --git a/frontend/components/Inbox/InboxItemDetail.tsx b/frontend/components/Inbox/InboxItemDetail.tsx index 2f7b49a..f44d354 100644 --- a/frontend/components/Inbox/InboxItemDetail.tsx +++ b/frontend/components/Inbox/InboxItemDetail.tsx @@ -216,65 +216,75 @@ const InboxItemDetail: React.FC = ({ const cleanedContent = cleanTextFromTagsAndProjects(item.content); const handleConvertToTask = () => { - // Convert hashtags to Tag objects - const taskTags = hashtags.map((hashtagName) => { - // Find existing tag or create a placeholder for new tag - const existingTag = tags.find( - (tag) => tag.name.toLowerCase() === hashtagName.toLowerCase() - ); - return existingTag || { name: hashtagName }; - }); + try { + // Convert hashtags to Tag objects + const taskTags = hashtags.map((hashtagName) => { + // Find existing tag or create a placeholder for new tag + const existingTag = tags.find( + (tag) => + tag.name.toLowerCase() === hashtagName.toLowerCase() + ); + return existingTag || { name: hashtagName }; + }); - // Find the project to assign (use first project reference if any) - let projectId = undefined; - if (projectRefs.length > 0) { - // Look for an existing project with the first project reference name - const projectName = projectRefs[0]; - const matchingProject = projects.find( - (project) => - project.name.toLowerCase() === projectName.toLowerCase() - ); - if (matchingProject) { - projectId = matchingProject.id; + // Find the project to assign (use first project reference if any) + let projectId = undefined; + if (projectRefs.length > 0) { + // Look for an existing project with the first project reference name + const projectName = projectRefs[0]; + const matchingProject = projects.find( + (project) => + project.name.toLowerCase() === projectName.toLowerCase() + ); + if (matchingProject) { + projectId = matchingProject.id; + } } - } - const newTask: Task = { - name: cleanedContent || item.content, - status: 'not_started', - priority: 'medium', - tags: taskTags, - project_id: projectId, - }; + const newTask: Task = { + name: cleanedContent || item.content, + status: 'not_started', + priority: 'medium', + tags: taskTags, + project_id: projectId, + }; - if (item.id !== undefined) { - openTaskModal(newTask, item.id); - } else { - openTaskModal(newTask); + if (item.id !== undefined) { + openTaskModal(newTask, item.id); + } else { + openTaskModal(newTask); + } + } catch (error) { + console.error('Error converting to task:', error); } }; const handleConvertToProject = () => { - // Convert hashtags to Tag objects (ignore any existing project references) - const projectTags = hashtags.map((hashtagName) => { - // Find existing tag or create a placeholder for new tag - const existingTag = tags.find( - (tag) => tag.name.toLowerCase() === hashtagName.toLowerCase() - ); - return existingTag || { name: hashtagName }; - }); + try { + // Convert hashtags to Tag objects (ignore any existing project references) + const projectTags = hashtags.map((hashtagName) => { + // Find existing tag or create a placeholder for new tag + const existingTag = tags.find( + (tag) => + tag.name.toLowerCase() === hashtagName.toLowerCase() + ); + return existingTag || { name: hashtagName }; + }); - const newProject: Project = { - name: cleanedContent || item.content, - description: '', - active: true, - tags: projectTags, - }; + const newProject: Project = { + name: cleanedContent || item.content, + description: '', + active: true, + tags: projectTags, + }; - if (item.id !== undefined) { - openProjectModal(newProject, item.id); - } else { - openProjectModal(newProject); + if (item.id !== undefined) { + openProjectModal(newProject, item.id); + } else { + openProjectModal(newProject); + } + } catch (error) { + console.error('Error converting to project:', error); } }; diff --git a/frontend/components/Inbox/InboxItems.tsx b/frontend/components/Inbox/InboxItems.tsx index 3c88d11..7e4620e 100644 --- a/frontend/components/Inbox/InboxItems.tsx +++ b/frontend/components/Inbox/InboxItems.tsx @@ -22,6 +22,7 @@ import { createTask } from '../../utils/tasksService'; import { createProject } from '../../utils/projectsService'; import { createNote } from '../../utils/notesService'; import { isUrl } from '../../utils/urlService'; +import { fetchAreas } from '../../utils/areasService'; import { useStore } from '../../store/useStore'; const InboxItems: React.FC = () => { @@ -30,6 +31,16 @@ const InboxItems: React.FC = () => { // Access store data const { inboxItems, isLoading } = useStore((state) => state.inboxStore); + const { + areas, + setAreas, + setError: setAreasError, + } = useStore((state) => state.areasStore); + const { + loadTags, + hasLoaded: tagsHasLoaded, + isLoading: tagsLoading, + } = useStore((state) => state.tagsStore); // Modal states const [isTaskModalOpen, setIsTaskModalOpen] = useState(false); @@ -70,6 +81,30 @@ const InboxItems: React.FC = () => { }; loadInitialProjects(); + // Load areas initially + const loadInitialAreas = async () => { + try { + const areasData = await fetchAreas(); + setAreas(areasData); + } catch (error) { + console.error('Failed to load initial areas:', error); + setAreasError(true); + } + }; + loadInitialAreas(); + + // Load tags initially + const loadInitialTags = async () => { + if (!tagsHasLoaded && !tagsLoading) { + try { + await loadTags(); + } catch (error) { + console.error('Failed to load initial tags:', error); + } + } + }; + loadInitialTags(); + // Set up an event listener for force reload const handleForceReload = () => { // Wait a short time to ensure the backend has processed the new item @@ -178,37 +213,57 @@ const InboxItems: React.FC = () => { // Modal handlers const handleOpenTaskModal = async (task: Task, inboxItemId?: number) => { - // Load projects first before opening the modal try { - const projectData = await fetchProjects(); - // Make sure we always set an array - setProjects(Array.isArray(projectData) ? projectData : []); + // Load projects first before opening the modal + try { + const projectData = await fetchProjects(); + // Make sure we always set an array + setProjects(Array.isArray(projectData) ? projectData : []); + } catch (error) { + console.error('Failed to load projects:', error); + showErrorToast( + t('project.loadError', 'Failed to load projects') + ); + setProjects([]); // Ensure we have an empty array even on error + } + + setTaskToEdit(task); + + if (inboxItemId) { + setCurrentConversionItemId(inboxItemId); + } + + setIsTaskModalOpen(true); } catch (error) { - console.error('Failed to load projects:', error); - showErrorToast(t('project.loadError', 'Failed to load projects')); - setProjects([]); // Ensure we have an empty array even on error + console.error('Failed to open task modal:', error); } - - setTaskToEdit(task); - - if (inboxItemId) { - setCurrentConversionItemId(inboxItemId); - } - - setIsTaskModalOpen(true); }; - const handleOpenProjectModal = ( + const handleOpenProjectModal = async ( project: Project | null, inboxItemId?: number ) => { - setProjectToEdit(project); + try { + // Load areas first before opening the modal (similar to task modal) + try { + const areasData = await fetchAreas(); + setAreas(areasData); + } catch (error) { + console.error('Failed to load areas:', error); + showErrorToast(t('area.loadError', 'Failed to load areas')); + setAreas([]); // Ensure we have an empty array even on error + } - if (inboxItemId) { - setCurrentConversionItemId(inboxItemId); + setProjectToEdit(project); + + if (inboxItemId) { + setCurrentConversionItemId(inboxItemId); + } + + setIsProjectModalOpen(true); + } catch (error) { + console.error('Failed to open project modal:', error); } - - setIsProjectModalOpen(true); }; const handleOpenNoteModal = async ( @@ -490,29 +545,35 @@ const InboxItems: React.FC = () => { })()} {/* Project Modal - Only render when needed to prevent infinite loops */} - {isProjectModalOpen && - (() => { - try { - return ( - { - setIsProjectModalOpen(false); - setProjectToEdit(null); - }} - onSave={handleSaveProject} - project={projectToEdit || undefined} - areas={[]} - /> - ); - } catch (error) { - console.error( - 'ProjectModal rendering error:', - error - ); - return null; - } - })()} + {(() => { + return ( + isProjectModalOpen && + (() => { + try { + return ( + { + setIsProjectModalOpen(false); + setProjectToEdit(null); + }} + onSave={handleSaveProject} + project={projectToEdit || undefined} + areas={ + Array.isArray(areas) ? areas : [] + } + /> + ); + } catch (error) { + console.error( + 'ProjectModal rendering error:', + error + ); + return null; + } + })() + ); + })()} {/* Note Modal - Always render it but control visibility with isOpen */} {(() => { diff --git a/frontend/components/Note/NoteModal.tsx b/frontend/components/Note/NoteModal.tsx index 083b46d..0a08f05 100644 --- a/frontend/components/Note/NoteModal.tsx +++ b/frontend/components/Note/NoteModal.tsx @@ -43,12 +43,7 @@ const NoteModal: React.FC = ({ }) => { const { t } = useTranslation(); const { - tagsStore: { - tags: availableTagsStore, - loadTags, - isLoading: tagsLoading, - hasLoaded: tagsHasLoaded, - }, + tagsStore: { tags: availableTagsStore }, } = useStore(); const [formData, setFormData] = useState( note || { @@ -87,17 +82,11 @@ const NoteModal: React.FC = ({ useEffect(() => { if (!isOpen) return; - if (!tagsHasLoaded && !tagsLoading) { - loadTags(); - } - // Auto-focus on the title input when modal opens - if (isOpen) { - setTimeout(() => { - titleInputRef.current?.focus(); - }, 100); - } - }, [isOpen, tagsHasLoaded, tagsLoading]); + setTimeout(() => { + titleInputRef.current?.focus(); + }, 100); + }, [isOpen]); // Initialize filtered projects from props - like TaskModal useEffect(() => { diff --git a/frontend/components/Project/ProjectModal.tsx b/frontend/components/Project/ProjectModal.tsx index a34a5e0..266c0be 100644 --- a/frontend/components/Project/ProjectModal.tsx +++ b/frontend/components/Project/ProjectModal.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { createPortal } from 'react-dom'; import { Area } from '../../entities/Area'; import { Project } from '../../entities/Project'; import ConfirmDialog from '../Shared/ConfirmDialog'; @@ -61,12 +62,7 @@ const ProjectModal: React.FC = ({ const [isUploading, setIsUploading] = useState(false); const { tagsStore } = useStore(); - const { - tags: availableTags, - loadTags, - isLoading: tagsLoading, - hasLoaded: tagsHasLoaded, - } = tagsStore; + const { tags: availableTags } = tagsStore; const modalRef = useRef(null); const fileInputRef = useRef(null); @@ -88,19 +84,26 @@ const ProjectModal: React.FC = ({ const { showSuccessToast, showErrorToast } = useToast(); const { t } = useTranslation(); - // Load tags when modal opens + // Auto-focus on the name input when modal opens useEffect(() => { - if (isOpen && !tagsHasLoaded && !tagsLoading) { - loadTags(); - } - - // Auto-focus on the name input when modal opens if (isOpen) { setTimeout(() => { nameInputRef.current?.focus(); - }, 100); + }, 200); } - }, [isOpen, tagsHasLoaded, tagsLoading]); + }, [isOpen]); + + // Manage body scroll when modal is open + useEffect(() => { + if (isOpen) { + document.body.style.overflow = 'hidden'; + } else { + document.body.style.overflow = 'unset'; + } + return () => { + document.body.style.overflow = 'unset'; + }; + }, [isOpen]); useEffect(() => { if (project) { @@ -404,8 +407,12 @@ const ProjectModal: React.FC = ({ if (!isOpen) return null; - return ( + // Don't render if areas aren't loaded yet (prevents race condition) + if (!areas || !Array.isArray(areas)) return null; + + return createPortal( <> + ,
= ({
- {showConfirmDialog && ( = ({ onCancel={() => setShowConfirmDialog(false)} /> )} - + , + document.body ); }; diff --git a/frontend/components/Task/TaskModal.tsx b/frontend/components/Task/TaskModal.tsx index 629d80f..227814d 100644 --- a/frontend/components/Task/TaskModal.tsx +++ b/frontend/components/Task/TaskModal.tsx @@ -52,12 +52,7 @@ const TaskModal: React.FC = ({ onEditParentTask, }) => { const { - tagsStore: { - tags: availableTags, - loadTags, - isLoading: tagsLoading, - hasLoaded: tagsHasLoaded, - }, + tagsStore: { tags: availableTags }, } = useStore(); const [formData, setFormData] = useState(task); const [tags, setTags] = useState( @@ -200,12 +195,8 @@ const TaskModal: React.FC = ({ })); }; - useEffect(() => { - // Load tags when modal opens if they're not already loaded - if (isOpen && !tagsHasLoaded && !tagsLoading) { - loadTags(); - } - }, [isOpen, tagsHasLoaded, tagsLoading]); + // Note: Tags loading removed to prevent modal closing issues + // Tags will be loaded by other components or on app startup const getPriorityString = ( priority: PriorityType | number | undefined