Skip to content

Commit

Permalink
fix: ANT-2712 - Handle navigation between tabs during study configura…
Browse files Browse the repository at this point in the history
…tion (#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 <[email protected]>
  • Loading branch information
marlenetienne and marlenetienne authored Mar 6, 2025
1 parent 2e7c46b commit 2749ff5
Show file tree
Hide file tree
Showing 19 changed files with 318 additions and 143 deletions.
8 changes: 4 additions & 4 deletions src/components/common/modal/ImportTrajectoryModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>;
trajectoryType: TRAJECTORY_TYPE;
studyHorizon: string;
}
Expand Down Expand Up @@ -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 (
<RdsModal size="small">
<RdsModal.Title onClose={() => onClose(null, 'empty')} icon="Upload">
<RdsModal.Title onClose={() => void onClose(null, 'empty')} icon="Upload">
{t('studyDetails.@import_from_file_system', {
trajectoryType: trajectoryType === TRAJECTORY_TYPE.AREA ? 'areas' : 'links',
})}
Expand Down
2 changes: 1 addition & 1 deletion src/components/forms/ProgressBar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getBgColor } from '@/shared/utils/formFormatter.ts';
import { getBgColor } from '@/shared/utils/trajectoryUtils';

export type FileInputStatus = 'loading' | 'success' | 'error' | 'empty';

Expand Down
4 changes: 3 additions & 1 deletion src/components/input/SelectAndSearchableInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -34,6 +35,7 @@ const SelectAndSearchableInput = ({
const [placeHolder] = useState<string>(defaultPlaceHolder);
const [valueInput, setValueInput] = useState<string>('');
const dropdownList = useRef<HTMLDivElement | null>(null);
const { t } = useTranslation();

const handleInputChange = async (value: string) => {
try {
Expand Down Expand Up @@ -110,7 +112,7 @@ const SelectAndSearchableInput = ({
setIsDropdownOpen(false);
}
}}
placeHolder={placeHolder}
placeHolder={!options?.length ? t('components.selectAnsSearchInput.@emptyList') : placeHolder}
variant="outlined"
value={valueInput}
disabled={isInputDisabled}
Expand Down
179 changes: 117 additions & 62 deletions src/components/tab/AreaLinkTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<AreaAndLinkRowData[]>([
{ 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<ReadOnlyObject>({ '0': false, '1': !data[0].trajectory });
const [readOnly, setReadOnly] = useState<ReadOnlyObject>({
'0': false,
'1': !trajectoryNameArea,
});
const [optionsDB, setOptionsDB] = useState<SelectOption[][]>();
const [optionsFS, setOptionsFS] = useState<SelectOption[]>();
const [rowIndexSelected, setRowIndexSelected] = useState<number>(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) {
Expand All @@ -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,
Expand All @@ -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);
Expand All @@ -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(
() =>
Expand All @@ -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}
/>
)}
</div>
Expand Down
23 changes: 14 additions & 9 deletions src/pages/pegase/home/components/MainContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,20 @@ const MainContent = () => {
<div className="flex h-full w-full flex-col">
<PegaseStar />
<Suspense>
<StudyProvider initialValue={{ isStudyGenerated: false, areaTrajectory: null, linkTrajectory: null }}>
<Routes>
<Route path="/study/:studyName" element={<StudyDetails />} />
<Route path="/project/:projectName" element={<ProjectDetails />} />
{Object.entries([...menuBottomData, ...menuTopData]).map(([key, route]) => (
<Route key={key} path={route.path} Component={route.component} />
))}
</Routes>
</StudyProvider>
<Routes>
<Route
path="/study/:studyName"
element={
<StudyProvider initialValue={{ isStudyGenerated: false }}>
<StudyDetails />
</StudyProvider>
}
/>
<Route path="/project/:projectName" element={<ProjectDetails />} />
{Object.entries([...menuBottomData, ...menuTopData]).map(([key, route]) => (
<Route key={key} path={route.path} Component={route.component} />
))}
</Routes>
</Suspense>
</div>
</UserSettingsContext.Provider>
Expand Down
10 changes: 7 additions & 3 deletions src/pages/pegase/studies/studyDetails/AreaLinkTableHeaders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const columnHelper = createColumnHelper<AreaAndLinkRowData>();
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<void>,
handleImport: (index: number) => Promise<void>,
handlerSearch: (index: number, value: string | undefined) => Promise<SelectOption[] | undefined>,
) => [
Expand All @@ -41,13 +41,17 @@ const getAreaLinkTableHeaders = (
return trajectory ? (
<div className="inline-flex w-[850px] space-x-2 py-3">
<span>{trajectory}</span>
<RdsIconButton icon={RdsIconId.Delete} size="small" onClick={() => handleUpdate(row.index, null, 'empty')} />
<RdsIconButton
icon={RdsIconId.Delete}
size="small"
onClick={() => void handleUpdate(row.index, null, 'empty')}
/>
</div>
) : (
<div className="inline-flex w-[850px] items-center space-x-2">
<SelectAndSearchableInput
options={options?.[row.index] ?? []}
onSelect={(value: SelectOption) => 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')
Expand Down
8 changes: 4 additions & 4 deletions src/pages/pegase/studies/studyDetails/StudyDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const StudyDetails = () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const location: Location<StudyState> = useLocation();
const { t } = useTranslation();
const { isStudyGenerated, areaTrajectory } = useStudy();
const { isStudyGenerated, AREA } = useStudy();
const dispatch = useStudyDispatch();
const [isGenerating, setIsGenerating] = useState(false);
const { study } = location.state || {};
Expand Down Expand Up @@ -56,19 +56,19 @@ const StudyDetails = () => {
</div>
<div className="flex gap-4 px-3 py-2">
<div className="flex h-10 items-end self-stretch">
<StudyNavigationMenu onRenderActiveComponent={setActiveContent} studyHorizon={study.horizon} />
<StudyNavigationMenu onRenderActiveComponent={setActiveContent} study={study} />
</div>
</div>
<div className="flex h-full flex-col justify-between space-x-4 p-4">
{activeContent}
<div className="flex flex-col gap-2">
<RdsDivider />
<div className="flex items-center gap-2 self-end">
{!areaTrajectory && <div className={'text-error-600'}>{t('studyDetails.@add_trajectories_message')}</div>}
{!AREA && <div className={'text-error-600'}>{t('studyDetails.@add_trajectories_message')}</div>}
<ButtonWithStdIcon
label={t('studyDetails.@generate')}
onClick={() => void handleGenerateStudy()}
disabled={!areaTrajectory || isStudyGenerated}
disabled={!AREA || !!isStudyGenerated}
icon={StdIconId.CheckCircle}
position="right"
isLoading={isGenerating}
Expand Down
Loading

0 comments on commit 2749ff5

Please sign in to comment.