From 2749ff58d884f237572f77d7e425b55c2dd1cd51 Mon Sep 17 00:00:00 2001 From: marlenetienne Date: Thu, 6 Mar 2025 11:45:20 +0100 Subject: [PATCH 1/3] fix: ANT-2712 - Handle navigation between tabs during study configuration (#66) * fix: ANT-2776 - Handle navigation between tabs during study configuration * fix: api message error * fix: add comment * fix: study trajectory name * fix: use API to link a trajectory to a study * fix: modify request method * fix: modify reducer * fix: modify reducer 2 * fix: modify reducer 3 * fix: modify reducer 4 * fix: modify reducer 5 * fix: modify reducer 6 * fix: modify reducer 7 * fix: modify reducer 8 * fix: memoized data * fix: memoized data 1 * fix: memoized data 2 * fix: memoized data 3 * fix: memoized data 4 --------- Co-authored-by: marlenetienne --- .../common/modal/ImportTrajectoryModal.tsx | 8 +- src/components/forms/ProgressBar.tsx | 2 +- .../input/SelectAndSearchableInput.tsx | 4 +- src/components/tab/AreaLinkTab.tsx | 179 ++++++++++++------ .../pegase/home/components/MainContent.tsx | 23 ++- .../studyDetails/AreaLinkTableHeaders.tsx | 10 +- .../studies/studyDetails/StudyDetails.tsx | 8 +- .../studyDetails/StudyNavigationMenu.tsx | 11 +- src/shared/const/apiEndPoint.ts | 1 + src/shared/enum/study.ts | 1 + src/shared/i18n/en.json | 3 + src/shared/services/trajectoryService.ts | 53 ++++++ src/shared/types/Study.type.ts | 12 +- src/shared/utils/formFormatter.ts | 33 +--- src/shared/utils/trajectoryUtils.ts | 33 ++++ src/store/contexts/ProjectProvider.tsx | 8 +- src/store/contexts/StudyContext.tsx | 4 +- src/store/contexts/StudyProvider.tsx | 44 ++++- src/store/reducers/studyReducer.tsx | 24 ++- 19 files changed, 318 insertions(+), 143 deletions(-) create mode 100644 src/shared/utils/trajectoryUtils.ts diff --git a/src/components/common/modal/ImportTrajectoryModal.tsx b/src/components/common/modal/ImportTrajectoryModal.tsx index 0292e7b..55c419a 100644 --- a/src/components/common/modal/ImportTrajectoryModal.tsx +++ b/src/components/common/modal/ImportTrajectoryModal.tsx @@ -9,7 +9,7 @@ import { addTrajectory } from '@/shared/services/trajectoryService.ts'; interface ImportTrajectoryModalProps { options: SelectOption[] | undefined; - onClose: (value: DbTrajectory | SelectOption | null, status: RowStatus) => void; + onClose: (value: DbTrajectory | SelectOption | null, status: RowStatus) => Promise; trajectoryType: TRAJECTORY_TYPE; studyHorizon: string; } @@ -47,17 +47,17 @@ export const ImportTrajectoryModal = ({ setProgress(+progressValue?.toFixed(0)); }); setFileStatus('success'); - onClose(newTrajectory, 'success'); + await onClose(newTrajectory, 'success'); } catch (error) { // TODO handle errors considered as warning ones setFileStatus('error'); - onClose(value, 'error'); + await onClose(value, 'error'); } }; return ( - onClose(null, 'empty')} icon="Upload"> + void onClose(null, 'empty')} icon="Upload"> {t('studyDetails.@import_from_file_system', { trajectoryType: trajectoryType === TRAJECTORY_TYPE.AREA ? 'areas' : 'links', })} diff --git a/src/components/forms/ProgressBar.tsx b/src/components/forms/ProgressBar.tsx index 300254e..ca6f593 100644 --- a/src/components/forms/ProgressBar.tsx +++ b/src/components/forms/ProgressBar.tsx @@ -1,4 +1,4 @@ -import { getBgColor } from '@/shared/utils/formFormatter.ts'; +import { getBgColor } from '@/shared/utils/trajectoryUtils'; export type FileInputStatus = 'loading' | 'success' | 'error' | 'empty'; diff --git a/src/components/input/SelectAndSearchableInput.tsx b/src/components/input/SelectAndSearchableInput.tsx index 37632d7..f95032e 100644 --- a/src/components/input/SelectAndSearchableInput.tsx +++ b/src/components/input/SelectAndSearchableInput.tsx @@ -6,6 +6,7 @@ import { useRef, useState } from 'react'; import { RdsButton, RdsIconId, RdsInputText } from 'rte-design-system-react'; +import { useTranslation } from 'react-i18next'; interface ProjectManagerProps { options: SelectOption[] | undefined; @@ -34,6 +35,7 @@ const SelectAndSearchableInput = ({ const [placeHolder] = useState(defaultPlaceHolder); const [valueInput, setValueInput] = useState(''); const dropdownList = useRef(null); + const { t } = useTranslation(); const handleInputChange = async (value: string) => { try { @@ -110,7 +112,7 @@ const SelectAndSearchableInput = ({ setIsDropdownOpen(false); } }} - placeHolder={placeHolder} + placeHolder={!options?.length ? t('components.selectAnsSearchInput.@emptyList') : placeHolder} variant="outlined" value={valueInput} disabled={isInputDisabled} diff --git a/src/components/tab/AreaLinkTab.tsx b/src/components/tab/AreaLinkTab.tsx index e8d99df..6252aa3 100644 --- a/src/components/tab/AreaLinkTab.tsx +++ b/src/components/tab/AreaLinkTab.tsx @@ -4,43 +4,58 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import StdSimpleTable from '@common/data/stdSimpleTable/StdSimpleTable.tsx'; import { ReadOnlyObject } from '@common/data/stdTable/types/readOnly.type'; import getAreaLinkTableHeaders from '@/pages/pegase/studies/studyDetails/AreaLinkTableHeaders.tsx'; import { useNewStudyModal } from '@/hooks/useNewStudyModal.ts'; import { useTranslation } from 'react-i18next'; -import { fetchTrajectoriesFromDB, fetchTrajectoriesFromFS } from '@/shared/services/trajectoryService.ts'; +import { + fetchTrajectoriesFromDB, + fetchTrajectoriesFromFS, + linkTrajectoryToStudy, +} from '@/shared/services/trajectoryService.ts'; import { TRAJECTORY_SELECTION_STATUS, TRAJECTORY_TYPE } from '@/shared/enum/trajectory.ts'; -import { AreaAndLinkRowData, DbTrajectory, RowStatus, StudyActionType } from '@/shared/types'; +import { AreaAndLinkRowData, DbTrajectory, RowStatus, StudyActionType, StudyDTO } from '@/shared/types'; import { useFetchTrajectoriesFromDB } from '@/hooks/useFetchTrajectoriesFromDB.ts'; -import { - convertToFSSelectionOptionType, - convertToSelectionOptionType, - getStatus, -} from '@/shared/utils/formFormatter.ts'; +import { getStatus, getTrajectoryDB } from '@/shared/utils/trajectoryUtils'; +import { convertToFSSelectionOptionType, convertToSelectionOptionType } from '@/shared/utils/formFormatter'; import { ImportTrajectoryModal } from '@common/modal/ImportTrajectoryModal.tsx'; -import { useStudyDispatch } from '@/store/contexts/StudyContext'; +import { useStudy, useStudyDispatch } from '@/store/contexts/StudyContext'; import { STUDY_ACTION } from '@/shared/enum/study.ts'; interface AreaLinkTabProps { - studyHorizon: string; + study: StudyDTO; } -const AreaLinkTab = ({ studyHorizon }: AreaLinkTabProps) => { +const AreaLinkTab = ({ study }: AreaLinkTabProps) => { + const studyState = useStudy(); + const trajectoryNameArea: string | null = studyState[`${TRAJECTORY_TYPE.AREA}`]?.trajectoryName ?? null; + const trajectoryNameLink: string | null = studyState[`${TRAJECTORY_TYPE.LINK}`]?.trajectoryName ?? null; const [data, setData] = useState([ - { hypothesis: 'Areas', trajectory: null, status: TRAJECTORY_SELECTION_STATUS.MISSING }, - { hypothesis: 'Links', trajectory: null, status: TRAJECTORY_SELECTION_STATUS.MISSING }, + { + hypothesis: 'Areas', + trajectory: trajectoryNameArea, + status: trajectoryNameArea ? TRAJECTORY_SELECTION_STATUS.OK : TRAJECTORY_SELECTION_STATUS.MISSING, + }, + { + hypothesis: 'Links', + trajectory: trajectoryNameLink, + status: trajectoryNameLink ? TRAJECTORY_SELECTION_STATUS.OK : TRAJECTORY_SELECTION_STATUS.MISSING, + }, ]); - const [readOnly, setReadOnly] = useState({ '0': false, '1': !data[0].trajectory }); + const [readOnly, setReadOnly] = useState({ + '0': false, + '1': !trajectoryNameArea, + }); const [optionsDB, setOptionsDB] = useState(); const [optionsFS, setOptionsFS] = useState(); const [rowIndexSelected, setRowIndexSelected] = useState(0); const { isModalOpen, toggleModal } = useNewStudyModal(); const dispatch = useStudyDispatch(); const { t } = useTranslation(); - const { trajectories: trajectoriesArea } = useFetchTrajectoriesFromDB(TRAJECTORY_TYPE.AREA, studyHorizon); - const { trajectories: trajectoriesLink } = useFetchTrajectoriesFromDB(TRAJECTORY_TYPE.LINK, studyHorizon); + const { trajectories: trajectoriesArea } = useFetchTrajectoriesFromDB(TRAJECTORY_TYPE.AREA, study.horizon); + const { trajectories: trajectoriesLink } = useFetchTrajectoriesFromDB(TRAJECTORY_TYPE.LINK, study.horizon); useEffect(() => { if (trajectoriesArea && trajectoriesLink) { @@ -58,49 +73,68 @@ const AreaLinkTab = ({ studyHorizon }: AreaLinkTabProps) => { } }; - const handleTrajectoryUpdate = (index: number, trajectory: SelectOption | DbTrajectory | null, status: RowStatus) => { - const updatedData = [...data]; - updatedData[index].trajectory = trajectory - ? ((trajectory as SelectOption)?.label ?? (trajectory as DbTrajectory)?.trajectoryName) - : null; - updatedData[index].status = getStatus(status); - if (status === 'success') { - dispatch?.({ - type: index === 0 ? STUDY_ACTION.ADD_TRAJECTORY_AREA : STUDY_ACTION.ADD_TRAJECTORY_LINK, - payload: trajectory, - } as StudyActionType); + const handleTrajectoryUpdate = useCallback( + async (index: number, trajectory: SelectOption | DbTrajectory | null, status: RowStatus) => { + const updatedData = [...data]; + const payload: DbTrajectory | null | undefined = + trajectory && 'label' in trajectory + ? getTrajectoryDB(index === 0 ? trajectoriesArea : trajectoriesLink, trajectory.id as number) + : trajectory; - setOptionsDB((prev) => { - if (prev && prev[index]?.length >= 0) { - prev[index] = [ - ...prev[index], - { - id: (trajectory as DbTrajectory).id, - label: (trajectory as DbTrajectory).trajectoryName, - }, - ]; - return prev; + try { + if (status === 'success' && payload) { + await linkTrajectoryToStudy(payload.type, payload.id, study.id); + // Update context + dispatch?.({ + type: index === 0 ? STUDY_ACTION.ADD_TRAJECTORY_AREA : STUDY_ACTION.ADD_TRAJECTORY_LINK, + payload, + } as StudyActionType); + // Update data state + updatedData[index].trajectory = trajectory + ? ((trajectory as SelectOption)?.label ?? (trajectory as DbTrajectory)?.trajectoryName) + : null; + updatedData[index].status = getStatus(status); + setReadOnly({ '0': false, '1': false }); } - }); - } - // Handle deletion case for areas - if (index === 0 && status === 'empty') { - updatedData[1].trajectory = null; - updatedData[1].status = TRAJECTORY_SELECTION_STATUS.MISSING; - dispatch?.({ - type: STUDY_ACTION.CLEAR_AREA_LINK_TRAJECTORY, - } as StudyActionType); - setReadOnly({ '0': false, '1': true }); - } else if (index === 1) { - dispatch?.({ - type: STUDY_ACTION.CLEAR_LINK_TRAJECTORY, - } as StudyActionType); - } else { - setReadOnly({ '0': false, '1': false }); - } - setData(updatedData); - }; + // Handle deletion case for areas + if (status === 'empty') { + // TODO: ANT-2892 (delete link between study and trajectory in data base) + if (index === 0) { + updatedData.forEach((rowData) => { + rowData.trajectory = null; + rowData.status = TRAJECTORY_SELECTION_STATUS.MISSING; + }); + dispatch?.({ + type: STUDY_ACTION.CLEAR_AREA_LINK_TRAJECTORY, + } as StudyActionType); + setReadOnly({ '0': false, '1': true }); + } else if (index === 1) { + updatedData[1].trajectory = null; + updatedData[1].status = TRAJECTORY_SELECTION_STATUS.MISSING; + dispatch?.({ + type: STUDY_ACTION.CLEAR_LINK_TRAJECTORY, + } as StudyActionType); + setReadOnly({ '0': false, '1': false }); + } + } + + if (status === 'error') { + updatedData[index].trajectory = + (trajectory as SelectOption).label || (trajectory as DbTrajectory).trajectoryName || null; + updatedData[index].status = TRAJECTORY_SELECTION_STATUS.ERROR; + } + } catch { + // Trajectory status is set to error one + updatedData[index].trajectory = + (trajectory as SelectOption).label || (trajectory as DbTrajectory).trajectoryName || null; + updatedData[index].status = TRAJECTORY_SELECTION_STATUS.ERROR; + } finally { + setData(updatedData); + } + }, + [trajectoriesArea, trajectoriesLink], + ); const handleTrajectorySearch = async ( index: number, @@ -109,7 +143,7 @@ const AreaLinkTab = ({ studyHorizon }: AreaLinkTabProps) => { try { const results = await fetchTrajectoriesFromDB( index === 0 ? TRAJECTORY_TYPE.AREA : TRAJECTORY_TYPE.LINK, - studyHorizon, + study.horizon, value, ); return convertToSelectionOptionType(results); @@ -118,10 +152,31 @@ const AreaLinkTab = ({ studyHorizon }: AreaLinkTabProps) => { } }; - const closeModal = (value: DbTrajectory | SelectOption | null, status: RowStatus) => { - value && handleTrajectoryUpdate(rowIndexSelected, value, status); - toggleModal(); - }; + const closeModal = useCallback( + async (value: DbTrajectory | SelectOption | null, status: RowStatus) => { + try { + await handleTrajectoryUpdate(rowIndexSelected, value, status); + // Update trajectory list options (synchronized with update of trajectory list in BDD after trajectory import) for dropdown + if (status !== 'error' && value) { + setOptionsDB((prev) => { + if (prev && prev[rowIndexSelected]?.length >= 0) { + prev[rowIndexSelected] = [ + ...prev[rowIndexSelected], + { + id: (value as DbTrajectory).id, + label: (value as DbTrajectory).trajectoryName, + }, + ]; + return prev; + } + }); + } + } finally { + toggleModal(); + } + }, + [rowIndexSelected], + ); const columns = useMemo( () => @@ -145,7 +200,7 @@ const AreaLinkTab = ({ studyHorizon }: AreaLinkTabProps) => { options={optionsFS} onClose={closeModal} trajectoryType={rowIndexSelected === 0 ? TRAJECTORY_TYPE.AREA : TRAJECTORY_TYPE.LINK} - studyHorizon={studyHorizon} + studyHorizon={study.horizon} /> )} diff --git a/src/pages/pegase/home/components/MainContent.tsx b/src/pages/pegase/home/components/MainContent.tsx index 867f685..dc5a465 100644 --- a/src/pages/pegase/home/components/MainContent.tsx +++ b/src/pages/pegase/home/components/MainContent.tsx @@ -39,15 +39,20 @@ const MainContent = () => {
- - - } /> - } /> - {Object.entries([...menuBottomData, ...menuTopData]).map(([key, route]) => ( - - ))} - - + + + + + } + /> + } /> + {Object.entries([...menuBottomData, ...menuTopData]).map(([key, route]) => ( + + ))} +
diff --git a/src/pages/pegase/studies/studyDetails/AreaLinkTableHeaders.tsx b/src/pages/pegase/studies/studyDetails/AreaLinkTableHeaders.tsx index 6a931d9..48cd0e2 100644 --- a/src/pages/pegase/studies/studyDetails/AreaLinkTableHeaders.tsx +++ b/src/pages/pegase/studies/studyDetails/AreaLinkTableHeaders.tsx @@ -18,7 +18,7 @@ const columnHelper = createColumnHelper(); const getAreaLinkTableHeaders = ( options: SelectOption[][] | undefined, t: (value: string) => string, - handleUpdate: (index: number, trajectory: SelectOption | null, status: RowStatus) => void, + handleUpdate: (index: number, trajectory: SelectOption | null, status: RowStatus) => Promise, handleImport: (index: number) => Promise, handlerSearch: (index: number, value: string | undefined) => Promise, ) => [ @@ -41,13 +41,17 @@ const getAreaLinkTableHeaders = ( return trajectory ? (
{trajectory} - handleUpdate(row.index, null, 'empty')} /> + void handleUpdate(row.index, null, 'empty')} + />
) : (
handleUpdate(row.index, value, 'success')} + onSelect={(value: SelectOption) => void handleUpdate(row.index, value, 'success')} setSearchTerm={async (value: string | undefined) => await handlerSearch(row.index, value)} defaultPlaceHolder={ row.getReadOnly() ? t('studyDetails.@select_link') : t('studyDetails.@select_trajectory') diff --git a/src/pages/pegase/studies/studyDetails/StudyDetails.tsx b/src/pages/pegase/studies/studyDetails/StudyDetails.tsx index 31de35f..2b7c455 100644 --- a/src/pages/pegase/studies/studyDetails/StudyDetails.tsx +++ b/src/pages/pegase/studies/studyDetails/StudyDetails.tsx @@ -27,7 +27,7 @@ const StudyDetails = () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const location: Location = useLocation(); const { t } = useTranslation(); - const { isStudyGenerated, areaTrajectory } = useStudy(); + const { isStudyGenerated, AREA } = useStudy(); const dispatch = useStudyDispatch(); const [isGenerating, setIsGenerating] = useState(false); const { study } = location.state || {}; @@ -56,7 +56,7 @@ const StudyDetails = () => {
- +
@@ -64,11 +64,11 @@ const StudyDetails = () => {
- {!areaTrajectory &&
{t('studyDetails.@add_trajectories_message')}
} + {!AREA &&
{t('studyDetails.@add_trajectories_message')}
} void handleGenerateStudy()} - disabled={!areaTrajectory || isStudyGenerated} + disabled={!AREA || !!isStudyGenerated} icon={StdIconId.CheckCircle} position="right" isLoading={isGenerating} diff --git a/src/pages/pegase/studies/studyDetails/StudyNavigationMenu.tsx b/src/pages/pegase/studies/studyDetails/StudyNavigationMenu.tsx index de1bd18..a238d36 100644 --- a/src/pages/pegase/studies/studyDetails/StudyNavigationMenu.tsx +++ b/src/pages/pegase/studies/studyDetails/StudyNavigationMenu.tsx @@ -15,21 +15,22 @@ import AreaLinkTab from '@/components/tab/AreaLinkTab.tsx'; import StdIcon from '@common/base/stdIcon/StdIcon'; import { TRAJECTORY_TYPE } from '@/shared/enum/trajectory.ts'; import { useTranslation } from 'react-i18next'; +import { StudyDTO } from '@/shared/types'; const StudyNavigationMenu = ({ onRenderActiveComponent, - studyHorizon, + study, }: { onRenderActiveComponent?: (content: ReactNode | null) => void; - studyHorizon: string; + study: StudyDTO; }) => { const { t } = useTranslation(); const [activeTab, setActiveTab] = useState(TRAJECTORY_TYPE.AREA); - const renderActiveComponent = (horizon: string): ReactNode | null => { + const renderActiveComponent = (studyData: StudyDTO): ReactNode | null => { switch (activeTab) { case TRAJECTORY_TYPE.AREA: - return ; + return ; case TRAJECTORY_TYPE.LOAD: return ; case TRAJECTORY_TYPE.THERMAL_COST: @@ -45,7 +46,7 @@ const StudyNavigationMenu = ({ useEffect(() => { if (onRenderActiveComponent) { - onRenderActiveComponent(renderActiveComponent(studyHorizon)); + onRenderActiveComponent(renderActiveComponent(study)); } }, [activeTab, onRenderActiveComponent]); diff --git a/src/shared/const/apiEndPoint.ts b/src/shared/const/apiEndPoint.ts index 6884167..b8e1ea3 100644 --- a/src/shared/const/apiEndPoint.ts +++ b/src/shared/const/apiEndPoint.ts @@ -28,3 +28,4 @@ export const PROJECT_SEARCH_ENDPOINT = `${BASE_URL}/v1/project/search`; export const TRAJECTORY_ENDPOINT = `${BASE_URL}/v1/trajectory`; export const TRAJECTORY_FILE_SYSTEM_ENDPOINT = `${BASE_URL}/v1/trajectory/fs`; export const TRAJECTORY_DATA_BASE_ENDPOINT = `${BASE_URL}/v1/trajectory/db`; +export const TRAJECTORY_LINK_TO_STUDY_ENDPOINT = `${BASE_URL}/v1/trajectory/link`; diff --git a/src/shared/enum/study.ts b/src/shared/enum/study.ts index 338c459..00885d0 100644 --- a/src/shared/enum/study.ts +++ b/src/shared/enum/study.ts @@ -4,4 +4,5 @@ export enum STUDY_ACTION { CLEAR_AREA_LINK_TRAJECTORY = 'CLEAR_AREA_LINK_TRAJECTORY', SET_IS_STUDY_GENERATED = 'SET_IS_STUDY_GENERATED', CLEAR_LINK_TRAJECTORY = 'CLEAR_LINK_TRAJECTORY', + ADD_TRAJECTORIES = 'ADD_TRAJECTORIES', } diff --git a/src/shared/i18n/en.json b/src/shared/i18n/en.json index f0cacda..f44d44a 100644 --- a/src/shared/i18n/en.json +++ b/src/shared/i18n/en.json @@ -13,6 +13,9 @@ "quickAccess": { "@confirmUnpin": "Project unppined", "@cancel": "Cancel" + }, + "selectAnsSearchInput": { + "@emptyList": "No result found" } }, "Pegase": { diff --git a/src/shared/services/trajectoryService.ts b/src/shared/services/trajectoryService.ts index 79b9ea3..c31ff42 100644 --- a/src/shared/services/trajectoryService.ts +++ b/src/shared/services/trajectoryService.ts @@ -8,10 +8,12 @@ import { TRAJECTORY_DATA_BASE_ENDPOINT, TRAJECTORY_ENDPOINT, TRAJECTORY_FILE_SYSTEM_ENDPOINT, + TRAJECTORY_LINK_TO_STUDY_ENDPOINT, } from '@/shared/const/apiEndPoint.ts'; import { DbTrajectory, FsTrajectory } from '@/shared/types'; import { AuthService } from '@/shared/services/authService.ts'; import { fetchWithProgress } from '@/shared/services/progressService.ts'; +import { TRAJECTORY_TYPE } from '@/shared/enum/trajectory.ts'; /** * Retrieve a list of trajectories by type and horizon from database @@ -86,3 +88,54 @@ export const addTrajectory = async ( return (await (response as Response).json()) as DbTrajectory; } }; + +/** + * Fetch trajectories linked to one or several studies + * @param {number} studyIds - Array of study ids + * @param {TRAJECTORY_TYPE} trajectoryType - Trajectory type + * + * @return {Promise} Array of trajectories (data base trajectories) + */ + +export const getStudyTrajectories = async ( + studyIds: number[], + trajectoryType: TRAJECTORY_TYPE, +): Promise => { + const studyParams = studyIds?.map((id) => `studyIds=${id}`).join(''); + const urlApi = `${TRAJECTORY_ENDPOINT}?${studyParams}&trajectoryType=${trajectoryType}`; + + const response = await AuthService.authFetch(urlApi); + if (!response.ok) { + throw new Error('Failed to fetch trajectories linked to studies'); + } + + return (await response.json()) as DbTrajectory[]; +}; + +/** + * Linked a trajectory to study + * @param {TRAJECTORY_TYPE} type - Trajectory type + * @param {number} trajectoryId - Trajectory id + * @param {number} studyId - Study id + * + * @return {Promise} - Trajectory linked to a study + */ + +export const linkTrajectoryToStudy = async ( + type: TRAJECTORY_TYPE, + trajectoryId: number, + studyId: number, +): Promise => { + const urlApi = `${TRAJECTORY_LINK_TO_STUDY_ENDPOINT}?type=${type}&trajectoryId=${trajectoryId}&studyId=${studyId}`; + const response = await AuthService.authFetch(urlApi, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + }); + if (!response.ok) { + throw new Error(`${(response as unknown as Error).message}`); + } + + return (await response.json()) as DbTrajectory; +}; diff --git a/src/shared/types/Study.type.ts b/src/shared/types/Study.type.ts index 4f44330..7d30a8e 100644 --- a/src/shared/types/Study.type.ts +++ b/src/shared/types/Study.type.ts @@ -6,6 +6,7 @@ import { DbTrajectory } from '@/shared/types/Trajectory.type.ts'; import { STUDY_ACTION } from '@/shared/enum/study.ts'; +import { TRAJECTORY_TYPE } from '@/shared/enum/trajectory.ts'; export interface StudyDTO { id: number; @@ -24,15 +25,16 @@ export interface PaginatedResponse { totalElements: number; } -export interface StudyState { +export type StudyState = { + [key in keyof typeof TRAJECTORY_TYPE]: DbTrajectory | null; +} & { isStudyGenerated: boolean; - areaTrajectory: DbTrajectory | null; - linkTrajectory: DbTrajectory | null; -} +}; export type StudyActionType = | { type: STUDY_ACTION.ADD_TRAJECTORY_AREA; payload: DbTrajectory } | { type: STUDY_ACTION.ADD_TRAJECTORY_LINK; payload: DbTrajectory } | { type: STUDY_ACTION.CLEAR_AREA_LINK_TRAJECTORY } | { type: STUDY_ACTION.SET_IS_STUDY_GENERATED } - | { type: STUDY_ACTION.CLEAR_LINK_TRAJECTORY }; + | { type: STUDY_ACTION.CLEAR_LINK_TRAJECTORY } + | { type: STUDY_ACTION.ADD_TRAJECTORIES; payload: DbTrajectory[] }; diff --git a/src/shared/utils/formFormatter.ts b/src/shared/utils/formFormatter.ts index 5ea07e4..fd775fd 100644 --- a/src/shared/utils/formFormatter.ts +++ b/src/shared/utils/formFormatter.ts @@ -4,9 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { DbTrajectory, FsTrajectory, RowStatus } from '@/shared/types'; -import { FileInputStatus } from 'rte-design-system-react'; -import { TRAJECTORY_SELECTION_STATUS } from '@/shared/enum/trajectory.ts'; +import { DbTrajectory, FsTrajectory } from '@/shared/types'; export const convertToSelectionOptionType = (trajectories: DbTrajectory[]): SelectOption[] => trajectories.map((trajectory) => ({ @@ -16,33 +14,6 @@ export const convertToSelectionOptionType = (trajectories: DbTrajectory[]): Sele export const convertToFSSelectionOptionType = (options: FsTrajectory[]): SelectOption[] => options.map((option, indexTrajectory) => ({ - id: `option-fs-${indexTrajectory}`, + id: indexTrajectory, label: option.trajectoryName ? option.trajectoryName.substring(0, option.trajectoryName.lastIndexOf('.')) : '', })); - -export const getStatus = (status: RowStatus) => { - switch (status) { - case 'error': - return TRAJECTORY_SELECTION_STATUS.ERROR; - case 'success': - return TRAJECTORY_SELECTION_STATUS.OK; - case 'warning': - return TRAJECTORY_SELECTION_STATUS.WARNING; - case 'empty': - default: - return TRAJECTORY_SELECTION_STATUS.MISSING; - } -}; - -export const getBgColor = (status: FileInputStatus) => { - switch (status) { - case 'loading': - return 'bg-acc1-600'; - case 'success': - case 'error': - return `bg-${status}-600`; - case 'empty': - default: - return 'bg-gray-600'; - } -}; diff --git a/src/shared/utils/trajectoryUtils.ts b/src/shared/utils/trajectoryUtils.ts new file mode 100644 index 0000000..88d1971 --- /dev/null +++ b/src/shared/utils/trajectoryUtils.ts @@ -0,0 +1,33 @@ +import { DbTrajectory, RowStatus } from '@/shared/types'; +import { TRAJECTORY_SELECTION_STATUS } from '@/shared/enum/trajectory.ts'; +import { FileInputStatus } from 'rte-design-system-react'; + +export const getTrajectoryDB = (trajectories: DbTrajectory[] | null, id: number) => + trajectories?.find((trajectory) => trajectory.id === id); + +export const getStatus = (status: RowStatus) => { + switch (status) { + case 'error': + return TRAJECTORY_SELECTION_STATUS.ERROR; + case 'success': + return TRAJECTORY_SELECTION_STATUS.OK; + case 'warning': + return TRAJECTORY_SELECTION_STATUS.WARNING; + case 'empty': + default: + return TRAJECTORY_SELECTION_STATUS.MISSING; + } +}; + +export const getBgColor = (status: FileInputStatus) => { + switch (status) { + case 'loading': + return 'bg-acc1-600'; + case 'success': + case 'error': + return `bg-${status}-600`; + case 'empty': + default: + return 'bg-gray-600'; + } +}; diff --git a/src/store/contexts/ProjectProvider.tsx b/src/store/contexts/ProjectProvider.tsx index 5cd7b2f..d4ab0a8 100644 --- a/src/store/contexts/ProjectProvider.tsx +++ b/src/store/contexts/ProjectProvider.tsx @@ -4,8 +4,8 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { ReactNode, useReducer } from 'react'; -import { ProjectState } from '@/shared/types/Project.type.ts'; +import { ReactNode, Reducer, useReducer } from 'react'; +import { ProjectActionType, ProjectState } from '@/shared/types/Project.type.ts'; import projectReducer from '@/store/reducers/projectReducer'; import { ProjectContext, ProjectDispatchContext } from '@/store/contexts/ProjectContext'; @@ -15,9 +15,7 @@ export interface ProjectProviderProps { } export const ProjectProvider = ({ children, initialValue }: ProjectProviderProps) => { - const initializer = (value = initialValue) => value; - - const [state, dispatch] = useReducer(projectReducer, initialValue, initializer); + const [state, dispatch] = useReducer>(projectReducer, initialValue); return ( diff --git a/src/store/contexts/StudyContext.tsx b/src/store/contexts/StudyContext.tsx index 5924581..d634e21 100644 --- a/src/store/contexts/StudyContext.tsx +++ b/src/store/contexts/StudyContext.tsx @@ -1,10 +1,8 @@ import { createContext, Dispatch, useContext } from 'react'; import { StudyActionType, StudyState } from '@/shared/types'; -const initialState: StudyState = { +const initialState: Partial = { isStudyGenerated: false, - areaTrajectory: null, - linkTrajectory: null, }; export const StudyContext = createContext(initialState); diff --git a/src/store/contexts/StudyProvider.tsx b/src/store/contexts/StudyProvider.tsx index 4b6c92a..494c72e 100644 --- a/src/store/contexts/StudyProvider.tsx +++ b/src/store/contexts/StudyProvider.tsx @@ -1,17 +1,51 @@ -import { ReactNode, useReducer } from 'react'; -import { StudyState } from '@/shared/types'; +import { Dispatch, ReactNode, Reducer, useEffect, useReducer } from 'react'; +import { DbTrajectory, StudyActionType, StudyDTO, StudyState } from '@/shared/types'; import { studyReducer } from '@/store/reducers/studyReducer.tsx'; import { StudyContext, StudyDispatchContext } from '@/store/contexts/StudyContext'; +import { useLocation } from 'react-router-dom'; +import { STUDY_ACTION } from '@/shared/enum/study.ts'; export interface StudyProviderProps { children: ReactNode; - initialValue: StudyState; + initialValue: Partial; +} + +interface LocationState { + study: StudyDTO; } export const StudyProvider = ({ children, initialValue }: StudyProviderProps) => { - const initializer = (value = initialValue) => value; + const location = useLocation(); + const study = (location.state as LocationState)?.study; + const [state, dispatch] = useReducer>(studyReducer, initialValue as StudyState); - const [state, dispatch] = useReducer(studyReducer, initialValue, initializer); + useEffect(() => { + const getTrajectories = async (d: Dispatch) => { + let response: unknown = []; + try { + response = [ + { + id: 103, + trajectoryName: 'areas_BP2030_A_ref_v8', + type: 'AREA', + version: 1, + userName: null, + creationDate: '2025-02-19T15:53:54.453608', + }, + ]; //await getStudyTrajectories([study.id], TRAJECTORY_TYPE.AREA); + } finally { + if (response && (response as DbTrajectory[]).length > 0) { + d({ + type: STUDY_ACTION.ADD_TRAJECTORIES, + payload: response as DbTrajectory[], + }); + } + } + }; + if (study?.trajectoryIds.length > 0) { + void getTrajectories(dispatch); + } + }, [study]); return ( diff --git a/src/store/reducers/studyReducer.tsx b/src/store/reducers/studyReducer.tsx index c976a06..2fbce17 100644 --- a/src/store/reducers/studyReducer.tsx +++ b/src/store/reducers/studyReducer.tsx @@ -1,19 +1,33 @@ -import { StudyActionType, StudyState } from '@/shared/types'; +import { DbTrajectory, StudyActionType, StudyState } from '@/shared/types'; import { STUDY_ACTION } from '@/shared/enum/study.ts'; +import { TRAJECTORY_TYPE } from '@/shared/enum/trajectory.ts'; + +const addTrajectories = (prevState: StudyState, trajectories: DbTrajectory[]): StudyState => { + const studyState = {}; + trajectories.forEach((trajectory) => Object.assign(studyState, { [`${trajectory.type}`]: trajectory })); + return { ...prevState, ...studyState }; +}; export const studyReducer = (prevState: StudyState, action?: StudyActionType): StudyState => { if (action) { switch (action.type) { case STUDY_ACTION.ADD_TRAJECTORY_AREA: - return { ...prevState, areaTrajectory: action.payload }; + return { ...prevState, [`${TRAJECTORY_TYPE.AREA}`]: action.payload }; case STUDY_ACTION.ADD_TRAJECTORY_LINK: - return { ...prevState, linkTrajectory: action.payload }; + return { ...prevState, [`${TRAJECTORY_TYPE.LINK}`]: action.payload }; case STUDY_ACTION.CLEAR_AREA_LINK_TRAJECTORY: - return { isStudyGenerated: false, areaTrajectory: null, linkTrajectory: null }; + return { + ...prevState, + isStudyGenerated: false, + [`${TRAJECTORY_TYPE.AREA}`]: null, + [`${TRAJECTORY_TYPE.LINK}`]: null, + }; case STUDY_ACTION.CLEAR_LINK_TRAJECTORY: - return { ...prevState, isStudyGenerated: false, linkTrajectory: null }; + return { ...prevState, isStudyGenerated: false, [`${TRAJECTORY_TYPE.LINK}`]: null }; case STUDY_ACTION.SET_IS_STUDY_GENERATED: return { ...prevState, isStudyGenerated: true }; + case STUDY_ACTION.ADD_TRAJECTORIES: + return { ...addTrajectories(prevState, action.payload) }; default: return prevState; } From e9bc830ae5270e04cb5b9d1cdf4abe0782202dae Mon Sep 17 00:00:00 2001 From: marlenetienne Date: Thu, 6 Mar 2025 15:12:15 +0100 Subject: [PATCH 2/3] fix: ANT-2712 - remove console log (#73) Co-authored-by: marlenetienne --- src/store/contexts/StudyProvider.tsx | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/store/contexts/StudyProvider.tsx b/src/store/contexts/StudyProvider.tsx index 494c72e..dbb1e62 100644 --- a/src/store/contexts/StudyProvider.tsx +++ b/src/store/contexts/StudyProvider.tsx @@ -4,6 +4,8 @@ import { studyReducer } from '@/store/reducers/studyReducer.tsx'; import { StudyContext, StudyDispatchContext } from '@/store/contexts/StudyContext'; import { useLocation } from 'react-router-dom'; import { STUDY_ACTION } from '@/shared/enum/study.ts'; +import { getStudyTrajectories } from '@/shared/services/trajectoryService.ts'; +import { TRAJECTORY_TYPE } from '@/shared/enum/trajectory.ts'; export interface StudyProviderProps { children: ReactNode; @@ -23,16 +25,7 @@ export const StudyProvider = ({ children, initialValue }: StudyProviderProps) => const getTrajectories = async (d: Dispatch) => { let response: unknown = []; try { - response = [ - { - id: 103, - trajectoryName: 'areas_BP2030_A_ref_v8', - type: 'AREA', - version: 1, - userName: null, - creationDate: '2025-02-19T15:53:54.453608', - }, - ]; //await getStudyTrajectories([study.id], TRAJECTORY_TYPE.AREA); + response = await getStudyTrajectories([study.id], TRAJECTORY_TYPE.AREA); } finally { if (response && (response as DbTrajectory[]).length > 0) { d({ From 47c698955ec760490470f653cf48b47bfd54a9c6 Mon Sep 17 00:00:00 2001 From: marlenetienne Date: Fri, 7 Mar 2025 14:52:32 +0100 Subject: [PATCH 3/3] fix: ANT-2905 - Study creation: project input should not appear inside modal (#72) * fix: ANT-2905 - Study creation: project input should not appear inside modal * fix: remove console log --------- Co-authored-by: marlenetienne --- .../common/modal/StudyCreationModal.tsx | 18 +++++++++++++----- .../home/components/StudyTableDisplay.tsx | 4 +++- .../projects/projectDetails/ProjectDetails.tsx | 2 +- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/components/common/modal/StudyCreationModal.tsx b/src/components/common/modal/StudyCreationModal.tsx index f428e4b..37137a8 100644 --- a/src/components/common/modal/StudyCreationModal.tsx +++ b/src/components/common/modal/StudyCreationModal.tsx @@ -18,13 +18,19 @@ interface StudyCreationModalProps { onClose: () => void; study?: StudyDTO | null; setReloadStudies: React.Dispatch>; + projectInfoName?: string; } -const StudyCreationModal: React.FC = ({ onClose, study, setReloadStudies }) => { +const StudyCreationModal: React.FC = ({ + onClose, + study, + setReloadStudies, + projectInfoName, +}) => { const { t } = useTranslation(); const [studyName, setStudyName] = useState(''); const [horizon, setHorizon] = useState(''); - const [projectName, setProjectName] = useState(study?.project || ''); + const [projectName, setProjectName] = useState(study?.project || projectInfoName || ''); const [keywords, setKeywords] = useState(study?.keywords || []); const [trajectoryIds] = useState(study?.trajectoryIds || []); const [isFormValid, setIsFormValid] = useState(false); @@ -107,9 +113,11 @@ const StudyCreationModal: React.FC = ({ onClose, study, minNbCharacters={3} />
-
- -
+ {study && ( +
+ +
+ )}
diff --git a/src/pages/pegase/home/components/StudyTableDisplay.tsx b/src/pages/pegase/home/components/StudyTableDisplay.tsx index a7579e0..0235190 100644 --- a/src/pages/pegase/home/components/StudyTableDisplay.tsx +++ b/src/pages/pegase/home/components/StudyTableDisplay.tsx @@ -23,9 +23,10 @@ import StudyCreationModal from '@common/modal/StudyCreationModal'; interface StudyTableDisplayProps { searchStudy: string | undefined; projectId?: string; + projectInfoName?: string; } -const StudyTableDisplay = ({ searchStudy, projectId }: StudyTableDisplayProps) => { +const StudyTableDisplay = ({ searchStudy, projectId, projectInfoName }: StudyTableDisplayProps) => { const { t } = useTranslation(); const [rowSelection, setRowSelection] = useState({}); const [isHeaderHovered, setIsHeaderHovered] = useState(false); @@ -135,6 +136,7 @@ const StudyTableDisplay = ({ searchStudy, projectId }: StudyTableDisplayProps) = onClose={toggleModal} study={selectedStudy} setReloadStudies={setReloadStudies} + projectInfoName={projectInfoName} /> )}
diff --git a/src/pages/pegase/projects/projectDetails/ProjectDetails.tsx b/src/pages/pegase/projects/projectDetails/ProjectDetails.tsx index 5b4d4c3..9db8661 100644 --- a/src/pages/pegase/projects/projectDetails/ProjectDetails.tsx +++ b/src/pages/pegase/projects/projectDetails/ProjectDetails.tsx @@ -92,7 +92,7 @@ const ProjectDetails = () => { status={activeChip ? 'secondary' : 'primary'} /> - + ); };