Skip to content

Commit

Permalink
Merge pull request #38 from AntaresSimulatorTeam/feature/ANT-2579_cre…
Browse files Browse the repository at this point in the history
…ate_new_project

feat: ANT-2579 - Create a new projet
  • Loading branch information
vargastat authored Jan 30, 2025
2 parents 6070eaa + 608bcd6 commit 189d712
Show file tree
Hide file tree
Showing 26 changed files with 469 additions and 184 deletions.
123 changes: 123 additions & 0 deletions src/components/common/modal/ProjectCreationModal.tsx
Original file line number Diff line number Diff line change
@@ -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<string>('');
const [description, setDescription] = useState<string>('');
const [keywords, setKeywords] = useState<string[]>([]);
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 (
<RdsModal size="small">
<RdsModal.Title onClose={onClose}>{t('home.@new_project')}</RdsModal.Title>
<RdsModal.Content>
<div className="flex w-7/12 flex-col items-start gap-4">
<RdsInputText
label="Name"
value={name}
onChange={(t) => {
if (t.length <= 40) setName(t || '');
}}
variant="outlined"
placeHolder="Name your project..."
required
maxLength={40}
autoFocus={true}
/>
<RdsInputTextArea
label="Description"
value={description}
onChange={(t) => {
if (t.length <= 500) setDescription(t || '');
}}
maxLength={500}
placeHolder="Add a few lines to describe your project..."
/>
<KeywordsInput
keywords={keywords}
setKeywords={setKeywords}
maxNbKeywords={6}
maxNbCharacters={15}
minNbCharacters={3}
width={'w-3/4'}
/>
</div>
</RdsModal.Content>
<RdsModal.Footer>
<RdsButton label="Cancel" onClick={onClose} color="secondary" />
<RdsButton
icon={RdsIconId.Add}
label="Create"
onClick={handleCreateProject}
variant="contained"
color="primary"
disabled={!isFormValid}
/>
</RdsModal.Footer>
</RdsModal>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,14 @@ const StudyCreationModal: React.FC<StudyCreationModalProps> = ({ 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 = () => {
Expand Down Expand Up @@ -88,22 +89,28 @@ const StudyCreationModal: React.FC<StudyCreationModalProps> = ({ onClose, study,
<RdsModal size="small">
<RdsModal.Title onClose={onClose}>{study ? t('home.@duplicate_study') : t('home.@new_study')}</RdsModal.Title>
<RdsModal.Content>
<div className="flex items-center gap-4 self-stretch">
<div className="flex w-[300px] flex-col items-start justify-center">
<div className="flex gap-4 self-stretch">
<div className="flex w-32 flex-col items-start justify-start">
<RdsInputText
label="Name"
value={studyName}
onChange={(t) => setStudyName(t || '')}
variant="outlined"
placeHolder="Name your study..."
/>
<HorizonInput value={horizon} onChange={handleHorizonChange} />
<KeywordsInput
keywords={keywords}
setKeywords={setKeywords}
maxNbKeywords={6}
maxNbCharacters={10}
minNbCharacters={3}
/>
</div>
<div className="flex w-[242px] flex-col items-start justify-center">
<div className="flex w-32 flex-col items-start justify-start">
<ProjectInput value={projectName} onChange={setProjectName} />
</div>
</div>
<HorizonInput value={horizon} onChange={handleHorizonChange} />
<KeywordsInput keywords={keywords} setKeywords={setKeywords} />
</RdsModal.Content>
<RdsModal.Footer>
<RdsButton label="Cancel" onClick={onClose} color="secondary" />
Expand Down
6 changes: 3 additions & 3 deletions src/hooks/test/useFetchProjectList.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand All @@ -57,15 +57,15 @@ 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');
});
});

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=');
Expand Down
36 changes: 16 additions & 20 deletions src/hooks/test/useHandlePinnedProjectList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
{
Expand Down Expand Up @@ -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(),
};
Expand All @@ -76,7 +72,7 @@ vi.mock('@/store/contexts/ProjectContext', async (importOriginal) => {
});

describe('useHandlePinnedProjectList', () => {
const mockUsePinnedProjectDispatch = usePinnedProjectDispatch as Mock<typeof usePinnedProjectDispatch>;
const mockUsePinnedProjectDispatch = useProjectDispatch as Mock<typeof useProjectDispatch>;
const mockDispatch = vi.fn().mockImplementation(vi.fn());

beforeEach(() => {
Expand All @@ -94,14 +90,14 @@ describe('useHandlePinnedProjectList', () => {

mockUsePinnedProjectDispatch.mockReturnValue(mockDispatch);

const wrapper = ({ children, initialValue }: PinnedProjectProviderProps) => (
<PinnedProjectProvider children={children} initialValue={initialValue}></PinnedProjectProvider>
const wrapper = ({ children, initialValue }: ProjectProviderProps) => (
<ProjectProvider children={children} initialValue={initialValue}></ProjectProvider>
);

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();
Expand All @@ -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,
});
});
Expand All @@ -137,22 +133,22 @@ describe('useHandlePinnedProjectList', () => {

const id = uuidv4();

const wrapper = ({ children, initialValue }: PinnedProjectProviderProps) => (
<PinnedProjectProvider children={children} initialValue={initialValue}></PinnedProjectProvider>
const wrapper = ({ children, initialValue }: ProjectProviderProps) => (
<ProjectProvider children={children} initialValue={initialValue}></ProjectProvider>
);

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'));

await waitFor(() => {
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({
Expand All @@ -171,8 +167,8 @@ describe('useHandlePinnedProjectList', () => {

const id = uuidv4();

const wrapper = ({ children, initialValue }: PinnedProjectProviderProps) => (
<PinnedProjectProvider children={children} initialValue={initialValue}></PinnedProjectProvider>
const wrapper = ({ children, initialValue }: ProjectProviderProps) => (
<ProjectProvider children={children} initialValue={initialValue}></ProjectProvider>
);

const { result } = renderHook(() => useHandlePinnedProjectList(), {
Expand Down
18 changes: 10 additions & 8 deletions src/hooks/useFetchProjectList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProjectInfo[]>([]);
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);
})
Expand All @@ -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 };
};
Loading

0 comments on commit 189d712

Please sign in to comment.