Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/questionnaire load version #892

Merged
merged 5 commits into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
VITE_API_URL=http://localhost:5000
VITE_ACTIVE_NAMESPACES='Components,Actions,Stores,Utils,Models'
VITE_PUBLIC_ENEMY_URL=
VITE_TROMBI_URL=https://trombi.insee.fr

# AUTH
# NONE or OIDC
Expand Down
38 changes: 38 additions & 0 deletions src/actions/app-state.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import {
getVisualization,
putQuestionnaire,
} from '../api/remote-api';
import { getVersion } from '../api/versions';
import { TCM } from '../constants/pogues-constants';
import { questionnaireRemoteToStores } from '../model/remote-to-stores';
import * as Questionnaire from '../model/transformations/questionnaire';
import { getCollectedVariablesByQuestion } from '../utils/variables/collected-variables-utils';
import { addVisualizationError } from './errors';

export const SET_ACTIVE_QUESTIONNAIRE = 'SET_ACTIVE_QUESTIONNAIRE';
Expand Down Expand Up @@ -181,6 +183,42 @@ export const updateActiveQuestionnaire = (updatedState) => {
};
};

/**
* Load a questionnaire version as the active questionnaire.
*
* Async action that fetch a questionnaire data from a version.
*/
export const loadQuestionnaireVersion = (id, token) => async (dispatch) => {
try {
const qr = await getVersion(id, token);
const newQr = questionnaireRemoteToStores(qr.data);
const questionnaireId = qr.data.id;
dispatch(
updateActiveQuestionnaire(newQr.questionnaireById[questionnaireId]),
);
dispatch(
setActiveComponents(newQr.componentByQuestionnaire[questionnaireId]),
);
dispatch(
setActiveCodeLists(newQr.codeListByQuestionnaire[questionnaireId]),
);
dispatch(
setActiveVariables({
activeCalculatedVariablesById:
newQr.calculatedVariableByQuestionnaire[questionnaireId],
activeExternalVariablesById:
newQr.externalVariableByQuestionnaire[questionnaireId],
collectedVariableByQuestion: getCollectedVariablesByQuestion(
newQr.componentByQuestionnaire[questionnaireId],
newQr.collectedVariableByQuestionnaire[questionnaireId],
),
}),
);
} catch (err) {
console.error(err);
}
};

/**
* Save active questionnaire success
*
Expand Down
6 changes: 3 additions & 3 deletions src/api/versions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Version } from '@/models/versions';
import type { Version, VersionWithData } from '@/models/versions';

import { getBaseURI } from './utils';

Expand All @@ -21,13 +21,13 @@ export const getVersions = async (
export const getVersion = async (
id: string,
token: string,
): Promise<Version> => {
): Promise<VersionWithData> => {
const b = await getBaseURI();
return fetch(`${b}/persistence/version/${id}?withData=true`, {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: token ? `Bearer ${token}` : '',
},
}).then((res) => res.json() as Promise<Version>);
}).then((res) => res.json() as Promise<VersionWithData>);
};
18 changes: 16 additions & 2 deletions src/constants/dictionary.js → src/constants/dictionary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ const {
PRECISION_EDIT,
} = CODELISTS_ACTIONS;

const dictionary = {
type Dictionary = { [key: string]: { en: string; fr: string } };

const dictionary: Dictionary = {
phLabel: {
en: 'Enter a title for the questionnaire',
fr: 'Entrez un titre pour le questionnaire',
Expand Down Expand Up @@ -203,6 +205,10 @@ const dictionary = {
en: 'Create',
fr: 'Créer',
},
load: {
en: 'Load',
fr: 'Charger',
},
generateCollectedVariables: {
en: 'Generate collected variables',
fr: 'Générer variables collectées',
Expand Down Expand Up @@ -908,9 +914,13 @@ const dictionary = {
en: 'Second information axis',
},
modification: {
en: 'Your modification is not saved ! Are you sure you want to leave ?',
en: 'Your modification is not saved! Are you sure you want to leave?',
fr: "Votre modification n'est pas sauvegardée ! Êtes-vous sûr de vouloir quitter ?",
},
modificationsNotSaved: {
en: 'Your modifications are not saved!',
fr: 'Vos modifications ne sont pas sauvegardées !',
},
saveLower: {
en: 'Thank you to validate all the actions on the page',
fr: "Merci de valider toutes les actions sur l'élément",
Expand Down Expand Up @@ -1083,6 +1093,10 @@ const dictionary = {
fr: 'Obligatoire',
en: 'Mandatory',
},
by: {
fr: 'par',
en: 'by',
},
durationformat1: {
fr: 'heures/minutes',
en: 'hours/minutes',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,36 +13,9 @@ import {
loadExternalQuestionnairesIfNeeded,
} from '../../../actions/metadata';
import { loadQuestionnaire } from '../../../actions/questionnaire';
import { COMPONENT_TYPE } from '../../../constants/pogues-constants';
import { getCollectedVariablesByQuestion } from '../../../utils/variables/collected-variables-utils';
import PageQuestionnaire from '../components/page-questionnaire';

const { QUESTION } = COMPONENT_TYPE;

// Utils

function getCollectedVariablesByQuestionnaire(
components = {},
collectedVariables = {},
) {
return Object.keys(components)
.filter((key) => components[key].type === QUESTION)
.filter((key) => components[key].collectedVariables.length > 0)
.reduce((acc, key) => {
return {
...acc,
[key]: components[key].collectedVariables.reduce(
(accInner, keyInner) => {
return {
...accInner,
[keyInner]: { ...collectedVariables[keyInner] },
};
},
{},
),
};
}, {});
}

// Prop types and default props

const propTypes = {
Expand Down Expand Up @@ -73,7 +46,7 @@ const mapStateToProps = (
codeLists: state.codeListByQuestionnaire[id],
calculatedVariables: state.calculatedVariableByQuestionnaire[id],
externalVariables: state.externalVariableByQuestionnaire[id],
collectedVariablesByQuestion: getCollectedVariablesByQuestionnaire(
collectedVariablesByQuestion: getCollectedVariablesByQuestion(
state.componentByQuestionnaire[id],
state.collectedVariableByQuestionnaire[id],
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,10 @@ const QuestionnaireListComponents = (props) => {
</button>
</div>
<div className="popup-body">
<Versions token={token} />
<Versions
token={token}
onSuccess={() => setShowVersionsModal(false)}
/>
</div>
</div>
</ReactModal>
Expand Down
44 changes: 38 additions & 6 deletions src/layout/versions/components/VersionDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,66 @@
import { useState } from 'react';

import dayjs from 'dayjs';
import 'dayjs/locale/fr';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import relativeTime from 'dayjs/plugin/relativeTime';
import { Clock } from 'iconoir-react';

import type { Version } from '@/models/versions';
import Dictionary from '@/utils/dictionary/dictionary';
import { getEnvVar } from '@/utils/env';
import ConfirmInline from '@/widgets/inlineConfirm';

dayjs.locale('fr');
dayjs.extend(relativeTime);
dayjs.extend(localizedFormat);

interface VersionProps {
isQuestionnaireModified?: boolean;
version: Version;
token: string;
onLoad: () => void;
}

export default function VersionDetails({ version }: Readonly<VersionProps>) {
export default function VersionDetails({
isQuestionnaireModified = false,
version,
onLoad,
}: Readonly<VersionProps>) {
const [confirmLoad, setConfirmLoad] = useState<boolean>(false);

const { author, timestamp } = version;

return (
<div className="grid grid-cols-[auto_1fr] gap-x-4">
<>
<div className="inline-flex gap-3 items-center">
<Clock height={18} />
<Clock height={18} width={18} />
<span>{dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}</span>
</div>
<div className="first-letter:uppercase text-slate-400">
<span className="italic" title={dayjs(timestamp).format('LLLL')}>
{dayjs(timestamp).fromNow()},
</span>{' '}
par <span>{author}</span>
{Dictionary.by}{' '}
<a href={`${getEnvVar('TROMBI_URL')}/${author}`} target="_blank">
{author}
</a>
</div>
<div className="inline-flex gap-3 items-center">
<button className="btn-white" onClick={() => setConfirmLoad(true)}>
{Dictionary.load}
</button>
{confirmLoad ? (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here you never set confirmLoad when trying to load another version (you can display the confirm component on every versions). Is this done on purpose ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit more tricky to do so I thought it was good enough for now. Especially since we want to move the version thing into a tab instead of a modal (if we make the ui more modern) so we won't need this anymore in the long run. since we'll be able to confirm it through a modal.

(I decided to go for the inline confirm for now because I didn't like to open a modal in a modal...)

<ConfirmInline
onConfirm={onLoad}
onCancel={() => setConfirmLoad(false)}
warningLabel={
isQuestionnaireModified
? `${Dictionary.modificationsNotSaved}`
: undefined
}
/>
) : null}
</div>
</div>
</>
);
}
23 changes: 20 additions & 3 deletions src/layout/versions/components/Versions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,20 @@ type Questionnaire = {
};

interface VersionsProps {
isQuestionnaireModified?: boolean;
loadQuestionnaireVersion: (id: string, token: string) => void;
onSuccess: () => void;
questionnaire: Questionnaire;
token: string;
}

export default function Versions({ questionnaire, token }: VersionsProps) {
export default function Versions({
isQuestionnaireModified = false,
loadQuestionnaireVersion,
onSuccess,
questionnaire,
token,
}: VersionsProps) {
const [versions, setVersions] = useState<Version[]>([]);
const [isLoading, setIsLoading] = useState(true);

Expand All @@ -33,9 +42,17 @@ export default function Versions({ questionnaire, token }: VersionsProps) {
return isLoading ? (
<div>Loading...</div>
) : (
<div className="space-y-2">
<div className="grid grid-cols-[auto_auto_1fr] gap-x-4 space-y-2 items-center">
{versions.map((version) => (
<VersionDetails key={version.id} version={version} token={token} />
<VersionDetails
key={version.id}
isQuestionnaireModified={isQuestionnaireModified}
onLoad={() => {
loadQuestionnaireVersion(version.id, token);
onSuccess();
}}
version={version}
/>
))}
</div>
);
Expand Down
10 changes: 9 additions & 1 deletion src/layout/versions/containers/versions-container.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { connect } from 'react-redux';

import { loadQuestionnaireVersion } from '../../../actions/app-state';
import Versions from '../components/Versions';

const mapStateToProps = (state) => ({
isQuestionnaireModified: state.appState.isQuestionnaireModified,
questionnaire: state.appState.activeQuestionnaire,
});
const mapDispatchToProps = {
loadQuestionnaireVersion,
};

const QuestionnaireListComponentsContainer = connect(mapStateToProps)(Versions);
const QuestionnaireListComponentsContainer = connect(
mapStateToProps,
mapDispatchToProps,
)(Versions);

export default QuestionnaireListComponentsContainer;
4 changes: 4 additions & 0 deletions src/models/versions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@ export type Version = {
poguesId: string;
timestamp: string; // ISO 8601
};

export type VersionWithData = Version & {
data: unknown;
};
32 changes: 0 additions & 32 deletions src/utils/dictionary/dictionary.jsx

This file was deleted.

25 changes: 0 additions & 25 deletions src/utils/dictionary/dictionary.spec.jsx

This file was deleted.

Loading
Loading