From fa8c753a65e0f3576dd00f73f1aa10e7a391c742 Mon Sep 17 00:00:00 2001
From: Arnaud Ambroselli <31724752+arnaudambro@users.noreply.github.com>
Date: Tue, 10 Oct 2023 04:52:36 +0200
Subject: [PATCH] fix(dashboard): nouveau DataLoader (#1692)
* fix(dashboard): nouveau DataLoader
* fix: sorting when necessary
* add logs
* fix: recoil default
* fix: clean
* fix: need to initiate default recoil values before setting new values
* fix: test
* fix: restricted roles
* fix: move API.get({ path: /now });
* fix and cleans
* fix: clean
* fix: remove data.length check
* fix: return from data migrator
* fix: setFullScreen(isStartingInitialLoad)
---
api/src/controllers/organisation.js | 22 +-
dashboard/src/app.js | 17 +-
dashboard/src/components/ActionModal.js | 4 +-
dashboard/src/components/DataLoader.js | 813 +++++++++---------
dashboard/src/components/DataMigrator.js | 14 +-
dashboard/src/recoil/actions.js | 10 +-
dashboard/src/recoil/comments.js | 12 +-
dashboard/src/recoil/groups.ts | 12 +-
dashboard/src/recoil/passages.js | 12 +-
dashboard/src/recoil/persons.ts | 10 +-
dashboard/src/recoil/places.js | 12 +-
dashboard/src/recoil/relPersonPlace.js | 12 +-
dashboard/src/recoil/rencontres.js | 12 +-
dashboard/src/recoil/reports.js | 10 +-
dashboard/src/recoil/selectors.js | 1 +
dashboard/src/recoil/territory.js | 12 +-
dashboard/src/recoil/territoryObservations.js | 10 +-
dashboard/src/scenes/auth/signin.js | 4 +-
dashboard/src/scenes/person/Places.js | 7 +-
dashboard/src/scenes/report/view.js | 3 +-
.../src/scenes/territory-observations/list.js | 8 +-
e2e/activate_passages_rencontres.spec.ts | 2 +-
22 files changed, 542 insertions(+), 477 deletions(-)
diff --git a/api/src/controllers/organisation.js b/api/src/controllers/organisation.js
index 44f37175f..fde1e8665 100644
--- a/api/src/controllers/organisation.js
+++ b/api/src/controllers/organisation.js
@@ -37,8 +37,6 @@ router.get(
passport.authenticate("user", { session: false }),
validateUser(["superadmin", "admin", "normal", "restricted-access"]),
catchErrors(async (req, res, next) => {
- const startLoadingDate = Date.now();
-
try {
z.object({
organisation: z.string().regex(looseUuidRegex),
@@ -77,7 +75,7 @@ router.get(
// Medical data is never saved in cache so we always have to download all at every page reload.
// In other words "after" param is intentionnaly ignored for consultations, treatments and medical files.
const medicalDataQuery =
- withAllMedicalData !== "true" ? query : { where: { organisation: req.query.organisation }, paranoid: withDeleted === "true" };
+ withAllMedicalData !== "true" ? query : { where: { organisation: req.query.organisation }, paranoid: withDeleted === "true" ? false : true };
const consultations = await Consultation.count(medicalDataQuery);
const medicalFiles = await MedicalFile.count(medicalDataQuery);
const treatments = await Treatment.count(medicalDataQuery);
@@ -85,20 +83,20 @@ router.get(
return res.status(200).send({
ok: true,
data: {
- actions,
- consultations,
- treatments,
- comments,
- passages,
- rencontres,
- medicalFiles,
persons,
groups,
+ reports,
+ passages,
+ rencontres,
+ actions,
+ territories,
places,
relsPersonPlace,
- territories,
territoryObservations,
- reports,
+ comments,
+ consultations,
+ treatments,
+ medicalFiles,
},
});
})
diff --git a/dashboard/src/app.js b/dashboard/src/app.js
index 926c65d87..8f1bc610e 100644
--- a/dashboard/src/app.js
+++ b/dashboard/src/app.js
@@ -33,7 +33,7 @@ import ScrollToTop from './components/ScrollToTop';
import TopBar from './components/TopBar';
import VersionOutdatedAlert from './components/VersionOutdatedAlert';
import ModalConfirm from './components/ModalConfirm';
-import DataLoader, { initialLoadingTextState, loadingTextState, useDataLoader } from './components/DataLoader';
+import DataLoader, { initialLoadIsDoneState, useDataLoader } from './components/DataLoader';
import { Bounce, cssTransition, ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import SentryRoute from './components/Sentryroute';
@@ -90,7 +90,7 @@ if (ENV === 'production') {
const App = ({ resetRecoil }) => {
const authToken = useRecoilValue(authTokenState);
const user = useRecoilValue(userState);
- const loadingText = useRecoilValue(loadingTextState);
+ const initialLoadIsDone = useRecoilValue(initialLoadIsDoneState);
const recoilResetKey = useRecoilValue(recoilResetKeyState);
useEffect(() => {
@@ -106,7 +106,7 @@ const App = ({ resetRecoil }) => {
if (authToken && e.newState === 'active') {
API.get({ path: '/check-auth' }) // will force logout if session is expired
.then(() => {
- if (loadingText !== initialLoadingTextState) {
+ if (initialLoadIsDone) {
// if the app is already loaded
// will refresh data if session is still valid
refresh();
@@ -120,7 +120,7 @@ const App = ({ resetRecoil }) => {
return () => {
lifecycle.removeEventListener('statechange', onWindowFocus);
};
- }, [authToken, refresh, loadingText]);
+ }, [authToken, refresh, initialLoadIsDone]);
return (
@@ -195,8 +195,13 @@ export default function ContextedApp() {
const [recoilKey, setRecoilKey] = useState(0);
return (
-
- setRecoilKey((k) => k + 1)} />
+ {/* We need React.Suspense here because default values for `person`, `action` etc. tables is an async cache request */}
+ {/* https://recoiljs.org/docs/guides/asynchronous-data-queries#query-default-atom-values */}
+ }>
+ {/* Recoil Nexus allows to use Recoil state outside React tree */}
+
+ setRecoilKey((k) => k + 1)} />
+
);
}
diff --git a/dashboard/src/components/ActionModal.js b/dashboard/src/components/ActionModal.js
index 0498e47f8..172d7a016 100644
--- a/dashboard/src/components/ActionModal.js
+++ b/dashboard/src/components/ActionModal.js
@@ -612,7 +612,9 @@ function ActionContent({ onClose, action, personId = null, personIds = null, isM
.filter(Boolean)
.join(' ')}>
({ ...comment, type: 'action', person: action.person }))}
+ comments={action?.comments
+ .map((comment) => ({ ...comment, type: 'action', person: action.person }))
+ .sort((a, b) => new Date(b.date || b.createdAt) - new Date(a.date || a.createdAt))}
color="main"
canToggleUrgentCheck
typeForNewComment="action"
diff --git a/dashboard/src/components/DataLoader.js b/dashboard/src/components/DataLoader.js
index 534dafb45..7f0f22009 100644
--- a/dashboard/src/components/DataLoader.js
+++ b/dashboard/src/components/DataLoader.js
@@ -1,9 +1,9 @@
-import { useEffect, useRef, useState } from 'react';
-import styled from 'styled-components';
-import { atom, useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
+import { useEffect } from 'react';
+import { atom, selector, useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { toast } from 'react-toastify';
import { personsState } from '../recoil/persons';
+import { groupsState } from '../recoil/groups';
import { treatmentsState } from '../recoil/treatments';
import { actionsState } from '../recoil/actions';
import { medicalFileState } from '../recoil/medicalFiles';
@@ -18,370 +18,420 @@ import { consultationsState, formatConsultation } from '../recoil/consultations'
import { commentsState } from '../recoil/comments';
import { organisationState, userState } from '../recoil/auth';
-import { clearCache, dashboardCurrentCacheKey, getCacheItem, getCacheItemDefaultValue, setCacheItem } from '../services/dataManagement';
+import { clearCache, dashboardCurrentCacheKey, getCacheItemDefaultValue, setCacheItem } from '../services/dataManagement';
import API from '../services/api';
import { RandomPicture, RandomPicturePreloader } from './LoaderRandomPicture';
import ProgressBar from './LoaderProgressBar';
import useDataMigrator from './DataMigrator';
-import { groupsState } from '../recoil/groups';
// Update to flush cache.
-const cacheEffect = ({ onSet }) => {
- onSet(async (newValue) => {
- await setCacheItem(dashboardCurrentCacheKey, newValue);
- });
-};
-
-const loaderTriggerState = atom({ key: 'loaderTriggerState', default: false });
const isLoadingState = atom({ key: 'isLoadingState', default: false });
-const initialLoadState = atom({ key: 'isInitialLoadState', default: false });
const fullScreenState = atom({ key: 'fullScreenState', default: true });
-export const lastLoadState = atom({ key: 'lastLoadState', default: null, effects: [cacheEffect] });
-export const initialLoadingTextState = 'En attente de chargement';
+const progressState = atom({ key: 'progressState', default: null });
+const totalState = atom({ key: 'totalState', default: null });
+const initialLoadingTextState = 'En attente de chargement';
export const loadingTextState = atom({ key: 'loadingTextState', default: initialLoadingTextState });
+export const initialLoadIsDoneState = atom({ key: 'initialLoadIsDoneState', default: false });
+export const lastLoadState = atom({
+ key: 'lastLoadState',
+ default: selector({
+ key: 'lastLoadState/default',
+ get: async () => {
+ const cache = await getCacheItemDefaultValue(dashboardCurrentCacheKey, 0);
+ return cache;
+ },
+ }),
+ effects: [
+ ({ onSet }) => {
+ onSet(async (newValue) => {
+ await setCacheItem(dashboardCurrentCacheKey, newValue);
+ });
+ },
+ ],
+});
export default function DataLoader() {
- const [user, setUser] = useRecoilState(userState);
+ const isLoading = useRecoilValue(isLoadingState);
+ const fullScreen = useRecoilValue(fullScreenState);
+ const loadingText = useRecoilValue(loadingTextState);
+ const progress = useRecoilValue(progressState);
+ const total = useRecoilValue(totalState);
+
+ if (!isLoading) return ;
+ if (!total && !fullScreen) return null;
+
+ if (fullScreen) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+}
+
+export function useDataLoader(options = { refreshOnMount: false }) {
+ const [fullScreen, setFullScreen] = useRecoilState(fullScreenState);
+ const [isLoading, setIsLoading] = useRecoilState(isLoadingState);
+ const setLoadingText = useSetRecoilState(loadingTextState);
+ const setInitialLoadIsDone = useSetRecoilState(initialLoadIsDoneState);
+ const [lastLoadValue, setLastLoad] = useRecoilState(lastLoadState);
+ const setProgress = useSetRecoilState(progressState);
+ const setTotal = useSetRecoilState(totalState);
+
+ const setUser = useSetRecoilState(userState);
+ const setOrganisation = useSetRecoilState(organisationState);
const { migrateData } = useDataMigrator();
const [persons, setPersons] = useRecoilState(personsState);
- const [actions, setActions] = useRecoilState(actionsState);
- const [consultations, setConsultations] = useRecoilState(consultationsState);
- const [treatments, setTreatments] = useRecoilState(treatmentsState);
- const [medicalFiles, setMedicalFiles] = useRecoilState(medicalFileState);
+ const [groups, setGroups] = useRecoilState(groupsState);
+ const [reports, setReports] = useRecoilState(reportsState);
const [passages, setPassages] = useRecoilState(passagesState);
const [rencontres, setRencontres] = useRecoilState(rencontresState);
- const [reports, setReports] = useRecoilState(reportsState);
+ const [actions, setActions] = useRecoilState(actionsState);
const [territories, setTerritories] = useRecoilState(territoriesState);
const [places, setPlaces] = useRecoilState(placesState);
const [relsPersonPlace, setRelsPersonPlace] = useRecoilState(relsPersonPlaceState);
const [territoryObservations, setTerritoryObservations] = useRecoilState(territoryObservationsState);
const [comments, setComments] = useRecoilState(commentsState);
- const [groups, setGroups] = useRecoilState(groupsState);
+ const [consultations, setConsultations] = useRecoilState(consultationsState);
+ const [treatments, setTreatments] = useRecoilState(treatmentsState);
+ const [medicalFiles, setMedicalFiles] = useRecoilState(medicalFileState);
- const [loaderTrigger, setLoaderTrigger] = useRecoilState(loaderTriggerState);
- const [lastLoad, setLastLoad] = useRecoilState(lastLoadState);
- const [isLoading, setIsLoading] = useRecoilState(isLoadingState);
- const [fullScreen, setFullScreen] = useRecoilState(fullScreenState);
- const [loadingText, setLoadingText] = useRecoilState(loadingTextState);
- const initialLoad = useRecoilValue(initialLoadState);
- const [organisation, setOrganisation] = useRecoilState(organisationState);
+ useEffect(function refreshOnMountEffect() {
+ if (options.refreshOnMount && !isLoading) loadOrRefreshData(false);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
- const [loadList, setLoadList] = useState({ list: [], offset: 0 });
- const [progressBuffer, setProgressBuffer] = useState(null);
- const [progress, setProgress] = useState(null);
- const [total, setTotal] = useState(null);
+ /*
+ Steps
+
+ INITIALLY: Recoil takes care of cache reconciliation, so the loader don't have to worry about it
+ - each atom `person`, `action` etc. has an async default value, which is a selector that will get the data from the cache
+ - each of those atoms has an effect that will update the cache when the data is set
+
+ Then, for initial load / refresh, the loader will:
+ 1. Start the UI:
+ - set isLoading to true
+ - set fullScreen to true if it's the initial load
+ - set loadingText to 'Chargement des données' if it's the initial load, 'Mise à jour des données' otherwise
+ 2. Get the latest organisation, in order to get the latest migrations data, and updated custom fields
+ 3. Get the latest user, in order to get the latest user roles
+ 4. Get the latest stats, in order to know what data to download, with the following parameters
+ - after lastLoadValue, which is a date provided by the server, and is the last time the data was downloaded
+ - withDeleted, to get the data deleted from other users, so that we can filter it out in here
+ - withAllMedicalData, only on initial load because medical data is never saved in cache
+ The stats will be used for
+ - knowing the total items count, and setting the progress bar
+ - knowing what data to download
+ 5. Get the server date, in order to know when the data was last updated
+ 6. Download all the data, with the following parameters
+ - withDeleted, to get the data deleted from other users, so that we can filter it out in here
+ - after lastLoadValue, which is a date provided by the server, and is the last time the data was downloaded
+
+ */
+ async function loadOrRefreshData(isStartingInitialLoad) {
+ setIsLoading(true);
+ setFullScreen(isStartingInitialLoad);
+ setLoadingText(isStartingInitialLoad ? 'Chargement des données' : 'Mise à jour des données');
+
+ /*
+ Refresh organisation (and user), to get the latest organisation fields
+ and the latest user roles
+ */
+ const userResponse = await API.get({ path: '/user/me' });
+ if (!userResponse.ok) return resetLoaderOnError();
+ const latestOrganisation = userResponse.user.organisation;
+ const latestUser = userResponse.user;
+ const organisationId = latestOrganisation._id;
+ setOrganisation(latestOrganisation);
+ setUser(latestUser);
+ if (isStartingInitialLoad) {
+ const migrationIsSuccessful = await migrateData(latestOrganisation);
+ if (!migrationIsSuccessful) return resetLoaderOnError();
+ }
- useEffect(() => {
- initLoader();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [progress, total, loaderTrigger, loadList.list.length, isLoading]);
+ const statsResponse = await API.get({
+ path: '/organisation/stats',
+ query: {
+ organisation: organisationId,
+ after: lastLoadValue,
+ withDeleted: true,
+ // Medical data is never saved in cache so we always have to download all at every page reload.
+ withAllMedicalData: isStartingInitialLoad,
+ },
+ });
- useEffect(() => {
- fetchData();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [loadList]);
- useEffect(() => {
- updateProgress();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [progress, progressBuffer, loadList.list.length]);
-
- const organisationId = organisation?._id;
-
- const serverDate = useRef(null);
-
- // Loader initialization: get data from cache, check stats, init recoils states, and start loader.
- function initLoader() {
- if (loadList.list.length > 0) return;
-
- const shouldStart = progress === null && total === null && loaderTrigger && isLoading;
- const shouldStop = progress !== null && total !== null && isLoading;
-
- if (shouldStart) {
- Promise.resolve()
- .then(async () => {
- /*
- Refresh organisation (and user), to get the latest organisation fields
- and the latest user roles
- */
- const userResponse = await API.get({ path: '/user/me' });
- if (!userResponse.ok) return resetLoaderOnError();
- setOrganisation(userResponse.user.organisation);
- setUser(userResponse.user);
- // Get date from server at the very beginning of the loader.
- const serverDateResponse = await API.get({ path: '/now' });
- serverDate.current = serverDateResponse.data;
- })
- .then(() => (initialLoad ? migrateData() : Promise.resolve()))
- .then(() => getCacheItem(dashboardCurrentCacheKey))
- .then((lastLoadValue) => {
- setLastLoad(lastLoadValue || 0);
- API.get({
- path: '/organisation/stats',
- query: {
- organisation: organisationId,
- after: lastLoadValue || 0,
- withDeleted: true,
- // Medical data is never saved in cache so we always have to download all at every page reload.
- withAllMedicalData: initialLoad,
- },
- }).then(({ data: stats }) => {
- if (!stats) return;
- const newList = [];
- let itemsCount =
- 0 +
- stats.persons +
- stats.consultations +
- stats.actions +
- stats.treatments +
- stats.medicalFiles +
- stats.passages +
- stats.rencontres +
- stats.reports +
- stats.territories +
- stats.places +
- stats.relsPersonPlace +
- stats.territoryObservations +
- stats.comments +
- stats.groups;
-
- if (stats.persons) newList.push('person');
- if (stats.groups) newList.push('group');
- if (stats.consultations) newList.push('consultation');
- if (['admin', 'normal'].includes(user.role)) {
- if (stats.treatments) newList.push('treatment');
- if (stats.medicalFiles) newList.push('medicalFile');
- }
- if (stats.reports) newList.push('report');
- if (stats.passages) newList.push('passage');
- if (stats.rencontres) newList.push('rencontre');
- if (stats.actions) newList.push('action');
- if (stats.territories) newList.push('territory');
- if (stats.places) newList.push('place');
- if (stats.relsPersonPlace) newList.push('relsPersonPlace');
- if (stats.territoryObservations) newList.push('territoryObservation');
- if (stats.comments) newList.push('comment');
-
- // In case this is not the initial load, we don't have to load from cache again.
- if (!initialLoad) {
- startLoader(newList, itemsCount);
- return;
- }
-
- setLoadingText('Récupération des données dans le cache');
- Promise.resolve()
- .then(() => getCacheItemDefaultValue('person', []))
- .then((persons) => setPersons([...persons]))
- .then(() => getCacheItemDefaultValue('group', []))
- .then((groups) => setGroups([...groups]))
- .then(() => getCacheItemDefaultValue('report', []))
- .then((reports) => setReports([...reports]))
- .then(() => getCacheItemDefaultValue('passage', []))
- .then((passages) => setPassages([...passages]))
- .then(() => getCacheItemDefaultValue('rencontre', []))
- .then((rencontres) => setRencontres([...rencontres]))
- .then(() => getCacheItemDefaultValue('action', []))
- .then((actions) => setActions([...actions]))
- .then(() => getCacheItemDefaultValue('territory', []))
- .then((territories) => setTerritories([...territories]))
- .then(() => getCacheItemDefaultValue('place', []))
- .then((places) => setPlaces([...places]))
- .then(() => getCacheItemDefaultValue('relPersonPlace', []))
- .then((relsPersonPlace) => setRelsPersonPlace([...relsPersonPlace]))
- .then(() => getCacheItemDefaultValue('territory-observation', []))
- .then((territoryObservations) => setTerritoryObservations([...territoryObservations]))
- .then(() => getCacheItemDefaultValue('comment', []))
- .then((comments) => setComments([...comments]))
- .then(() => startLoader(newList, itemsCount));
- });
- });
- } else if (shouldStop) stopLoader();
- }
+ if (!statsResponse.ok) return false;
+
+ // Get date from server just after getting all the stats
+ // We'll set the `lastLoadValue` to this date after all the data is downloaded
+ const serverDateResponse = await API.get({ path: '/now' });
+ const serverDate = serverDateResponse.data;
+
+ const stats = statsResponse.data;
+ let itemsCount =
+ 0 +
+ stats.persons +
+ stats.groups +
+ stats.reports +
+ stats.passages +
+ stats.rencontres +
+ stats.actions +
+ stats.territories +
+ stats.places +
+ stats.relsPersonPlace +
+ stats.territoryObservations +
+ stats.comments +
+ stats.consultations;
+
+ if (['admin', 'normal'].includes(latestUser.role)) {
+ itemsCount += stats.treatments + stats.medicalFiles;
+ }
- // Fetch data from API, handle loader progress.
- async function fetchData() {
- if (loadList.list.length === 0) return;
+ setProgress(0);
+ setTotal(itemsCount);
- const [current] = loadList.list;
const query = {
organisation: organisationId,
limit: String(10000),
- page: String(loadList.offset),
- after: lastLoad,
- withDeleted: Boolean(lastLoad),
+ after: lastLoadValue,
+ withDeleted: true,
};
- function handleMore(hasMore) {
- if (hasMore) setLoadList({ list: loadList.list, offset: loadList.offset + 1 });
- else setLoadList({ list: loadList.list.slice(1), offset: 0 });
- }
-
- if (current === 'person') {
+ if (stats.persons > 0) {
+ let newItems = [];
setLoadingText('Chargement des personnes');
- const res = await API.get({ path: '/person', query });
- if (!res.data) return resetLoaderOnError();
- setPersons(
- res.hasMore
- ? mergeItems(persons, res.decryptedData)
- : mergeItems(persons, res.decryptedData)
- .map((p) => ({ ...p, followedSince: p.followedSince || p.createdAt }))
- .sort((p1, p2) => (p1.name || '').localeCompare(p2.name || ''))
- );
- handleMore(res.hasMore);
- setProgressBuffer(res.data.length);
- } else if (current === 'group') {
+ async function loadPersons(page = 0) {
+ const res = await API.get({ path: '/person', query: { ...query, page: String(page) } });
+ if (!res.ok) return resetLoaderOnError();
+ setProgress((p) => p + res.data.length);
+ newItems.push(...res.decryptedData);
+ if (res.hasMore) return loadPersons(page + 1);
+ setPersons(mergeItems(persons, newItems));
+ return true;
+ }
+ const personSuccess = await loadPersons(0);
+ if (!personSuccess) return false;
+ }
+ if (stats.groups > 0) {
+ let newItems = [];
setLoadingText('Chargement des familles');
- const res = await API.get({ path: '/group', query });
- if (!res.data) return resetLoaderOnError();
- setGroups(() => {
- const mergedItems = mergeItems(groups, res.decryptedData);
- if (res.hasMore) return mergedItems;
- if (mergedItems.length > groups.length) {
- return mergedItems.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
- }
- return mergedItems;
- });
- handleMore(res.hasMore);
- setProgressBuffer(res.data.length);
- } else if (current === 'consultation') {
- setLoadingText('Chargement des consultations');
- const res = await API.get({ path: '/consultation', query: { ...query, after: initialLoad ? 0 : lastLoad } });
- if (!res.data) return resetLoaderOnError();
- setConsultations(
- res.hasMore ? mergeItems(consultations, res.decryptedData) : mergeItems(consultations, res.decryptedData).map(formatConsultation)
- );
- handleMore(res.hasMore);
- setProgressBuffer(res.data.length);
- } else if (current === 'treatment') {
- setLoadingText('Chargement des traitements');
- const res = await API.get({ path: '/treatment', query: { ...query, after: initialLoad ? 0 : lastLoad } });
- if (!res.data) return resetLoaderOnError();
- setTreatments(mergeItems(treatments, res.decryptedData));
- handleMore(res.hasMore);
- setProgressBuffer(res.data.length);
- } else if (current === 'medicalFile') {
- setLoadingText('Chargement des fichiers médicaux');
- const res = await API.get({ path: '/medical-file', query: { ...query, after: initialLoad ? 0 : lastLoad } });
- if (!res.data) return resetLoaderOnError();
- setMedicalFiles(mergeItems(medicalFiles, res.decryptedData));
- handleMore(res.hasMore);
- setProgressBuffer(res.data.length);
- } else if (current === 'report') {
- setLoadingText('Chargement des rapports');
- const res = await API.get({ path: '/report', query });
- if (!res.data) return resetLoaderOnError();
- setReports(
- res.hasMore
- ? mergeItems(reports, res.decryptedData)
- : mergeItems(reports, res.decryptedData)
- // This line should be removed when `clean-reports-with-no-team-nor-date` migration has run on all organisations.
- .filter((r) => !!r.team && !!r.date)
- );
- handleMore(res.hasMore);
- setProgressBuffer(res.data.length);
- } else if (current === 'passage') {
+ async function loadGroups(page = 0) {
+ const res = await API.get({ path: '/group', query: { ...query, page: String(page) } });
+ if (!res.ok) return resetLoaderOnError();
+ setProgress((p) => p + res.data.length);
+ newItems.push(...res.decryptedData);
+ if (res.hasMore) return loadGroups(page + 1);
+ setGroups(mergeItems(groups, newItems));
+ return true;
+ }
+ const groupsSuccess = await loadGroups(0);
+ if (!groupsSuccess) return false;
+ }
+ if (stats.reports > 0) {
+ let newItems = [];
+ setLoadingText('Chargement des comptes-rendus');
+ async function loadReports(page = 0) {
+ const res = await API.get({ path: '/report', query: { ...query, page: String(page) } });
+ if (!res.ok) return resetLoaderOnError();
+ setProgress((p) => p + res.data.length);
+ newItems.push(...res.decryptedData);
+ if (res.hasMore) return loadReports(page + 1);
+ setReports(mergeItems(reports, newItems, { filterNewItemsFunction: (r) => !!r.team && !!r.date }));
+ return true;
+ }
+ const reportsSuccess = await loadReports(0);
+ if (!reportsSuccess) return false;
+ }
+ if (stats.passages > 0) {
+ let newItems = [];
setLoadingText('Chargement des passages');
- const res = await API.get({ path: '/passage', query });
- if (!res.data) return resetLoaderOnError();
- setPassages(() => {
- const mergedItems = mergeItems(passages, res.decryptedData);
- if (res.hasMore) return mergedItems;
- return mergedItems.sort((a, b) => new Date(b.date || b.createdAt) - new Date(a.date || a.createdAt));
- });
- handleMore(res.hasMore);
- setProgressBuffer(res.data.length);
- } else if (current === 'rencontre') {
+ async function loadPassages(page = 0) {
+ const res = await API.get({ path: '/passage', query: { ...query, page: String(page) } });
+ if (!res.ok) return resetLoaderOnError();
+ setProgress((p) => p + res.data.length);
+ newItems.push(...res.decryptedData);
+ if (res.hasMore) return loadPassages(page + 1);
+ setPassages(mergeItems(passages, newItems));
+ return true;
+ }
+ const passagesSuccess = await loadPassages(0);
+ if (!passagesSuccess) return false;
+ }
+ if (stats.rencontres > 0) {
+ let newItems = [];
setLoadingText('Chargement des rencontres');
- const res = await API.get({ path: '/rencontre', query });
- if (!res.data) return resetLoaderOnError();
- setRencontres(() => {
- const mergedItems = mergeItems(rencontres, res.decryptedData);
- if (res.hasMore) return mergedItems;
- return mergedItems.sort((a, b) => new Date(b.date || b.createdAt) - new Date(a.date || a.createdAt));
- });
- handleMore(res.hasMore);
- setProgressBuffer(res.data.length);
- } else if (current === 'action') {
- setFullScreen(false);
+ async function loadRencontres(page = 0) {
+ const res = await API.get({ path: '/rencontre', query: { ...query, page: String(page) } });
+ if (!res.ok) return resetLoaderOnError();
+ setProgress((p) => p + res.data.length);
+ newItems.push(...res.decryptedData);
+ if (res.hasMore) return loadRencontres(page + 1);
+ setRencontres(mergeItems(rencontres, newItems));
+ return true;
+ }
+ const rencontresSuccess = await loadRencontres(0);
+ if (!rencontresSuccess) return false;
+ }
+ if (stats.actions > 0) {
+ let newItems = [];
setLoadingText('Chargement des actions');
- const res = await API.get({ path: '/action', query });
- if (!res.data) return resetLoaderOnError();
- setActions(mergeItems(actions, res.decryptedData));
- handleMore(res.hasMore);
- setProgressBuffer(res.data.length);
- } else if (current === 'territory') {
+ async function loadActions(page = 0) {
+ const res = await API.get({ path: '/action', query: { ...query, page: String(page) } });
+ if (!res.ok) return resetLoaderOnError();
+ setProgress((p) => p + res.data.length);
+ newItems.push(...res.decryptedData);
+ if (res.hasMore) return loadActions(page + 1);
+ setActions(mergeItems(actions, newItems));
+ return true;
+ }
+ const actionsSuccess = await loadActions(0);
+ if (!actionsSuccess) return false;
+ }
+ if (stats.territories > 0) {
+ let newItems = [];
setLoadingText('Chargement des territoires');
- const res = await API.get({ path: '/territory', query });
- if (!res.data) return resetLoaderOnError();
- setTerritories(() => {
- const mergedItems = mergeItems(territories, res.decryptedData);
- if (res.hasMore) return mergedItems;
- return mergedItems.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
- });
- handleMore(res.hasMore);
- setProgressBuffer(res.data.length);
- } else if (current === 'place') {
+ async function loadTerritories(page = 0) {
+ const res = await API.get({ path: '/territory', query: { ...query, page: String(page) } });
+ if (!res.ok) return resetLoaderOnError();
+ setProgress((p) => p + res.data.length);
+ newItems.push(...res.decryptedData);
+ if (res.hasMore) return loadTerritories(page + 1);
+ setTerritories(mergeItems(territories, newItems));
+ return true;
+ }
+ const territoriesSuccess = await loadTerritories(0);
+ if (!territoriesSuccess) return false;
+ }
+ if (stats.places > 0) {
+ let newItems = [];
setLoadingText('Chargement des lieux');
- const res = await API.get({ path: '/place', query });
- if (!res.data) return resetLoaderOnError();
- setPlaces(() => {
- const mergedItems = mergeItems(places, res.decryptedData);
- if (res.hasMore) return mergedItems;
- return mergedItems.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
- });
- handleMore(res.hasMore);
- setProgressBuffer(res.data.length);
- } else if (current === 'relsPersonPlace') {
+ async function loadPlaces(page = 0) {
+ const res = await API.get({ path: '/place', query: { ...query, page: String(page) } });
+ if (!res.ok) return resetLoaderOnError();
+ setProgress((p) => p + res.data.length);
+ newItems.push(...res.decryptedData);
+ if (res.hasMore) return loadPlaces(page + 1);
+ setPlaces(mergeItems(places, newItems));
+ return true;
+ }
+ const placesSuccess = await loadPlaces(0);
+ if (!placesSuccess) return false;
+ }
+ if (stats.relsPersonPlace > 0) {
+ let newItems = [];
setLoadingText('Chargement des relations personne-lieu');
- const res = await API.get({ path: '/relPersonPlace', query });
- if (!res.data) return resetLoaderOnError();
- setRelsPersonPlace(() => {
- const mergedItems = mergeItems(relsPersonPlace, res.decryptedData);
- if (res.hasMore) return mergedItems;
- return mergedItems.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
- });
- handleMore(res.hasMore);
- setProgressBuffer(res.data.length);
- } else if (current === 'territoryObservation') {
+ async function loadRelPersonPlaces(page = 0) {
+ const res = await API.get({ path: '/relPersonPlace', query: { ...query, page: String(page) } });
+ if (!res.ok) return resetLoaderOnError();
+ setProgress((p) => p + res.data.length);
+ newItems.push(...res.decryptedData);
+ if (res.hasMore) return loadRelPersonPlaces(page + 1);
+ setRelsPersonPlace(mergeItems(relsPersonPlace, newItems));
+ return true;
+ }
+ const relsPersonPlacesSuccess = await loadRelPersonPlaces(0);
+ if (!relsPersonPlacesSuccess) return false;
+ }
+ if (stats.territoryObservations > 0) {
+ let newItems = [];
setLoadingText('Chargement des observations de territoire');
- const res = await API.get({ path: '/territory-observation', query });
- if (!res.data) return resetLoaderOnError();
- setTerritoryObservations(() => {
- const mergedItems = mergeItems(territoryObservations, res.decryptedData);
- if (res.hasMore) return mergedItems;
- return mergedItems.sort((a, b) => new Date(b.observedAt || b.createdAt) - new Date(a.observedAt || a.createdAt));
- });
- handleMore(res.hasMore);
- setProgressBuffer(res.data.length);
- } else if (current === 'comment') {
+ async function loadObservations(page = 0) {
+ const res = await API.get({ path: '/territory-observation', query: { ...query, page: String(page) } });
+ if (!res.ok) return resetLoaderOnError();
+ setProgress((p) => p + res.data.length);
+ newItems.push(...res.decryptedData);
+ if (res.hasMore) return loadObservations(page + 1);
+ setTerritoryObservations(mergeItems(territoryObservations, newItems));
+ return true;
+ }
+ const territoryObservationsSuccess = await loadObservations(0);
+ if (!territoryObservationsSuccess) return false;
+ }
+ if (stats.comments > 0) {
+ let newItems = [];
setLoadingText('Chargement des commentaires');
- const res = await API.get({ path: '/comment', query });
- if (!res.data) return resetLoaderOnError();
- setComments(() => {
- const mergedItems = mergeItems(comments, res.decryptedData);
- if (res.hasMore) return mergedItems;
- return mergedItems.sort((a, b) => new Date(b.date || b.createdAt) - new Date(a.date || a.createdAt));
- });
- handleMore(res.hasMore);
- setProgressBuffer(res.data.length);
+ async function loadComments(page = 0) {
+ const res = await API.get({ path: '/comment', query: { ...query, page: String(page) } });
+ if (!res.ok) return resetLoaderOnError();
+ setProgress((p) => p + res.data.length);
+ newItems.push(...res.decryptedData);
+ if (res.hasMore) return loadComments(page + 1);
+ setComments(mergeItems(comments, newItems));
+ return true;
+ }
+ const commentsSuccess = await loadComments(0);
+ if (!commentsSuccess) return false;
+ }
+ if (stats.consultations > 0) {
+ let newItems = [];
+ setLoadingText('Chargement des consultations');
+ async function loadConsultations(page = 0) {
+ const res = await API.get({
+ path: '/consultation',
+ query: { ...query, page: String(page), after: isStartingInitialLoad ? 0 : lastLoadValue },
+ });
+ if (!res.ok) return resetLoaderOnError();
+ setProgress((p) => p + res.data.length);
+ newItems.push(...res.decryptedData);
+ if (res.hasMore) return loadConsultations(page + 1);
+ setConsultations(mergeItems(consultations, newItems, { formatNewItemsFunction: formatConsultation }));
+ return true;
+ }
+ const consultationsSuccess = await loadConsultations(0);
+ if (!consultationsSuccess) return false;
+ }
+ if (['admin', 'normal'].includes(latestUser.role) && stats.treatments > 0) {
+ let newItems = [];
+ setLoadingText('Chargement des traitements');
+ async function loadTreatments(page = 0) {
+ const res = await API.get({ path: '/treatment', query: { ...query, page: String(page), after: isStartingInitialLoad ? 0 : lastLoadValue } });
+ if (!res.ok) return resetLoaderOnError();
+ setProgress((p) => p + res.data.length);
+ newItems.push(...res.decryptedData);
+ if (res.hasMore) return loadTreatments(page + 1);
+ setTreatments(mergeItems(treatments, newItems));
+ return true;
+ }
+ const treatmentsSuccess = await loadTreatments(0);
+ if (!treatmentsSuccess) return false;
+ }
+ if (['admin', 'normal'].includes(latestUser.role) && stats.medicalFiles > 0) {
+ let newItems = [];
+ setLoadingText('Chargement des fichiers médicaux');
+ async function loadMedicalFiles(page = 0) {
+ const res = await API.get({
+ path: '/medical-file',
+ query: { ...query, page: String(page), after: isStartingInitialLoad ? 0 : lastLoadValue },
+ });
+ if (!res.ok) return resetLoaderOnError();
+ setProgress((p) => p + res.data.length);
+ newItems.push(...res.decryptedData);
+ if (res.hasMore) return loadMedicalFiles(page + 1);
+ setMedicalFiles(mergeItems(medicalFiles, newItems));
+ return true;
+ }
+ const medicalFilesSuccess = await loadMedicalFiles(0);
+ if (!medicalFilesSuccess) return false;
}
- }
-
- function startLoader(list, itemsCount) {
- setLoadList({ list, offset: 0 });
- setLoaderTrigger(false);
- setProgress(0);
- setTotal(itemsCount);
- }
- function stopLoader() {
setIsLoading(false);
- setLastLoad(serverDate.current);
- setLoadingText('En attente de chargement');
- setProgressBuffer(null);
+ setLastLoad(serverDate);
+ setLoadingText('En attente de rafraichissement');
setProgress(null);
setTotal(null);
+ setInitialLoadIsDone(true);
+ return true;
}
async function resetLoaderOnError() {
@@ -393,64 +443,7 @@ export default function DataLoader() {
onClose: () => window.location.replace('/auth'),
autoClose: 5000,
});
- }
-
- function updateProgress() {
- if (!loadList.list.length) return;
-
- if (progressBuffer !== null) {
- setProgress((progress || 0) + progressBuffer);
- setProgressBuffer(null);
- }
- }
-
- if (!isLoading) return ;
- if (!total && !fullScreen) return null;
-
- if (fullScreen) {
- return (
-
-
-
-
-
-
- );
- }
-
- return (
-
-
-
- );
-}
-
-export function useDataLoader(options = { refreshOnMount: false }) {
- const [fullScreen, setFullScreen] = useRecoilState(fullScreenState);
- const [isLoading, setIsLoading] = useRecoilState(isLoadingState);
- const setLoaderTrigger = useSetRecoilState(loaderTriggerState);
- const setInitialLoad = useSetRecoilState(initialLoadState);
- const setLoadingText = useSetRecoilState(loadingTextState);
- const setLastLoad = useSetRecoilState(lastLoadState);
-
- useEffect(function refreshOnMountEffect() {
- if (options.refreshOnMount && !isLoading) refresh();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- function refresh() {
- setIsLoading(true);
- setFullScreen(false);
- setInitialLoad(false);
- setLoaderTrigger(true);
- setLoadingText('Mise à jour des données');
- }
- function load() {
- setIsLoading(true);
- setFullScreen(true);
- setInitialLoad(true);
- setLoaderTrigger(true);
- setLoadingText('Chargement des données');
+ return false;
}
async function resetCache() {
@@ -459,54 +452,38 @@ export function useDataLoader(options = { refreshOnMount: false }) {
}
return {
- refresh,
- load,
+ refresh: () => loadOrRefreshData(false),
+ startInitialLoad: () => loadOrRefreshData(true),
resetCache,
isLoading: Boolean(isLoading),
isFullScreen: Boolean(fullScreen),
};
}
-export const mergeItems = (oldItems, newItems = []) => {
+export function mergeItems(oldItems, newItems = [], { formatNewItemsFunction, filterNewItemsFunction } = {}) {
+ const newItemsCleanedAndFormatted = [];
const newItemIds = {};
+
for (const newItem of newItems) {
newItemIds[newItem._id] = true;
+ if (newItem.deletedAt) continue;
+ if (filterNewItemsFunction) {
+ if (!filterNewItemsFunction(newItem)) continue;
+ }
+ if (formatNewItemsFunction) {
+ newItemsCleanedAndFormatted.push(formatNewItemsFunction(newItem));
+ } else {
+ newItemsCleanedAndFormatted.push(newItem);
+ }
+ }
+
+ const oldItemsPurged = [];
+ for (const oldItem of oldItems) {
+ if (oldItem.deletedAt) continue;
+ if (!newItemIds[oldItem._id]) {
+ oldItemsPurged.push(oldItem);
+ }
}
- const oldItemsPurged = oldItems.filter((item) => !newItemIds[item._id] && !item.deletedAt);
- return [...oldItemsPurged, ...newItems.filter((item) => !item.deletedAt)];
-};
-
-const FullScreenContainer = styled.div`
- width: 100%;
- z-index: 1000;
- position: absolute;
- top: 0;
- left: 0;
- bottom: 0;
- right: 0;
- box-sizing: border-box;
- background-color: #fff;
- display: flex;
- justify-content: center;
- align-items: center;
-`;
-
-const InsideContainer = styled.div`
- display: flex;
- flex-direction: column;
- width: 50vw;
- max-width: 50vh;
- height: 50vh;
- max-height: 50vw;
- justify-content: center;
- align-items: center;
-`;
-
-const Container = styled.div`
- width: 100%;
- z-index: 1000;
- position: absolute;
- top: 0;
- left: 0;
- box-sizing: border-box;
-`;
+
+ return [...oldItemsPurged, ...newItemsCleanedAndFormatted];
+}
diff --git a/dashboard/src/components/DataMigrator.js b/dashboard/src/components/DataMigrator.js
index b4df9933f..36c7a6d84 100644
--- a/dashboard/src/components/DataMigrator.js
+++ b/dashboard/src/components/DataMigrator.js
@@ -1,4 +1,4 @@
-import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
+import { useRecoilValue, useSetRecoilState } from 'recoil';
import { mappedIdsToLabels, prepareActionForEncryption } from '../recoil/actions';
import { organisationState, userState } from '../recoil/auth';
import { usePreparePersonForEncryption } from '../recoil/persons';
@@ -17,16 +17,15 @@ const LOADING_TEXT = 'Mise à jour des données de votre organisation…';
export default function useDataMigrator() {
const setLoadingText = useSetRecoilState(loadingTextState);
const user = useRecoilValue(userState);
- const [organisation, setOrganisation] = useRecoilState(organisationState);
-
- const organisationId = organisation?._id;
+ const setOrganisation = useSetRecoilState(organisationState);
const preparePersonForEncryption = usePreparePersonForEncryption();
return {
// One "if" for each migration.
// `migrationLastUpdateAt` should be set after each migration and send in every PUT/POST/PATCH request to server.
- migrateData: async () => {
+ migrateData: async (organisation) => {
+ const organisationId = organisation?._id;
let migrationLastUpdateAt = organisation.migrationLastUpdateAt;
/*
// Example of migration:
@@ -50,6 +49,8 @@ export default function useDataMigrator() {
if (response.ok) {
setOrganisation(response.organisation);
migrationLastUpdateAt = response.organisation.migrationLastUpdateAt;
+ else {
+ return false;
}
}
// End of example of migration.
@@ -109,8 +110,11 @@ export default function useDataMigrator() {
if (response.ok) {
setOrganisation(response.organisation);
migrationLastUpdateAt = response.organisation.migrationLastUpdateAt;
+ } else {
+ return false;
}
}
+ return true;
},
};
}
diff --git a/dashboard/src/recoil/actions.js b/dashboard/src/recoil/actions.js
index 5be0c19ac..5646c8cec 100644
--- a/dashboard/src/recoil/actions.js
+++ b/dashboard/src/recoil/actions.js
@@ -1,4 +1,4 @@
-import { setCacheItem } from '../services/dataManagement';
+import { getCacheItemDefaultValue, setCacheItem } from '../services/dataManagement';
import { atom, selector } from 'recoil';
import { organisationState } from './auth';
import { looseUuidRegex } from '../utils';
@@ -8,7 +8,13 @@ import { capture } from '../services/sentry';
const collectionName = 'action';
export const actionsState = atom({
key: collectionName,
- default: [],
+ default: selector({
+ key: 'action/default',
+ get: async () => {
+ const cache = await getCacheItemDefaultValue('action', []);
+ return cache;
+ },
+ }),
effects: [({ onSet }) => onSet(async (newValue) => setCacheItem(collectionName, newValue))],
});
diff --git a/dashboard/src/recoil/comments.js b/dashboard/src/recoil/comments.js
index 52378399b..6c692f06e 100644
--- a/dashboard/src/recoil/comments.js
+++ b/dashboard/src/recoil/comments.js
@@ -1,5 +1,5 @@
-import { setCacheItem } from '../services/dataManagement';
-import { atom } from 'recoil';
+import { getCacheItemDefaultValue, setCacheItem } from '../services/dataManagement';
+import { atom, selector } from 'recoil';
import { looseUuidRegex } from '../utils';
import { toast } from 'react-toastify';
import { capture } from '../services/sentry';
@@ -7,7 +7,13 @@ import { capture } from '../services/sentry';
const collectionName = 'comment';
export const commentsState = atom({
key: collectionName,
- default: [],
+ default: selector({
+ key: 'comment/default',
+ get: async () => {
+ const cache = await getCacheItemDefaultValue('comment', []);
+ return cache;
+ },
+ }),
effects: [({ onSet }) => onSet(async (newValue) => setCacheItem(collectionName, newValue))],
});
diff --git a/dashboard/src/recoil/groups.ts b/dashboard/src/recoil/groups.ts
index 03524dc29..e2d9a7293 100644
--- a/dashboard/src/recoil/groups.ts
+++ b/dashboard/src/recoil/groups.ts
@@ -1,12 +1,18 @@
-import { setCacheItem } from '../services/dataManagement';
-import { atom, selectorFamily } from 'recoil';
+import { getCacheItemDefaultValue, setCacheItem } from '../services/dataManagement';
+import { atom, selector, selectorFamily } from 'recoil';
import type { GroupInstance } from '../types/group';
import type { UUIDV4 } from '../types/uuid';
const collectionName = 'group';
export const groupsState = atom({
key: collectionName,
- default: [],
+ default: selector({
+ key: 'group/default',
+ get: async () => {
+ const cache = await getCacheItemDefaultValue('group', []);
+ return cache;
+ },
+ }),
effects: [({ onSet }) => onSet(async (newValue) => setCacheItem(collectionName, newValue))],
});
diff --git a/dashboard/src/recoil/passages.js b/dashboard/src/recoil/passages.js
index 599d29985..3f93e5399 100644
--- a/dashboard/src/recoil/passages.js
+++ b/dashboard/src/recoil/passages.js
@@ -1,5 +1,5 @@
-import { setCacheItem } from '../services/dataManagement';
-import { atom } from 'recoil';
+import { getCacheItemDefaultValue, setCacheItem } from '../services/dataManagement';
+import { atom, selector } from 'recoil';
import { looseUuidRegex } from '../utils';
import { toast } from 'react-toastify';
import { capture } from '../services/sentry';
@@ -7,7 +7,13 @@ import { capture } from '../services/sentry';
const collectionName = 'passage';
export const passagesState = atom({
key: collectionName,
- default: [],
+ default: selector({
+ key: 'passage/default',
+ get: async () => {
+ const cache = await getCacheItemDefaultValue('passage', []);
+ return cache;
+ },
+ }),
effects: [({ onSet }) => onSet(async (newValue) => setCacheItem(collectionName, newValue))],
});
diff --git a/dashboard/src/recoil/persons.ts b/dashboard/src/recoil/persons.ts
index 146ab1d32..3b3b6d6e7 100644
--- a/dashboard/src/recoil/persons.ts
+++ b/dashboard/src/recoil/persons.ts
@@ -1,4 +1,4 @@
-import { setCacheItem } from '../services/dataManagement';
+import { getCacheItemDefaultValue, setCacheItem } from '../services/dataManagement';
import { atom, selector, useRecoilValue } from 'recoil';
import { organisationState } from './auth';
import { toast } from 'react-toastify';
@@ -9,7 +9,13 @@ import type { PredefinedField, CustomField } from '../types/field';
const collectionName = 'person';
export const personsState = atom({
key: collectionName,
- default: [],
+ default: selector({
+ key: 'person/default',
+ get: async () => {
+ const cache = await getCacheItemDefaultValue('person', []);
+ return cache;
+ },
+ }),
effects: [({ onSet }) => onSet(async (newValue) => setCacheItem(collectionName, newValue))],
});
diff --git a/dashboard/src/recoil/places.js b/dashboard/src/recoil/places.js
index d869eb37d..45f73106f 100644
--- a/dashboard/src/recoil/places.js
+++ b/dashboard/src/recoil/places.js
@@ -1,5 +1,5 @@
-import { setCacheItem } from '../services/dataManagement';
-import { atom } from 'recoil';
+import { getCacheItemDefaultValue, setCacheItem } from '../services/dataManagement';
+import { atom, selector } from 'recoil';
import { looseUuidRegex } from '../utils';
import { toast } from 'react-toastify';
import { capture } from '../services/sentry';
@@ -7,7 +7,13 @@ import { capture } from '../services/sentry';
const collectionName = 'place';
export const placesState = atom({
key: collectionName,
- default: [],
+ default: selector({
+ key: 'place/default',
+ get: async () => {
+ const cache = await getCacheItemDefaultValue('place', []);
+ return cache;
+ },
+ }),
effects: [({ onSet }) => onSet(async (newValue) => setCacheItem(collectionName, newValue))],
});
diff --git a/dashboard/src/recoil/relPersonPlace.js b/dashboard/src/recoil/relPersonPlace.js
index ec6d72c23..4a972be4f 100644
--- a/dashboard/src/recoil/relPersonPlace.js
+++ b/dashboard/src/recoil/relPersonPlace.js
@@ -1,5 +1,5 @@
-import { setCacheItem } from '../services/dataManagement';
-import { atom } from 'recoil';
+import { getCacheItemDefaultValue, setCacheItem } from '../services/dataManagement';
+import { atom, selector } from 'recoil';
import { looseUuidRegex } from '../utils';
import { toast } from 'react-toastify';
import { capture } from '../services/sentry';
@@ -7,7 +7,13 @@ import { capture } from '../services/sentry';
const collectionName = 'relPersonPlace';
export const relsPersonPlaceState = atom({
key: collectionName,
- default: [],
+ default: selector({
+ key: 'relPersonPlace/default',
+ get: async () => {
+ const cache = await getCacheItemDefaultValue('relPersonPlace', []);
+ return cache;
+ },
+ }),
effects: [({ onSet }) => onSet(async (newValue) => setCacheItem(collectionName, newValue))],
});
diff --git a/dashboard/src/recoil/rencontres.js b/dashboard/src/recoil/rencontres.js
index a1705d963..d664b0d58 100644
--- a/dashboard/src/recoil/rencontres.js
+++ b/dashboard/src/recoil/rencontres.js
@@ -1,5 +1,5 @@
-import { setCacheItem } from '../services/dataManagement';
-import { atom } from 'recoil';
+import { getCacheItemDefaultValue, setCacheItem } from '../services/dataManagement';
+import { atom, selector } from 'recoil';
import { looseUuidRegex } from '../utils';
import { toast } from 'react-toastify';
import { capture } from '../services/sentry';
@@ -7,7 +7,13 @@ import { capture } from '../services/sentry';
const collectionName = 'rencontre';
export const rencontresState = atom({
key: collectionName,
- default: [],
+ default: selector({
+ key: 'rencontre/default',
+ get: async () => {
+ const cache = await getCacheItemDefaultValue('rencontre', []);
+ return cache;
+ },
+ }),
effects: [({ onSet }) => onSet(async (newValue) => setCacheItem(collectionName, newValue))],
});
diff --git a/dashboard/src/recoil/reports.js b/dashboard/src/recoil/reports.js
index b918f1090..7916e53e1 100644
--- a/dashboard/src/recoil/reports.js
+++ b/dashboard/src/recoil/reports.js
@@ -1,4 +1,4 @@
-import { setCacheItem } from '../services/dataManagement';
+import { getCacheItemDefaultValue, setCacheItem } from '../services/dataManagement';
import { atom, selector } from 'recoil';
import { capture } from '../services/sentry';
import { organisationState } from './auth';
@@ -8,7 +8,13 @@ import { toast } from 'react-toastify';
const collectionName = 'report';
export const reportsState = atom({
key: collectionName,
- default: [],
+ default: selector({
+ key: 'report/default',
+ get: async () => {
+ const cache = await getCacheItemDefaultValue('report', []);
+ return cache;
+ },
+ }),
effects: [
({ onSet }) =>
onSet(async (newValue) => {
diff --git a/dashboard/src/recoil/selectors.js b/dashboard/src/recoil/selectors.js
index 12ba91d40..f0d4c0df1 100644
--- a/dashboard/src/recoil/selectors.js
+++ b/dashboard/src/recoil/selectors.js
@@ -102,6 +102,7 @@ export const itemsGroupedByPersonSelector = selector({
originalPersonsObject[person._id] = { name: person.name, _id: person._id };
personsObject[person._id] = {
...person,
+ followedSince: person.followedSince || person.createdAt,
userPopulated: usersObject[person.user],
formattedBirthDate: formatBirthDate(person.birthdate),
age: formatAge(person.birthdate),
diff --git a/dashboard/src/recoil/territory.js b/dashboard/src/recoil/territory.js
index 9f054cbd3..8a5736250 100644
--- a/dashboard/src/recoil/territory.js
+++ b/dashboard/src/recoil/territory.js
@@ -1,5 +1,5 @@
-import { setCacheItem } from '../services/dataManagement';
-import { atom } from 'recoil';
+import { getCacheItemDefaultValue, setCacheItem } from '../services/dataManagement';
+import { atom, selector } from 'recoil';
import { looseUuidRegex } from '../utils';
import { toast } from 'react-toastify';
import { capture } from '../services/sentry';
@@ -7,7 +7,13 @@ import { capture } from '../services/sentry';
const collectionName = 'territory';
export const territoriesState = atom({
key: collectionName,
- default: [],
+ default: selector({
+ key: 'territory/default',
+ get: async () => {
+ const cache = await getCacheItemDefaultValue('territory', []);
+ return cache;
+ },
+ }),
effects: [({ onSet }) => onSet(async (newValue) => setCacheItem(collectionName, newValue))],
});
diff --git a/dashboard/src/recoil/territoryObservations.js b/dashboard/src/recoil/territoryObservations.js
index a9faf3c64..bcbc5fc05 100644
--- a/dashboard/src/recoil/territoryObservations.js
+++ b/dashboard/src/recoil/territoryObservations.js
@@ -1,6 +1,6 @@
import { organisationState } from './auth';
import { atom, selector } from 'recoil';
-import { setCacheItem } from '../services/dataManagement';
+import { getCacheItemDefaultValue, setCacheItem } from '../services/dataManagement';
import { looseUuidRegex } from '../utils';
import { toast } from 'react-toastify';
import { capture } from '../services/sentry';
@@ -8,7 +8,13 @@ import { capture } from '../services/sentry';
const collectionName = 'territory-observation';
export const territoryObservationsState = atom({
key: collectionName,
- default: [],
+ default: selector({
+ key: 'territory-observation/default',
+ get: async () => {
+ const cache = await getCacheItemDefaultValue('territory-observation', []);
+ return cache;
+ },
+ }),
effects: [({ onSet }) => onSet(async (newValue) => setCacheItem(collectionName, newValue))],
});
diff --git a/dashboard/src/scenes/auth/signin.js b/dashboard/src/scenes/auth/signin.js
index 565a592ef..372738c15 100644
--- a/dashboard/src/scenes/auth/signin.js
+++ b/dashboard/src/scenes/auth/signin.js
@@ -28,7 +28,7 @@ const SignIn = () => {
const [showPassword, setShowPassword] = useState(false);
const [loading, setLoading] = useState(true);
const [authViaCookie, setAuthViaCookie] = useState(false);
- const { load: runDataLoader, isLoading, resetCache } = useDataLoader();
+ const { startInitialLoad, isLoading, resetCache } = useDataLoader();
const setToken = useSetRecoilState(authTokenState);
const [signinForm, setSigninForm] = useState({ email: '', password: '', orgEncryptionKey: DEFAULT_ORGANISATION_KEY || '' });
@@ -45,7 +45,7 @@ const SignIn = () => {
}
}, [history, organisation, isLoading, isDesktop]);
- const onSigninValidated = async () => runDataLoader();
+ const onSigninValidated = () => startInitialLoad();
const onLogout = async () => {
await API.logout();
diff --git a/dashboard/src/scenes/person/Places.js b/dashboard/src/scenes/person/Places.js
index 1ed929490..6051c8839 100644
--- a/dashboard/src/scenes/person/Places.js
+++ b/dashboard/src/scenes/person/Places.js
@@ -40,6 +40,11 @@ const PersonPlaces = ({ person }) => {
return personPlaces.length !== new Set(personPlaces).size;
}, [person.relsPersonPlace]);
+ const sortedPlaces = useMemo(() => {
+ if (!person.relsPersonPlace?.length) return [];
+ return [...person.relsPersonPlace]?.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
+ }, [person.relsPersonPlace]);
+
return (
<>
@@ -74,7 +79,7 @@ const PersonPlaces = ({ person }) => {
- {person.relsPersonPlace?.map((relPersonPlace) => {
+ {sortedPlaces?.map((relPersonPlace) => {
const { place: placeId, createdAt, user } = relPersonPlace;
const place = places.find((p) => p._id === placeId);
return (
diff --git a/dashboard/src/scenes/report/view.js b/dashboard/src/scenes/report/view.js
index d5472bf3e..83dda19bb 100644
--- a/dashboard/src/scenes/report/view.js
+++ b/dashboard/src/scenes/report/view.js
@@ -348,7 +348,8 @@ const View = () => {
{ referenceStartDay: dateString, referenceEndDay: dateString },
currentTeam?.nightSession ? 12 : 0
);
- }),
+ })
+ .sort((a, b) => new Date(b.observedAt || b.createdAt) - new Date(a.observedAt || a.createdAt)),
[dateString, selectedTeamsObject, territoryObservations]
);
diff --git a/dashboard/src/scenes/territory-observations/list.js b/dashboard/src/scenes/territory-observations/list.js
index ef88195e5..05e1f6804 100644
--- a/dashboard/src/scenes/territory-observations/list.js
+++ b/dashboard/src/scenes/territory-observations/list.js
@@ -15,7 +15,13 @@ const List = ({ territory = {} }) => {
const [observation, setObservation] = useState({});
const [openObservationModale, setOpenObservationModale] = useState(null);
- const observations = useMemo(() => territoryObservations.filter((obs) => obs.territory === territory._id), [territory._id, territoryObservations]);
+ const observations = useMemo(
+ () =>
+ territoryObservations
+ .filter((obs) => obs.territory === territory._id)
+ .sort((a, b) => new Date(b.observedAt || b.createdAt) - new Date(a.observedAt || a.createdAt)),
+ [territory._id, territoryObservations]
+ );
if (!observations) return null;
diff --git a/e2e/activate_passages_rencontres.spec.ts b/e2e/activate_passages_rencontres.spec.ts
index febd3793f..a10a0cf47 100644
--- a/e2e/activate_passages_rencontres.spec.ts
+++ b/e2e/activate_passages_rencontres.spec.ts
@@ -78,7 +78,7 @@ test("test", async ({ page }) => {
await page.getByRole("link", { name: "Statistiques" }).click();
await page.getByRole("button", { name: "Passages" }).click();
- await expect(page.getByText("Nombre de passages ?Non-anonyme375%Anonyme125%Total4100%Non-anonymeAnonyme3 (75%")).toBeVisible();
+ await expect(page.getByText("Nombre de passages ?Non-anonyme375%Anonyme125%Total4100%AnonymeNon-anonyme1 (25%")).toBeVisible();
await expect(page.getByRole("button", { name: "Rencontres" })).not.toBeVisible();