diff --git a/src/components/common/modal/ProjectCreationModal.tsx b/src/components/common/modal/ProjectCreationModal.tsx new file mode 100644 index 0000000..7af114f --- /dev/null +++ b/src/components/common/modal/ProjectCreationModal.tsx @@ -0,0 +1,123 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { RdsButton, RdsIconId, RdsInputText, RdsInputTextArea, RdsModal } from 'rte-design-system-react'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import KeywordsInput from '@/pages/pegase/studies/KeywordsInput'; +import { createProject } from '@/shared/services/projectService'; +import { notifyToast } from '@/shared/notification/notification.tsx'; +import { PROJECT_ACTION } from '@/shared/enum/project.ts'; +import { ProjectActionType } from '@/shared/types/pegase/Project.type.ts'; +import { useProjectDispatch } from '@/store/contexts/ProjectContext.tsx'; + +interface ProjectCreationModalProps { + onClose: () => void; +} + +export const ProjectCreationModal = ({ onClose }: ProjectCreationModalProps) => { + const { t } = useTranslation(); + const [name, setName] = useState(''); + const [description, setDescription] = useState(''); + const [keywords, setKeywords] = useState([]); + const [isFormValid, setIsFormValid] = useState(false); + const dispatch = useProjectDispatch(); + + const validateForm = () => { + if (name) { + setIsFormValid(true); + } else { + setIsFormValid(false); + } + }; + + useEffect(() => { + validateForm(); + }, [name]); + + const handleCreateProject = async () => { + try { + const projectData = { + name: name, + tags: keywords, + description: description, + }; + + const newProject = await createProject(projectData); + if (newProject) { + dispatch?.({ + type: PROJECT_ACTION.ADD_PROJECT, + payload: newProject, + } as ProjectActionType); + } + notifyToast({ + type: 'success', + message: 'Successful project save', + }); + setName(''); + setDescription(''); + setKeywords([]); + onClose(); + } catch (error: unknown) { + if (error instanceof Error) { + notifyToast({ + type: 'error', + message: `${error.message}`, + }); + } + } + }; + + return ( + + {t('home.@new_project')} + +
+ { + if (t.length <= 40) setName(t || ''); + }} + variant="outlined" + placeHolder="Name your project..." + required + maxLength={40} + autoFocus={true} + /> + { + if (t.length <= 500) setDescription(t || ''); + }} + maxLength={500} + placeHolder="Add a few lines to describe your project..." + /> + +
+
+ + + + +
+ ); +}; diff --git a/src/pages/pegase/studies/StudyCreationModal.tsx b/src/components/common/modal/StudyCreationModal.tsx similarity index 87% rename from src/pages/pegase/studies/StudyCreationModal.tsx rename to src/components/common/modal/StudyCreationModal.tsx index dcb9d6e..dc72ffd 100644 --- a/src/pages/pegase/studies/StudyCreationModal.tsx +++ b/src/components/common/modal/StudyCreationModal.tsx @@ -40,13 +40,14 @@ const StudyCreationModal: React.FC = ({ onClose, study, trajectoryIds: trajectoryIds, }; - await saveStudy(studyData, onClose); + await saveStudy(studyData); // Clear form fields setReloadStudies((prev) => !prev); // Trigger reload after successful save setStudyName(''); setProjectName(''); setHorizon(''); setKeywords([]); + onClose(); }; const validateForm = () => { @@ -88,8 +89,8 @@ const StudyCreationModal: React.FC = ({ onClose, study, {study ? t('home.@duplicate_study') : t('home.@new_study')} -
-
+
+
= ({ onClose, study, variant="outlined" placeHolder="Name your study..." /> + +
-
+
- - diff --git a/src/hooks/test/useFetchProjectList.test.ts b/src/hooks/test/useFetchProjectList.test.ts index a9e17e5..87836d4 100644 --- a/src/hooks/test/useFetchProjectList.test.ts +++ b/src/hooks/test/useFetchProjectList.test.ts @@ -40,7 +40,7 @@ describe('useFetchProjectList', () => { }); it('fetches projects on mount', async () => { - const { result } = renderHook(() => useFetchProjectList('mouad', 0, 9, false)); + const { result } = renderHook(() => useFetchProjectList('mouad', 0, 9)); await waitFor(() => { expect(result.current.projects).toEqual([ @@ -57,7 +57,7 @@ describe('useFetchProjectList', () => { }); it('fetches projects with search term', async () => { - renderHook(() => useFetchProjectList('test', 0, 9, false)); + renderHook(() => useFetchProjectList('test', 0, 9)); await waitFor(() => { expect(global.fetch).toHaveBeenCalledWith('https://mockapi.com/v1/project/search?page=1&size=9&search=test'); @@ -65,7 +65,7 @@ describe('useFetchProjectList', () => { }); it('fetches projects with pagination', async () => { - renderHook(() => useFetchProjectList('', 1, 9, false)); + renderHook(() => useFetchProjectList('', 1, 9)); await waitFor(() => { expect(global.fetch).toHaveBeenCalledWith('https://mockapi.com/v1/project/search?page=2&size=9&search='); diff --git a/src/hooks/test/useHandlePinnedProjectList.test.tsx b/src/hooks/test/useHandlePinnedProjectList.test.tsx index 54bf11c..b98445c 100644 --- a/src/hooks/test/useHandlePinnedProjectList.test.tsx +++ b/src/hooks/test/useHandlePinnedProjectList.test.tsx @@ -7,15 +7,11 @@ import { act, Queries, renderHook, RenderHookOptions, waitFor } from '@testing-library/react'; import { useHandlePinnedProjectList } from '@/hooks/useHandlePinnedProjectList'; import { afterEach, beforeEach, describe, expectTypeOf, it, Mock, vi } from 'vitest'; -import { - PinnedProjectProvider, - PinnedProjectProviderProps, - usePinnedProjectDispatch, -} from '@/store/contexts/ProjectContext'; +import { ProjectProvider, ProjectProviderProps, useProjectDispatch } from '@/store/contexts/ProjectContext'; import { fetchPinnedProjects, pinProject } from '@/shared/services/pinnedProjectService.ts'; -import { PINNED_PROJECT_ACTION } from '@/shared/enum/project.ts'; import { v4 as uuidv4 } from 'uuid'; import { notifyToast } from '@/shared/notification/notification.tsx'; +import { PROJECT_ACTION } from '@/shared/enum/project.ts'; const mockProjectsApiResponse = [ { @@ -66,8 +62,8 @@ vi.mock('@/store/contexts/ProjectContext', async (importOriginal) => { const actual: Mock = await importOriginal(); return { ...actual, - usePinnedProject: vi.fn(), - usePinnedProjectDispatch: vi.fn(() => { + useProject: vi.fn(), + useProjectDispatch: vi.fn(() => { return { dispatch: vi.fn(), }; @@ -76,7 +72,7 @@ vi.mock('@/store/contexts/ProjectContext', async (importOriginal) => { }); describe('useHandlePinnedProjectList', () => { - const mockUsePinnedProjectDispatch = usePinnedProjectDispatch as Mock; + const mockUsePinnedProjectDispatch = useProjectDispatch as Mock; const mockDispatch = vi.fn().mockImplementation(vi.fn()); beforeEach(() => { @@ -94,14 +90,14 @@ describe('useHandlePinnedProjectList', () => { mockUsePinnedProjectDispatch.mockReturnValue(mockDispatch); - const wrapper = ({ children, initialValue }: PinnedProjectProviderProps) => ( - + const wrapper = ({ children, initialValue }: ProjectProviderProps) => ( + ); const { result } = renderHook(() => useHandlePinnedProjectList(), { wrapper, - initialProps: { initialValue: { pinnedProject: [] } }, - } as RenderHookOptions<{ initialValue: { pinnedProject: never[] } }, Queries>); + initialProps: { initialValue: { projects: [], pinnedProject: [] } }, + } as RenderHookOptions<{ initialValue: { projects: never[]; pinnedProject: never[] } }, Queries>); await waitFor(() => { expectTypeOf(result.current.getPinnedProjects).toBeFunction(); @@ -112,7 +108,7 @@ describe('useHandlePinnedProjectList', () => { expect(mockUsePinnedProjectDispatch).toHaveBeenCalledTimes(1); expect(mockDispatch).toHaveBeenCalledTimes(1); expect(mockDispatch).toHaveBeenCalledWith({ - type: PINNED_PROJECT_ACTION.INIT_LIST, + type: PROJECT_ACTION.INIT_PINNED_PROJECT_LIST, payload: mockProjectsApiResponse, }); }); @@ -137,14 +133,14 @@ describe('useHandlePinnedProjectList', () => { const id = uuidv4(); - const wrapper = ({ children, initialValue }: PinnedProjectProviderProps) => ( - + const wrapper = ({ children, initialValue }: ProjectProviderProps) => ( + ); const { result } = renderHook(() => useHandlePinnedProjectList(), { wrapper, initialProps: { initialValue: { pinnedProject: [] } }, - } as RenderHookOptions<{ initialValue: { pinnedProject: never[] } }, Queries>); + } as RenderHookOptions<{ initialValue: { pinnedProject: never[]; projects: [] } }, Queries>); await act(async () => result.current.handlePinProject('me00247')); @@ -152,7 +148,7 @@ describe('useHandlePinnedProjectList', () => { expect(pinProject).toHaveBeenCalledWith('me00247'); expect(mockDispatch).toHaveBeenCalledTimes(1); expect(mockDispatch).toHaveBeenCalledWith({ - type: PINNED_PROJECT_ACTION.ADD_ITEM, + type: PROJECT_ACTION.ADD_PINNED_PROJECT, payload: mockPinProjectResponse, }); expect(notifyToast).toHaveBeenCalledWith({ @@ -171,8 +167,8 @@ describe('useHandlePinnedProjectList', () => { const id = uuidv4(); - const wrapper = ({ children, initialValue }: PinnedProjectProviderProps) => ( - + const wrapper = ({ children, initialValue }: ProjectProviderProps) => ( + ); const { result } = renderHook(() => useHandlePinnedProjectList(), { diff --git a/src/hooks/useFetchProjectList.ts b/src/hooks/useFetchProjectList.ts index 3c8321a..0970502 100644 --- a/src/hooks/useFetchProjectList.ts +++ b/src/hooks/useFetchProjectList.ts @@ -5,22 +5,24 @@ */ import { useCallback, useEffect, useState } from 'react'; -import { ProjectInfo } from '@/shared/types/pegase/Project.type.ts'; +import { ProjectActionType, ProjectInfo } from '@/shared/types/pegase/Project.type.ts'; import { fetchProjectFromSearchTerm } from '@/shared/services/projectService.ts'; +import { PROJECT_ACTION } from '@/shared/enum/project.ts'; +import { useProjectDispatch } from '@/store/contexts/ProjectContext.tsx'; -export const useFetchProjectList = ( - searchTerm: string, - current: number, - intervalSize: number, - shouldRefetch: boolean, -) => { +export const useFetchProjectList = (searchTerm: string, current: number, intervalSize: number) => { const [projects, setProjects] = useState([]); const [count, setCount] = useState(0); + const dispatch = useProjectDispatch(); const fetchProjects = useCallback( async (searchTerm: string, current: number, intervalSize: number) => { fetchProjectFromSearchTerm(searchTerm, current, intervalSize) .then((json) => { + dispatch?.({ + type: PROJECT_ACTION.INIT_PROJECT_LIST, + payload: json.content, + } as ProjectActionType); setProjects(json.content); setCount(json.totalElements); }) @@ -31,7 +33,7 @@ export const useFetchProjectList = ( useEffect(() => { void fetchProjects(searchTerm, current, intervalSize); - }, [current, searchTerm, intervalSize, shouldRefetch]); + }, [current, searchTerm, intervalSize]); return { projects, count, refetch: fetchProjects }; }; diff --git a/src/hooks/useHandlePinnedProjectList.ts b/src/hooks/useHandlePinnedProjectList.ts index 694b264..cb03f93 100644 --- a/src/hooks/useHandlePinnedProjectList.ts +++ b/src/hooks/useHandlePinnedProjectList.ts @@ -5,17 +5,17 @@ */ import { useCallback, useEffect } from 'react'; -import { PinnedProjectActionType } from '@/shared/types/pegase/Project.type'; +import { ProjectActionType } from '@/shared/types/pegase/Project.type'; import { fetchPinnedProjects, pinProject, unpinProject } from '@/shared/services/pinnedProjectService'; -import { PINNED_PROJECT_ACTION } from '@/shared/enum/project'; -import { usePinnedProjectDispatch } from '@/store/contexts/ProjectContext.tsx'; import { v4 as uuidv4 } from 'uuid'; import { dismissToast, notifyToast, NotifyWithActionProps } from '@/shared/notification/notification.tsx'; import { useTranslation } from 'react-i18next'; +import { useProjectDispatch } from '@/store/contexts/ProjectContext.tsx'; +import { PROJECT_ACTION } from '@/shared/enum/project.ts'; export const useHandlePinnedProjectList = () => { const userId = 'me00247'; - const dispatch = usePinnedProjectDispatch(); + const dispatch = useProjectDispatch(); const { t } = useTranslation(); const getPinnedProjects = useCallback(async () => { @@ -23,9 +23,9 @@ export const useHandlePinnedProjectList = () => { const projects = await fetchPinnedProjects(userId); if (projects?.length) { dispatch?.({ - type: PINNED_PROJECT_ACTION.INIT_LIST, + type: PROJECT_ACTION.INIT_PINNED_PROJECT_LIST, payload: projects, - } as PinnedProjectActionType); + } as ProjectActionType); } } catch (error) { // silent handler @@ -47,9 +47,9 @@ export const useHandlePinnedProjectList = () => { const newProject = await pinProject(projectId); if (newProject) { dispatch?.({ - type: PINNED_PROJECT_ACTION.ADD_ITEM, + type: PROJECT_ACTION.ADD_PINNED_PROJECT, payload: newProject, - } as PinnedProjectActionType); + } as ProjectActionType); } notifyToast({ @@ -80,9 +80,9 @@ export const useHandlePinnedProjectList = () => { const currentPinnedProjects = await fetchPinnedProjects(userId); dispatch?.({ - type: PINNED_PROJECT_ACTION.REMOVE_ITEM, + type: PROJECT_ACTION.UNPIN_PINNED_PROJECT, payload: projectId, - } as PinnedProjectActionType); + } as ProjectActionType); notifyToast({ id: toastId, @@ -94,9 +94,9 @@ export const useHandlePinnedProjectList = () => { dismissToast(toastId); clearTimeout(apiCallTimeout!); dispatch?.({ - type: PINNED_PROJECT_ACTION.INIT_LIST, + type: PROJECT_ACTION.INIT_PINNED_PROJECT_LIST, payload: currentPinnedProjects, - } as PinnedProjectActionType); + } as ProjectActionType); }, }, } as NotifyWithActionProps); @@ -104,9 +104,9 @@ export const useHandlePinnedProjectList = () => { apiCallTimeout = setTimeout(() => { unpinProject(userId, projectId).catch((error) => { dispatch?.({ - type: PINNED_PROJECT_ACTION.INIT_LIST, + type: PROJECT_ACTION.INIT_PINNED_PROJECT_LIST, payload: currentPinnedProjects, - } as PinnedProjectActionType); + } as ProjectActionType); notifyToast({ id: toastId, diff --git a/src/pages/pegase/home/HomePage.tsx b/src/pages/pegase/home/HomePage.tsx index b4fdd1e..75c9c64 100644 --- a/src/pages/pegase/home/HomePage.tsx +++ b/src/pages/pegase/home/HomePage.tsx @@ -6,16 +6,16 @@ import HomePageContent from './components/HomePageContent'; import PinnedProject from '@/pages/pegase/home/pinnedProjects/PinnedProject'; -import { PinnedProjectProvider } from '@/store/contexts/ProjectContext.tsx'; +import { ProjectProvider } from '@/store/contexts/ProjectContext.tsx'; const HomePage = () => { return ( - +
-
+ ); }; diff --git a/src/pages/pegase/home/components/StudyTableDisplay.tsx b/src/pages/pegase/home/components/StudyTableDisplay.tsx index 56afd87..f6dd190 100644 --- a/src/pages/pegase/home/components/StudyTableDisplay.tsx +++ b/src/pages/pegase/home/components/StudyTableDisplay.tsx @@ -12,7 +12,7 @@ import getStudyTableHeaders from './StudyTableHeaders'; import { addSortColumn } from './StudyTableUtils'; import StudiesPagination from './StudiesPagination'; import { RowSelectionState } from '@tanstack/react-table'; -import StudyCreationModal from '../../studies/StudyCreationModal'; +import StudyCreationModal from '@common/modal/StudyCreationModal'; import { deleteStudy } from '@/shared/services/studyService'; import StdSimpleTable from '@/components/common/data/stdSimpleTable/StdSimpleTable'; import { RdsButton } from 'rte-design-system-react'; diff --git a/src/pages/pegase/home/pinnedProjects/PinnedProject.tsx b/src/pages/pegase/home/pinnedProjects/PinnedProject.tsx index 581f933..5472e4e 100644 --- a/src/pages/pegase/home/pinnedProjects/PinnedProject.tsx +++ b/src/pages/pegase/home/pinnedProjects/PinnedProject.tsx @@ -6,17 +6,12 @@ import PinnedProjectCards from '@/pages/pegase/home/pinnedProjects/PinnedProjectCard'; import ProjectCreator from '@/pages/pegase/home/pinnedProjects/ProjectCreator'; -import { Dispatch, SetStateAction } from 'react'; -interface PinnedProjectProps { - setShouldRefetchProjectList?: Dispatch>; -} - -const PinnedProject = ({ setShouldRefetchProjectList }: PinnedProjectProps) => { +const PinnedProject = () => { return (
- +
); }; diff --git a/src/pages/pegase/home/pinnedProjects/PinnedProjectCard.tsx b/src/pages/pegase/home/pinnedProjects/PinnedProjectCard.tsx index 2181962..a2f5312 100644 --- a/src/pages/pegase/home/pinnedProjects/PinnedProjectCard.tsx +++ b/src/pages/pegase/home/pinnedProjects/PinnedProjectCard.tsx @@ -12,23 +12,18 @@ import { useDropdownOptions } from '@/hooks/useDropdownOptions'; import { useProjectNavigation } from '@/hooks/useProjectNavigation'; import { RdsIcon, RdsIconId, RdsTagList } from 'rte-design-system-react'; import { deleteProjectById } from '@/shared/services/projectService'; -import { usePinnedProject, usePinnedProjectDispatch } from '@/store/contexts/ProjectContext'; import { useHandlePinnedProjectList } from '@/hooks/useHandlePinnedProjectList.ts'; -import { PINNED_PROJECT_ACTION } from '@/shared/enum/project.ts'; -import { PinnedProjectActionType } from '@/shared/types/pegase/Project.type.ts'; import { notifyToast } from '@/shared/notification/notification.tsx'; -import { Dispatch, SetStateAction } from 'react'; +import { useProject, useProjectDispatch } from '@/store/contexts/ProjectContext.tsx'; +import { ProjectActionType } from '@/shared/types/pegase/Project.type.ts'; +import { PROJECT_ACTION } from '@/shared/enum/project.ts'; -interface PinnedProjectCardsProps { - setShouldRefetchProjectList?: Dispatch>; -} - -const PinnedProjectCards = ({ setShouldRefetchProjectList }: PinnedProjectCardsProps) => { +const PinnedProjectCards = () => { const { t } = useTranslation(); const { navigateToProject } = useProjectNavigation(); const { settingOption, deleteOption, pinOption } = useDropdownOptions(); - const { pinnedProjects } = usePinnedProject(); - const dispatch = usePinnedProjectDispatch(); + const { pinnedProjects } = useProject(); + const dispatch = useProjectDispatch(); const { handleUnpinProject } = useHandlePinnedProjectList(); const handleCardClick = (projectId: string, projectName: string) => { @@ -38,11 +33,11 @@ const PinnedProjectCards = ({ setShouldRefetchProjectList }: PinnedProjectCardsP const deleteProject = async (projectId: string) => { try { await deleteProjectById(projectId); - setShouldRefetchProjectList?.(true); + // Update pinned project list dispatch?.({ - type: PINNED_PROJECT_ACTION.REMOVE_ITEM, + type: PROJECT_ACTION.REMOVE_PROJECT, payload: projectId, - } as PinnedProjectActionType); + } as ProjectActionType); notifyToast({ type: 'success', message: 'Project deleted successfully', diff --git a/src/pages/pegase/home/pinnedProjects/ProjectCreator.tsx b/src/pages/pegase/home/pinnedProjects/ProjectCreator.tsx index a812c1d..1e49d03 100644 --- a/src/pages/pegase/home/pinnedProjects/ProjectCreator.tsx +++ b/src/pages/pegase/home/pinnedProjects/ProjectCreator.tsx @@ -6,12 +6,17 @@ import { useTranslation } from 'react-i18next'; import { RdsButton } from 'rte-design-system-react'; +import { useNewStudyModal } from '@/hooks/useNewStudyModal.ts'; +import { ProjectCreationModal } from '@common/modal/ProjectCreationModal'; export const ProjectCreator = () => { const { t } = useTranslation(); + const { isModalOpen, toggleModal } = useNewStudyModal(); + return (
- + + {isModalOpen && }
); }; diff --git a/src/pages/pegase/projects/ProjectContent.tsx b/src/pages/pegase/projects/ProjectContent.tsx index 97b8101..9b1a6b6 100644 --- a/src/pages/pegase/projects/ProjectContent.tsx +++ b/src/pages/pegase/projects/ProjectContent.tsx @@ -17,30 +17,22 @@ import { deleteProjectById } from '@/shared/services/projectService.ts'; import { RdsChip, RdsTagList } from 'rte-design-system-react'; import { useFetchProjectList } from '@/hooks/useFetchProjectList'; import { useHandlePinnedProjectList } from '@/hooks/useHandlePinnedProjectList.ts'; -import { PINNED_PROJECT_ACTION } from '@/shared/enum/project.ts'; -import { PinnedProjectActionType } from '@/shared/types/pegase/Project.type.ts'; -import { usePinnedProjectDispatch } from '@/store/contexts/ProjectContext.tsx'; +import { ProjectActionType } from '@/shared/types/pegase/Project.type.ts'; +import { useProject, useProjectDispatch } from '@/store/contexts/ProjectContext.tsx'; +import { PROJECT_ACTION } from '@/shared/enum/project.ts'; -interface ProjectContentProps { - shouldRefetchProjectList: boolean; -} - -const ProjectContent = ({ shouldRefetchProjectList }: ProjectContentProps) => { +const ProjectContent = () => { const { t } = useTranslation(); const intervalSize = 9; const userName = 'mouad'; // Replace with actual user name const [searchTerm, setSearchTerm] = useState(''); const [activeChip, setActiveChip] = useState(false); const [current, setCurrent] = useState(0); - const { projects, count, refetch } = useFetchProjectList( - searchTerm || '', - current, - intervalSize, - shouldRefetchProjectList, - ); + const { count } = useFetchProjectList(searchTerm || '', current, intervalSize); const { navigateToProject } = useProjectNavigation(); const { handlePinProject } = useHandlePinnedProjectList(); - const dispatch = usePinnedProjectDispatch(); + const { projects } = useProject(); + const dispatch = useProjectDispatch(); const searchProject = (value?: string | undefined) => { value && setSearchTerm(value); @@ -58,11 +50,11 @@ const ProjectContent = ({ shouldRefetchProjectList }: ProjectContentProps) => { const deleteProject = async (projectId: string) => { await deleteProjectById(projectId); + // Met à jour la liste des projets (et les projets épinglés) dispatch?.({ - type: PINNED_PROJECT_ACTION.REMOVE_ITEM, + type: PROJECT_ACTION.REMOVE_PROJECT, payload: projectId, - } as PinnedProjectActionType); - await refetch(searchTerm, current, intervalSize); // Actualiser les projets après suppression + } as ProjectActionType); }; const handleCardClick = (projectId: string, projectName: string) => { @@ -82,7 +74,7 @@ const ProjectContent = ({ shouldRefetchProjectList }: ProjectContentProps) => { />
- {projects.map((project) => { + {(projects || []).map((project) => { const dropdownItems = [ pinOption(false, async () => handlePinProject(project.id)), settingOption(() => {}, t('project.@setting')), diff --git a/src/pages/pegase/projects/ProjectsPage.tsx b/src/pages/pegase/projects/ProjectsPage.tsx index 9b809ff..2ee6c17 100644 --- a/src/pages/pegase/projects/ProjectsPage.tsx +++ b/src/pages/pegase/projects/ProjectsPage.tsx @@ -6,18 +6,16 @@ import PinnedProject from '@/pages/pegase/home/pinnedProjects/PinnedProject'; import ProjectContent from '@/pages/pegase/projects/ProjectContent'; -import { PinnedProjectProvider } from '@/store/contexts/ProjectContext.tsx'; -import { useState } from 'react'; +import { ProjectProvider } from '@/store/contexts/ProjectContext.tsx'; const ProjectsPage = () => { - const [shouldRefetchProjectList, setShouldRefetchProjectList] = useState(false); return ( - +
- - + +
-
+ ); }; diff --git a/src/pages/pegase/projects/projectDetails/ProjectDetailsHeader.tsx b/src/pages/pegase/projects/projectDetails/ProjectDetailsHeader.tsx index 44f357f..82218f9 100644 --- a/src/pages/pegase/projects/projectDetails/ProjectDetailsHeader.tsx +++ b/src/pages/pegase/projects/projectDetails/ProjectDetailsHeader.tsx @@ -5,6 +5,8 @@ */ import StdAvatar from '@/components/common/layout/stdAvatar/StdAvatar'; +import { ProjectCreationModal } from '@/components/common/modal/ProjectCreationModal'; +import { useNewStudyModal } from '@/hooks/useNewStudyModal'; import { useTranslation } from 'react-i18next'; import { RdsButton, RdsHeading } from 'rte-design-system-react'; @@ -15,11 +17,13 @@ type ProjectDetailsHeaderProps = { const ProjectDetailsHeader = ({ projectName, createdBy }: ProjectDetailsHeaderProps) => { const { t } = useTranslation(); + const { isModalOpen, toggleModal } = useNewStudyModal(); return (
- + + {isModalOpen && } >; + maxNbKeywords?: number; + maxNbCharacters?: number; + minNbCharacters?: number; + width?: string; } -const KeywordsInput: React.FC = ({ keywords, setKeywords }) => { +const KeywordsInput: React.FC = ({ + keywords, + setKeywords, + maxNbKeywords, + maxNbCharacters, + minNbCharacters, + width, +}) => { + const { t } = useTranslation(); const [keywordInput, setKeywordInput] = useState(''); const [errorMessage, setErrorMessage] = useState(''); const [suggestedKeywords, setSuggestedKeywords] = useState([]); @@ -34,11 +46,19 @@ const KeywordsInput: React.FC = ({ keywords, setKeywords }) const handleAddKeyword = (suggestedKeyword = keywordInput) => { if (suggestedKeyword.trim()) { if (keywords.includes(suggestedKeyword.trim())) { - setErrorMessage('Keyword already exists'); - } else if (suggestedKeyword.trim().length < 3 || suggestedKeyword.trim().length > 10) { - setErrorMessage('Keyword must be between 3 and 10 characters'); - } else if (keywords.length >= MAX_KEYWORDS) { - setErrorMessage('Cannot add more than 6 keywords'); + setErrorMessage(t('projectModal.@keyword_already_exists')); + } else if ( + minNbCharacters && + maxNbCharacters && + (suggestedKeyword.trim().length < minNbCharacters || suggestedKeyword.trim().length > maxNbCharacters) + ) { + setErrorMessage(t('projectModal.@keyword_length_error', { min: minNbCharacters, max: maxNbCharacters })); + } else if (minNbCharacters && !maxNbCharacters && suggestedKeyword.trim().length < minNbCharacters) { + setErrorMessage(t('projectModal.@keyword_minimum_error', { min: minNbCharacters })); + } else if (!minNbCharacters && maxNbCharacters && suggestedKeyword.trim().length > maxNbCharacters) { + setErrorMessage(t('projectModal.@keyword_maximum_error', { max: maxNbCharacters })); + } else if (maxNbKeywords && keywords.length >= maxNbKeywords) { + setErrorMessage(t('projectModal.@keyword_max_keys_errors', { maxNbKey: maxNbKeywords })); } else { setKeywords((prevKeywords) => [...prevKeywords, suggestedKeyword.trim()]); setKeywordInput(''); @@ -56,9 +76,9 @@ const KeywordsInput: React.FC = ({ keywords, setKeywords }) }; return ( -
-
-
+
+
+
= ({ keywords, setKeywords }) placeHolder="Add a keyword" variant="outlined" /> - {keywordInput && keywordInput.length >= 3 && ( + {keywordInput && (minNbCharacters ? keywordInput.length >= minNbCharacters : true) && ( handleAddKeyword()} icon={RdsIconId.Add} @@ -78,7 +98,7 @@ const KeywordsInput: React.FC = ({ keywords, setKeywords })
{/* Suggested Keywords Dropdown */} - {keywordInput && ( + {keywordInput && suggestedKeywords.length > 0 && (
= ({ keywords, setKeywords })
{/* Error Message */} - {errorMessage &&
{errorMessage}
} + {errorMessage &&
{errorMessage}
} {/* Keywords Display and Clear All Button */}
{keywords.map((keyword, index) => ( -
+
{keyword} = ({ keywords, setKeywords }) {/* Clear All Keywords Button */} {keywords.length > 0 && ( -
+
Clear all
diff --git a/src/shared/enum/project.ts b/src/shared/enum/project.ts index 41bcb59..14f7c61 100644 --- a/src/shared/enum/project.ts +++ b/src/shared/enum/project.ts @@ -4,8 +4,11 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -export enum PINNED_PROJECT_ACTION { - ADD_ITEM = 'ADD_ITEM', - REMOVE_ITEM = 'REMOVE_ITEM', - INIT_LIST = 'INIT_LIST', +export enum PROJECT_ACTION { + ADD_PINNED_PROJECT = 'ADD_PINNED_PROJECT', + UNPIN_PINNED_PROJECT = 'UNPIN_PINNED_PROJECT', + INIT_PINNED_PROJECT_LIST = 'INIT_PINNED_PROJECT_LIST', + ADD_PROJECT = 'ADD_PROJECT', + REMOVE_PROJECT = 'REMOVE_PROJECT', + INIT_PROJECT_LIST = 'INIT_PROJECT_LIST', } diff --git a/src/shared/i18n/en.json b/src/shared/i18n/en.json index aeb6473..2a52bbd 100644 --- a/src/shared/i18n/en.json +++ b/src/shared/i18n/en.json @@ -39,6 +39,7 @@ "@my_studies": "My studies", "@my_projects": "My projects", "@new_study": "Create a study", + "@new_project": "Create a project", "@duplicate_study": "Duplicate a study" }, "project": { @@ -52,5 +53,12 @@ }, "projectDetails": { "@buttonEdit": "Edit" + }, + "projectModal": { + "@keyword_already_exists": "Keyword already exists", + "@keyword_length_error": "Keyword must be between {{min}} and {{max}} characters", + "@keyword_minimum_error": "Keyword must be at least {{min}} characters", + "@keyword_maximum_error": "Keyword must not exceed {{max}} characters", + "@keyword_max_keys_errors": "Cannot add more than ${maxNbKey} keywords`" } } diff --git a/src/shared/i18n/fr.json b/src/shared/i18n/fr.json index 0fae96c..315b91e 100644 --- a/src/shared/i18n/fr.json +++ b/src/shared/i18n/fr.json @@ -50,5 +50,12 @@ }, "projectDetails": { "@buttonEdit": "Editer" + }, + "projectModal": { + "@keyword_already_exists": "Le mot-clé existe déjà", + "@keyword_length_error": "Le mot-clé doit avoir entre {{min}} et {{max}} lettres", + "@keyword_minimum_error": "Le mot-clé doit avoir à minumum {{min}} lettres", + "@keyword_maximum_error": "Le mot-clé ne doit pas dépasser {{max}} lettres", + "@keyword_max_keys_errors": "Il n'est pas possible d'ajouter plus que ${maxNbKey} mot-clés`" } } diff --git a/src/shared/services/projectService.ts b/src/shared/services/projectService.ts index b1e02ce..10c4b79 100644 --- a/src/shared/services/projectService.ts +++ b/src/shared/services/projectService.ts @@ -5,6 +5,7 @@ */ import { PROJECT_AUTOCOMPLETE_ENDPOINT, PROJECT_ENDPOINT, PROJECT_SEARCH_ENDPOINT } from '@/shared/const/apiEndPoint'; +import { ProjectInfo, ProjectResponse } from '@/shared/types/pegase/Project.type'; export const deleteProjectById = async (projectId: string) => { const response = await fetch(`${PROJECT_ENDPOINT}/${projectId}`, { @@ -66,3 +67,30 @@ export const fetchProjectFromSearchTerm = async (searchTerm: string, current: nu return await response.json(); }; + +/** + * Create a new project + * + * @param {Pick} projectData - Body data request + * @return {Promise} + */ +export const createProject = async ( + projectData: Pick, +): Promise => { + const apiUrl = `${PROJECT_ENDPOINT}`; + + const response = await fetch(apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(projectData), + }); + + if (!response.ok) { + const errorText = await response.text(); + const errorData = JSON.parse(errorText); + throw new Error(`${errorData.message}`); + } + return await response.json(); +}; diff --git a/src/shared/services/studyService.ts b/src/shared/services/studyService.ts index 3cfc0f7..ef00bd7 100644 --- a/src/shared/services/studyService.ts +++ b/src/shared/services/studyService.ts @@ -63,12 +63,8 @@ export const fetchSuggestedKeywords = async (query: string): Promise = * Display toast if creation succeeds or fails * * @param {Omit} studyData - Partial study data - * @param {function} toggleModal - Handle toggle of opening modal boolean */ -export const saveStudy = async ( - studyData: Omit, - toggleModal: () => void, -) => { +export const saveStudy = async (studyData: Omit) => { try { const response = await fetch(`${STUDY_ENDPOINT}`, { method: 'POST', @@ -86,7 +82,6 @@ export const saveStudy = async ( type: 'success', message: 'Study created successfully', }); - toggleModal(); } catch (error: unknown) { if (error instanceof Error) { notifyToast({ diff --git a/src/shared/services/test/projectService.test.tsx b/src/shared/services/test/projectService.test.tsx index 7b1e498..8a49018 100644 --- a/src/shared/services/test/projectService.test.tsx +++ b/src/shared/services/test/projectService.test.tsx @@ -4,12 +4,15 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { deleteProjectById, fetchProjectDetails, fetchProjectsFromPartialName } from '../projectService.ts'; +import { + createProject, + deleteProjectById, + fetchProjectDetails, + fetchProjectsFromPartialName, +} from '@/shared/services/projectService'; import { vi } from 'vitest'; import { waitFor } from '@testing-library/react'; -const mockFetch = vi.fn(); -global.fetch = mockFetch; vi.mock('@/shared/notification/notification'); vi.mock('@/envVariables', () => ({ getEnvVariables: vi.fn(() => 'https://mockapi.com'), @@ -21,10 +24,17 @@ describe('deleteProjectById', () => { beforeEach(() => { global.fetch = vi.fn(); vi.clearAllMocks(); + vi.stubGlobal('JSON', { + parse: (text: string) => { + return { message: text }; + }, + stringify: (text: string) => text, + }); }); afterEach(() => { vi.restoreAllMocks(); + vi.unstubAllGlobals(); }); it('should delete a pinned project from pinned project list', async () => { @@ -49,9 +59,8 @@ describe('deleteProjectById', () => { // Failed fetch response moc global.fetch = vi.fn().mockResolvedValueOnce({ ok: false, - text: async () => 'Failed to delete project', + text: () => 'Failed to delete project', }); - vi.stubGlobal('JSON', { parse: (text: string) => text }); await expect(async () => deleteProjectById(projectId)).rejects.toThrowError('Failed to delete project'); }); @@ -81,7 +90,7 @@ describe('fetchProjectDetails', () => { //Successful fetch response mock global.fetch = vi.fn().mockResolvedValueOnce({ ok: true, - json: async () => mockResponse, + json: async () => Promise.resolve(mockResponse), }); await fetchProjectDetails(projectId); @@ -113,17 +122,24 @@ describe('fetchProjectsFromPartialName', () => { beforeEach(() => { global.fetch = vi.fn(); vi.clearAllMocks(); + vi.stubGlobal('JSON', { + parse: (text: string) => { + return { message: text }; + }, + stringify: (text: string) => text, + }); }); afterEach(() => { vi.restoreAllMocks(); + vi.unstubAllGlobals(); }); it('should delete a pinned project from pinned project list', async () => { global.fetch = vi.fn().mockResolvedValueOnce({ ok: true, - json: async () => { - return [ + json: () => { + return Promise.resolve([ { id: '123', name: 'Bilan prévisionnel 2023', @@ -140,7 +156,7 @@ describe('fetchProjectsFromPartialName', () => { creationDate: '2013-08-01', tags: ['tag3', 'tag4'], }, - ]; + ]); }, }); @@ -155,11 +171,69 @@ describe('fetchProjectsFromPartialName', () => { it('should handle delete failure gracefully', async () => { // Failed fetch response moc + vi.stubGlobal('JSON', { parse: (text: string) => text }); global.fetch = vi.fn().mockResolvedValueOnce({ ok: false, - text: async () => 'Error', + text: () => 'Error', }); await expect(async () => fetchProjectsFromPartialName('name')).rejects.toThrowError('Failed to fetch projects'); }); }); + +describe('createProject', () => { + const projectData = { name: 'Bilan prévisionnel 2050', description: '', tags: ['tag1'] }; + + beforeEach(() => { + global.fetch = vi.fn(); + vi.clearAllMocks(); + vi.stubGlobal('JSON', { + parse: (text: string) => { + return { message: text }; + }, + stringify: (text: string) => text, + }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + }); + + it('should create de project', async () => { + const mockProjectResponse = { + id: 107, + name: 'Bilan prévisionnel 2050', + createdBy: 'pegase', + creationDate: '2025-01-30T10:32:10.631003175', + studies: [], + tags: [], + description: '', + }; + global.fetch = vi.fn().mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(mockProjectResponse), + }); + + await createProject(projectData); + + expect(global.fetch).toHaveBeenCalledTimes(1); + expect(global.fetch).toHaveBeenCalledWith(`https://mockapi.com/v1/project`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(projectData), + }); + }); + + it('should handle delete failure gracefully', async () => { + // Failed fetch response moc + global.fetch = vi.fn().mockResolvedValueOnce({ + ok: false, + text: () => 'Failed to create project', + }); + + await expect(async () => createProject(projectData)).rejects.toThrowError('Failed to create project'); + }); +}); diff --git a/src/shared/services/test/studyService.test.tsx b/src/shared/services/test/studyService.test.tsx index a4af1c6..1a82ec1 100644 --- a/src/shared/services/test/studyService.test.tsx +++ b/src/shared/services/test/studyService.test.tsx @@ -129,9 +129,8 @@ describe('saveStudy', () => { ok: true, json: () => Promise.resolve(), }); - const toggleModal = vi.fn(); - await saveStudy(mockStudy, toggleModal); + await saveStudy(mockStudy); expect(global.fetch).toHaveBeenCalledTimes(1); expect(global.fetch).toHaveBeenCalledWith('https://mockapi.com/v1/study', { @@ -145,7 +144,6 @@ describe('saveStudy', () => { type: 'success', message: 'Study created successfully', }); - expect(toggleModal).toHaveBeenCalledTimes(1); }); it('should throw an error message', async () => { @@ -155,7 +153,7 @@ describe('saveStudy', () => { text: () => 'error', }); - const result = await saveStudy(mockStudy, vi.fn()); + const result = await saveStudy(mockStudy); expect(result).toEqual(undefined); }); @@ -163,7 +161,7 @@ describe('saveStudy', () => { // Failed fetch response moc global.fetch = vi.fn().mockRejectedValueOnce(new Error('Failed to create study')); - await saveStudy(mockStudy, vi.fn()); + await saveStudy(mockStudy); expect(notifyToast).toHaveBeenCalledWith({ type: 'error', diff --git a/src/shared/types/pegase/Project.type.ts b/src/shared/types/pegase/Project.type.ts index 8540169..25cb75a 100644 --- a/src/shared/types/pegase/Project.type.ts +++ b/src/shared/types/pegase/Project.type.ts @@ -4,32 +4,46 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { PINNED_PROJECT_ACTION } from '@/shared/enum/project.ts'; +import { PROJECT_ACTION } from '@/shared/enum/project.ts'; -export interface ProjectInfo { +export interface ProjectResponse { id: string; name: string; description: string; createdBy: string; creationDate: Date; + tags: string[]; + studies: number[]; +} + +export interface ProjectInfo extends ProjectResponse { + description: string; archived?: boolean; pinned?: boolean; path: string; - tags: string[]; - studies: number[]; } -export type PinnedProjectActionType = - | { type: PINNED_PROJECT_ACTION.ADD_ITEM; payload: ProjectInfo } +export type ProjectActionType = + | { type: PROJECT_ACTION.ADD_PINNED_PROJECT; payload: ProjectInfo } + | { type: PROJECT_ACTION.ADD_PROJECT; payload: ProjectInfo } | { - type: PINNED_PROJECT_ACTION.REMOVE_ITEM; + type: PROJECT_ACTION.REMOVE_PROJECT; payload: string; } | { - type: PINNED_PROJECT_ACTION.INIT_LIST; + type: PROJECT_ACTION.UNPIN_PINNED_PROJECT; + payload: string; + } + | { + type: PROJECT_ACTION.INIT_PINNED_PROJECT_LIST; + payload: ProjectInfo[]; + } + | { + type: PROJECT_ACTION.INIT_PROJECT_LIST; payload: ProjectInfo[]; }; -export interface PinnedProjectState { +export interface ProjectState { + projects: ProjectInfo[]; pinnedProjects: ProjectInfo[]; } diff --git a/src/store/contexts/ProjectContext.tsx b/src/store/contexts/ProjectContext.tsx index 934df7a..c11b507 100644 --- a/src/store/contexts/ProjectContext.tsx +++ b/src/store/contexts/ProjectContext.tsx @@ -5,30 +5,30 @@ */ import { createContext, Dispatch, ReactNode, useContext, useReducer } from 'react'; -import { PinnedProjectActionType, PinnedProjectState } from '@/shared/types/pegase/Project.type'; -import pinnedProjectReducer from '@/store/reducers/projectReducer'; +import { ProjectActionType, ProjectState } from '@/shared/types/pegase/Project.type'; +import projectReducer from '@/store/reducers/projectReducer'; -const initialValue: PinnedProjectState = { pinnedProjects: [] }; +const initialValue: ProjectState = { projects: [], pinnedProjects: [] }; -export const PinnedProjectContext = createContext(initialValue); -export const PinnedProjectDispatchContext = createContext | null>(null); +export const ProjectContext = createContext(initialValue); +export const ProjectDispatchContext = createContext | null>(null); -export const usePinnedProject = () => useContext(PinnedProjectContext); -export const usePinnedProjectDispatch = () => useContext(PinnedProjectDispatchContext); +export const useProject = () => useContext(ProjectContext); +export const useProjectDispatch = () => useContext(ProjectDispatchContext); -export interface PinnedProjectProviderProps { +export interface ProjectProviderProps { children: ReactNode; - initialValue: PinnedProjectState; + initialValue: ProjectState; } -export const PinnedProjectProvider = ({ children, initialValue }: PinnedProjectProviderProps) => { +export const ProjectProvider = ({ children, initialValue }: ProjectProviderProps) => { const initializer = (value = initialValue) => value; - const [state, dispatch] = useReducer(pinnedProjectReducer, initialValue, initializer); + const [state, dispatch] = useReducer(projectReducer, initialValue, initializer); return ( - - {children} - + + {children} + ); }; diff --git a/src/store/reducers/projectReducer.tsx b/src/store/reducers/projectReducer.tsx index d27f283..cb845d2 100644 --- a/src/store/reducers/projectReducer.tsx +++ b/src/store/reducers/projectReducer.tsx @@ -4,26 +4,47 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { PinnedProjectActionType, PinnedProjectState, ProjectInfo } from '@/shared/types/pegase/Project.type'; -import { PINNED_PROJECT_ACTION } from '@/shared/enum/project.ts'; +import { ProjectActionType, ProjectInfo, ProjectState } from '@/shared/types/pegase/Project.type'; +import { PROJECT_ACTION } from '@/shared/enum/project'; -const addItem = (currentState: ProjectInfo[], payload: ProjectInfo) => { - return { pinnedProjects: [...currentState, payload] }; +// PROJECTS +const addProject = (currentState: ProjectState, payload: ProjectInfo) => { + const { pinnedProjects, projects } = currentState; + return { projects: [...projects, payload], pinnedProjects }; }; -const removeItem = (currentState: ProjectInfo[], payload: string) => { - return { pinnedProjects: [...currentState.filter((p) => p.id !== payload)] }; +const removeProject = (currentState: ProjectState, payload: string) => { + const { pinnedProjects, projects } = currentState; + return { + projects: [...projects.filter((p) => p.id !== payload)], + pinnedProjects: [...pinnedProjects.filter((p) => p.id !== payload)], + }; +}; +// PINNED PROJECTS +const addPinnedProject = (currentState: ProjectState, payload: ProjectInfo) => { + const { pinnedProjects, projects } = currentState; + payload.pinned = true; + return { projects, pinnedProjects: [...pinnedProjects, payload] }; +}; +const unpinPinnedProject = (currentState: ProjectState, payload: string) => { + const { pinnedProjects, projects } = currentState; + return { projects, pinnedProjects: [...pinnedProjects.filter((p) => p.id !== payload)] }; }; -const pinnedProjectReducer = (prevState: PinnedProjectState, action?: PinnedProjectActionType): PinnedProjectState => { - const { pinnedProjects } = prevState; +const projectReducer = (prevState: ProjectState, action?: ProjectActionType): ProjectState => { if (action) { switch (action.type) { - case PINNED_PROJECT_ACTION.ADD_ITEM: - return addItem(pinnedProjects, action.payload); - case PINNED_PROJECT_ACTION.REMOVE_ITEM: - return removeItem(pinnedProjects, action.payload); - case PINNED_PROJECT_ACTION.INIT_LIST: - return { pinnedProjects: [...action.payload] }; + case PROJECT_ACTION.ADD_PROJECT: + return addProject(prevState, action.payload); + case PROJECT_ACTION.REMOVE_PROJECT: + return removeProject(prevState, action.payload); + case PROJECT_ACTION.INIT_PROJECT_LIST: + return { projects: [...action.payload], pinnedProjects: [...prevState.pinnedProjects] }; + case PROJECT_ACTION.ADD_PINNED_PROJECT: + return addPinnedProject(prevState, action.payload); + case PROJECT_ACTION.UNPIN_PINNED_PROJECT: + return unpinPinnedProject(prevState, action.payload); + case PROJECT_ACTION.INIT_PINNED_PROJECT_LIST: + return { projects: [...prevState.projects], pinnedProjects: [...action.payload] }; default: return prevState; } @@ -32,4 +53,4 @@ const pinnedProjectReducer = (prevState: PinnedProjectState, action?: PinnedProj return prevState; }; -export default pinnedProjectReducer; +export default projectReducer; diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo index 5d15a36..9d4e3cb 100644 --- a/tsconfig.tsbuildinfo +++ b/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/App.tsx","./src/envVariables.ts","./src/i18n.ts","./src/main.tsx","./src/routes.tsx","./src/testSetup.ts","./src/vite-env.d.ts","./src/components/common/base/stdIcon/Icon.tsx","./src/components/common/base/stdIcon/StdIcon.tsx","./src/components/common/base/stdIcon/iconClassBuilder.ts","./src/components/common/data/stdSimpleTable/StdSimpleTable.tsx","./src/components/common/data/stdSimpleTable/tests/stdSimpleTable.test.tsx","./src/components/common/data/stdTable/TableContext.tsx","./src/components/common/data/stdTable/TableCore.tsx","./src/components/common/data/stdTable/tableCoreRowClassBuilder.ts","./src/components/common/data/stdTable/useTableContext.ts","./src/components/common/data/stdTable/cells/ExpandableCell.tsx","./src/components/common/data/stdTable/cells/tests/expandableCell.test.tsx","./src/components/common/data/stdTable/features/readOnly.ts","./src/components/common/data/stdTable/lineRender/StdCollapseIcon.tsx","./src/components/common/data/stdTable/tests/TableCore.test.tsx","./src/components/common/data/stdTable/tests/tableCoreRowClassBuilder.test.ts","./src/components/common/data/stdTable/tests/testTableUtils.ts","./src/components/common/data/stdTable/types/readOnly.type.d.ts","./src/components/common/data/stdTable/types/sizeClassNames.d.ts","./src/components/common/handler/ThemeHandler.tsx","./src/components/common/handler/test/ThemeHandler.test.tsx","./src/components/common/layout/stdAvatar/StdAvatar.tsx","./src/components/common/layout/stdAvatar/avatarClassBuilder.ts","./src/components/common/layout/stdAvatar/tests/StdAvatar.test.tsx","./src/components/common/layout/stdAvatar/tests/avatarClassBuilder.test.ts","./src/components/common/layout/stdAvatarGroup/StdAvatarGroup.tsx","./src/components/common/layout/stdAvatarGroup/avatarGroupClassBuilder.ts","./src/components/common/layout/stdAvatarGroup/avatarTools.ts","./src/components/common/layout/stdAvatarGroup/tests/StdAvatarGroup.test.tsx","./src/components/common/layout/stdAvatarGroup/tests/avatarTools.test.ts","./src/components/common/layout/stdNavbar/StdNavbar.tsx","./src/components/common/layout/stdNavbar/StdNavbarController.tsx","./src/components/common/layout/stdNavbar/StdNavbarHeader.tsx","./src/components/common/layout/stdNavbar/StdNavbarMenu.tsx","./src/components/common/layout/stdNavbar/StdNavbarMenuItem.tsx","./src/components/common/layout/stdNavbar/navbarClassBuilder.ts","./src/components/common/layout/stdNavbar/tests/StdNavbar.test.tsx","./src/components/common/layout/stdNavbar/tests/StdNavbarController.test.tsx","./src/components/common/layout/stdNavbar/tests/StdNavbarHeader.test.tsx","./src/components/common/layout/stdNavbar/tests/StdNavbarMenuItem.test.tsx","./src/components/common/layout/stdNavbar/tests/navbarClassBuilder.test.ts","./src/components/common/layout/stdTextWithTooltip/StdTextWithTooltip.tsx","./src/components/pegase/header/Header.tsx","./src/components/pegase/navbar/Navbar.tsx","./src/components/pegase/pegaseCard/cardClassBuilder.ts","./src/components/pegase/pegaseCard/pegaseCard.tsx","./src/components/pegase/pegaseCard/pegaseCardTitle/cardTitleClassBuilder.tsx","./src/components/pegase/pegaseCard/pegaseCardTitle/pegaseCardTitle.tsx","./src/components/pegase/pegaseCard/pegaseCardTitle/tests/cardTitleClassBuilder.test.ts","./src/components/pegase/pegaseCard/pegaseCardTitle/tests/pegaseCardTitle.test.tsx","./src/components/pegase/pegaseCard/tests/cardTripleActionClassBuilder.test.ts","./src/components/pegase/pegaseCard/tests/pegaseCardTripleAction.test.tsx","./src/components/pegase/star/PegaseStar.tsx","./src/hooks/useDateFormatter.ts","./src/hooks/useDropdownOptions.ts","./src/hooks/useFetchProjectList.ts","./src/hooks/useHandlePinnedProjectList.ts","./src/hooks/useNewStudyModal.ts","./src/hooks/useProjectNavigation.ts","./src/hooks/useStudyNavigation.ts","./src/hooks/useStudyTableDisplay.ts","./src/hooks/common/usePrevious.ts","./src/hooks/common/test/usePrevious.test.ts","./src/hooks/test/useDropdownOptions.test.ts","./src/hooks/test/useFetchProjectList.test.ts","./src/hooks/test/useHandlePinnedProjectList.test.tsx","./src/hooks/test/useNewStudyModal.test.ts","./src/hooks/test/useProjectNavigation.test.tsx","./src/hooks/test/useStudyNavigation.test.tsx","./src/hooks/test/useStudyTableDisplay.test.ts","./src/mocks/mockTools.ts","./src/mocks/data/components/dropdownItems.mock.ts","./src/mocks/data/components/navbarHeader.ts","./src/mocks/data/features/menuItemData.mock.tsx","./src/mocks/data/list/keywords.ts","./src/mocks/data/list/names.ts","./src/mocks/data/list/projectName.ts","./src/mocks/data/list/studyName.ts","./src/mocks/data/list/user.mocks.ts","./src/mocks/data/list/user.ts","./src/pages/pegase/antares/Antares.tsx","./src/pages/pegase/home/HomePage.tsx","./src/pages/pegase/home/components/HomePageContent.tsx","./src/pages/pegase/home/components/SearchBar.tsx","./src/pages/pegase/home/components/StudiesPagination.tsx","./src/pages/pegase/home/components/StudyTableDisplay.tsx","./src/pages/pegase/home/components/StudyTableHeaders.tsx","./src/pages/pegase/home/components/StudyTableUtils.tsx","./src/pages/pegase/home/pinnedProjects/PinnedProject.tsx","./src/pages/pegase/home/pinnedProjects/PinnedProjectCard.tsx","./src/pages/pegase/home/pinnedProjects/ProjectCreator.tsx","./src/pages/pegase/logout/Logout.tsx","./src/pages/pegase/projects/ProjectContent.tsx","./src/pages/pegase/projects/ProjectsPage.tsx","./src/pages/pegase/projects/ProjectsPagination.tsx","./src/pages/pegase/projects/projectDetails/ProjectDetails.tsx","./src/pages/pegase/projects/projectDetails/ProjectDetailsContent.tsx","./src/pages/pegase/projects/projectDetails/ProjectDetailsHeader.tsx","./src/pages/pegase/reports/LogsPage.tsx","./src/pages/pegase/settings/Settings.tsx","./src/pages/pegase/studies/HorizonInput.tsx","./src/pages/pegase/studies/KeywordsInput.tsx","./src/pages/pegase/studies/ProjectInput.tsx","./src/pages/pegase/studies/StudyCreationModal.tsx","./src/pages/pegase/studies/studyDetails/AreaLinkTab.tsx","./src/pages/pegase/studies/studyDetails/AreaLinkTableHeaders.tsx","./src/pages/pegase/studies/studyDetails/EnrTab.tsx","./src/pages/pegase/studies/studyDetails/LoadTab.tsx","./src/pages/pegase/studies/studyDetails/MiscLinkTab.tsx","./src/pages/pegase/studies/studyDetails/StudyDetailsContent.tsx","./src/pages/pegase/studies/studyDetails/StudyNavigationMenu.tsx","./src/pages/pegase/studies/studyDetails/ThermalTab.tsx","./src/pages/pegase/studies/studyDetails/areaLinkTable.tsx","./src/pages/pegase/studies/studyDetails/studyDetails.tsx","./src/pages/pegase/studies/studyDetails/studyHeader.tsx","./src/shared/constants.ts","./src/shared/const/apiEndPoint.ts","./src/shared/enum/project.ts","./src/shared/notification/containers.tsx","./src/shared/notification/notification.tsx","./src/shared/services/pinnedProjectService.ts","./src/shared/services/projectService.ts","./src/shared/services/studyService.ts","./src/shared/services/test/pinnedProjectService.test.tsx","./src/shared/services/test/projectService.test.tsx","./src/shared/services/test/studyService.test.tsx","./src/shared/types/index.ts","./src/shared/types/common/DisplayStatus.type.ts","./src/shared/types/common/MenuNavItem.type.ts","./src/shared/types/common/StdBase.type.ts","./src/shared/types/common/StudyStatus.type.ts","./src/shared/types/common/Tailwind.type.ts","./src/shared/types/common/TailwindColorClass.type.ts","./src/shared/types/common/User.type.ts","./src/shared/types/common/UserSettings.type.ts","./src/shared/types/common/tests/testUtils.tsx","./src/shared/types/pegase/Project.type.ts","./src/shared/types/pegase/Study.type.ts","./src/shared/utils/dateFormatter.ts","./src/shared/utils/slotsUtils.ts","./src/shared/utils/tabIndexUtils.ts","./src/shared/utils/common/defaultUtils.ts","./src/shared/utils/common/displayUtils.ts","./src/shared/utils/common/slotsUtils.ts","./src/shared/utils/common/classes/classMerger.ts","./src/shared/utils/common/classes/test/classMerger.test.ts","./src/shared/utils/common/dom/getDimensions.ts","./src/shared/utils/common/dom/test/getDimensions.test.tsx","./src/shared/utils/common/mappings/iconMaps.ts","./src/store/contexts/ModalContext.tsx","./src/store/contexts/ProjectContext.tsx","./src/store/contexts/UserContext.tsx","./src/store/contexts/createFastContext.tsx","./src/store/reducers/projectReducer.tsx","./tailwind.config.ts","./vite-env.d.ts"],"version":"5.7.2"} \ No newline at end of file +{"root":["./src/App.tsx","./src/envVariables.ts","./src/i18n.ts","./src/main.tsx","./src/routes.tsx","./src/testSetup.ts","./src/vite-env.d.ts","./src/components/common/base/stdIcon/Icon.tsx","./src/components/common/base/stdIcon/StdIcon.tsx","./src/components/common/base/stdIcon/iconClassBuilder.ts","./src/components/common/data/stdSimpleTable/StdSimpleTable.tsx","./src/components/common/data/stdSimpleTable/tests/stdSimpleTable.test.tsx","./src/components/common/data/stdTable/TableContext.tsx","./src/components/common/data/stdTable/TableCore.tsx","./src/components/common/data/stdTable/tableCoreRowClassBuilder.ts","./src/components/common/data/stdTable/useTableContext.ts","./src/components/common/data/stdTable/cells/ExpandableCell.tsx","./src/components/common/data/stdTable/cells/tests/expandableCell.test.tsx","./src/components/common/data/stdTable/features/readOnly.ts","./src/components/common/data/stdTable/lineRender/StdCollapseIcon.tsx","./src/components/common/data/stdTable/tests/TableCore.test.tsx","./src/components/common/data/stdTable/tests/tableCoreRowClassBuilder.test.ts","./src/components/common/data/stdTable/tests/testTableUtils.ts","./src/components/common/data/stdTable/types/readOnly.type.d.ts","./src/components/common/data/stdTable/types/sizeClassNames.d.ts","./src/components/common/handler/ThemeHandler.tsx","./src/components/common/handler/test/ThemeHandler.test.tsx","./src/components/common/layout/stdAvatar/StdAvatar.tsx","./src/components/common/layout/stdAvatar/avatarClassBuilder.ts","./src/components/common/layout/stdAvatar/tests/StdAvatar.test.tsx","./src/components/common/layout/stdAvatar/tests/avatarClassBuilder.test.ts","./src/components/common/layout/stdAvatarGroup/StdAvatarGroup.tsx","./src/components/common/layout/stdAvatarGroup/avatarGroupClassBuilder.ts","./src/components/common/layout/stdAvatarGroup/avatarTools.ts","./src/components/common/layout/stdAvatarGroup/tests/StdAvatarGroup.test.tsx","./src/components/common/layout/stdAvatarGroup/tests/avatarTools.test.ts","./src/components/common/layout/stdNavbar/StdNavbar.tsx","./src/components/common/layout/stdNavbar/StdNavbarController.tsx","./src/components/common/layout/stdNavbar/StdNavbarHeader.tsx","./src/components/common/layout/stdNavbar/StdNavbarMenu.tsx","./src/components/common/layout/stdNavbar/StdNavbarMenuItem.tsx","./src/components/common/layout/stdNavbar/navbarClassBuilder.ts","./src/components/common/layout/stdNavbar/tests/StdNavbar.test.tsx","./src/components/common/layout/stdNavbar/tests/StdNavbarController.test.tsx","./src/components/common/layout/stdNavbar/tests/StdNavbarHeader.test.tsx","./src/components/common/layout/stdNavbar/tests/StdNavbarMenuItem.test.tsx","./src/components/common/layout/stdNavbar/tests/navbarClassBuilder.test.ts","./src/components/common/layout/stdTextWithTooltip/StdTextWithTooltip.tsx","./src/components/common/modal/ProjectCreationModal.tsx","./src/components/common/modal/StudyCreationModal.tsx","./src/components/pegase/header/Header.tsx","./src/components/pegase/navbar/Navbar.tsx","./src/components/pegase/pegaseCard/cardClassBuilder.ts","./src/components/pegase/pegaseCard/pegaseCard.tsx","./src/components/pegase/pegaseCard/pegaseCardTitle/cardTitleClassBuilder.tsx","./src/components/pegase/pegaseCard/pegaseCardTitle/pegaseCardTitle.tsx","./src/components/pegase/pegaseCard/pegaseCardTitle/tests/cardTitleClassBuilder.test.ts","./src/components/pegase/pegaseCard/pegaseCardTitle/tests/pegaseCardTitle.test.tsx","./src/components/pegase/pegaseCard/tests/cardTripleActionClassBuilder.test.ts","./src/components/pegase/pegaseCard/tests/pegaseCardTripleAction.test.tsx","./src/components/pegase/star/PegaseStar.tsx","./src/hooks/useDateFormatter.ts","./src/hooks/useDropdownOptions.ts","./src/hooks/useFetchProjectList.ts","./src/hooks/useHandlePinnedProjectList.ts","./src/hooks/useNewStudyModal.ts","./src/hooks/useProjectNavigation.ts","./src/hooks/useStudyNavigation.ts","./src/hooks/useStudyTableDisplay.ts","./src/hooks/common/usePrevious.ts","./src/hooks/common/test/usePrevious.test.ts","./src/hooks/test/useDropdownOptions.test.ts","./src/hooks/test/useFetchProjectList.test.ts","./src/hooks/test/useHandlePinnedProjectList.test.tsx","./src/hooks/test/useNewStudyModal.test.ts","./src/hooks/test/useProjectNavigation.test.tsx","./src/hooks/test/useStudyNavigation.test.tsx","./src/hooks/test/useStudyTableDisplay.test.ts","./src/mocks/mockTools.ts","./src/mocks/data/components/dropdownItems.mock.ts","./src/mocks/data/components/navbarHeader.ts","./src/mocks/data/features/menuItemData.mock.tsx","./src/mocks/data/list/keywords.ts","./src/mocks/data/list/names.ts","./src/mocks/data/list/projectName.ts","./src/mocks/data/list/studyName.ts","./src/mocks/data/list/user.mocks.ts","./src/mocks/data/list/user.ts","./src/pages/pegase/antares/Antares.tsx","./src/pages/pegase/home/HomePage.tsx","./src/pages/pegase/home/components/HomePageContent.tsx","./src/pages/pegase/home/components/SearchBar.tsx","./src/pages/pegase/home/components/StudiesPagination.tsx","./src/pages/pegase/home/components/StudyTableDisplay.tsx","./src/pages/pegase/home/components/StudyTableHeaders.tsx","./src/pages/pegase/home/components/StudyTableUtils.tsx","./src/pages/pegase/home/pinnedProjects/PinnedProject.tsx","./src/pages/pegase/home/pinnedProjects/PinnedProjectCard.tsx","./src/pages/pegase/home/pinnedProjects/ProjectCreator.tsx","./src/pages/pegase/logout/Logout.tsx","./src/pages/pegase/projects/ProjectContent.tsx","./src/pages/pegase/projects/ProjectsPage.tsx","./src/pages/pegase/projects/ProjectsPagination.tsx","./src/pages/pegase/projects/projectDetails/ProjectDetails.tsx","./src/pages/pegase/projects/projectDetails/ProjectDetailsContent.tsx","./src/pages/pegase/projects/projectDetails/ProjectDetailsHeader.tsx","./src/pages/pegase/reports/LogsPage.tsx","./src/pages/pegase/settings/Settings.tsx","./src/pages/pegase/studies/HorizonInput.tsx","./src/pages/pegase/studies/KeywordsInput.tsx","./src/pages/pegase/studies/ProjectInput.tsx","./src/pages/pegase/studies/studyDetails/AreaLinkTab.tsx","./src/pages/pegase/studies/studyDetails/AreaLinkTableHeaders.tsx","./src/pages/pegase/studies/studyDetails/EnrTab.tsx","./src/pages/pegase/studies/studyDetails/LoadTab.tsx","./src/pages/pegase/studies/studyDetails/MiscLinkTab.tsx","./src/pages/pegase/studies/studyDetails/StudyDetailsContent.tsx","./src/pages/pegase/studies/studyDetails/StudyNavigationMenu.tsx","./src/pages/pegase/studies/studyDetails/ThermalTab.tsx","./src/pages/pegase/studies/studyDetails/areaLinkTable.tsx","./src/pages/pegase/studies/studyDetails/studyDetails.tsx","./src/pages/pegase/studies/studyDetails/studyHeader.tsx","./src/shared/constants.ts","./src/shared/const/apiEndPoint.ts","./src/shared/enum/project.ts","./src/shared/notification/containers.tsx","./src/shared/notification/notification.tsx","./src/shared/services/pinnedProjectService.ts","./src/shared/services/projectService.ts","./src/shared/services/studyService.ts","./src/shared/services/test/pinnedProjectService.test.tsx","./src/shared/services/test/projectService.test.tsx","./src/shared/services/test/studyService.test.tsx","./src/shared/types/index.ts","./src/shared/types/common/DisplayStatus.type.ts","./src/shared/types/common/MenuNavItem.type.ts","./src/shared/types/common/StdBase.type.ts","./src/shared/types/common/StudyStatus.type.ts","./src/shared/types/common/Tailwind.type.ts","./src/shared/types/common/TailwindColorClass.type.ts","./src/shared/types/common/User.type.ts","./src/shared/types/common/UserSettings.type.ts","./src/shared/types/common/tests/testUtils.tsx","./src/shared/types/pegase/Project.type.ts","./src/shared/types/pegase/Study.type.ts","./src/shared/utils/dateFormatter.ts","./src/shared/utils/slotsUtils.ts","./src/shared/utils/tabIndexUtils.ts","./src/shared/utils/common/defaultUtils.ts","./src/shared/utils/common/displayUtils.ts","./src/shared/utils/common/slotsUtils.ts","./src/shared/utils/common/classes/classMerger.ts","./src/shared/utils/common/classes/test/classMerger.test.ts","./src/shared/utils/common/dom/getDimensions.ts","./src/shared/utils/common/dom/test/getDimensions.test.tsx","./src/shared/utils/common/mappings/iconMaps.ts","./src/shared/utils/tests/dateFormatter.test.tsx","./src/store/contexts/ModalContext.tsx","./src/store/contexts/ProjectContext.tsx","./src/store/contexts/UserContext.tsx","./src/store/contexts/createFastContext.tsx","./src/store/reducers/projectReducer.tsx","./tailwind.config.ts","./vite-env.d.ts"],"version":"5.7.2"} \ No newline at end of file