From 8c492c6e6aee8085ac2ad0735b5a32c59f25f792 Mon Sep 17 00:00:00 2001
From: rbisson <remi.bisson@inrae.fr>
Date: Mon, 2 Dec 2024 14:00:04 +0100
Subject: [PATCH 01/17] [Results] Now building table data from user settings
 [UserContext] Now fetching user Id at page load

---
 src/actions/user.js                           | 103 +++++++-------
 src/components/Header/HeaderUserMenu.js       |   4 +-
 src/context/InSylvaGatekeeperClient.js        |  28 +++-
 src/context/InSylvaKeycloakClient.js          |   3 +-
 src/context/UserContext.js                    |  69 +++++-----
 src/pages/profile/Profile.js                  |   8 +-
 src/pages/results/ResultsTableMUI.js          | 130 ++++++++++++------
 .../search/AdvancedSearch/AdvancedSearch.js   |   6 +-
 src/pages/search/Search.js                    |  46 +++----
 src/store/index.js                            |   2 +
 10 files changed, 229 insertions(+), 170 deletions(-)

diff --git a/src/actions/user.js b/src/actions/user.js
index 8aa8453..4e50a27 100644
--- a/src/actions/user.js
+++ b/src/actions/user.js
@@ -9,7 +9,7 @@ igClient.baseUrl = process.env.REACT_APP_IN_SYLVA_GATEKEEPER_PORT
 
 export const findOneUser = async (id, request = igClient) => {
   if (tokenTimedOut(process.env.REACT_KEYCLOAK_TOKEN_VALIDITY)) {
-    refreshToken();
+    await refreshToken();
   }
   igClient.token = sessionStorage.getItem('access_token');
   try {
@@ -24,7 +24,7 @@ export const findOneUser = async (id, request = igClient) => {
 
 export const findOneUserWithGroupAndRole = async (id, request = igClient) => {
   if (tokenTimedOut(process.env.REACT_KEYCLOAK_TOKEN_VALIDITY)) {
-    refreshToken();
+    await refreshToken();
   }
   igClient.token = sessionStorage.getItem('access_token');
   try {
@@ -37,9 +37,9 @@ export const findOneUserWithGroupAndRole = async (id, request = igClient) => {
   }
 };
 
-export async function getGroups() {
+export const getGroups = async () => {
   if (tokenTimedOut(process.env.REACT_KEYCLOAK_TOKEN_VALIDITY)) {
-    refreshToken();
+    await refreshToken();
   }
   igClient.token = sessionStorage.getItem('access_token');
   try {
@@ -50,11 +50,11 @@ export async function getGroups() {
   } catch (error) {
     console.error(error);
   }
-}
+};
 
-export async function getRoles() {
+export const getRoles = async () => {
   if (tokenTimedOut(process.env.REACT_KEYCLOAK_TOKEN_VALIDITY)) {
-    refreshToken();
+    await refreshToken();
   }
   igClient.token = sessionStorage.getItem('access_token');
   try {
@@ -65,11 +65,11 @@ export async function getRoles() {
   } catch (error) {
     console.error(error);
   }
-}
+};
 
 export const sendMail = async (subject, message, request = igClient) => {
   if (tokenTimedOut(process.env.REACT_KEYCLOAK_TOKEN_VALIDITY)) {
-    refreshToken();
+    await refreshToken();
   }
   igClient.token = sessionStorage.getItem('access_token');
   try {
@@ -79,39 +79,17 @@ export const sendMail = async (subject, message, request = igClient) => {
   }
 };
 
-/* export async function findUserDetails(store, id, request = igClient) {
-    try {
-        store.setState({ isLoading: true });
-        const userDetails = await request.getUserDetails(id);
-        if (userDetails) {
-            const status = "SUCCESS";
-            store.setState({ userDetails, status, isLoading: false });
-        }
-    } catch (error) {
-        const isError404 = error.response && error.response.status === 404;
-        const status = isError404 ? "NOT_FOUND" : "ERROR";
-        store.setState({ status, isLoading: false });
-    }
-} */
-
-/* export async function findUserFields(store, kcId, request = igClient) {
-    try {
-        // store.setState({ isLoading: true });
-        const fields = await request.getUserFields(kcId);
-        if (userDetails) {
-            const status = "SUCCESS";
-            // store.setState({ fields, status, isLoading: false });
-        }
-    } catch (error) {
-        const isError404 = error.response && error.response.status === 404;
-        const status = isError404 ? "NOT_FOUND" : "ERROR";
-        // store.setState({ status, isLoading: false });
-    }
-} */
+export const fetchUserDetails = async (kcId) => {
+  try {
+    return await igClient.getUserDetails(kcId);
+  } catch (error) {
+    console.error(error);
+  }
+};
 
-export async function fetchUserRequests(kcId) {
+export const fetchUserRequests = async (kcId) => {
   if (tokenTimedOut(process.env.REACT_KEYCLOAK_TOKEN_VALIDITY)) {
-    refreshToken();
+    await refreshToken();
   }
   igClient.token = sessionStorage.getItem('access_token');
   try {
@@ -122,11 +100,11 @@ export async function fetchUserRequests(kcId) {
   } catch (error) {
     console.error(error);
   }
-}
+};
 
-export async function createUserRequest(kcId, message) {
+export const createUserRequest = async (kcId, message) => {
   if (tokenTimedOut(process.env.REACT_KEYCLOAK_TOKEN_VALIDITY)) {
-    refreshToken();
+    await refreshToken();
   }
   igClient.token = sessionStorage.getItem('access_token');
   try {
@@ -134,11 +112,11 @@ export async function createUserRequest(kcId, message) {
   } catch (error) {
     console.error(error);
   }
-}
+};
 
-export async function deleteUserRequest(requestId) {
+export const deleteUserRequest = async (requestId) => {
   if (tokenTimedOut(process.env.REACT_KEYCLOAK_TOKEN_VALIDITY)) {
-    refreshToken();
+    await refreshToken();
   }
   igClient.token = sessionStorage.getItem('access_token');
   try {
@@ -146,11 +124,11 @@ export async function deleteUserRequest(requestId) {
   } catch (error) {
     console.error(error);
   }
-}
+};
 
-export async function addUserHistory(kcId, query, name, uiStructure, description) {
+export const addUserHistory = async (kcId, query, name, uiStructure, description) => {
   if (tokenTimedOut(process.env.REACT_KEYCLOAK_TOKEN_VALIDITY)) {
-    refreshToken();
+    await refreshToken();
   }
   igClient.token = sessionStorage.getItem('access_token');
   try {
@@ -159,11 +137,11 @@ export async function addUserHistory(kcId, query, name, uiStructure, description
   } catch (error) {
     console.error(error);
   }
-}
+};
 
-export async function fetchUserHistory(kcId) {
+export const fetchUserHistory = async (kcId) => {
   if (tokenTimedOut(process.env.REACT_KEYCLOAK_TOKEN_VALIDITY)) {
-    refreshToken();
+    await refreshToken();
   }
   igClient.token = sessionStorage.getItem('access_token');
   try {
@@ -174,11 +152,11 @@ export async function fetchUserHistory(kcId) {
   } catch (error) {
     console.error(error);
   }
-}
+};
 
-export async function deleteUserHistory(id) {
+export const deleteUserHistory = async (id) => {
   if (tokenTimedOut(process.env.REACT_KEYCLOAK_TOKEN_VALIDITY)) {
-    refreshToken();
+    await refreshToken();
   }
   igClient.token = sessionStorage.getItem('access_token');
   try {
@@ -186,4 +164,17 @@ export async function deleteUserHistory(id) {
   } catch (error) {
     console.error(error);
   }
-}
+};
+
+export const fetchUserFieldsDisplaySettings = async (userId) => {
+  if (tokenTimedOut(process.env.REACT_KEYCLOAK_TOKEN_VALIDITY)) {
+    await refreshToken();
+  }
+  igClient.token = sessionStorage.getItem('access_token');
+  try {
+    const row = await igClient.fetchUserFieldsDisplaySettings(userId);
+    return row.std_fields_ids;
+  } catch (error) {
+    console.error(error);
+  }
+};
diff --git a/src/components/Header/HeaderUserMenu.js b/src/components/Header/HeaderUserMenu.js
index 524faa1..ac71fa0 100644
--- a/src/components/Header/HeaderUserMenu.js
+++ b/src/components/Header/HeaderUserMenu.js
@@ -28,8 +28,8 @@ const HeaderUserMenu = () => {
 
   useEffect(() => {
     const loadUser = () => {
-      if (sessionStorage.getItem('user_id')) {
-        findOneUser(sessionStorage.getItem('user_id')).then((user) => {
+      if (sessionStorage.getItem('kcId')) {
+        findOneUser(sessionStorage.getItem('kcId')).then((user) => {
           setUser(user);
         });
       }
diff --git a/src/context/InSylvaGatekeeperClient.js b/src/context/InSylvaGatekeeperClient.js
index 51997da..e4a467a 100644
--- a/src/context/InSylvaGatekeeperClient.js
+++ b/src/context/InSylvaGatekeeperClient.js
@@ -75,7 +75,7 @@ class InSylvaGatekeeperClient {
 
   async getUserDetails(kcId) {
     const path = `/user/detail`;
-    return await this.post('GET', `${path}`, {
+    return await this.post('POST', `${path}`, {
       kcId,
     });
   }
@@ -104,6 +104,32 @@ class InSylvaGatekeeperClient {
       id,
     });
   }
+
+  // GET Fetches user fields display settings
+  async fetchUserFieldsDisplaySettings(userId) {
+    return await this.post('GET', `/user/fields-display-settings/${userId}`);
+  }
+
+  // POST Creates user fields display settings
+  async createUserFieldsDisplaySettings(userId, stdFieldsIds) {
+    return await this.post('POST', `/user/fields-display-settings/`, {
+      userId,
+      stdFieldsIds,
+    });
+  }
+
+  // PUT Updates user fields display settings
+  async updateUserFieldsDisplaySettings(userId, stdFieldsIds) {
+    return await this.post('PUT', `/user/fields-display-settings/`, {
+      userId,
+      stdFieldsIds,
+    });
+  }
+
+  // DELETE Remove user fields display settings
+  async deleteUserFieldsDisplaySettings(userId) {
+    return await this.post('DELETE', `/user/fields-display-settings/${userId}`);
+  }
 }
 
 InSylvaGatekeeperClient.prototype.baseUrl = null;
diff --git a/src/context/InSylvaKeycloakClient.js b/src/context/InSylvaKeycloakClient.js
index 5a20fd6..092f27c 100644
--- a/src/context/InSylvaKeycloakClient.js
+++ b/src/context/InSylvaKeycloakClient.js
@@ -22,7 +22,7 @@ class InSylvaKeycloakClient {
     });
     if (!response.ok) {
       await this.logout();
-      sessionStorage.removeItem('user_id');
+      sessionStorage.removeItem('kcId');
       sessionStorage.removeItem('access_token');
       sessionStorage.removeItem('refresh_token');
       window.location.replace(getLoginUrl() + '?requestType=search');
@@ -35,7 +35,6 @@ class InSylvaKeycloakClient {
   async refreshToken({
     realm = this.realm,
     client_id = this.client_id,
-    // client_secret : 'optional depending on the type of client',
     grant_type = 'refresh_token',
     refresh_token,
   }) {
diff --git a/src/context/UserContext.js b/src/context/UserContext.js
index d7578be..e0c90d7 100644
--- a/src/context/UserContext.js
+++ b/src/context/UserContext.js
@@ -2,6 +2,7 @@ import React, { createContext, useContext, useReducer } from 'react';
 import { InSylvaGatekeeperClient } from './InSylvaGatekeeperClient';
 import { InSylvaKeycloakClient } from './InSylvaKeycloakClient';
 import { getLoginUrl } from '../Utils';
+import { fetchUserDetails } from '../actions/user';
 
 const UserStateContext = createContext(null);
 const UserDispatchContext = createContext(null);
@@ -75,49 +76,51 @@ function useUserDispatch() {
   return context;
 }
 
-async function checkUserLogin(userId, accessToken, refreshToken) {
-  if (!!userId && !!accessToken && !!refreshToken) {
-    sessionStorage.setItem('user_id', userId);
-    sessionStorage.setItem('access_token', accessToken);
-    sessionStorage.setItem('refresh_token', refreshToken);
-    //To Do:
-    // Load the user histories from UserHistory(userId) endpoint
-    // Load the user result filters from Result_Filter(userId) endpoints
-    // Load the user policies from Policy(userId) endpoint
-    if (!sessionStorage.getItem('token_refresh_time')) {
-      sessionStorage.setItem('token_refresh_time', Date.now().toString());
-    }
-    // dispatch({ type: "USER_LOGGED_IN" });
-  } else {
-    // dispatch({ type: "USER_NOT_LOGGED_IN" });
+const getUserIdFromKcId = async (kcId) => {
+  if (!kcId) {
+    return;
   }
-}
+  const userDetails = await fetchUserDetails(kcId);
+  if (!userDetails) {
+    return;
+  }
+  return userDetails.id;
+};
+
+const checkUserLogin = async (kcId, accessToken, refreshToken) => {
+  if (!kcId || !accessToken || !refreshToken) {
+    return;
+  }
+  const userId = await getUserIdFromKcId(kcId);
+  sessionStorage.setItem('userId', userId.toString());
+  sessionStorage.setItem('kcId', kcId);
+  sessionStorage.setItem('access_token', accessToken);
+  sessionStorage.setItem('refresh_token', refreshToken);
+  if (!sessionStorage.getItem('token_refresh_time')) {
+    sessionStorage.setItem('token_refresh_time', Date.now().toString());
+  }
+};
 
 async function refreshToken() {
-  if (!!sessionStorage.getItem('user_id')) {
-    setTimeout(async () => {
-      const result = await ikcClient.refreshToken({
-        refresh_token: sessionStorage.getItem('refresh_token'),
-      });
-      if (result) {
-        sessionStorage.setItem('access_token', result.token.access_token);
-        sessionStorage.setItem('token_refresh_time', Date.now().toString());
-        // dispatch({ type: "USER_LOGGED_IN" });
-      } else {
-        // dispatch({ type: "LOGIN_FAILURE" });
-      }
-    }, 3000);
-  } else {
-    // dispatch({ type: "EXPIRED_SESSION" });
+  if (!sessionStorage.getItem('kcId')) {
+    return;
   }
+  setTimeout(async () => {
+    const result = await ikcClient.refreshToken({
+      refresh_token: sessionStorage.getItem('refresh_token'),
+    });
+    if (result) {
+      sessionStorage.setItem('access_token', result.token.access_token);
+      sessionStorage.setItem('token_refresh_time', Date.now().toString());
+    }
+  }, 3000);
 }
 
 async function signOut() {
   await ikcClient.logout();
-  sessionStorage.removeItem('user_id');
+  sessionStorage.removeItem('kcId');
   sessionStorage.removeItem('access_token');
   sessionStorage.removeItem('refresh_token');
-  // dispatch({ type: "SIGN_OUT_SUCCESS" });
   window.location.replace(getLoginUrl() + '?requestType=search');
 }
 
diff --git a/src/pages/profile/Profile.js b/src/pages/profile/Profile.js
index f41b256..824e888 100644
--- a/src/pages/profile/Profile.js
+++ b/src/pages/profile/Profile.js
@@ -34,11 +34,11 @@ const Profile = () => {
 
   useEffect(() => {
     const loadUser = () => {
-      if (sessionStorage.getItem('user_id')) {
-        findOneUser(sessionStorage.getItem('user_id')).then((user) => {
+      if (sessionStorage.getItem('kcId')) {
+        findOneUser(sessionStorage.getItem('kcId')).then((user) => {
           setUser(user);
         });
-        findOneUserWithGroupAndRole(sessionStorage.getItem('user_id')).then((result) => {
+        findOneUserWithGroupAndRole(sessionStorage.getItem('kcId')).then((result) => {
           const userGroupList = userGroups;
           result.forEach((user) => {
             if (user.groupname) {
@@ -90,7 +90,7 @@ const Profile = () => {
   };
 
   const getUserRequests = () => {
-    fetchUserRequests(sessionStorage.getItem('user_id')).then((result) => {
+    fetchUserRequests(sessionStorage.getItem('kcId')).then((result) => {
       setUserRequests([...result]);
     });
   };
diff --git a/src/pages/results/ResultsTableMUI.js b/src/pages/results/ResultsTableMUI.js
index 9427f0b..8592ed5 100644
--- a/src/pages/results/ResultsTableMUI.js
+++ b/src/pages/results/ResultsTableMUI.js
@@ -1,7 +1,9 @@
-import React, { useEffect, useMemo, useState } from 'react';
+import React, { useEffect, useState } from 'react';
 import { Trans, useTranslation } from 'react-i18next';
 import MUIDataTable from 'mui-datatables';
 import { createTheme, ThemeProvider } from '@mui/material';
+import { fetchPublicFields } from '../../actions/source';
+import { fetchUserFieldsDisplaySettings } from '../../actions/user';
 
 const getMuiTheme = () =>
   createTheme({
@@ -28,13 +30,63 @@ const ResultsTableMUI = ({
 }) => {
   const { t } = useTranslation('results');
   const [selectedRows, setSelectedRows] = useState([]);
+  const [publicFields, setPublicFields] = useState([]);
+  const [rows, setRows] = useState([]);
+  const [columns, setColumns] = useState([]);
+  const [isLoading, setIsLoading] = useState(true);
 
+  // On page load, check table rows from selected resources from map
   useEffect(() => {
     setSelectedRows(selectedRowsIds.map((id) => getRowIdFromResourceData(id)));
   }, [selectedRowsIds]);
 
+  // On search results change, fetch all public fields
+  useEffect(() => {
+    fetchPublicFields().then((publicFieldsResults) => {
+      setPublicFields(publicFieldsResults);
+    });
+  }, [searchResults]);
+
+  // Build table data (columns and rows)
+  useEffect(() => {
+    if (!searchResults && searchResults.length > 0) {
+      return;
+    }
+    getColumnFields().then((columnFields) => {
+      setColumns(buildColumns(columnFields));
+      setRows(buildRows(searchResults, columnFields));
+      setIsLoading(false);
+    });
+  }, [publicFields]);
+
+  // Get fields ids settings from user
+  const getStdFieldsIds = async () => {
+    const stdFieldsIds = await fetchUserFieldsDisplaySettings(
+      sessionStorage.getItem('userId')
+    );
+    if (!stdFieldsIds) {
+      const defaultFieldsIds = [1];
+      // TODO replace hard-coded array by gatekeeper fetch on default settings
+      return defaultFieldsIds;
+    }
+    return stdFieldsIds;
+  };
+
+  // Get fields data from ids
+  const getColumnFields = async () => {
+    const stdFieldsIds = await getStdFieldsIds();
+    return publicFields.filter((stdField) => {
+      return stdFieldsIds.includes(stdField.id);
+    });
+  };
+
+  // Returns value from JSON obj associated to key string.
+  const getValueByPath = (obj, path) => {
+    return path.split('.').reduce((acc, key) => acc && acc[key], obj);
+  };
+
   // Build each row in table from search results data
-  const buildRows = (results) => {
+  const buildRows = (results, columnFields) => {
     let dataRows = [];
     if (results.length === 0) {
       return dataRows;
@@ -43,26 +95,29 @@ const ResultsTableMUI = ({
       let row = {
         id: result.id,
       };
-      for (const fieldName in result.resource) {
-        if (typeof result.resource[fieldName] === 'string') {
-          row[fieldName] = result.resource[fieldName];
+      columnFields.forEach((columnField) => {
+        const fieldValue = getValueByPath(result, columnField.field_name);
+        if (typeof fieldValue === 'string') {
+          row[columnField.field_name] = fieldValue;
         }
-      }
+      });
       dataRows.push(row);
     });
     return dataRows;
   };
 
+  // Build column name string from obj key. Replaces dots by spaces.
   const buildColumnName = (name) => {
-    name = name.split('_').join(' ');
+    if (!name) {
+      return '';
+    }
+    name = name.split('.').join(' ');
     name = name.charAt(0).toUpperCase() + name.slice(1);
     return name;
   };
 
-  const buildColumns = (results) => {
-    if (!results[0]?.resource) {
-      return [];
-    }
+  // Build table columns names (label and name)
+  const buildColumns = (columnFields) => {
     let dataColumns = [];
     dataColumns.push({
       name: 'id',
@@ -71,38 +126,20 @@ const ResultsTableMUI = ({
         display: 'excluded',
       },
     });
-    // TODO: get columns settings from user to replace hard-coded array
-    for (const fieldName in results[0].resource) {
-      if (typeof results[0].resource[fieldName] === 'string') {
-        dataColumns.push({
-          name: fieldName,
-          label: buildColumnName(fieldName),
-          options: {
-            display: true,
-          },
-        });
-      }
-    }
+    columnFields.forEach((columnField) => {
+      dataColumns.push({
+        name: columnField.field_name,
+        label: buildColumnName(columnField.field_name),
+        options: {
+          display: true,
+        },
+      });
+    });
     return dataColumns;
   };
 
-  const buildResultsTable = (results) => {
-    if (!results && results.length > 0) {
-      return;
-    }
-    return {
-      rows: buildRows(results),
-      columns: buildColumns(results),
-    };
-  };
-
-  const { rows, columns } = useMemo(
-    () => buildResultsTable(searchResults),
-    [searchResults]
-  );
-
   const getResourceDataFromRowId = (id) => {
-    return searchResults.find((result) => result.id === id);
+    return searchResults.find((resource) => resource.id === id);
   };
 
   const getRowIdFromResourceData = (id) => {
@@ -113,6 +150,7 @@ const ResultsTableMUI = ({
     }
   };
 
+  // Add row to list of selected on checkbox click
   const onRowSelectionCallback = (selectedRow, allSelectedRows) => {
     setSelectedRowsIds(
       allSelectedRows.map((row) => {
@@ -192,12 +230,14 @@ const ResultsTableMUI = ({
 
   return (
     <ThemeProvider theme={getMuiTheme()}>
-      <MUIDataTable
-        title={<Trans i18nKey={'results:table.title'} components={{ searchQuery }} />}
-        data={rows}
-        columns={columns}
-        options={tableOptions}
-      />
+      {!isLoading && (
+        <MUIDataTable
+          title={<Trans i18nKey={'results:table.title'} components={{ searchQuery }} />}
+          data={rows}
+          columns={columns}
+          options={tableOptions}
+        />
+      )}
     </ThemeProvider>
   );
 };
diff --git a/src/pages/search/AdvancedSearch/AdvancedSearch.js b/src/pages/search/AdvancedSearch/AdvancedSearch.js
index 93c7473..14eeac8 100644
--- a/src/pages/search/AdvancedSearch/AdvancedSearch.js
+++ b/src/pages/search/AdvancedSearch/AdvancedSearch.js
@@ -138,7 +138,7 @@ const addHistory = (
   setUserHistory
 ) => {
   addUserHistory(
-    sessionStorage.getItem('user_id'),
+    sessionStorage.getItem('kcId'),
     search,
     searchName,
     searchFields,
@@ -149,7 +149,7 @@ const addHistory = (
 };
 
 const fetchHistory = (setUserHistory) => {
-  fetchUserHistory(sessionStorage.getItem('user_id')).then((result) => {
+  fetchUserHistory(sessionStorage.getItem('kcId')).then((result) => {
     if (result[0] && result[0].ui_structure) {
       result.forEach((item) => {
         item.ui_structure = JSON.parse(item.ui_structure);
@@ -326,7 +326,7 @@ const SearchBar = ({
   const onClickSaveSearch = () => {
     if (!!searchName) {
       addHistory(
-        sessionStorage.getItem('user_id'),
+        sessionStorage.getItem('kcId'),
         search,
         searchName,
         searchFields,
diff --git a/src/pages/search/Search.js b/src/pages/search/Search.js
index 6acc61e..61d1be4 100644
--- a/src/pages/search/Search.js
+++ b/src/pages/search/Search.js
@@ -31,31 +31,29 @@ const Search = () => {
         field.sources = [];
       });
       setStandardFields(resultStdFields);
-      fetchUserPolicyFields(sessionStorage.getItem('user_id')).then(
-        (resultPolicyFields) => {
-          const userFields = resultStdFields;
-          resultPolicyFields.forEach((polField) => {
-            const stdFieldIndex = userFields.findIndex(
-              (stdField) => stdField.id === polField.std_id
-            );
-            if (stdFieldIndex >= 0) {
-              if (!userFields[stdFieldIndex].sources.includes(polField.source_id))
-                userFields[stdFieldIndex].sources.push(polField.source_id);
-            } else {
-              const newField = {
-                id: polField.std_id,
-                sources: [polField.source_id],
-                ...polField,
-              };
-              userFields.push(newField);
-            }
-          });
-          userFields.sort((a, b) => (a.id > b.id ? 1 : b.id > a.id ? -1 : 0));
-          setStandardFields(removeNullFields(userFields));
-        }
-      );
+      fetchUserPolicyFields(sessionStorage.getItem('kcId')).then((resultPolicyFields) => {
+        const userFields = resultStdFields;
+        resultPolicyFields.forEach((polField) => {
+          const stdFieldIndex = userFields.findIndex(
+            (stdField) => stdField.id === polField.std_id
+          );
+          if (stdFieldIndex >= 0) {
+            if (!userFields[stdFieldIndex].sources.includes(polField.source_id))
+              userFields[stdFieldIndex].sources.push(polField.source_id);
+          } else {
+            const newField = {
+              id: polField.std_id,
+              sources: [polField.source_id],
+              ...polField,
+            };
+            userFields.push(newField);
+          }
+        });
+        userFields.sort((a, b) => (a.id > b.id ? 1 : b.id > a.id ? -1 : 0));
+        setStandardFields(removeNullFields(userFields));
+      });
     });
-    fetchSources(sessionStorage.getItem('user_id')).then((result) => {
+    fetchSources(sessionStorage.getItem('kcId')).then((result) => {
       setSources(result);
       setAvailableSources(result);
     });
diff --git a/src/store/index.js b/src/store/index.js
index 2861ee8..684ec59 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -12,5 +12,7 @@ const initialState = {
   fields: [],
   resources: [],
 };
+
 const store = useStore(React, initialState, actions);
+
 export default store;
-- 
GitLab


From 67570a6529c220ff011f8ce83e67b7b0fd157c1b Mon Sep 17 00:00:00 2001
From: rbisson <remi.bisson@inrae.fr>
Date: Mon, 2 Dec 2024 17:30:48 +0100
Subject: [PATCH 02/17] [ResultsTableMUI]: added a state variable to keep
 rowsPerPage number

---
 src/pages/results/ResultsTableMUI.js | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/pages/results/ResultsTableMUI.js b/src/pages/results/ResultsTableMUI.js
index 8592ed5..50d1241 100644
--- a/src/pages/results/ResultsTableMUI.js
+++ b/src/pages/results/ResultsTableMUI.js
@@ -34,6 +34,7 @@ const ResultsTableMUI = ({
   const [rows, setRows] = useState([]);
   const [columns, setColumns] = useState([]);
   const [isLoading, setIsLoading] = useState(true);
+  const [rowsPerPage, setRowsPerPage] = useState(15);
 
   // On page load, check table rows from selected resources from map
   useEffect(() => {
@@ -216,7 +217,10 @@ const ResultsTableMUI = ({
     selectableRows: 'multiple',
     selectableRowsOnClick: false,
     rowsSelected: selectedRows,
-    rowsPerPage: 15,
+    rowsPerPage: rowsPerPage,
+    onChangeRowsPerPage: (newRowsPerPage) => {
+      setRowsPerPage(newRowsPerPage);
+    },
     rowsPerPageOptions: [15, 30, 50, 100],
     jumpToPage: true,
     searchPlaceholder: t('results:table.search'),
-- 
GitLab


From cbd95f0df5286e32df7e70dd84cbee4214b170ff Mon Sep 17 00:00:00 2001
From: rbisson <remi.bisson@inrae.fr>
Date: Wed, 4 Dec 2024 17:48:06 +0100
Subject: [PATCH 03/17] [Profile] divided page with tabs
 [UserFieldsDisplaySettings] Added a working selectable component

---
 public/locales/en/profile.json                |   8 +
 public/locales/fr/profile.json                |   8 +
 src/actions/user.js                           |  24 ++
 src/components/Header/HeaderUserMenu.js       |   2 +-
 src/pages/profile/GroupAndRolesSettings.js    | 253 ++++++++++++++++++
 src/pages/profile/Profile.js                  | 252 ++---------------
 .../profile/UserFieldsDisplaySettings.js      | 141 ++++++++++
 7 files changed, 453 insertions(+), 235 deletions(-)
 create mode 100644 src/pages/profile/GroupAndRolesSettings.js
 create mode 100644 src/pages/profile/UserFieldsDisplaySettings.js

diff --git a/public/locales/en/profile.json b/public/locales/en/profile.json
index dd6afb3..adc897b 100644
--- a/public/locales/en/profile.json
+++ b/public/locales/en/profile.json
@@ -1,5 +1,13 @@
 {
   "pageTitle": "Profile management",
+  "tabs": {
+    "groups": "Handle groups and roles",
+    "fieldsDisplaySettings": "Search results display settings"
+  },
+  "fieldsDisplaySettings": {
+    "selectedOptionsLabel": "User display options for fields in results page",
+    "selectedOptionsNumber": "<strong>{selectedOptionsIds.length}</strong> fields selected"
+  },
   "groups": {
     "groupsList": "Group list",
     "groupName": "Name",
diff --git a/public/locales/fr/profile.json b/public/locales/fr/profile.json
index 826c160..a7d1fe6 100644
--- a/public/locales/fr/profile.json
+++ b/public/locales/fr/profile.json
@@ -1,5 +1,13 @@
 {
   "pageTitle": "Gestion du profil",
+  "tabs": {
+    "groups": "Gestion de vos groupes et rôles",
+    "fieldsDisplaySettings": "Paramétrage des résultats de recherche"
+  },
+  "fieldsDisplaySettings": {
+    "selectedOptionsLabel": "Options de paramétrage de la page d'affichage des résultats",
+    "selectedOptionsNumber": "Vous avez séléctionné <strong>{{count}}</strong> champs"
+  },
   "groups": {
     "groupsList": "Liste des groupes",
     "groupName": "Nom",
diff --git a/src/actions/user.js b/src/actions/user.js
index 4e50a27..3f064b9 100644
--- a/src/actions/user.js
+++ b/src/actions/user.js
@@ -178,3 +178,27 @@ export const fetchUserFieldsDisplaySettings = async (userId) => {
     console.error(error);
   }
 };
+
+export const createUserFieldsDisplaySettings = async (userId, stdFieldsIds) => {
+  if (tokenTimedOut(process.env.REACT_KEYCLOAK_TOKEN_VALIDITY)) {
+    await refreshToken();
+  }
+  igClient.token = sessionStorage.getItem('access_token');
+  try {
+    return await igClient.createUserFieldsDisplaySettings(userId, stdFieldsIds);
+  } catch (error) {
+    console.error(error);
+  }
+};
+
+export const updateUserFieldsDisplaySettings = async (userId, stdFieldsIds) => {
+  if (tokenTimedOut(process.env.REACT_KEYCLOAK_TOKEN_VALIDITY)) {
+    await refreshToken();
+  }
+  igClient.token = sessionStorage.getItem('access_token');
+  try {
+    return await igClient.updateUserFieldsDisplaySettings(userId, stdFieldsIds);
+  } catch (error) {
+    console.error(error);
+  }
+};
diff --git a/src/components/Header/HeaderUserMenu.js b/src/components/Header/HeaderUserMenu.js
index ac71fa0..1c749b3 100644
--- a/src/components/Header/HeaderUserMenu.js
+++ b/src/components/Header/HeaderUserMenu.js
@@ -67,7 +67,7 @@ const HeaderUserMenu = () => {
             <EuiFlexItem>
               <EuiFlexGroup justifyContent="spaceBetween">
                 <EuiFlexItem grow={false}>
-                  <NavLink to={'/profile'}>
+                  <NavLink to={'/profile'} onClick={() => closeMenu()}>
                     {t('header:userMenu.editProfileButton')}
                   </NavLink>
                 </EuiFlexItem>
diff --git a/src/pages/profile/GroupAndRolesSettings.js b/src/pages/profile/GroupAndRolesSettings.js
new file mode 100644
index 0000000..365a2b1
--- /dev/null
+++ b/src/pages/profile/GroupAndRolesSettings.js
@@ -0,0 +1,253 @@
+import React, { useState, useEffect } from 'react';
+import {
+  EuiTitle,
+  EuiSpacer,
+  EuiSelect,
+  EuiButton,
+  EuiFormRow,
+  EuiComboBox,
+  EuiBasicTable,
+} from '@elastic/eui';
+import {
+  findOneUser,
+  findOneUserWithGroupAndRole,
+  getGroups,
+  getRoles,
+  sendMail,
+  fetchUserRequests,
+  createUserRequest,
+  deleteUserRequest,
+} from '../../actions/user';
+import { useTranslation } from 'react-i18next';
+import styles from './styles';
+
+const GroupAndRolesSettings = () => {
+  const { t } = useTranslation(['profile', 'common', 'validation']);
+  const [user, setUser] = useState({});
+  const [userRole, setUserRole] = useState('');
+  const [groups, setGroups] = useState([]);
+  const [roles, setRoles] = useState([]);
+  const [userGroups, setUserGroups] = useState([]);
+  const [userRequests, setUserRequests] = useState([]);
+  const [selectedRole, setSelectedRole] = useState();
+  const [valueError, setValueError] = useState(undefined);
+
+  useEffect(() => {
+    const loadUser = () => {
+      if (sessionStorage.getItem('kcId')) {
+        findOneUser(sessionStorage.getItem('kcId')).then((user) => {
+          setUser(user);
+        });
+        findOneUserWithGroupAndRole(sessionStorage.getItem('kcId')).then((result) => {
+          const userGroupList = userGroups;
+          result.forEach((user) => {
+            if (user.groupname) {
+              userGroupList.push({
+                id: user.groupid,
+                label: user.groupname,
+                description: user.groupdescription,
+              });
+            }
+            setUserRole(user.rolename);
+          });
+          setUserGroups(userGroupList);
+        });
+      }
+    };
+    loadUser();
+    getUserRequests();
+    getUserGroups();
+    getUserRoles();
+  }, [userGroups]);
+
+  const groupColumns = [
+    { field: 'label', name: t('profile:groups.groupName'), width: '30%' },
+    { field: 'description', name: t('profile:groups.groupDescription') },
+  ];
+
+  const getUserRoles = () => {
+    getRoles().then((rolesResult) => {
+      const rolesArray = [];
+      rolesResult.forEach((role) => {
+        rolesArray.push({ id: role.id, text: role.name });
+      });
+      setRoles(rolesArray);
+    });
+  };
+
+  const getUserGroups = () => {
+    getGroups().then((groupsResult) => {
+      const groupsArray = [];
+      groupsResult.forEach((group) => {
+        groupsArray.push({
+          id: group.id,
+          label: group.name,
+          description: group.description,
+        });
+      });
+      setGroups(groupsArray);
+    });
+  };
+
+  const getUserRequests = () => {
+    fetchUserRequests(sessionStorage.getItem('kcId')).then((result) => {
+      setUserRequests([...result]);
+    });
+  };
+
+  const onDeleteRequest = async (request) => {
+    const request_id = request.id;
+    await deleteUserRequest(request_id);
+    getUserRequests();
+    getUserGroups();
+    getUserRoles();
+  };
+
+  const requestActions = [
+    {
+      name: t('common:validationActions.cancel'),
+      description: t('profile:requestsList.cancelRequest'),
+      icon: 'trash',
+      type: 'icon',
+      onClick: onDeleteRequest,
+    },
+  ];
+
+  const requestsColumns = [
+    {
+      field: 'request_message',
+      name: t('profile:requestsList.requestsMessage'),
+      width: '85%',
+    },
+    { field: 'is_processed', name: t('profile:requestsList.processed') },
+    { name: t('common:validationActions.cancel'), actions: requestActions },
+  ];
+
+  const getUserGroupLabels = () => {
+    let labelList = '';
+    if (!!userGroups) {
+      userGroups.forEach((group) => {
+        labelList = `${labelList} ${group.label},`;
+      });
+      if (labelList.endsWith(',')) {
+        labelList = labelList.substring(0, labelList.length - 1);
+      }
+    }
+    return labelList;
+  };
+
+  const onValueSearchChange = (value, hasMatchingOptions) => {
+    setValueError(
+      value.length === 0 || hasMatchingOptions
+        ? undefined
+        : `"${value}" is not a valid option`
+    );
+  };
+
+  const onSendRoleRequest = () => {
+    if (selectedRole) {
+      const message = `The user ${user.username} (${user.email}) has made a request to get the role : ${selectedRole}.`;
+      createUserRequest(user.id, message);
+      sendMail('User role request', message);
+      alert(t('validation:requestSent'));
+    }
+    getUserRequests();
+  };
+
+  const onSendGroupRequest = () => {
+    let groupList = [];
+    if (userGroups) {
+      userGroups.forEach((group) => {
+        groupList.push(group.label);
+      });
+      const message = `The user ${user.username} (${user.email}) has made a request to be part of these groups : ${groupList}.`;
+      createUserRequest(user.id, message);
+      sendMail('User group request', message);
+      alert(t('validation:requestSent'));
+    }
+    getUserRequests();
+  };
+
+  return (
+    <>
+      <EuiSpacer size="l" />
+      <EuiTitle size="s">
+        <h3>{t('profile:groups.groupsList')}</h3>
+      </EuiTitle>
+      <EuiFormRow fullWidth label="">
+        <EuiBasicTable items={groups} columns={groupColumns} />
+      </EuiFormRow>
+      <EuiSpacer size="l" />
+      <EuiTitle size="s">
+        <h3>{t('profile:requestsList.requestsList')}</h3>
+      </EuiTitle>
+      <EuiFormRow fullWidth label="">
+        <EuiBasicTable items={userRequests} columns={requestsColumns} />
+      </EuiFormRow>
+      <EuiSpacer size="l" />
+      <EuiTitle size="s">
+        <h3>{t('profile:groupRequests.requestGroupAssignment')}</h3>
+      </EuiTitle>
+      {getUserGroupLabels() ? (
+        <p
+          style={styles.currentRoleOrGroupText}
+        >{`${t('profile:groupRequests.currentGroups')} ${getUserGroupLabels()}`}</p>
+      ) : (
+        <p>{t('profile:groupRequests.noGroup')}</p>
+      )}
+      <EuiFormRow error={valueError} isInvalid={valueError !== undefined}>
+        <EuiComboBox
+          placeholder={t('profile:groupRequests.selectGroup')}
+          options={groups}
+          selectedOptions={userGroups}
+          onChange={(selectedOptions) => {
+            setValueError(undefined);
+            setUserGroups(selectedOptions);
+          }}
+          onSearchChange={onValueSearchChange}
+        />
+      </EuiFormRow>
+      <EuiSpacer size="m" />
+      <EuiButton
+        onClick={() => {
+          onSendGroupRequest();
+        }}
+        fill
+      >
+        {t('common:validationActions.send')}
+      </EuiButton>
+      <EuiSpacer size="l" />
+      <EuiTitle size="s">
+        <h3>{t('profile:roleRequests.requestRoleAssignment')}</h3>
+      </EuiTitle>
+      {userRole ? (
+        <p
+          style={styles.currentRoleOrGroupText}
+        >{`${t('profile:roleRequests.currentRole')} ${userRole}`}</p>
+      ) : (
+        <></>
+      )}
+      <EuiFormRow>
+        <EuiSelect
+          hasNoInitialSelection
+          options={roles}
+          value={selectedRole}
+          onChange={(e) => {
+            setSelectedRole(e.target.value);
+          }}
+        />
+      </EuiFormRow>
+      <EuiSpacer size="m" />
+      <EuiButton
+        onClick={() => {
+          onSendRoleRequest();
+        }}
+        fill
+      >
+        {t('common:validationActions.send')}
+      </EuiButton>
+    </>
+  );
+};
+
+export default GroupAndRolesSettings;
diff --git a/src/pages/profile/Profile.js b/src/pages/profile/Profile.js
index 824e888..95db876 100644
--- a/src/pages/profile/Profile.js
+++ b/src/pages/profile/Profile.js
@@ -1,254 +1,38 @@
-import React, { useState, useEffect } from 'react';
-import {
-  EuiTitle,
-  EuiSpacer,
-  EuiSelect,
-  EuiButton,
-  EuiFormRow,
-  EuiComboBox,
-  EuiBasicTable,
-} from '@elastic/eui';
-import {
-  findOneUser,
-  findOneUserWithGroupAndRole,
-  getGroups,
-  getRoles,
-  sendMail,
-  fetchUserRequests,
-  createUserRequest,
-  deleteUserRequest,
-} from '../../actions/user';
+import React, { useState } from 'react';
 import { useTranslation } from 'react-i18next';
-import styles from './styles';
+import { EuiTabbedContent, EuiTitle } from '@elastic/eui';
+import UserFieldsDisplaySettings from './UserFieldsDisplaySettings';
+import GroupAndRolesSettings from './GroupAndRolesSettings';
 
 const Profile = () => {
   const { t } = useTranslation(['profile', 'common', 'validation']);
-  const [user, setUser] = useState({});
-  const [userRole, setUserRole] = useState('');
-  const [groups, setGroups] = useState([]);
-  const [roles, setRoles] = useState([]);
-  const [userGroups, setUserGroups] = useState([]);
-  const [userRequests, setUserRequests] = useState([]);
-  const [selectedRole, setSelectedRole] = useState();
-  const [valueError, setValueError] = useState(undefined);
+  const [selectedTabNumber, setSelectedTabNumber] = useState(1);
 
-  useEffect(() => {
-    const loadUser = () => {
-      if (sessionStorage.getItem('kcId')) {
-        findOneUser(sessionStorage.getItem('kcId')).then((user) => {
-          setUser(user);
-        });
-        findOneUserWithGroupAndRole(sessionStorage.getItem('kcId')).then((result) => {
-          const userGroupList = userGroups;
-          result.forEach((user) => {
-            if (user.groupname) {
-              userGroupList.push({
-                id: user.groupid,
-                label: user.groupname,
-                description: user.groupdescription,
-              });
-            }
-            setUserRole(user.rolename);
-          });
-          setUserGroups(userGroupList);
-        });
-      }
-    };
-    loadUser();
-    getUserRequests();
-    getUserGroups();
-    getUserRoles();
-  }, [userGroups]);
-
-  const groupColumns = [
-    { field: 'label', name: t('profile:groups.groupName'), width: '30%' },
-    { field: 'description', name: t('profile:groups.groupDescription') },
-  ];
-
-  const getUserRoles = () => {
-    getRoles().then((rolesResult) => {
-      const rolesArray = [];
-      rolesResult.forEach((role) => {
-        rolesArray.push({ id: role.id, text: role.name });
-      });
-      setRoles(rolesArray);
-    });
-  };
-
-  const getUserGroups = () => {
-    getGroups().then((groupsResult) => {
-      const groupsArray = [];
-      groupsResult.forEach((group) => {
-        groupsArray.push({
-          id: group.id,
-          label: group.name,
-          description: group.description,
-        });
-      });
-      setGroups(groupsArray);
-    });
-  };
-
-  const getUserRequests = () => {
-    fetchUserRequests(sessionStorage.getItem('kcId')).then((result) => {
-      setUserRequests([...result]);
-    });
-  };
-
-  const onDeleteRequest = async (request) => {
-    const request_id = request.id;
-    await deleteUserRequest(request_id);
-    getUserRequests();
-    getUserGroups();
-    getUserRoles();
-  };
-
-  const requestActions = [
+  const tabsContent = [
     {
-      name: t('common:validationActions.cancel'),
-      description: t('profile:requestsList.cancelRequest'),
-      icon: 'trash',
-      type: 'icon',
-      onClick: onDeleteRequest,
+      id: 'tab1',
+      name: t('profile:tabs.groups'),
+      content: <GroupAndRolesSettings />,
     },
-  ];
-
-  const requestsColumns = [
     {
-      field: 'request_message',
-      name: t('profile:requestsList.requestsMessage'),
-      width: '85%',
+      id: 'tab2',
+      name: t('profile:tabs.fieldsDisplaySettings'),
+      content: <UserFieldsDisplaySettings />,
     },
-    { field: 'is_processed', name: t('profile:requestsList.processed') },
-    { name: t('common:validationActions.cancel'), actions: requestActions },
   ];
 
-  const getUserGroupLabels = () => {
-    let labelList = '';
-    if (!!userGroups) {
-      userGroups.forEach((group) => {
-        labelList = `${labelList} ${group.label},`;
-      });
-      if (labelList.endsWith(',')) {
-        labelList = labelList.substring(0, labelList.length - 1);
-      }
-    }
-    return labelList;
-  };
-
-  const onValueSearchChange = (value, hasMatchingOptions) => {
-    setValueError(
-      value.length === 0 || hasMatchingOptions
-        ? undefined
-        : `"${value}" is not a valid option`
-    );
-  };
-
-  const onSendRoleRequest = () => {
-    if (selectedRole) {
-      const message = `The user ${user.username} (${user.email}) has made a request to get the role : ${selectedRole}.`;
-      createUserRequest(user.id, message);
-      sendMail('User role request', message);
-      alert(t('validation:requestSent'));
-    }
-    getUserRequests();
-  };
-
-  const onSendGroupRequest = () => {
-    let groupList = [];
-    if (userGroups) {
-      userGroups.forEach((group) => {
-        groupList.push(group.label);
-      });
-      const message = `The user ${user.username} (${user.email}) has made a request to be part of these groups : ${groupList}.`;
-      createUserRequest(user.id, message);
-      sendMail('User group request', message);
-      alert(t('validation:requestSent'));
-    }
-    getUserRequests();
-  };
-
   return (
     <>
       <EuiTitle>
         <h2>{t('profile:pageTitle')}</h2>
       </EuiTitle>
-      <EuiSpacer size={'l'} />
-      <EuiTitle size="s">
-        <h3>{t('profile:groups.groupsList')}</h3>
-      </EuiTitle>
-      <EuiFormRow fullWidth label="">
-        <EuiBasicTable items={groups} columns={groupColumns} />
-      </EuiFormRow>
-      <EuiSpacer size="l" />
-      <EuiTitle size="s">
-        <h3>{t('profile:requestsList.requestsList')}</h3>
-      </EuiTitle>
-      <EuiFormRow fullWidth label="">
-        <EuiBasicTable items={userRequests} columns={requestsColumns} />
-      </EuiFormRow>
-      <EuiSpacer size="l" />
-      <EuiTitle size="s">
-        <h3>{t('profile:groupRequests.requestGroupAssignment')}</h3>
-      </EuiTitle>
-      {getUserGroupLabels() ? (
-        <p
-          style={styles.currentRoleOrGroupText}
-        >{`${t('profile:groupRequests.currentGroups')} ${getUserGroupLabels()}`}</p>
-      ) : (
-        <p>{t('profile:groupRequests.noGroup')}</p>
-      )}
-      <EuiFormRow error={valueError} isInvalid={valueError !== undefined}>
-        <EuiComboBox
-          placeholder={t('profile:groupRequests.selectGroup')}
-          options={groups}
-          selectedOptions={userGroups}
-          onChange={(selectedOptions) => {
-            setValueError(undefined);
-            setUserGroups(selectedOptions);
-          }}
-          onSearchChange={onValueSearchChange}
-        />
-      </EuiFormRow>
-      <EuiSpacer size="m" />
-      <EuiButton
-        onClick={() => {
-          onSendGroupRequest();
-        }}
-        fill
-      >
-        {t('common:validationActions.send')}
-      </EuiButton>
-      <EuiSpacer size="l" />
-      <EuiTitle size="s">
-        <h3>{t('profile:roleRequests.requestRoleAssignment')}</h3>
-      </EuiTitle>
-      {userRole ? (
-        <p
-          style={styles.currentRoleOrGroupText}
-        >{`${t('profile:roleRequests.currentRole')} ${userRole}`}</p>
-      ) : (
-        <></>
-      )}
-      <EuiFormRow>
-        <EuiSelect
-          hasNoInitialSelection
-          options={roles}
-          value={selectedRole}
-          onChange={(e) => {
-            setSelectedRole(e.target.value);
-          }}
-        />
-      </EuiFormRow>
-      <EuiSpacer size="m" />
-      <EuiButton
-        onClick={() => {
-          onSendRoleRequest();
+      <EuiTabbedContent
+        tabs={tabsContent}
+        selectedTab={tabsContent[selectedTabNumber]}
+        onTabClick={(tab) => {
+          setSelectedTabNumber(tabsContent.indexOf(tab));
         }}
-        fill
-      >
-        {t('common:validationActions.send')}
-      </EuiButton>
+      />
     </>
   );
 };
diff --git a/src/pages/profile/UserFieldsDisplaySettings.js b/src/pages/profile/UserFieldsDisplaySettings.js
new file mode 100644
index 0000000..8e58efd
--- /dev/null
+++ b/src/pages/profile/UserFieldsDisplaySettings.js
@@ -0,0 +1,141 @@
+import React, { useEffect, useState } from 'react';
+import {
+  EuiFlexGroup,
+  EuiSpacer,
+  EuiSelectable,
+  EuiFlexItem,
+  EuiButton,
+} from '@elastic/eui';
+import {
+  createUserFieldsDisplaySettings,
+  fetchUserFieldsDisplaySettings,
+  updateUserFieldsDisplaySettings,
+} from '../../actions/user';
+import { Trans, useTranslation } from 'react-i18next';
+import { fetchPublicFields } from '../../actions/source';
+
+const UserFieldsDisplaySettings = () => {
+  const { t } = useTranslation(['profile', 'common']);
+  const [isLoading, setIsLoading] = useState(true);
+  const [settingsOptions, setSettingsOptions] = useState([]);
+  const [selectedOptionsIds, setSelectedOptionsIds] = useState([]);
+  const [userSettings, setUserSettings] = useState(null);
+
+  const buildSettingsOptions = (publicFields) => {
+    const newSettingsOptions = [];
+    // TODO replace disabled for fields where cardinality is [1-n]
+    publicFields.forEach((field) => {
+      if (field && field.field_name) {
+        newSettingsOptions.push({
+          id: field.id,
+          label: field.field_name,
+          checked: selectedOptionsIds.includes(field.id) ? 'on' : undefined,
+          disabled: field.field_type === 'List',
+          toolTipContent: field.definition_and_comment,
+        });
+      }
+    });
+    return newSettingsOptions;
+  };
+
+  useEffect(() => {
+    // Fetch user settings or default ones if they have none.
+    fetchUserFieldsDisplaySettings(sessionStorage.getItem('userId')).then(
+      (userSettings) => {
+        if (userSettings) {
+          setUserSettings(userSettings);
+          setSelectedOptionsIds(userSettings);
+        } else {
+          // TODO fetch global default settings so they can be selected by default.
+          setSelectedOptionsIds([1]);
+        }
+      }
+    );
+  }, []);
+
+  useEffect(() => {
+    // Fetch public fields and build settings options
+    fetchPublicFields().then((publicFieldsResults) => {
+      if (publicFieldsResults) {
+        setSettingsOptions(buildSettingsOptions(publicFieldsResults));
+      }
+      setIsLoading(false);
+    });
+  }, [userSettings]);
+
+  // On each settingsOptions change, update selected ids accordingly
+  useEffect(() => {
+    const newSelectedOptionsIds = [];
+    settingsOptions.forEach((option) => {
+      if (option.checked === 'on') {
+        newSelectedOptionsIds.push(option.id);
+      }
+    });
+    setSelectedOptionsIds(newSelectedOptionsIds);
+  }, [settingsOptions]);
+
+  // On save settings button click, create or update user settings in database
+  const onSaveSettings = async () => {
+    if (!selectedOptionsIds || selectedOptionsIds.length === 0) {
+      // TODO add a toast to say "Select at least one option"
+      return;
+    }
+    setIsLoading(true);
+    if (userSettings) {
+      const result = await updateUserFieldsDisplaySettings(
+        sessionStorage.getItem('userId'),
+        selectedOptionsIds
+      );
+      console.log(result);
+    } else {
+      const result = await createUserFieldsDisplaySettings(
+        sessionStorage.getItem('userId'),
+        selectedOptionsIds
+      );
+      console.log(result);
+    }
+    // TODO add a validation toast
+    setIsLoading(false);
+  };
+
+  return (
+    !isLoading && (
+      <>
+        <EuiSpacer size={'l'} />
+        <EuiSelectable
+          style={{ height: '65vh' }}
+          aria-label={t('profile:fieldsDisplaySettings.selectedOptionsLabel')}
+          options={settingsOptions}
+          onChange={(newOptions) => setSettingsOptions(newOptions)}
+          searchable
+          isLoading={isLoading}
+          listProps={{ bordered: true }}
+          height={'full'}
+        >
+          {(list, search) => (
+            <>
+              {search}
+              <EuiSpacer size={'xs'} />
+              {list}
+            </>
+          )}
+        </EuiSelectable>
+        <EuiSpacer size={'l'} />
+        <Trans
+          i18nKey={'profile:fieldsDisplaySettings.selectedOptionsNumber'}
+          count={selectedOptionsIds.length}
+        />
+        <EuiSpacer size={'l'} />
+        <EuiButton
+          onClick={() => onSaveSettings()}
+          fill
+          disabled={selectedOptionsIds.length === 0}
+        >
+          {t('common:validationActions.save')}
+        </EuiButton>
+      </>
+    )
+  );
+};
+
+export default UserFieldsDisplaySettings;
-- 
GitLab


From 0ebc655b3d2f3967e82509bd04e6c29a7fc2e89f Mon Sep 17 00:00:00 2001
From: rbisson <remi.bisson@inrae.fr>
Date: Fri, 6 Dec 2024 12:01:35 +0100
Subject: [PATCH 04/17] [UserFieldsDisplaySettings] Added a toast on settings
 creation and update

---
 public/locales/en/profile.json                |  4 +-
 public/locales/fr/profile.json                |  4 +-
 src/pages/profile/Profile.js                  |  2 +-
 .../profile/UserFieldsDisplaySettings.js      | 50 ++++++++++++++-----
 src/pages/results/ResultsTable.js             |  1 -
 5 files changed, 44 insertions(+), 17 deletions(-)

diff --git a/public/locales/en/profile.json b/public/locales/en/profile.json
index adc897b..f56b254 100644
--- a/public/locales/en/profile.json
+++ b/public/locales/en/profile.json
@@ -6,7 +6,9 @@
   },
   "fieldsDisplaySettings": {
     "selectedOptionsLabel": "User display options for fields in results page",
-    "selectedOptionsNumber": "<strong>{selectedOptionsIds.length}</strong> fields selected"
+    "selectedOptionsNumber": "<strong>{selectedOptionsIds.length}</strong> fields selected",
+    "updatedSettingsSuccess": "Your settings have been updated.",
+    "updatedSettingsFailure": "An error occurred:"
   },
   "groups": {
     "groupsList": "Group list",
diff --git a/public/locales/fr/profile.json b/public/locales/fr/profile.json
index a7d1fe6..1362394 100644
--- a/public/locales/fr/profile.json
+++ b/public/locales/fr/profile.json
@@ -6,7 +6,9 @@
   },
   "fieldsDisplaySettings": {
     "selectedOptionsLabel": "Options de paramétrage de la page d'affichage des résultats",
-    "selectedOptionsNumber": "Vous avez séléctionné <strong>{{count}}</strong> champs"
+    "selectedOptionsNumber": "Vous avez séléctionné <strong>{{count}}</strong> champs",
+    "updatedSettingsSuccess": "Vos réglages ont été modifés.",
+    "updatedSettingsFailure": "Une erreur a eu lieu :"
   },
   "groups": {
     "groupsList": "Liste des groupes",
diff --git a/src/pages/profile/Profile.js b/src/pages/profile/Profile.js
index 95db876..b7d047e 100644
--- a/src/pages/profile/Profile.js
+++ b/src/pages/profile/Profile.js
@@ -6,7 +6,7 @@ import GroupAndRolesSettings from './GroupAndRolesSettings';
 
 const Profile = () => {
   const { t } = useTranslation(['profile', 'common', 'validation']);
-  const [selectedTabNumber, setSelectedTabNumber] = useState(1);
+  const [selectedTabNumber, setSelectedTabNumber] = useState(0);
 
   const tabsContent = [
     {
diff --git a/src/pages/profile/UserFieldsDisplaySettings.js b/src/pages/profile/UserFieldsDisplaySettings.js
index 8e58efd..5ff47fb 100644
--- a/src/pages/profile/UserFieldsDisplaySettings.js
+++ b/src/pages/profile/UserFieldsDisplaySettings.js
@@ -1,11 +1,5 @@
 import React, { useEffect, useState } from 'react';
-import {
-  EuiFlexGroup,
-  EuiSpacer,
-  EuiSelectable,
-  EuiFlexItem,
-  EuiButton,
-} from '@elastic/eui';
+import { EuiSpacer, EuiSelectable, EuiButton, EuiGlobalToastList } from '@elastic/eui';
 import {
   createUserFieldsDisplaySettings,
   fetchUserFieldsDisplaySettings,
@@ -14,13 +8,17 @@ import {
 import { Trans, useTranslation } from 'react-i18next';
 import { fetchPublicFields } from '../../actions/source';
 
+/*
+  User fields display settings are used to choose which fields are displayed in results table after a search.
+  If the user has no settings, the default are used. Default settings are the same for all users, chosen by admin at standard setup.
+ */
 const UserFieldsDisplaySettings = () => {
   const { t } = useTranslation(['profile', 'common']);
+  const [notificationToasts, setNotificationToasts] = useState([]);
   const [isLoading, setIsLoading] = useState(true);
   const [settingsOptions, setSettingsOptions] = useState([]);
   const [selectedOptionsIds, setSelectedOptionsIds] = useState([]);
   const [userSettings, setUserSettings] = useState(null);
-
   const buildSettingsOptions = (publicFields) => {
     const newSettingsOptions = [];
     // TODO replace disabled for fields where cardinality is [1-n]
@@ -81,20 +79,36 @@ const UserFieldsDisplaySettings = () => {
       return;
     }
     setIsLoading(true);
+    let result;
     if (userSettings) {
-      const result = await updateUserFieldsDisplaySettings(
+      result = await updateUserFieldsDisplaySettings(
         sessionStorage.getItem('userId'),
         selectedOptionsIds
       );
-      console.log(result);
     } else {
-      const result = await createUserFieldsDisplaySettings(
+      result = await createUserFieldsDisplaySettings(
         sessionStorage.getItem('userId'),
         selectedOptionsIds
       );
-      console.log(result);
     }
-    // TODO add a validation toast
+    if (result.error) {
+      setNotificationToasts([
+        {
+          id: '0',
+          title: t('profile:fieldsDisplaySettings.updatedSettingsFailure'),
+          text: result.error,
+          color: 'danger',
+        },
+      ]);
+    } else {
+      setNotificationToasts([
+        {
+          id: '0',
+          title: t('profile:fieldsDisplaySettings.updatedSettingsSuccess'),
+          color: 'success',
+        },
+      ]);
+    }
     setIsLoading(false);
   };
 
@@ -133,6 +147,16 @@ const UserFieldsDisplaySettings = () => {
         >
           {t('common:validationActions.save')}
         </EuiButton>
+        {/* TODO add a button to 'reset to default' (delete current user settings) */}
+        <EuiGlobalToastList
+          toasts={notificationToasts}
+          dismissToast={(removedToast) => {
+            setNotificationToasts(
+              notificationToasts.filter((toast) => toast.id !== removedToast.id)
+            );
+          }}
+          toastLifeTimeMs={3000}
+        />
       </>
     )
   );
diff --git a/src/pages/results/ResultsTable.js b/src/pages/results/ResultsTable.js
index f48df5c..9a7ceaa 100644
--- a/src/pages/results/ResultsTable.js
+++ b/src/pages/results/ResultsTable.js
@@ -70,7 +70,6 @@ const ResultsTable = ({ searchResults }) => {
     if (!results && results.length > 0) {
       return;
     }
-    // TODO: get columns settings from user to replace hard-coded array
     const userColumns = [
       'id',
       'resource.title',
-- 
GitLab


From 56bfc82a32b027236b31ca1140182f16733caaba Mon Sep 17 00:00:00 2001
From: rbisson <remi.bisson@inrae.fr>
Date: Fri, 6 Dec 2024 12:09:33 +0100
Subject: [PATCH 05/17] [UserFieldsDisplaySettings] now displaying fields names
 without dots and underscores

---
 src/Utils.js                                   | 12 ++++++++++++
 src/pages/profile/UserFieldsDisplaySettings.js |  3 ++-
 src/pages/results/ResultsTableMUI.js           | 13 ++-----------
 3 files changed, 16 insertions(+), 12 deletions(-)

diff --git a/src/Utils.js b/src/Utils.js
index 3c4f472..7042c6f 100644
--- a/src/Utils.js
+++ b/src/Utils.js
@@ -113,6 +113,18 @@ export const changeNameToLabel = (object) => {
   return object;
 };
 
+// Build field name string from object key.
+// Replaces dots and underscores by spaces and add an uppercase first letter.
+export const buildFieldName = (name) => {
+  if (!name) {
+    return '';
+  }
+  name = name.split('.').join(' ');
+  name = name.split('_').join(' ');
+  name = name.charAt(0).toUpperCase() + name.slice(1);
+  return name;
+};
+
 export const redirect = (url, condition = true) => {
   if (condition) {
     window.location.replace(url);
diff --git a/src/pages/profile/UserFieldsDisplaySettings.js b/src/pages/profile/UserFieldsDisplaySettings.js
index 5ff47fb..5a3a1c7 100644
--- a/src/pages/profile/UserFieldsDisplaySettings.js
+++ b/src/pages/profile/UserFieldsDisplaySettings.js
@@ -7,6 +7,7 @@ import {
 } from '../../actions/user';
 import { Trans, useTranslation } from 'react-i18next';
 import { fetchPublicFields } from '../../actions/source';
+import { buildFieldName } from '../../Utils';
 
 /*
   User fields display settings are used to choose which fields are displayed in results table after a search.
@@ -26,7 +27,7 @@ const UserFieldsDisplaySettings = () => {
       if (field && field.field_name) {
         newSettingsOptions.push({
           id: field.id,
-          label: field.field_name,
+          label: buildFieldName(field.field_name),
           checked: selectedOptionsIds.includes(field.id) ? 'on' : undefined,
           disabled: field.field_type === 'List',
           toolTipContent: field.definition_and_comment,
diff --git a/src/pages/results/ResultsTableMUI.js b/src/pages/results/ResultsTableMUI.js
index 50d1241..01e8b5d 100644
--- a/src/pages/results/ResultsTableMUI.js
+++ b/src/pages/results/ResultsTableMUI.js
@@ -4,6 +4,7 @@ import MUIDataTable from 'mui-datatables';
 import { createTheme, ThemeProvider } from '@mui/material';
 import { fetchPublicFields } from '../../actions/source';
 import { fetchUserFieldsDisplaySettings } from '../../actions/user';
+import { buildFieldName } from '../../Utils';
 
 const getMuiTheme = () =>
   createTheme({
@@ -107,16 +108,6 @@ const ResultsTableMUI = ({
     return dataRows;
   };
 
-  // Build column name string from obj key. Replaces dots by spaces.
-  const buildColumnName = (name) => {
-    if (!name) {
-      return '';
-    }
-    name = name.split('.').join(' ');
-    name = name.charAt(0).toUpperCase() + name.slice(1);
-    return name;
-  };
-
   // Build table columns names (label and name)
   const buildColumns = (columnFields) => {
     let dataColumns = [];
@@ -130,7 +121,7 @@ const ResultsTableMUI = ({
     columnFields.forEach((columnField) => {
       dataColumns.push({
         name: columnField.field_name,
-        label: buildColumnName(columnField.field_name),
+        label: buildFieldName(columnField.field_name),
         options: {
           display: true,
         },
-- 
GitLab


From a5376aadddf7024537ebf99db6fb1982cd7f959d Mon Sep 17 00:00:00 2001
From: rbisson <remi.bisson@inrae.fr>
Date: Tue, 10 Dec 2024 11:51:29 +0100
Subject: [PATCH 06/17] [Profile] added a new myProfile tab with link to edit
 tabs ; divided groups and roles tabs [Profile/UserFieldsDiplaySettings]
 improved logic and added reset selection and delete settings features

---
 public/locales/en/common.json                 |   5 +-
 public/locales/en/header.json                 |   2 +-
 public/locales/en/profile.json                |  24 +-
 public/locales/fr/common.json                 |   5 +-
 public/locales/fr/header.json                 |   2 +-
 public/locales/fr/profile.json                |  45 ++-
 src/actions/user.js                           |  16 +-
 .../BulletPointList/BulletPointList.js        |   8 +
 src/components/Header/HeaderUserMenu.js       |  21 +-
 src/context/InSylvaGatekeeperClient.js        |   5 +-
 src/context/UserContext.js                    |  11 +-
 src/pages/profile/GroupAndRolesSettings.js    | 253 --------------
 src/pages/profile/GroupSettings.js            | 137 ++++++++
 src/pages/profile/MyProfile.js                | 166 +++++++++
 src/pages/profile/Profile.js                  | 145 +++++++-
 src/pages/profile/RoleSettings.js             |  79 +++++
 .../profile/UserFieldsDisplaySettings.js      | 316 ++++++++++++------
 17 files changed, 820 insertions(+), 420 deletions(-)
 create mode 100644 src/components/BulletPointList/BulletPointList.js
 delete mode 100644 src/pages/profile/GroupAndRolesSettings.js
 create mode 100644 src/pages/profile/GroupSettings.js
 create mode 100644 src/pages/profile/MyProfile.js
 create mode 100644 src/pages/profile/RoleSettings.js

diff --git a/public/locales/en/common.json b/public/locales/en/common.json
index 7be8195..1810c43 100644
--- a/public/locales/en/common.json
+++ b/public/locales/en/common.json
@@ -8,7 +8,10 @@
     "cancel": "Cancel",
     "send": "Send",
     "save": "Save",
-    "validate": "Validate"
+    "validate": "Validate",
+    "reset": "Reset",
+    "yes": "Yes",
+    "no": "No"
   },
   "errorPage": {
     "title": "An error has occurred.",
diff --git a/public/locales/en/header.json b/public/locales/en/header.json
index 889a13d..4094312 100644
--- a/public/locales/en/header.json
+++ b/public/locales/en/header.json
@@ -5,7 +5,7 @@
   },
   "userMenu": {
     "title": "User profile",
-    "editProfileButton": "Edit profile",
+    "editProfileButton": "My profile",
     "logOutButton": "Log out"
   }
 }
diff --git a/public/locales/en/profile.json b/public/locales/en/profile.json
index f56b254..23bbccd 100644
--- a/public/locales/en/profile.json
+++ b/public/locales/en/profile.json
@@ -1,17 +1,26 @@
 {
-  "pageTitle": "Profile management",
   "tabs": {
-    "groups": "Handle groups and roles",
+    "profile": "My profile",
+    "groups": "My groups",
+    "role": "My role",
     "fieldsDisplaySettings": "Search results display settings"
   },
   "fieldsDisplaySettings": {
     "selectedOptionsLabel": "User display options for fields in results page",
-    "selectedOptionsNumber": "<strong>{selectedOptionsIds.length}</strong> fields selected",
+    "selectedOptionsNumber": "<strong>{selectedOptionsIds.length}</strong> fields selected:",
+    "deleteSettings": "Delete settings",
+    "noSettings": "You don't have any settings.",
+    "resetSelection": "Reset selection",
+    "selectionReset": "Selection reset.",
+    "selectionResetFailure": "Selection cannot be reset.",
     "updatedSettingsSuccess": "Your settings have been updated.",
-    "updatedSettingsFailure": "An error occurred:"
+    "updatedSettingsFailure": "An error occurred:",
+    "deleteSettingsSuccess": "Your settings have been deleted.",
+    "deleteSettingsSuccessDefault": "Default settings will be used.",
+    "deleteSettingsFailure": "An error occurred:"
   },
   "groups": {
-    "groupsList": "Group list",
+    "groupsList": "Existing groups",
     "groupName": "Name",
     "groupDescription": "Description"
   },
@@ -24,8 +33,9 @@
   "groupRequests": {
     "selectGroup": "Select group(s)",
     "requestGroupAssignment": "Request a group assignment",
-    "currentGroups": "You currently belong to (or have a pending request for) these groups:",
-    "noGroup": "You currently don't belong to any group."
+    "currentGroups": "You belong to (or have a pending request for) these groups:",
+    "noGroup": "You don't belong to any group.",
+    "invalidOption": "Invalid option."
   },
   "roleRequests": {
     "requestRoleAssignment": "Request an application role",
diff --git a/public/locales/fr/common.json b/public/locales/fr/common.json
index e77b65a..235d279 100644
--- a/public/locales/fr/common.json
+++ b/public/locales/fr/common.json
@@ -8,7 +8,10 @@
     "cancel": "Annuler",
     "send": "Envoyer",
     "save": "Sauvegarder",
-    "validate": "Valider"
+    "validate": "Valider",
+    "reset": "Réinitialiser",
+    "yes": "Oui",
+    "no": "Non"
   },
   "errorPage": {
     "title": "Une erreur est survenue.",
diff --git a/public/locales/fr/header.json b/public/locales/fr/header.json
index a5e28a5..23c3d26 100644
--- a/public/locales/fr/header.json
+++ b/public/locales/fr/header.json
@@ -5,7 +5,7 @@
   },
   "userMenu": {
     "title": "Profil utilisateur",
-    "editProfileButton": "Modifier mon profil",
+    "editProfileButton": "Mon profil",
     "logOutButton": "Déconnexion"
   }
 }
diff --git a/public/locales/fr/profile.json b/public/locales/fr/profile.json
index 1362394..c1f210a 100644
--- a/public/locales/fr/profile.json
+++ b/public/locales/fr/profile.json
@@ -1,17 +1,47 @@
 {
-  "pageTitle": "Gestion du profil",
   "tabs": {
-    "groups": "Gestion de vos groupes et rôles",
+    "profile": "Mon profil utilisateur",
+    "groups": "Mes groupes",
+    "role": "Mon role",
     "fieldsDisplaySettings": "Paramétrage des résultats de recherche"
   },
+  "myProfile": {
+    "groupPanel": {
+      "title": "Mes groupes :",
+      "edit": "Modifier mes groupes"
+    },
+    "rolePanel": {
+      "title": "Mon rôle :",
+      "edit": "Modifier mon rôle"
+    },
+    "requestsPanel": {
+      "title": "Mes requêtes en cours :"
+    },
+    "fieldsDisplaySettingsPanel": {
+      "title": "",
+      "edit": ""
+    },
+    "fieldsDownloadSettingsPanel": {
+      "title": "",
+      "edit": ""
+    }
+  },
   "fieldsDisplaySettings": {
     "selectedOptionsLabel": "Options de paramétrage de la page d'affichage des résultats",
-    "selectedOptionsNumber": "Vous avez séléctionné <strong>{{count}}</strong> champs",
+    "selectedOptionsNumber": "<strong>{{count}}</strong> champs sélectionnés :",
+    "deleteSettings": "Supprimer mes paramètres",
+    "noSettings": "Aucun paramètre actuellement.",
+    "resetSelection": "Réinitialiser la sélection",
+    "selectionReset": "Sélection réintialisée.",
+    "selectionResetFailure": "Votre sélection ne peut pas être réinitialisée.",
     "updatedSettingsSuccess": "Vos réglages ont été modifés.",
-    "updatedSettingsFailure": "Une erreur a eu lieu :"
+    "updatedSettingsFailure": "Une erreur a eu lieu :",
+    "deleteSettingsSuccess": "Vos réglages ont été supprimés.",
+    "deleteSettingsSuccessDefault": "Les réglages par défaut seront appliqués.",
+    "deleteSettingsFailure": "Une erreur a eu lieu :"
   },
   "groups": {
-    "groupsList": "Liste des groupes",
+    "groupsList": "Groupes existants",
     "groupName": "Nom",
     "groupDescription": "Description"
   },
@@ -24,8 +54,9 @@
   "groupRequests": {
     "selectGroup": "Selectionnez un groupe",
     "requestGroupAssignment": "Demander à faire parti d'un groupe",
-    "currentGroups": "Vous faites actuellement parti (ou avez une demande pour) de ces groupes :",
-    "noGroup": "Vous ne faites actuellement parti d'aucun groupe."
+    "currentGroups": "Vous faites parti de (ou avez une demande pour) ces groupes :",
+    "noGroup": "Vous ne faites parti d'aucun groupe.",
+    "invalidOption": "Option non valable."
   },
   "roleRequests": {
     "requestRoleAssignment": "Demander un rôle",
diff --git a/src/actions/user.js b/src/actions/user.js
index 3f064b9..7bc1ff4 100644
--- a/src/actions/user.js
+++ b/src/actions/user.js
@@ -22,13 +22,13 @@ export const findOneUser = async (id, request = igClient) => {
   }
 };
 
-export const findOneUserWithGroupAndRole = async (id, request = igClient) => {
+export const findOneUserWithGroupAndRole = async (kcId) => {
   if (tokenTimedOut(process.env.REACT_KEYCLOAK_TOKEN_VALIDITY)) {
     await refreshToken();
   }
   igClient.token = sessionStorage.getItem('access_token');
   try {
-    const user = await request.findOneUserWithGroupAndRole(id);
+    const user = await igClient.findOneUserWithGroupAndRole(kcId);
     if (user) {
       return user;
     }
@@ -202,3 +202,15 @@ export const updateUserFieldsDisplaySettings = async (userId, stdFieldsIds) => {
     console.error(error);
   }
 };
+
+export const deleteUserFieldsDisplaySettings = async (userId) => {
+  if (tokenTimedOut(process.env.REACT_KEYCLOAK_TOKEN_VALIDITY)) {
+    await refreshToken();
+  }
+  igClient.token = sessionStorage.getItem('access_token');
+  try {
+    return await igClient.deleteUserFieldsDisplaySettings(userId);
+  } catch (error) {
+    console.error(error);
+  }
+};
diff --git a/src/components/BulletPointList/BulletPointList.js b/src/components/BulletPointList/BulletPointList.js
new file mode 100644
index 0000000..24d52ce
--- /dev/null
+++ b/src/components/BulletPointList/BulletPointList.js
@@ -0,0 +1,8 @@
+import React from 'react';
+
+const BulletPointList = ({ children }) => {
+  // Have to style list manually because of display flex container
+  return <ul style={{ listStyleType: 'disc', marginLeft: '20px' }}>{children}</ul>;
+};
+
+export default BulletPointList;
diff --git a/src/components/Header/HeaderUserMenu.js b/src/components/Header/HeaderUserMenu.js
index 1c749b3..0b4a4a9 100644
--- a/src/components/Header/HeaderUserMenu.js
+++ b/src/components/Header/HeaderUserMenu.js
@@ -9,14 +9,13 @@ import {
   EuiButtonIcon,
 } from '@elastic/eui';
 import { signOut } from '../../context/UserContext';
-import { findOneUser } from '../../actions/user';
 import { useTranslation } from 'react-i18next';
 import { NavLink } from 'react-router-dom';
 
 const HeaderUserMenu = () => {
   const { t } = useTranslation('header');
   const [isOpen, setIsOpen] = useState(false);
-  const [user, setUser] = useState({});
+  const [username, setUsername] = useState('');
 
   const onMenuButtonClick = () => {
     setIsOpen(!isOpen);
@@ -27,15 +26,7 @@ const HeaderUserMenu = () => {
   };
 
   useEffect(() => {
-    const loadUser = () => {
-      if (sessionStorage.getItem('kcId')) {
-        findOneUser(sessionStorage.getItem('kcId')).then((user) => {
-          setUser(user);
-        });
-      }
-    };
-
-    loadUser();
+    setUsername(sessionStorage.getItem('username'));
   }, []);
 
   const HeaderUserButton = (
@@ -48,7 +39,7 @@ const HeaderUserMenu = () => {
     />
   );
 
-  return user.username ? (
+  return (
     <EuiPopover
       isOpen={isOpen}
       closePopover={closeMenu}
@@ -58,10 +49,10 @@ const HeaderUserMenu = () => {
     >
       <EuiFlexGroup gutterSize="m" responsive={false}>
         <EuiFlexItem grow={false}>
-          <EuiAvatar name={user.username} size="xl" />
+          <EuiAvatar name={username} size="xl" />
         </EuiFlexItem>
         <EuiFlexItem>
-          <EuiText>{user.username}</EuiText>
+          <EuiText>{username}</EuiText>
           <EuiSpacer size="m" />
           <EuiFlexGroup>
             <EuiFlexItem>
@@ -82,8 +73,6 @@ const HeaderUserMenu = () => {
         </EuiFlexItem>
       </EuiFlexGroup>
     </EuiPopover>
-  ) : (
-    <></>
   );
 };
 
diff --git a/src/context/InSylvaGatekeeperClient.js b/src/context/InSylvaGatekeeperClient.js
index e4a467a..7c78df2 100644
--- a/src/context/InSylvaGatekeeperClient.js
+++ b/src/context/InSylvaGatekeeperClient.js
@@ -66,10 +66,11 @@ class InSylvaGatekeeperClient {
     });
   }
 
-  async findOneUserWithGroupAndRole(id) {
+  // Returns an array containing objects for each user group.
+  async findOneUserWithGroupAndRole(kcId) {
     const path = `/user/one-with-groups-and-roles`;
     return await this.post('POST', `${path}`, {
-      id,
+      id: kcId,
     });
   }
 
diff --git a/src/context/UserContext.js b/src/context/UserContext.js
index e0c90d7..1b6d8b7 100644
--- a/src/context/UserContext.js
+++ b/src/context/UserContext.js
@@ -2,7 +2,7 @@ import React, { createContext, useContext, useReducer } from 'react';
 import { InSylvaGatekeeperClient } from './InSylvaGatekeeperClient';
 import { InSylvaKeycloakClient } from './InSylvaKeycloakClient';
 import { getLoginUrl } from '../Utils';
-import { fetchUserDetails } from '../actions/user';
+import { fetchUserDetails, findOneUser } from '../actions/user';
 
 const UserStateContext = createContext(null);
 const UserDispatchContext = createContext(null);
@@ -92,7 +92,14 @@ const checkUserLogin = async (kcId, accessToken, refreshToken) => {
     return;
   }
   const userId = await getUserIdFromKcId(kcId);
-  sessionStorage.setItem('userId', userId.toString());
+  if (userId) {
+    sessionStorage.setItem('userId', userId.toString());
+  }
+  const user = await findOneUser(kcId);
+  if (user) {
+    sessionStorage.setItem('username', user.username);
+    sessionStorage.setItem('email', user.email);
+  }
   sessionStorage.setItem('kcId', kcId);
   sessionStorage.setItem('access_token', accessToken);
   sessionStorage.setItem('refresh_token', refreshToken);
diff --git a/src/pages/profile/GroupAndRolesSettings.js b/src/pages/profile/GroupAndRolesSettings.js
deleted file mode 100644
index 365a2b1..0000000
--- a/src/pages/profile/GroupAndRolesSettings.js
+++ /dev/null
@@ -1,253 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import {
-  EuiTitle,
-  EuiSpacer,
-  EuiSelect,
-  EuiButton,
-  EuiFormRow,
-  EuiComboBox,
-  EuiBasicTable,
-} from '@elastic/eui';
-import {
-  findOneUser,
-  findOneUserWithGroupAndRole,
-  getGroups,
-  getRoles,
-  sendMail,
-  fetchUserRequests,
-  createUserRequest,
-  deleteUserRequest,
-} from '../../actions/user';
-import { useTranslation } from 'react-i18next';
-import styles from './styles';
-
-const GroupAndRolesSettings = () => {
-  const { t } = useTranslation(['profile', 'common', 'validation']);
-  const [user, setUser] = useState({});
-  const [userRole, setUserRole] = useState('');
-  const [groups, setGroups] = useState([]);
-  const [roles, setRoles] = useState([]);
-  const [userGroups, setUserGroups] = useState([]);
-  const [userRequests, setUserRequests] = useState([]);
-  const [selectedRole, setSelectedRole] = useState();
-  const [valueError, setValueError] = useState(undefined);
-
-  useEffect(() => {
-    const loadUser = () => {
-      if (sessionStorage.getItem('kcId')) {
-        findOneUser(sessionStorage.getItem('kcId')).then((user) => {
-          setUser(user);
-        });
-        findOneUserWithGroupAndRole(sessionStorage.getItem('kcId')).then((result) => {
-          const userGroupList = userGroups;
-          result.forEach((user) => {
-            if (user.groupname) {
-              userGroupList.push({
-                id: user.groupid,
-                label: user.groupname,
-                description: user.groupdescription,
-              });
-            }
-            setUserRole(user.rolename);
-          });
-          setUserGroups(userGroupList);
-        });
-      }
-    };
-    loadUser();
-    getUserRequests();
-    getUserGroups();
-    getUserRoles();
-  }, [userGroups]);
-
-  const groupColumns = [
-    { field: 'label', name: t('profile:groups.groupName'), width: '30%' },
-    { field: 'description', name: t('profile:groups.groupDescription') },
-  ];
-
-  const getUserRoles = () => {
-    getRoles().then((rolesResult) => {
-      const rolesArray = [];
-      rolesResult.forEach((role) => {
-        rolesArray.push({ id: role.id, text: role.name });
-      });
-      setRoles(rolesArray);
-    });
-  };
-
-  const getUserGroups = () => {
-    getGroups().then((groupsResult) => {
-      const groupsArray = [];
-      groupsResult.forEach((group) => {
-        groupsArray.push({
-          id: group.id,
-          label: group.name,
-          description: group.description,
-        });
-      });
-      setGroups(groupsArray);
-    });
-  };
-
-  const getUserRequests = () => {
-    fetchUserRequests(sessionStorage.getItem('kcId')).then((result) => {
-      setUserRequests([...result]);
-    });
-  };
-
-  const onDeleteRequest = async (request) => {
-    const request_id = request.id;
-    await deleteUserRequest(request_id);
-    getUserRequests();
-    getUserGroups();
-    getUserRoles();
-  };
-
-  const requestActions = [
-    {
-      name: t('common:validationActions.cancel'),
-      description: t('profile:requestsList.cancelRequest'),
-      icon: 'trash',
-      type: 'icon',
-      onClick: onDeleteRequest,
-    },
-  ];
-
-  const requestsColumns = [
-    {
-      field: 'request_message',
-      name: t('profile:requestsList.requestsMessage'),
-      width: '85%',
-    },
-    { field: 'is_processed', name: t('profile:requestsList.processed') },
-    { name: t('common:validationActions.cancel'), actions: requestActions },
-  ];
-
-  const getUserGroupLabels = () => {
-    let labelList = '';
-    if (!!userGroups) {
-      userGroups.forEach((group) => {
-        labelList = `${labelList} ${group.label},`;
-      });
-      if (labelList.endsWith(',')) {
-        labelList = labelList.substring(0, labelList.length - 1);
-      }
-    }
-    return labelList;
-  };
-
-  const onValueSearchChange = (value, hasMatchingOptions) => {
-    setValueError(
-      value.length === 0 || hasMatchingOptions
-        ? undefined
-        : `"${value}" is not a valid option`
-    );
-  };
-
-  const onSendRoleRequest = () => {
-    if (selectedRole) {
-      const message = `The user ${user.username} (${user.email}) has made a request to get the role : ${selectedRole}.`;
-      createUserRequest(user.id, message);
-      sendMail('User role request', message);
-      alert(t('validation:requestSent'));
-    }
-    getUserRequests();
-  };
-
-  const onSendGroupRequest = () => {
-    let groupList = [];
-    if (userGroups) {
-      userGroups.forEach((group) => {
-        groupList.push(group.label);
-      });
-      const message = `The user ${user.username} (${user.email}) has made a request to be part of these groups : ${groupList}.`;
-      createUserRequest(user.id, message);
-      sendMail('User group request', message);
-      alert(t('validation:requestSent'));
-    }
-    getUserRequests();
-  };
-
-  return (
-    <>
-      <EuiSpacer size="l" />
-      <EuiTitle size="s">
-        <h3>{t('profile:groups.groupsList')}</h3>
-      </EuiTitle>
-      <EuiFormRow fullWidth label="">
-        <EuiBasicTable items={groups} columns={groupColumns} />
-      </EuiFormRow>
-      <EuiSpacer size="l" />
-      <EuiTitle size="s">
-        <h3>{t('profile:requestsList.requestsList')}</h3>
-      </EuiTitle>
-      <EuiFormRow fullWidth label="">
-        <EuiBasicTable items={userRequests} columns={requestsColumns} />
-      </EuiFormRow>
-      <EuiSpacer size="l" />
-      <EuiTitle size="s">
-        <h3>{t('profile:groupRequests.requestGroupAssignment')}</h3>
-      </EuiTitle>
-      {getUserGroupLabels() ? (
-        <p
-          style={styles.currentRoleOrGroupText}
-        >{`${t('profile:groupRequests.currentGroups')} ${getUserGroupLabels()}`}</p>
-      ) : (
-        <p>{t('profile:groupRequests.noGroup')}</p>
-      )}
-      <EuiFormRow error={valueError} isInvalid={valueError !== undefined}>
-        <EuiComboBox
-          placeholder={t('profile:groupRequests.selectGroup')}
-          options={groups}
-          selectedOptions={userGroups}
-          onChange={(selectedOptions) => {
-            setValueError(undefined);
-            setUserGroups(selectedOptions);
-          }}
-          onSearchChange={onValueSearchChange}
-        />
-      </EuiFormRow>
-      <EuiSpacer size="m" />
-      <EuiButton
-        onClick={() => {
-          onSendGroupRequest();
-        }}
-        fill
-      >
-        {t('common:validationActions.send')}
-      </EuiButton>
-      <EuiSpacer size="l" />
-      <EuiTitle size="s">
-        <h3>{t('profile:roleRequests.requestRoleAssignment')}</h3>
-      </EuiTitle>
-      {userRole ? (
-        <p
-          style={styles.currentRoleOrGroupText}
-        >{`${t('profile:roleRequests.currentRole')} ${userRole}`}</p>
-      ) : (
-        <></>
-      )}
-      <EuiFormRow>
-        <EuiSelect
-          hasNoInitialSelection
-          options={roles}
-          value={selectedRole}
-          onChange={(e) => {
-            setSelectedRole(e.target.value);
-          }}
-        />
-      </EuiFormRow>
-      <EuiSpacer size="m" />
-      <EuiButton
-        onClick={() => {
-          onSendRoleRequest();
-        }}
-        fill
-      >
-        {t('common:validationActions.send')}
-      </EuiButton>
-    </>
-  );
-};
-
-export default GroupAndRolesSettings;
diff --git a/src/pages/profile/GroupSettings.js b/src/pages/profile/GroupSettings.js
new file mode 100644
index 0000000..d63764c
--- /dev/null
+++ b/src/pages/profile/GroupSettings.js
@@ -0,0 +1,137 @@
+import React, { useState, useEffect } from 'react';
+import {
+  EuiTitle,
+  EuiButton,
+  EuiFormRow,
+  EuiComboBox,
+  EuiBasicTable,
+  EuiFlexItem,
+  EuiPanel,
+  EuiFlexGroup,
+  EuiSpacer,
+} from '@elastic/eui';
+import { getGroups, sendMail, createUserRequest } from '../../actions/user';
+import { useTranslation } from 'react-i18next';
+import styles from './styles';
+
+const GroupSettings = ({ userGroups }) => {
+  const { t } = useTranslation(['profile', 'common', 'validation']);
+  const [groups, setGroups] = useState([]);
+  const [selectedUserGroups, setSelectedUserGroups] = useState([]);
+  const [valueError, setValueError] = useState(undefined);
+
+  useEffect(() => {
+    setSelectedUserGroups(userGroups);
+    getUserGroups();
+  }, []);
+
+  const groupColumns = [
+    { field: 'label', name: t('profile:groups.groupName'), width: '30%' },
+    { field: 'description', name: t('profile:groups.groupDescription') },
+  ];
+
+  const getUserGroups = () => {
+    getGroups().then((groupsResult) => {
+      const groupsArray = [];
+      groupsResult.forEach((group) => {
+        groupsArray.push({
+          id: group.id,
+          label: group.name,
+          description: group.description,
+        });
+      });
+      setGroups(groupsArray);
+    });
+  };
+
+  const getUserGroupLabels = (groups) => {
+    let labelList = '';
+    if (!groups || groups.length === 0) {
+      return labelList;
+    }
+    groups.forEach((group) => {
+      labelList = `${labelList} ${group.label},`;
+    });
+    if (labelList.endsWith(',')) {
+      labelList = labelList.substring(0, labelList.length - 1);
+    }
+    return labelList;
+  };
+
+  const onValueSearchChange = (value, hasMatchingOptions) => {
+    if (value.length !== 0 || !hasMatchingOptions) {
+      setValueError(t('profile:groupRequests.invalidOption'));
+    }
+  };
+
+  const onSendGroupRequest = async () => {
+    if (!selectedUserGroups || selectedUserGroups.length === 0) {
+      return;
+    }
+    const groupList = getUserGroupLabels(selectedUserGroups);
+    const message = `The user ${sessionStorage.getItem('username')} (${sessionStorage.getItem('email')}) has made a request to be part of these groups : ${groupList}.`;
+    await createUserRequest(sessionStorage.getItem('kcId'), message);
+    await sendMail('User group request', message);
+    // TODO replace alert by toasts
+    alert(t('validation:requestSent'));
+  };
+
+  return (
+    <EuiFlexGroup>
+      <EuiFlexItem>
+        <EuiPanel paddingSize="l" hasShadow={false} hasBorder={true}>
+          <EuiTitle size="s">
+            <h3>{t('profile:groups.groupsList')}</h3>
+          </EuiTitle>
+          <EuiSpacer size={'l'} />
+          <EuiBasicTable items={groups} columns={groupColumns} />
+        </EuiPanel>
+      </EuiFlexItem>
+
+      <EuiFlexItem>
+        <EuiPanel paddingSize="l" hasShadow={false} hasBorder={true}>
+          <EuiTitle size="s">
+            <h3>{t('profile:groupRequests.requestGroupAssignment')}</h3>
+          </EuiTitle>
+          <EuiSpacer size={'l'} />
+          {userGroups ? (
+            <p
+              style={styles.currentRoleOrGroupText}
+            >{`${t('profile:groupRequests.currentGroups')} ${getUserGroupLabels(userGroups)}`}</p>
+          ) : (
+            <p>{t('profile:groupRequests.noGroup')}</p>
+          )}
+          <EuiSpacer size={'l'} />
+          <EuiFormRow error={valueError} isInvalid={valueError !== undefined}>
+            <EuiComboBox
+              placeholder={t('profile:groupRequests.selectGroup')}
+              options={groups}
+              selectedOptions={selectedUserGroups}
+              onChange={(selectedOptions) => {
+                setValueError(undefined);
+                setSelectedUserGroups(selectedOptions);
+              }}
+              onSearchChange={onValueSearchChange}
+            />
+          </EuiFormRow>
+          <EuiSpacer size={'l'} />
+
+          <EuiFlexItem>
+            <div>
+              <EuiButton
+                onClick={() => {
+                  onSendGroupRequest();
+                }}
+                fill
+              >
+                {t('common:validationActions.send')}
+              </EuiButton>
+            </div>
+          </EuiFlexItem>
+        </EuiPanel>
+      </EuiFlexItem>
+    </EuiFlexGroup>
+  );
+};
+
+export default GroupSettings;
diff --git a/src/pages/profile/MyProfile.js b/src/pages/profile/MyProfile.js
new file mode 100644
index 0000000..c177b14
--- /dev/null
+++ b/src/pages/profile/MyProfile.js
@@ -0,0 +1,166 @@
+import { useTranslation } from 'react-i18next';
+import React from 'react';
+import {
+  EuiBasicTable,
+  EuiButtonEmpty,
+  EuiFlexGroup,
+  EuiFlexItem,
+  EuiPanel,
+  EuiSpacer,
+  EuiTitle,
+} from '@elastic/eui';
+import { deleteUserRequest } from '../../actions/user';
+import { buildFieldName } from '../../Utils';
+import BulletPointList from '../../components/BulletPointList/BulletPointList';
+
+const MyProfile = ({
+  setSelectedTabNumber,
+  userGroups,
+  userRole,
+  userRequests,
+  fieldsDisplaySettingsIds,
+  publicFields,
+  getUserRequests,
+}) => {
+  const { t } = useTranslation(['profile']);
+
+  const MyProfileCustomPanel = ({
+    title,
+    linkedTabNumber,
+    linkedTabButton,
+    children,
+  }) => {
+    return (
+      <EuiFlexItem>
+        <EuiPanel paddingSize="l" hasShadow={false} hasBorder={true}>
+          <EuiFlexGroup justifyContent={'spaceBetween'} alignItems={'center'}>
+            <EuiTitle size="xs">
+              <p>{title}</p>
+            </EuiTitle>
+            {linkedTabNumber && (
+              <EuiButtonEmpty
+                size="s"
+                onClick={() => setSelectedTabNumber(linkedTabNumber)}
+              >
+                {linkedTabButton}
+              </EuiButtonEmpty>
+            )}
+          </EuiFlexGroup>
+          <EuiSpacer size={'m'} />
+          <EuiFlexGroup direction={'column'}>{children}</EuiFlexGroup>
+        </EuiPanel>
+      </EuiFlexItem>
+    );
+  };
+
+  const onDeleteRequest = async (request) => {
+    await deleteUserRequest(request.id);
+    // Refresh requests table
+    getUserRequests();
+    // TODO add a completion toast
+  };
+
+  const requestActions = [
+    {
+      name: t('common:validationActions.cancel'),
+      description: t('profile:requestsList.cancelRequest'),
+      icon: 'trash',
+      type: 'icon',
+      onClick: onDeleteRequest,
+    },
+  ];
+
+  const requestsColumns = [
+    {
+      field: 'request_message',
+      name: t('profile:requestsList.requestsMessage'),
+      width: '85%',
+    },
+    {
+      field: 'is_processed',
+      name: t('profile:requestsList.processed'),
+      render: (isProcessed) => {
+        return (
+          <p>
+            {isProcessed
+              ? t('common:validationActions.yes')
+              : t('common:validationActions.no')}
+          </p>
+        );
+      },
+    },
+    { name: t('common:validationActions.cancel'), actions: requestActions },
+  ];
+
+  const GroupList = () => {
+    if (!userGroups || userGroups.length === 0) {
+      return <p>{t('profile:groupRequests.noGroup')}</p>;
+    }
+    const listItems = userGroups.map((group, index) => (
+      <li key={index}>{group.label}</li>
+    ));
+    return <BulletPointList>{listItems}</BulletPointList>;
+  };
+
+  const FieldsDisplaySettings = () => {
+    if (!fieldsDisplaySettingsIds || fieldsDisplaySettingsIds.length === 0) {
+      return <p>{t('profile:fieldsDisplaySettings.noSettings')}</p>;
+    }
+    const fieldsDisplaySettings = [];
+    publicFields.forEach((field) => {
+      if (fieldsDisplaySettingsIds.includes(field.id)) {
+        fieldsDisplaySettings.push(buildFieldName(field.field_name));
+      }
+    });
+    const listItems = fieldsDisplaySettings.map((fieldName, index) => (
+      <li key={index}>{fieldName}</li>
+    ));
+    // Have to style list manually because of display flex container
+    return <BulletPointList>{listItems}</BulletPointList>;
+  };
+
+  return (
+    <>
+      <EuiTitle>
+        <h3>{sessionStorage.getItem('username')}</h3>
+      </EuiTitle>
+      <EuiFlexGroup>
+        <MyProfileCustomPanel
+          title={t('profile:myProfile.groupPanel.title')}
+          linkedTabButton={t('profile:myProfile.groupPanel.edit')}
+          linkedTabNumber={1}
+        >
+          <GroupList />
+        </MyProfileCustomPanel>
+        <MyProfileCustomPanel
+          title={t('profile:myProfile.rolePanel.title')}
+          linkedTabButton={t('profile:myProfile.rolePanel.edit')}
+          linkedTabNumber={2}
+        >
+          <p>{userRole}</p>
+        </MyProfileCustomPanel>
+      </EuiFlexGroup>
+      <MyProfileCustomPanel title={t('profile:myProfile.requestsPanel.title')}>
+        <EuiBasicTable items={userRequests} columns={requestsColumns} />
+      </MyProfileCustomPanel>
+      <EuiFlexGroup>
+        <MyProfileCustomPanel
+          title={'Mes paramètres de visualisation des résultats:'}
+          linkedTabButton={'Modifier mes paramètres de visualisation'}
+          linkedTabNumber={3}
+        >
+          <FieldsDisplaySettings />
+        </MyProfileCustomPanel>
+        <MyProfileCustomPanel
+          title={"Mes paramètres d'export des résultats:"}
+          linkedTabButton={"Modifier mes paramètres d'export"}
+          linkedTabNumber={3}
+        >
+          <p>Fonctionnalité à venir prochainement.</p>
+        </MyProfileCustomPanel>
+      </EuiFlexGroup>
+    </>
+  );
+};
+
+export default MyProfile;
diff --git a/src/pages/profile/Profile.js b/src/pages/profile/Profile.js
index b7d047e..b64cbf7 100644
--- a/src/pages/profile/Profile.js
+++ b/src/pages/profile/Profile.js
@@ -1,39 +1,148 @@
-import React, { useState } from 'react';
+import React, { useEffect, useState } from 'react';
 import { useTranslation } from 'react-i18next';
-import { EuiTabbedContent, EuiTitle } from '@elastic/eui';
+import { EuiPanel, EuiTabbedContent, EuiFlexGroup } from '@elastic/eui';
 import UserFieldsDisplaySettings from './UserFieldsDisplaySettings';
-import GroupAndRolesSettings from './GroupAndRolesSettings';
+import GroupSettings from './GroupSettings';
+import RoleSettings from './RoleSettings';
+import MyProfile from './MyProfile';
+import {
+  fetchUserFieldsDisplaySettings,
+  fetchUserRequests,
+  findOneUserWithGroupAndRole,
+} from '../../actions/user';
+import { fetchPublicFields } from '../../actions/source';
 
 const Profile = () => {
   const { t } = useTranslation(['profile', 'common', 'validation']);
   const [selectedTabNumber, setSelectedTabNumber] = useState(0);
+  const [userGroups, setUserGroups] = useState([]);
+  const [userRole, setUserRole] = useState('');
+  const [userRequests, setUserRequests] = useState([]);
+  const [fieldsDisplaySettings, setFieldsDisplaySettings] = useState(null);
+  const [publicFields, setPublicFields] = useState([]);
+
+  useEffect(() => {
+    findOneUserWithGroupAndRole(sessionStorage.getItem('kcId')).then((result) => {
+      if (result) {
+        if (result[0]) {
+          setUserRole(result[0].rolename);
+        }
+        const userGroupList = [];
+        result.forEach((userGroup) => {
+          if (userGroup.groupname) {
+            userGroupList.push({
+              id: userGroup.groupid,
+              label: userGroup.groupname,
+              description: userGroup.groupdescription,
+            });
+          }
+        });
+        setUserGroups(userGroupList);
+      }
+    });
+    getUserRequests();
+    getUserFieldsDisplaySettings();
+    getPublicFields();
+  }, []);
+
+  const getUserRequests = () => {
+    fetchUserRequests(sessionStorage.getItem('kcId')).then((userRequests) => {
+      if (userRequests) {
+        setUserRequests([...userRequests]);
+      }
+    });
+  };
+
+  const getUserFieldsDisplaySettings = () => {
+    fetchUserFieldsDisplaySettings(sessionStorage.getItem('userId')).then(
+      (userSettings) => {
+        if (userSettings) {
+          setFieldsDisplaySettings(userSettings);
+        }
+      }
+    );
+  };
+
+  const getPublicFields = () => {
+    fetchPublicFields().then((publicFieldsResults) => {
+      if (publicFieldsResults) {
+        setPublicFields(publicFieldsResults);
+      }
+    });
+  };
+
+  const Tab = ({ children }) => {
+    return (
+      <EuiPanel
+        style={{
+          minHeight: '85vh',
+        }}
+        paddingSize="l"
+      >
+        <EuiFlexGroup direction={'column'}>{children}</EuiFlexGroup>
+      </EuiPanel>
+    );
+  };
 
   const tabsContent = [
     {
       id: 'tab1',
-      name: t('profile:tabs.groups'),
-      content: <GroupAndRolesSettings />,
+      name: t('profile:tabs.profile'),
+      content: (
+        <Tab>
+          <MyProfile
+            setSelectedTabNumber={setSelectedTabNumber}
+            userGroups={userGroups}
+            userRole={userRole}
+            userRequests={userRequests}
+            fieldsDisplaySettingsIds={fieldsDisplaySettings}
+            publicFields={publicFields}
+            getUserRequests={getUserRequests}
+          />
+        </Tab>
+      ),
     },
     {
       id: 'tab2',
+      name: t('profile:tabs.groups'),
+      content: (
+        <Tab>
+          <GroupSettings userGroups={userGroups} />
+        </Tab>
+      ),
+    },
+    {
+      id: 'tab3',
+      name: t('profile:tabs.role'),
+      content: (
+        <Tab>
+          <RoleSettings userRole={userRole} />
+        </Tab>
+      ),
+    },
+    {
+      id: 'tab4',
       name: t('profile:tabs.fieldsDisplaySettings'),
-      content: <UserFieldsDisplaySettings />,
+      content: (
+        <Tab>
+          <UserFieldsDisplaySettings
+            userSettings={fieldsDisplaySettings}
+            setUserSettings={setFieldsDisplaySettings}
+            publicFields={publicFields}
+          />
+        </Tab>
+      ),
     },
   ];
 
   return (
-    <>
-      <EuiTitle>
-        <h2>{t('profile:pageTitle')}</h2>
-      </EuiTitle>
-      <EuiTabbedContent
-        tabs={tabsContent}
-        selectedTab={tabsContent[selectedTabNumber]}
-        onTabClick={(tab) => {
-          setSelectedTabNumber(tabsContent.indexOf(tab));
-        }}
-      />
-    </>
+    <EuiTabbedContent
+      tabs={tabsContent}
+      selectedTab={tabsContent[selectedTabNumber]}
+      onTabClick={(tab) => {
+        setSelectedTabNumber(tabsContent.indexOf(tab));
+      }}
+    />
   );
 };
 
diff --git a/src/pages/profile/RoleSettings.js b/src/pages/profile/RoleSettings.js
new file mode 100644
index 0000000..0c24240
--- /dev/null
+++ b/src/pages/profile/RoleSettings.js
@@ -0,0 +1,79 @@
+import React, { useState, useEffect } from 'react';
+import {
+  EuiTitle,
+  EuiSelect,
+  EuiButton,
+  EuiFormRow,
+  EuiFlexItem,
+  EuiPanel,
+  EuiSpacer,
+  EuiFlexGroup,
+} from '@elastic/eui';
+import { getRoles, sendMail, createUserRequest } from '../../actions/user';
+import { useTranslation } from 'react-i18next';
+import styles from './styles';
+
+const RoleSettings = ({ userRole }) => {
+  const { t } = useTranslation(['profile', 'common', 'validation']);
+  const [roles, setRoles] = useState([]);
+  const [selectedRole, setSelectedRole] = useState(undefined);
+
+  useEffect(() => {
+    getUserRoles();
+  }, []);
+
+  const getUserRoles = () => {
+    getRoles().then((rolesResult) => {
+      const rolesArray = [];
+      rolesResult.forEach((role) => {
+        rolesArray.push({ id: role.id, text: role.name });
+      });
+      setRoles(rolesArray);
+    });
+  };
+
+  const onSendRoleRequest = async () => {
+    if (selectedRole) {
+      const message = `The user ${sessionStorage.getItem('username')} (${sessionStorage.getItem('email')}) has made a request to get the role : ${selectedRole}.`;
+      await createUserRequest(sessionStorage.getItem('kcId'), message);
+      await sendMail('User role request', message);
+      alert(t('validation:requestSent'));
+      // TODO replace alert by toasts
+    }
+  };
+
+  return (
+    <EuiFlexGroup>
+      <EuiFlexItem grow={false}>
+        <EuiPanel paddingSize="l" hasShadow={false} hasBorder={true}>
+          <EuiTitle size="s">
+            <p>{t('profile:roleRequests.requestRoleAssignment')}</p>
+          </EuiTitle>
+          <EuiSpacer size={'l'} />
+          {userRole && (
+            <p style={styles.currentRoleOrGroupText}>
+              {`${t('profile:roleRequests.currentRole')} ${userRole}`}
+            </p>
+          )}
+          <EuiSpacer size={'l'} />
+          <EuiFormRow>
+            <EuiSelect
+              hasNoInitialSelection
+              options={roles}
+              value={selectedRole}
+              onChange={(e) => {
+                setSelectedRole(e.target.value);
+              }}
+            />
+          </EuiFormRow>
+          <EuiSpacer size={'l'} />
+          <EuiButton onClick={() => onSendRoleRequest()} fill>
+            {t('common:validationActions.send')}
+          </EuiButton>
+        </EuiPanel>
+      </EuiFlexItem>
+    </EuiFlexGroup>
+  );
+};
+
+export default RoleSettings;
diff --git a/src/pages/profile/UserFieldsDisplaySettings.js b/src/pages/profile/UserFieldsDisplaySettings.js
index 5a3a1c7..4da78a0 100644
--- a/src/pages/profile/UserFieldsDisplaySettings.js
+++ b/src/pages/profile/UserFieldsDisplaySettings.js
@@ -1,85 +1,92 @@
 import React, { useEffect, useState } from 'react';
-import { EuiSpacer, EuiSelectable, EuiButton, EuiGlobalToastList } from '@elastic/eui';
+import {
+  EuiSpacer,
+  EuiSelectable,
+  EuiButton,
+  EuiGlobalToastList,
+  EuiFlexItem,
+  EuiFlexGroup,
+  EuiPanel,
+  EuiTitle,
+} from '@elastic/eui';
 import {
   createUserFieldsDisplaySettings,
-  fetchUserFieldsDisplaySettings,
+  deleteUserFieldsDisplaySettings,
   updateUserFieldsDisplaySettings,
 } from '../../actions/user';
 import { Trans, useTranslation } from 'react-i18next';
-import { fetchPublicFields } from '../../actions/source';
 import { buildFieldName } from '../../Utils';
+import BulletPointList from '../../components/BulletPointList/BulletPointList';
 
 /*
   User fields display settings are used to choose which fields are displayed in results table after a search.
   If the user has no settings, the default are used. Default settings are the same for all users, chosen by admin at standard setup.
  */
-const UserFieldsDisplaySettings = () => {
+const UserFieldsDisplaySettings = ({ userSettings, setUserSettings, publicFields }) => {
   const { t } = useTranslation(['profile', 'common']);
   const [notificationToasts, setNotificationToasts] = useState([]);
-  const [isLoading, setIsLoading] = useState(true);
   const [settingsOptions, setSettingsOptions] = useState([]);
   const [selectedOptionsIds, setSelectedOptionsIds] = useState([]);
-  const [userSettings, setUserSettings] = useState(null);
-  const buildSettingsOptions = (publicFields) => {
-    const newSettingsOptions = [];
-    // TODO replace disabled for fields where cardinality is [1-n]
-    publicFields.forEach((field) => {
-      if (field && field.field_name) {
-        newSettingsOptions.push({
-          id: field.id,
-          label: buildFieldName(field.field_name),
-          checked: selectedOptionsIds.includes(field.id) ? 'on' : undefined,
-          disabled: field.field_type === 'List',
-          toolTipContent: field.definition_and_comment,
-        });
-      }
-    });
-    return newSettingsOptions;
-  };
+  const [selectedOptions, setSelectedOptions] = useState([]);
 
   useEffect(() => {
-    // Fetch user settings or default ones if they have none.
-    fetchUserFieldsDisplaySettings(sessionStorage.getItem('userId')).then(
-      (userSettings) => {
-        if (userSettings) {
-          setUserSettings(userSettings);
-          setSelectedOptionsIds(userSettings);
-        } else {
-          // TODO fetch global default settings so they can be selected by default.
-          setSelectedOptionsIds([1]);
-        }
-      }
-    );
+    if (userSettings) {
+      setSelectedOptionsIds(userSettings);
+    } else {
+      // TODO fetch global default settings so they can be selected by default.
+      setSelectedOptionsIds([1]);
+    }
+    if (publicFields) {
+      setSettingsOptions(buildSettingsOptions());
+    }
   }, []);
 
-  useEffect(() => {
-    // Fetch public fields and build settings options
-    fetchPublicFields().then((publicFieldsResults) => {
-      if (publicFieldsResults) {
-        setSettingsOptions(buildSettingsOptions(publicFieldsResults));
-      }
-      setIsLoading(false);
-    });
-  }, [userSettings]);
-
-  // On each settingsOptions change, update selected ids accordingly
+  // On each settingsOptions change, update selected ids and names accordingly
   useEffect(() => {
     const newSelectedOptionsIds = [];
+    const newSelectedOptions = [];
     settingsOptions.forEach((option) => {
       if (option.checked === 'on') {
         newSelectedOptionsIds.push(option.id);
+        newSelectedOptions.push(option.label);
       }
     });
     setSelectedOptionsIds(newSelectedOptionsIds);
+    setSelectedOptions(newSelectedOptions);
   }, [settingsOptions]);
 
+  const isOptionChecked = (fieldId) => {
+    if (!userSettings) {
+      return undefined;
+    }
+    return userSettings.includes(fieldId) ? 'on' : undefined;
+  };
+
+  const buildSettingsOptions = () => {
+    if (!publicFields) {
+      return [];
+    }
+    const newSettingsOptions = [];
+    // TODO disabled attribute only to fields where cardinality is [1-n]
+    publicFields.forEach((field) => {
+      if (field && field.field_name) {
+        newSettingsOptions.push({
+          id: field.id,
+          label: buildFieldName(field.field_name),
+          checked: isOptionChecked(field.id),
+          disabled: field.field_type === 'List',
+          toolTipContent: field.definition_and_comment,
+        });
+      }
+    });
+    return newSettingsOptions;
+  };
+
   // On save settings button click, create or update user settings in database
   const onSaveSettings = async () => {
     if (!selectedOptionsIds || selectedOptionsIds.length === 0) {
-      // TODO add a toast to say "Select at least one option"
       return;
     }
-    setIsLoading(true);
     let result;
     if (userSettings) {
       result = await updateUserFieldsDisplaySettings(
@@ -92,74 +99,165 @@ const UserFieldsDisplaySettings = () => {
         selectedOptionsIds
       );
     }
+    setUserSettings(selectedOptionsIds);
+    let toast;
     if (result.error) {
-      setNotificationToasts([
-        {
-          id: '0',
-          title: t('profile:fieldsDisplaySettings.updatedSettingsFailure'),
-          text: result.error,
-          color: 'danger',
-        },
-      ]);
+      toast = {
+        id: `${notificationToasts.length + 1}`,
+        title: t('profile:fieldsDisplaySettings.updatedSettingsFailure'),
+        text: result.error,
+        color: 'danger',
+      };
     } else {
-      setNotificationToasts([
-        {
-          id: '0',
-          title: t('profile:fieldsDisplaySettings.updatedSettingsSuccess'),
-          color: 'success',
-        },
-      ]);
+      toast = {
+        id: `${notificationToasts.length + 1}`,
+        title: t('profile:fieldsDisplaySettings.updatedSettingsSuccess'),
+        color: 'success',
+      };
+    }
+    setNotificationToasts(notificationToasts.concat(toast));
+  };
+
+  // Reset selection to currently saved user settings.
+  const onSelectionReset = () => {
+    if (!settingsOptions || !userSettings) {
+      const toast = {
+        id: `${notificationToasts.length + 1}`,
+        title: t('profile:fieldsDisplaySettings.selectionResetFailure'),
+      };
+      setNotificationToasts(notificationToasts.concat(toast));
+      return;
     }
-    setIsLoading(false);
+    const newSettingsOptions = [];
+    settingsOptions.forEach((option) => {
+      option.checked = isOptionChecked(option.id);
+      newSettingsOptions.push(option);
+    });
+    setSettingsOptions(newSettingsOptions);
+    const toast = {
+      id: `${notificationToasts.length + 1}`,
+      title: t('profile:fieldsDisplaySettings.selectionReset'),
+    };
+    setNotificationToasts(notificationToasts.concat(toast));
+  };
+
+  // Reset user settings to system default.
+  // With current logic, no user settings means it should use default.
+  // So in this case 'reset' means delete current user settings.
+  const onSettingsReset = async () => {
+    // TODO add a confirmation modal ?
+    const result = await deleteUserFieldsDisplaySettings(
+      sessionStorage.getItem('userId')
+    );
+    let toast;
+    if (result.error) {
+      toast = {
+        id: `${notificationToasts.length + 1}`,
+        title: t('profile:fieldsDisplaySettings.deleteSettingsFailure'),
+        text: result.error,
+        color: 'danger',
+      };
+    } else {
+      setUserSettings(null);
+      toast = {
+        id: `${notificationToasts.length + 1}`,
+        title: t('profile:fieldsDisplaySettings.deleteSettingsSuccess'),
+        text: t('profile:fieldsDisplaySettings.deleteSettingsSuccessDefault'),
+        color: 'success',
+      };
+    }
+    setNotificationToasts(notificationToasts.concat(toast));
+  };
+
+  const SelectableSettingsPanel = () => {
+    return (
+      <EuiFlexItem>
+        <EuiPanel paddingSize="l" hasShadow={false} hasBorder={true}>
+          <EuiSelectable
+            aria-label={t('profile:fieldsDisplaySettings.selectedOptionsLabel')}
+            options={settingsOptions}
+            onChange={(newOptions) => setSettingsOptions(newOptions)}
+            searchable
+            listProps={{ bordered: true }}
+            style={{ minHeight: '70vh' }}
+            height={'full'}
+          >
+            {(list, search) => (
+              <>
+                {search}
+                <EuiSpacer size={'xs'} />
+                {list}
+              </>
+            )}
+          </EuiSelectable>
+        </EuiPanel>
+      </EuiFlexItem>
+    );
+  };
+
+  const SelectedSettingsPanel = () => {
+    const SelectedSettingsList = () => {
+      return (
+        <BulletPointList>
+          {selectedOptions.map((option, index) => {
+            return <li key={index}>{option}</li>;
+          })}
+        </BulletPointList>
+      );
+    };
+
+    return (
+      <EuiFlexItem>
+        <EuiPanel grow={false} paddingSize="l" hasShadow={false} hasBorder={true}>
+          <EuiTitle size={'s'}>
+            <p>
+              <Trans
+                i18nKey={'profile:fieldsDisplaySettings.selectedOptionsNumber'}
+                count={selectedOptionsIds.length}
+              />
+            </p>
+          </EuiTitle>
+          <EuiSpacer size={'l'} />
+          <SelectedSettingsList />
+          <EuiSpacer size={'l'} />
+          <EuiFlexGroup justifyContent={'spaceBetween'}>
+            <EuiButton
+              onClick={() => onSaveSettings()}
+              fill
+              disabled={selectedOptionsIds.length === 0}
+            >
+              {t('common:validationActions.save')}
+            </EuiButton>
+            <div>
+              <EuiFlexGroup>
+                <EuiButton onClick={() => onSelectionReset()}>
+                  {t('profile:fieldsDisplaySettings.resetSelection')}
+                </EuiButton>
+                <EuiButton color={'danger'} onClick={() => onSettingsReset()}>
+                  {t('profile:fieldsDisplaySettings.deleteSettings')}
+                </EuiButton>
+              </EuiFlexGroup>
+            </div>
+          </EuiFlexGroup>
+        </EuiPanel>
+      </EuiFlexItem>
+    );
   };
 
   return (
-    !isLoading && (
-      <>
-        <EuiSpacer size={'l'} />
-        <EuiSelectable
-          style={{ height: '65vh' }}
-          aria-label={t('profile:fieldsDisplaySettings.selectedOptionsLabel')}
-          options={settingsOptions}
-          onChange={(newOptions) => setSettingsOptions(newOptions)}
-          searchable
-          isLoading={isLoading}
-          listProps={{ bordered: true }}
-          height={'full'}
-        >
-          {(list, search) => (
-            <>
-              {search}
-              <EuiSpacer size={'xs'} />
-              {list}
-            </>
-          )}
-        </EuiSelectable>
-        <EuiSpacer size={'l'} />
-        <Trans
-          i18nKey={'profile:fieldsDisplaySettings.selectedOptionsNumber'}
-          count={selectedOptionsIds.length}
-        />
-        <EuiSpacer size={'l'} />
-        <EuiButton
-          onClick={() => onSaveSettings()}
-          fill
-          disabled={selectedOptionsIds.length === 0}
-        >
-          {t('common:validationActions.save')}
-        </EuiButton>
-        {/* TODO add a button to 'reset to default' (delete current user settings) */}
-        <EuiGlobalToastList
-          toasts={notificationToasts}
-          dismissToast={(removedToast) => {
-            setNotificationToasts(
-              notificationToasts.filter((toast) => toast.id !== removedToast.id)
-            );
-          }}
-          toastLifeTimeMs={3000}
-        />
-      </>
-    )
+    <EuiFlexGroup>
+      <SelectableSettingsPanel />
+      <SelectedSettingsPanel />
+      <EuiGlobalToastList
+        toasts={notificationToasts}
+        dismissToast={(removedToast) => {
+          setNotificationToasts(
+            notificationToasts.filter((toast) => toast.id !== removedToast.id)
+          );
+        }}
+        toastLifeTimeMs={3000}
+      />
+    </EuiFlexGroup>
   );
 };
 
-- 
GitLab


From e1f1c9517f6aceeb61029732f9165703debe8142 Mon Sep 17 00:00:00 2001
From: rbisson <remi.bisson@inrae.fr>
Date: Tue, 10 Dec 2024 15:49:25 +0100
Subject: [PATCH 07/17] [package.json] added react-toastify [Layout] added
 global toast container [*] replaced existing toasts

---
 package.json                                  |  1 +
 public/locales/en/profile.json                |  4 +-
 public/locales/en/search.json                 | 13 ++-
 public/locales/fr/profile.json                |  4 +-
 public/locales/fr/search.json                 | 13 ++-
 src/components/Layout/Layout.js               | 15 +++
 .../profile/UserFieldsDisplaySettings.js      | 61 +++---------
 .../search/AdvancedSearch/AdvancedSearch.js   | 98 +++++++------------
 8 files changed, 82 insertions(+), 127 deletions(-)

diff --git a/package.json b/package.json
index fc3084f..893d7db 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,7 @@
     "react-markdown": "^9.0.1",
     "react-router-dom": "^6.24.1",
     "react-scripts": "^5.0.1",
+    "react-toastify": "^10.0.6",
     "react-use-storage": "^0.5.1"
   },
   "scripts": {
diff --git a/public/locales/en/profile.json b/public/locales/en/profile.json
index 23bbccd..00a15ba 100644
--- a/public/locales/en/profile.json
+++ b/public/locales/en/profile.json
@@ -14,10 +14,10 @@
     "selectionReset": "Selection reset.",
     "selectionResetFailure": "Selection cannot be reset.",
     "updatedSettingsSuccess": "Your settings have been updated.",
-    "updatedSettingsFailure": "An error occurred:",
+    "updatedSettingsFailure": "Error:",
     "deleteSettingsSuccess": "Your settings have been deleted.",
     "deleteSettingsSuccessDefault": "Default settings will be used.",
-    "deleteSettingsFailure": "An error occurred:"
+    "deleteSettingsFailure": "Error:"
   },
   "groups": {
     "groupsList": "Existing groups",
diff --git a/public/locales/en/search.json b/public/locales/en/search.json
index 3ad7159..ebcd7b4 100644
--- a/public/locales/en/search.json
+++ b/public/locales/en/search.json
@@ -74,18 +74,17 @@
     "policyToast": {
       "title": "Private field selected",
       "content": [
-        "You selected a private field.",
         "Access to this field was granted for specific sources, which means that your search will be restricted to those.",
-        "Please check the sources list before searching."
+        "Please check sources before searching."
       ]
     },
     "editableQueryToast": {
-      "title": "Proceed with caution",
+      "title": "Warning",
       "content": {
-        "part1": "Manually editing can spoil query results. Syntax must be respected:",
-        "part2": "Fields and their values should be put between brackets: { } - Make sure every opened bracket is closed",
-        "part3": "\"AND\" and \"OR\" should be capitalized between fields and lowercase within a field expression",
-        "part4": "Make sure to check for typing errors"
+        "part1": "Manually editing can spoil query results.",
+        "part2": "Syntax must be respected:",
+        "part3": "- Fields and their values should be put between brackets: { }",
+        "part4": "- \"AND\" and \"OR\" should be capitalized between fields and lowercase within a field expression"
       }
     }
   }
diff --git a/public/locales/fr/profile.json b/public/locales/fr/profile.json
index c1f210a..36adc2f 100644
--- a/public/locales/fr/profile.json
+++ b/public/locales/fr/profile.json
@@ -35,10 +35,10 @@
     "selectionReset": "Sélection réintialisée.",
     "selectionResetFailure": "Votre sélection ne peut pas être réinitialisée.",
     "updatedSettingsSuccess": "Vos réglages ont été modifés.",
-    "updatedSettingsFailure": "Une erreur a eu lieu :",
+    "updatedSettingsFailure": "Erreur :",
     "deleteSettingsSuccess": "Vos réglages ont été supprimés.",
     "deleteSettingsSuccessDefault": "Les réglages par défaut seront appliqués.",
-    "deleteSettingsFailure": "Une erreur a eu lieu :"
+    "deleteSettingsFailure": "Erreur :"
   },
   "groups": {
     "groupsList": "Groupes existants",
diff --git a/public/locales/fr/search.json b/public/locales/fr/search.json
index 36458a6..57080e8 100644
--- a/public/locales/fr/search.json
+++ b/public/locales/fr/search.json
@@ -74,18 +74,17 @@
     "policyToast": {
       "title": "Champ privé sélectionné",
       "content": [
-        "Vous avez sélectionné un champ privé.",
         "L'accès à ce champ à été donné par certaines sources, ce qui veut dire que votre recherche va être limitée à celles-ci.",
-        "Veuillez prếter attention à la liste des sources avant de continuer."
+        "Vérifiez la liste de sources avant de continuer."
       ]
     },
     "editableQueryToast": {
-      "title": "Procéder avec prudence",
+      "title": "Attention",
       "content": {
-        "part1": "En éditant manuellement la recherche, vous pouvez facilement gâcher les résultats. Veuillez respecter la syntaxe :",
-        "part2": "Les champs et leurs valeurs doivent être comprises entre accolade: { } - Bien fermer toute accolade ouverte",
-        "part3": "\"AND\" et \"OR\" en majuscule entre les champs et en minuscule à l'intérieur d'une valeur de champ",
-        "part4": "Attention à corriger vos fautes de frappe"
+        "part1": "En éditant manuellement la recherche, il est facile de gâcher les résultats.",
+        "part2": "Veuillez respecter la syntaxe :",
+        "part3": "- Les champs et leurs valeurs doivent être comprises entre accolade: { }",
+        "part4": "- \"AND\" et \"OR\" en majuscule entre les champs et en minuscule à l'intérieur d'une valeur de champ"
       }
     }
   }
diff --git a/src/components/Layout/Layout.js b/src/components/Layout/Layout.js
index 40c6164..087c5ce 100644
--- a/src/components/Layout/Layout.js
+++ b/src/components/Layout/Layout.js
@@ -3,11 +3,26 @@ import { Outlet } from 'react-router-dom';
 import Header from '../../components/Header';
 import { EuiPage, EuiPageBody, EuiPageSection } from '@elastic/eui';
 import styles from './styles.js';
+import { Slide, ToastContainer } from 'react-toastify';
+import 'react-toastify/dist/ReactToastify.css';
 
 const Layout = () => {
   return (
     <EuiPage style={styles.page} restrictWidth={false}>
       <EuiPageBody>
+        <ToastContainer
+          position="bottom-right"
+          autoClose={5000}
+          hideProgressBar
+          newestOnTop={false}
+          closeOnClick
+          rtl={false}
+          pauseOnFocusLoss
+          draggable
+          pauseOnHover
+          theme="light"
+          transition={Slide}
+        />
         <Header />
         <EuiPageSection style={styles.pageContent} grow={true}>
           <Outlet />
diff --git a/src/pages/profile/UserFieldsDisplaySettings.js b/src/pages/profile/UserFieldsDisplaySettings.js
index 4da78a0..a53bc04 100644
--- a/src/pages/profile/UserFieldsDisplaySettings.js
+++ b/src/pages/profile/UserFieldsDisplaySettings.js
@@ -3,7 +3,6 @@ import {
   EuiSpacer,
   EuiSelectable,
   EuiButton,
-  EuiGlobalToastList,
   EuiFlexItem,
   EuiFlexGroup,
   EuiPanel,
@@ -17,6 +16,7 @@ import {
 import { Trans, useTranslation } from 'react-i18next';
 import { buildFieldName } from '../../Utils';
 import BulletPointList from '../../components/BulletPointList/BulletPointList';
+import { toast } from 'react-toastify';
 
 /*
   User fields display settings are used to choose which fields are displayed in results table after a search.
@@ -24,7 +24,6 @@ import BulletPointList from '../../components/BulletPointList/BulletPointList';
  */
 const UserFieldsDisplaySettings = ({ userSettings, setUserSettings, publicFields }) => {
   const { t } = useTranslation(['profile', 'common']);
-  const [notificationToasts, setNotificationToasts] = useState([]);
   const [settingsOptions, setSettingsOptions] = useState([]);
   const [selectedOptionsIds, setSelectedOptionsIds] = useState([]);
   const [selectedOptions, setSelectedOptions] = useState([]);
@@ -100,32 +99,19 @@ const UserFieldsDisplaySettings = ({ userSettings, setUserSettings, publicFields
       );
     }
     setUserSettings(selectedOptionsIds);
-    let toast;
     if (result.error) {
-      toast = {
-        id: `${notificationToasts.length + 1}`,
-        title: t('profile:fieldsDisplaySettings.updatedSettingsFailure'),
-        text: result.error,
-        color: 'danger',
-      };
+      toast.error(
+        `${t('profile:fieldsDisplaySettings.updatedSettingsFailure')}\n${result.error}`
+      );
     } else {
-      toast = {
-        id: `${notificationToasts.length + 1}`,
-        title: t('profile:fieldsDisplaySettings.updatedSettingsSuccess'),
-        color: 'success',
-      };
+      toast.success(t('profile:fieldsDisplaySettings.updatedSettingsSuccess'));
     }
-    setNotificationToasts(notificationToasts.concat(toast));
   };
 
   // Reset selection to currently saved user settings.
   const onSelectionReset = () => {
     if (!settingsOptions || !userSettings) {
-      const toast = {
-        id: `${notificationToasts.length + 1}`,
-        title: t('profile:fieldsDisplaySettings.selectionResetFailure'),
-      };
-      setNotificationToasts(notificationToasts.concat(toast));
+      toast.error(t('profile:fieldsDisplaySettings.selectionResetFailure'));
       return;
     }
     const newSettingsOptions = [];
@@ -134,11 +120,7 @@ const UserFieldsDisplaySettings = ({ userSettings, setUserSettings, publicFields
       newSettingsOptions.push(option);
     });
     setSettingsOptions(newSettingsOptions);
-    const toast = {
-      id: `${notificationToasts.length + 1}`,
-      title: t('profile:fieldsDisplaySettings.selectionReset'),
-    };
-    setNotificationToasts(notificationToasts.concat(toast));
+    toast(t('profile:fieldsDisplaySettings.selectionReset'));
   };
 
   // Reset user settings to system default.
@@ -149,24 +131,16 @@ const UserFieldsDisplaySettings = ({ userSettings, setUserSettings, publicFields
     const result = await deleteUserFieldsDisplaySettings(
       sessionStorage.getItem('userId')
     );
-    let toast;
     if (result.error) {
-      toast = {
-        id: `${notificationToasts.length + 1}`,
-        title: t('profile:fieldsDisplaySettings.deleteSettingsFailure'),
-        text: result.error,
-        color: 'danger',
-      };
+      toast.error(
+        `${t('profile:fieldsDisplaySettings.deleteSettingsFailure')}\n${result.error}`
+      );
     } else {
       setUserSettings(null);
-      toast = {
-        id: `${notificationToasts.length + 1}`,
-        title: t('profile:fieldsDisplaySettings.deleteSettingsSuccess'),
-        text: t('profile:fieldsDisplaySettings.deleteSettingsSuccessDefault'),
-        color: 'success',
-      };
+      toast.error(
+        `${t('profile:fieldsDisplaySettings.deleteSettingsSuccess')}\n${t('profile:fieldsDisplaySettings.deleteSettingsSuccessDefault')}`
+      );
     }
-    setNotificationToasts(notificationToasts.concat(toast));
   };
 
   const SelectableSettingsPanel = () => {
@@ -248,15 +222,6 @@ const UserFieldsDisplaySettings = ({ userSettings, setUserSettings, publicFields
     <EuiFlexGroup>
       <SelectableSettingsPanel />
       <SelectedSettingsPanel />
-      <EuiGlobalToastList
-        toasts={notificationToasts}
-        dismissToast={(removedToast) => {
-          setNotificationToasts(
-            notificationToasts.filter((toast) => toast.id !== removedToast.id)
-          );
-        }}
-        toastLifeTimeMs={3000}
-      />
     </EuiFlexGroup>
   );
 };
diff --git a/src/pages/search/AdvancedSearch/AdvancedSearch.js b/src/pages/search/AdvancedSearch/AdvancedSearch.js
index 14eeac8..44b14ae 100644
--- a/src/pages/search/AdvancedSearch/AdvancedSearch.js
+++ b/src/pages/search/AdvancedSearch/AdvancedSearch.js
@@ -8,7 +8,6 @@ import {
   EuiFlexItem,
   EuiForm,
   EuiFormRow,
-  EuiGlobalToastList,
   EuiHealth,
   EuiIcon,
   EuiModal,
@@ -48,6 +47,7 @@ import { useTranslation } from 'react-i18next';
 import styles from './styles.js';
 import moment from 'moment';
 import SearchModeSwitcher from '../SearchModeSwitcher';
+import { toast } from 'react-toastify';
 
 const updateSources = (
   searchFields,
@@ -274,7 +274,6 @@ const SearchBar = ({
   searchCount,
   setSearchCount,
   setFieldCount,
-  createEditableQueryToast,
 }) => {
   const { t } = useTranslation(['search', 'common']);
   const [isLoading, setIsLoading] = useState(false);
@@ -396,6 +395,26 @@ const SearchBar = ({
     );
   };
 
+  const EditableQueryToast = () => {
+    return (
+      <>
+        <EuiTitle>
+          <p>{t('search:advancedSearch.editableQueryToast.title')}</p>
+        </EuiTitle>
+        <EuiSpacer size={'s'} />
+        <p>{t('search:advancedSearch.editableQueryToast.content.part1')}</p>
+        <EuiSpacer size={'s'} />
+        <p>{t('search:advancedSearch.editableQueryToast.content.part2')}</p>
+        <EuiSpacer size={'s'} />
+        <ul>
+          <li>{t('search:advancedSearch.editableQueryToast.content.part3')}</li>
+          <EuiSpacer size={'s'} />
+          <li>{t('search:advancedSearch.editableQueryToast.content.part4')}</li>
+        </ul>
+      </>
+    );
+  };
+
   return (
     <>
       {isSaveSearchModalOpen && <SaveSearchModal />}
@@ -457,7 +476,7 @@ const SearchBar = ({
             onChange={() => {
               setReadOnlyQuery(!readOnlyQuery);
               if (readOnlyQuery) {
-                createEditableQueryToast();
+                toast.warning(<EditableQueryToast />, { autoClose: false });
               }
             }}
           />
@@ -606,7 +625,6 @@ const PopoverValueContent = ({
   setFieldCount,
   setIsPopoverValueOpen,
   selectedOperatorId,
-  createPolicyToast,
   selectedSources,
   setSelectedSources,
   availableSources,
@@ -623,6 +641,20 @@ const PopoverValueContent = ({
     }
   };
 
+  const PolicyToast = () => {
+    return (
+      <>
+        <EuiTitle size={'xs'}>
+          <p>{t('search:advancedSearch.policyToast.title')}</p>
+        </EuiTitle>
+        <EuiSpacer size={'s'} />
+        <p>{t('search:advancedSearch.policyToast.content.0')}</p>
+        <EuiSpacer size={'s'} />
+        <p>{t('search:advancedSearch.policyToast.content.1')}</p>
+      </>
+    );
+  };
+
   const validateFieldValues = () => {
     let fieldValues;
     if (Array.isArray(searchFields[index].values)) {
@@ -665,7 +697,7 @@ const PopoverValueContent = ({
       });
       setAvailableSources(filteredSources);
       setSelectedSources(filteredSources);
-      createPolicyToast();
+      toast.warning(<PolicyToast />, { autoClose: false });
     }
   };
 
@@ -1057,7 +1089,6 @@ const PopoverValueButton = ({
   fieldCount,
   setFieldCount,
   selectedOperatorId,
-  createPolicyToast,
   selectedSources,
   setSelectedSources,
   availableSources,
@@ -1096,7 +1127,6 @@ const PopoverValueButton = ({
           isPopoverValueOpen={isPopoverValueOpen}
           setIsPopoverValueOpen={setIsPopoverValueOpen}
           selectedOperatorId={selectedOperatorId}
-          createPolicyToast={createPolicyToast}
           selectedSources={selectedSources}
           setSelectedSources={setSelectedSources}
           availableSources={availableSources}
@@ -1121,7 +1151,6 @@ const FieldsPanel = ({
   selectedSources,
   setSelectedSources,
   sources,
-  createPolicyToast,
 }) => {
   const { t } = useTranslation('search');
 
@@ -1293,7 +1322,6 @@ const FieldsPanel = ({
                     fieldCount={fieldCount}
                     setFieldCount={setFieldCount}
                     selectedOperatorId={selectedOperatorId}
-                    createPolicyToast={createPolicyToast}
                     selectedSources={selectedSources}
                     setSelectedSources={setSelectedSources}
                     availableSources={availableSources}
@@ -1408,56 +1436,11 @@ const AdvancedSearch = ({
   setIsAdvancedSearch,
   isAdvancedSearch,
 }) => {
-  const { t } = useTranslation('search');
-  const [notificationToasts, setNotificationToasts] = useState([]);
   const [selectedOperatorId, setSelectedOperatorId] = useState('0');
   const [searchFields, setSearchFields] = useState([]);
   const [fieldCount, setFieldCount] = useState([]);
   const [searchCount, setSearchCount] = useState();
 
-  const createPolicyToast = () => {
-    const toast = {
-      title: t('search:advancedSearch.policyToast.title'),
-      color: 'warning',
-      iconType: 'alert',
-      toastLifeTimeMs: 15000,
-      text: (
-        <>
-          <p>{t('search:advancedSearch.policyToast.content.0')}</p>
-          <p>{t('search:advancedSearch.policyToast.content.1')}</p>
-          <p>{t('search:advancedSearch.policyToast.content.2')}</p>
-        </>
-      ),
-    };
-    setNotificationToasts(notificationToasts.concat(toast));
-  };
-
-  const createEditableQueryToast = () => {
-    const toast = {
-      title: t('search:advancedSearch.policyToast.title'),
-      color: 'warning',
-      iconType: 'alert',
-      toastLifeTimeMs: 15000,
-      text: (
-        <>
-          <p>{t('search:advancedSearch.editableQueryToast.content.part1')}</p>
-          <ul>
-            <li>{t('search:advancedSearch.editableQueryToast.content.part2')}</li>
-            <li>{t('search:advancedSearch.editableQueryToast.content.part3')}</li>
-            <li>{t('search:advancedSearch.editableQueryToast.content.part4')}</li>
-          </ul>
-        </>
-      ),
-    };
-    setNotificationToasts(notificationToasts.concat(toast));
-  };
-
-  const removeToast = (removedToast) => {
-    setNotificationToasts(
-      notificationToasts.filter((toast) => toast.id !== removedToast.id)
-    );
-  };
-
   return (
     <>
       <SearchModeSwitcher
@@ -1483,7 +1466,6 @@ const AdvancedSearch = ({
             searchCount={searchCount}
             setSearchCount={setSearchCount}
             setFieldCount={setFieldCount}
-            createEditableQueryToast={createEditableQueryToast}
           />
         </EuiFlexItem>
       </EuiFlexGroup>
@@ -1507,7 +1489,6 @@ const AdvancedSearch = ({
             selectedSources={selectedSources}
             setSelectedSources={setSelectedSources}
             sources={sources}
-            createPolicyToast={createPolicyToast}
           />
           <SearchOperatorSelection
             setSearch={setSearch}
@@ -1524,11 +1505,6 @@ const AdvancedSearch = ({
           />
         </EuiFlexItem>
       </EuiFlexGroup>
-      <EuiGlobalToastList
-        toasts={notificationToasts}
-        dismissToast={removeToast}
-        toastLifeTimeMs={2500}
-      />
     </>
   );
 };
-- 
GitLab


From 9aaaa50f633ec4170584eaf875bb6f1510ef85f7 Mon Sep 17 00:00:00 2001
From: rbisson <remi.bisson@inrae.fr>
Date: Wed, 11 Dec 2024 11:34:17 +0100
Subject: [PATCH 08/17] [ToastMessage] Created a general component for toasts
 with title and message [GroupSettings][RoleSettings] Replaced alert by toasts

---
 public/locales/en/profile.json                | 28 ++++++++++++++++---
 public/locales/en/validation.json             |  3 +-
 public/locales/fr/profile.json                | 15 +++++-----
 public/locales/fr/validation.json             |  3 +-
 src/actions/user.js                           |  4 +--
 src/components/ToastMessage/ToastMessage.js   | 16 +++++++++++
 src/pages/profile/GroupSettings.js            | 14 +++++++---
 src/pages/profile/MyProfile.js                | 22 +++++++++------
 src/pages/profile/Profile.js                  |  2 +-
 src/pages/profile/RoleSettings.js             | 13 +++++++--
 .../profile/UserFieldsDisplaySettings.js      | 18 ++++++------
 11 files changed, 97 insertions(+), 41 deletions(-)
 create mode 100644 src/components/ToastMessage/ToastMessage.js

diff --git a/public/locales/en/profile.json b/public/locales/en/profile.json
index 00a15ba..cdd782d 100644
--- a/public/locales/en/profile.json
+++ b/public/locales/en/profile.json
@@ -5,6 +5,27 @@
     "role": "My role",
     "fieldsDisplaySettings": "Search results display settings"
   },
+  "myProfile": {
+    "groupPanel": {
+      "title": "Groups:",
+      "edit": "Change my groups"
+    },
+    "rolePanel": {
+      "title": "Role",
+      "edit": "Change role"
+    },
+    "requestsPanel": {
+      "title": "My requests:"
+    },
+    "fieldsDisplaySettingsPanel": {
+      "title": "My search results display settings",
+      "edit": "Change my settings"
+    },
+    "fieldsDownloadSettingsPanel": {
+      "title": "My search results download settings",
+      "edit": "Change my settings"
+    }
+  },
   "fieldsDisplaySettings": {
     "selectedOptionsLabel": "User display options for fields in results page",
     "selectedOptionsNumber": "<strong>{selectedOptionsIds.length}</strong> fields selected:",
@@ -14,10 +35,8 @@
     "selectionReset": "Selection reset.",
     "selectionResetFailure": "Selection cannot be reset.",
     "updatedSettingsSuccess": "Your settings have been updated.",
-    "updatedSettingsFailure": "Error:",
     "deleteSettingsSuccess": "Your settings have been deleted.",
-    "deleteSettingsSuccessDefault": "Default settings will be used.",
-    "deleteSettingsFailure": "Error:"
+    "deleteSettingsSuccessDefault": "Default settings will be used."
   },
   "groups": {
     "groupsList": "Existing groups",
@@ -28,7 +47,8 @@
     "requestsList": "Requests list",
     "requestsMessage": "Message",
     "processed": "Processed",
-    "cancelRequest": "Cancel this request"
+    "cancelRequest": "Cancel this request",
+    "requestCanceled": "Request deleted"
   },
   "groupRequests": {
     "selectGroup": "Select group(s)",
diff --git a/public/locales/en/validation.json b/public/locales/en/validation.json
index b12bd6a..dea09f1 100644
--- a/public/locales/en/validation.json
+++ b/public/locales/en/validation.json
@@ -1,3 +1,4 @@
 {
-  "requestSent": "Your request has been sent to the administrators."
+  "requestSent": "Your request has been sent to the administrators.",
+  "error": "Error:"
 }
diff --git a/public/locales/fr/profile.json b/public/locales/fr/profile.json
index 36adc2f..5e23adf 100644
--- a/public/locales/fr/profile.json
+++ b/public/locales/fr/profile.json
@@ -18,12 +18,12 @@
       "title": "Mes requêtes en cours :"
     },
     "fieldsDisplaySettingsPanel": {
-      "title": "",
-      "edit": ""
+      "title": "Mes paramètres de visualisation des résultats :",
+      "edit": "Modifier mes paramètres de visualisation"
     },
     "fieldsDownloadSettingsPanel": {
-      "title": "",
-      "edit": ""
+      "title": "Mes paramètres d'export des résultats:",
+      "edit": "Modifier mes paramètres d'export"
     }
   },
   "fieldsDisplaySettings": {
@@ -35,10 +35,8 @@
     "selectionReset": "Sélection réintialisée.",
     "selectionResetFailure": "Votre sélection ne peut pas être réinitialisée.",
     "updatedSettingsSuccess": "Vos réglages ont été modifés.",
-    "updatedSettingsFailure": "Erreur :",
     "deleteSettingsSuccess": "Vos réglages ont été supprimés.",
-    "deleteSettingsSuccessDefault": "Les réglages par défaut seront appliqués.",
-    "deleteSettingsFailure": "Erreur :"
+    "deleteSettingsSuccessDefault": "Les réglages par défaut seront appliqués."
   },
   "groups": {
     "groupsList": "Groupes existants",
@@ -49,7 +47,8 @@
     "requestsList": "Liste des requêtes",
     "requestsMessage": "Message",
     "processed": "Traitée",
-    "cancelRequest": "Annuler cette requête"
+    "cancelRequest": "Annuler cette requête",
+    "requestCanceled": "Requête supprimée"
   },
   "groupRequests": {
     "selectGroup": "Selectionnez un groupe",
diff --git a/public/locales/fr/validation.json b/public/locales/fr/validation.json
index 0f29592..f762974 100644
--- a/public/locales/fr/validation.json
+++ b/public/locales/fr/validation.json
@@ -1,3 +1,4 @@
 {
-  "requestSent": "Votre requête à bien été envoyée."
+  "requestSent": "Votre requête à bien été envoyée.",
+  "error": "Erreur:"
 }
diff --git a/src/actions/user.js b/src/actions/user.js
index 7bc1ff4..6cfe2cf 100644
--- a/src/actions/user.js
+++ b/src/actions/user.js
@@ -108,7 +108,7 @@ export const createUserRequest = async (kcId, message) => {
   }
   igClient.token = sessionStorage.getItem('access_token');
   try {
-    await igClient.createUserRequest(kcId, message);
+    return await igClient.createUserRequest(kcId, message);
   } catch (error) {
     console.error(error);
   }
@@ -120,7 +120,7 @@ export const deleteUserRequest = async (requestId) => {
   }
   igClient.token = sessionStorage.getItem('access_token');
   try {
-    await igClient.deleteUserRequest(requestId);
+    return await igClient.deleteUserRequest(requestId);
   } catch (error) {
     console.error(error);
   }
diff --git a/src/components/ToastMessage/ToastMessage.js b/src/components/ToastMessage/ToastMessage.js
new file mode 100644
index 0000000..72e024d
--- /dev/null
+++ b/src/components/ToastMessage/ToastMessage.js
@@ -0,0 +1,16 @@
+import React from 'react';
+import { EuiTitle, EuiSpacer } from '@elastic/eui';
+
+const ToastMessage = ({ title, message }) => {
+  return (
+    <>
+      <EuiTitle size={'xs'}>
+        <p>{title}</p>
+      </EuiTitle>
+      <EuiSpacer size={'m'} />
+      <p>{message}</p>
+    </>
+  );
+};
+
+export default ToastMessage;
diff --git a/src/pages/profile/GroupSettings.js b/src/pages/profile/GroupSettings.js
index d63764c..1eeb85b 100644
--- a/src/pages/profile/GroupSettings.js
+++ b/src/pages/profile/GroupSettings.js
@@ -13,6 +13,8 @@ import {
 import { getGroups, sendMail, createUserRequest } from '../../actions/user';
 import { useTranslation } from 'react-i18next';
 import styles from './styles';
+import { toast } from 'react-toastify';
+import ToastMessage from '../../components/ToastMessage/ToastMessage';
 
 const GroupSettings = ({ userGroups }) => {
   const { t } = useTranslation(['profile', 'common', 'validation']);
@@ -59,7 +61,7 @@ const GroupSettings = ({ userGroups }) => {
   };
 
   const onValueSearchChange = (value, hasMatchingOptions) => {
-    if (value.length !== 0 || !hasMatchingOptions) {
+    if (value.length !== 0 && !hasMatchingOptions) {
       setValueError(t('profile:groupRequests.invalidOption'));
     }
   };
@@ -70,10 +72,13 @@ const GroupSettings = ({ userGroups }) => {
     }
     const groupList = getUserGroupLabels(selectedUserGroups);
     const message = `The user ${sessionStorage.getItem('username')} (${sessionStorage.getItem('email')}) has made a request to be part of these groups : ${groupList}.`;
-    await createUserRequest(sessionStorage.getItem('kcId'), message);
+    const result = await createUserRequest(sessionStorage.getItem('kcId'), message);
     await sendMail('User group request', message);
-    // TODO replace alert by toasts
-    alert(t('validation:requestSent'));
+    if (result.error) {
+      toast.error(<ToastMessage title={t('validation:error')} message={result.error} />);
+    } else {
+      toast.success(t('validation:requestSent'));
+    }
   };
 
   return (
@@ -119,6 +124,7 @@ const GroupSettings = ({ userGroups }) => {
           <EuiFlexItem>
             <div>
               <EuiButton
+                disabled={selectedUserGroups.length === 0}
                 onClick={() => {
                   onSendGroupRequest();
                 }}
diff --git a/src/pages/profile/MyProfile.js b/src/pages/profile/MyProfile.js
index c177b14..a0cb9a8 100644
--- a/src/pages/profile/MyProfile.js
+++ b/src/pages/profile/MyProfile.js
@@ -12,6 +12,8 @@ import {
 import { deleteUserRequest } from '../../actions/user';
 import { buildFieldName } from '../../Utils';
 import BulletPointList from '../../components/BulletPointList/BulletPointList';
+import { toast } from 'react-toastify';
+import ToastMessage from '../../components/ToastMessage/ToastMessage';
 
 const MyProfile = ({
   setSelectedTabNumber,
@@ -54,10 +56,14 @@ const MyProfile = ({
   };
 
   const onDeleteRequest = async (request) => {
-    await deleteUserRequest(request.id);
-    // Refresh requests table
-    getUserRequests();
-    // TODO add a completion toast
+    const result = await deleteUserRequest(request.id);
+    if (result.error) {
+      toast.error(<ToastMessage title={t('validation:error')} message={result.error} />);
+    } else {
+      toast.success(t('profile:requestsList.requestCanceled'));
+      // Refresh requests table
+      getUserRequests();
+    }
   };
 
   const requestActions = [
@@ -145,15 +151,15 @@ const MyProfile = ({
       </MyProfileCustomPanel>
       <EuiFlexGroup>
         <MyProfileCustomPanel
-          title={'Mes paramètres de visualisation des résultats:'}
-          linkedTabButton={'Modifier mes paramètres de visualisation'}
+          title={t('profile:myProfile.fieldsDisplaySettingsPanel.title')}
+          linkedTabButton={t('profile:myProfile.fieldsDisplaySettingsPanel.edit')}
           linkedTabNumber={3}
         >
           <FieldsDisplaySettings />
         </MyProfileCustomPanel>
         <MyProfileCustomPanel
-          title={"Mes paramètres d'export des résultats:"}
-          linkedTabButton={"Modifier mes paramètres d'export"}
+          title={t('profile:myProfile.fieldsDownloadSettingsPanel.title')}
+          linkedTabButton={t('profile:myProfile.fieldsDownloadSettingsPanel.edit')}
           linkedTabNumber={3}
         >
           <p>Fonctionnalité à venir prochainement.</p>
diff --git a/src/pages/profile/Profile.js b/src/pages/profile/Profile.js
index b64cbf7..636eda1 100644
--- a/src/pages/profile/Profile.js
+++ b/src/pages/profile/Profile.js
@@ -97,7 +97,7 @@ const Profile = () => {
             userRequests={userRequests}
             fieldsDisplaySettingsIds={fieldsDisplaySettings}
             publicFields={publicFields}
-            getUserRequests={getUserRequests}
+            getUserRequests={() => getUserRequests()}
           />
         </Tab>
       ),
diff --git a/src/pages/profile/RoleSettings.js b/src/pages/profile/RoleSettings.js
index 0c24240..dc91ad2 100644
--- a/src/pages/profile/RoleSettings.js
+++ b/src/pages/profile/RoleSettings.js
@@ -12,6 +12,8 @@ import {
 import { getRoles, sendMail, createUserRequest } from '../../actions/user';
 import { useTranslation } from 'react-i18next';
 import styles from './styles';
+import { toast } from 'react-toastify';
+import ToastMessage from '../../components/ToastMessage/ToastMessage';
 
 const RoleSettings = ({ userRole }) => {
   const { t } = useTranslation(['profile', 'common', 'validation']);
@@ -35,10 +37,15 @@ const RoleSettings = ({ userRole }) => {
   const onSendRoleRequest = async () => {
     if (selectedRole) {
       const message = `The user ${sessionStorage.getItem('username')} (${sessionStorage.getItem('email')}) has made a request to get the role : ${selectedRole}.`;
-      await createUserRequest(sessionStorage.getItem('kcId'), message);
+      const result = await createUserRequest(sessionStorage.getItem('kcId'), message);
       await sendMail('User role request', message);
-      alert(t('validation:requestSent'));
-      // TODO replace alert by toasts
+      if (result.error) {
+        toast.error(
+          <ToastMessage title={t('validation:error')} message={result.error} />
+        );
+      } else {
+        toast.success(t('validation:requestSent'));
+      }
     }
   };
 
diff --git a/src/pages/profile/UserFieldsDisplaySettings.js b/src/pages/profile/UserFieldsDisplaySettings.js
index a53bc04..4ae90c5 100644
--- a/src/pages/profile/UserFieldsDisplaySettings.js
+++ b/src/pages/profile/UserFieldsDisplaySettings.js
@@ -17,13 +17,14 @@ import { Trans, useTranslation } from 'react-i18next';
 import { buildFieldName } from '../../Utils';
 import BulletPointList from '../../components/BulletPointList/BulletPointList';
 import { toast } from 'react-toastify';
+import ToastMessage from '../../components/ToastMessage/ToastMessage';
 
 /*
   User fields display settings are used to choose which fields are displayed in results table after a search.
   If the user has no settings, the default are used. Default settings are the same for all users, chosen by admin at standard setup.
  */
 const UserFieldsDisplaySettings = ({ userSettings, setUserSettings, publicFields }) => {
-  const { t } = useTranslation(['profile', 'common']);
+  const { t } = useTranslation(['profile', 'common', 'validation']);
   const [settingsOptions, setSettingsOptions] = useState([]);
   const [selectedOptionsIds, setSelectedOptionsIds] = useState([]);
   const [selectedOptions, setSelectedOptions] = useState([]);
@@ -100,9 +101,7 @@ const UserFieldsDisplaySettings = ({ userSettings, setUserSettings, publicFields
     }
     setUserSettings(selectedOptionsIds);
     if (result.error) {
-      toast.error(
-        `${t('profile:fieldsDisplaySettings.updatedSettingsFailure')}\n${result.error}`
-      );
+      toast.error(<ToastMessage title={t('validation:error')} message={result.error} />);
     } else {
       toast.success(t('profile:fieldsDisplaySettings.updatedSettingsSuccess'));
     }
@@ -132,13 +131,14 @@ const UserFieldsDisplaySettings = ({ userSettings, setUserSettings, publicFields
       sessionStorage.getItem('userId')
     );
     if (result.error) {
-      toast.error(
-        `${t('profile:fieldsDisplaySettings.deleteSettingsFailure')}\n${result.error}`
-      );
+      toast.error(<ToastMessage title={t('validation:error')} message={result.error} />);
     } else {
       setUserSettings(null);
-      toast.error(
-        `${t('profile:fieldsDisplaySettings.deleteSettingsSuccess')}\n${t('profile:fieldsDisplaySettings.deleteSettingsSuccessDefault')}`
+      toast.success(
+        <ToastMessage
+          title={t('profile:fieldsDisplaySettings.deleteSettingsSuccess')}
+          message={t('profile:fieldsDisplaySettings.deleteSettingsSuccessDefault')}
+        />
       );
     }
   };
-- 
GitLab


From 5bb5ed356ec4a6905adc51d85ba308dd531e9906 Mon Sep 17 00:00:00 2001
From: rbisson <remi.bisson@inrae.fr>
Date: Wed, 11 Dec 2024 16:31:43 +0100
Subject: [PATCH 09/17] [locales] corrected profile titles

---
 public/locales/en/profile.json |  4 ++--
 public/locales/fr/profile.json | 12 ++++++------
 2 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/public/locales/en/profile.json b/public/locales/en/profile.json
index cdd782d..172f9b3 100644
--- a/public/locales/en/profile.json
+++ b/public/locales/en/profile.json
@@ -7,7 +7,7 @@
   },
   "myProfile": {
     "groupPanel": {
-      "title": "Groups:",
+      "title": "Groups",
       "edit": "Change my groups"
     },
     "rolePanel": {
@@ -15,7 +15,7 @@
       "edit": "Change role"
     },
     "requestsPanel": {
-      "title": "My requests:"
+      "title": "My pending requests:"
     },
     "fieldsDisplaySettingsPanel": {
       "title": "My search results display settings",
diff --git a/public/locales/fr/profile.json b/public/locales/fr/profile.json
index 5e23adf..3694fa2 100644
--- a/public/locales/fr/profile.json
+++ b/public/locales/fr/profile.json
@@ -7,28 +7,28 @@
   },
   "myProfile": {
     "groupPanel": {
-      "title": "Mes groupes :",
+      "title": "Mes groupes",
       "edit": "Modifier mes groupes"
     },
     "rolePanel": {
-      "title": "Mon rôle :",
+      "title": "Mon rôle",
       "edit": "Modifier mon rôle"
     },
     "requestsPanel": {
-      "title": "Mes requêtes en cours :"
+      "title": "Mes requêtes en cours"
     },
     "fieldsDisplaySettingsPanel": {
-      "title": "Mes paramètres de visualisation des résultats :",
+      "title": "Mes paramètres de visualisation des résultats",
       "edit": "Modifier mes paramètres de visualisation"
     },
     "fieldsDownloadSettingsPanel": {
-      "title": "Mes paramètres d'export des résultats:",
+      "title": "Mes paramètres d'export des résultats",
       "edit": "Modifier mes paramètres d'export"
     }
   },
   "fieldsDisplaySettings": {
     "selectedOptionsLabel": "Options de paramétrage de la page d'affichage des résultats",
-    "selectedOptionsNumber": "<strong>{{count}}</strong> champs sélectionnés :",
+    "selectedOptionsNumber": "<strong>{{count}}</strong> champs sélectionnés",
     "deleteSettings": "Supprimer mes paramètres",
     "noSettings": "Aucun paramètre actuellement.",
     "resetSelection": "Réinitialiser la sélection",
-- 
GitLab


From c590e2ab1f1de381e5a73ec7249574bc2b8d70be Mon Sep 17 00:00:00 2001
From: rbisson <remi.bisson@inrae.fr>
Date: Thu, 12 Dec 2024 11:34:59 +0100
Subject: [PATCH 10/17] [MyProfile] replaced empty table by a warning message
 when user has no requests

---
 public/locales/en/profile.json | 3 ++-
 public/locales/fr/profile.json | 5 +++--
 src/pages/profile/MyProfile.js | 8 +++++++-
 3 files changed, 12 insertions(+), 4 deletions(-)

diff --git a/public/locales/en/profile.json b/public/locales/en/profile.json
index 172f9b3..de7f16c 100644
--- a/public/locales/en/profile.json
+++ b/public/locales/en/profile.json
@@ -48,7 +48,8 @@
     "requestsMessage": "Message",
     "processed": "Processed",
     "cancelRequest": "Cancel this request",
-    "requestCanceled": "Request deleted"
+    "requestCanceled": "Request deleted",
+    "noCurrentRequest": "You don't have any pending requests."
   },
   "groupRequests": {
     "selectGroup": "Select group(s)",
diff --git a/public/locales/fr/profile.json b/public/locales/fr/profile.json
index 3694fa2..f6944cc 100644
--- a/public/locales/fr/profile.json
+++ b/public/locales/fr/profile.json
@@ -3,7 +3,7 @@
     "profile": "Mon profil utilisateur",
     "groups": "Mes groupes",
     "role": "Mon role",
-    "fieldsDisplaySettings": "Paramétrage des résultats de recherche"
+    "fieldsDisplaySettings": "Visualisation des résultats de recherche"
   },
   "myProfile": {
     "groupPanel": {
@@ -48,7 +48,8 @@
     "requestsMessage": "Message",
     "processed": "Traitée",
     "cancelRequest": "Annuler cette requête",
-    "requestCanceled": "Requête supprimée"
+    "requestCanceled": "Requête supprimée",
+    "noCurrentRequest": "Vous n'avez pas de requêtes en cours."
   },
   "groupRequests": {
     "selectGroup": "Selectionnez un groupe",
diff --git a/src/pages/profile/MyProfile.js b/src/pages/profile/MyProfile.js
index a0cb9a8..f2a64b6 100644
--- a/src/pages/profile/MyProfile.js
+++ b/src/pages/profile/MyProfile.js
@@ -147,7 +147,13 @@ const MyProfile = ({
         </MyProfileCustomPanel>
       </EuiFlexGroup>
       <MyProfileCustomPanel title={t('profile:myProfile.requestsPanel.title')}>
-        <EuiBasicTable items={userRequests} columns={requestsColumns} />
+        {userRequests.length !== 0 ? (
+          <EuiBasicTable items={userRequests} columns={requestsColumns} />
+        ) : (
+          <EuiFlexGroup justifyContent={'center'}>
+            <p>{t('profile:requestsList.noCurrentRequest')}</p>
+          </EuiFlexGroup>
+        )}
       </MyProfileCustomPanel>
       <EuiFlexGroup>
         <MyProfileCustomPanel
-- 
GitLab


From fd42dc29bfbcff532e1336f8e5e1f579c9c5a622 Mon Sep 17 00:00:00 2001
From: rbisson <remi.bisson@inrae.fr>
Date: Thu, 12 Dec 2024 15:23:19 +0100
Subject: [PATCH 11/17] [Profile] added descriptive texts on MyProfile and
 other profile tabs

---
 public/locales/en/profile.json                |  9 +++--
 public/locales/fr/profile.json                |  7 +++-
 src/pages/profile/MyProfile.js                | 30 +++++++++++++---
 src/pages/profile/Profile.js                  | 35 +++++++++++++++----
 .../profile/UserFieldsDisplaySettings.js      |  3 +-
 5 files changed, 70 insertions(+), 14 deletions(-)

diff --git a/public/locales/en/profile.json b/public/locales/en/profile.json
index de7f16c..4daa91e 100644
--- a/public/locales/en/profile.json
+++ b/public/locales/en/profile.json
@@ -8,27 +8,32 @@
   "myProfile": {
     "groupPanel": {
       "title": "Groups",
+      "description": "Belonging to a group gives access to \"private\" fields from certain sources when you search.",
       "edit": "Change my groups"
     },
     "rolePanel": {
       "title": "Role",
+      "description": "Your role allows to manage your rights within the application, including access to certain tools.",
       "edit": "Change role"
     },
     "requestsPanel": {
-      "title": "My pending requests:"
+      "title": "My pending requests:",
+      "description": "Your requests are managed by the in-sylva IS administrators. They are used to obtain a group or a role."
     },
     "fieldsDisplaySettingsPanel": {
       "title": "My search results display settings",
+      "description": "Modify the fields displayed when the results table appears after a search.",
       "edit": "Change my settings"
     },
     "fieldsDownloadSettingsPanel": {
       "title": "My search results download settings",
+      "description": "Modify the fields downloaded when exporting results table after a search.",
       "edit": "Change my settings"
     }
   },
   "fieldsDisplaySettings": {
     "selectedOptionsLabel": "User display options for fields in results page",
-    "selectedOptionsNumber": "<strong>{selectedOptionsIds.length}</strong> fields selected:",
+    "selectedOptionsNumber": "<strong>{{count}}</strong> fields selected:",
     "deleteSettings": "Delete settings",
     "noSettings": "You don't have any settings.",
     "resetSelection": "Reset selection",
diff --git a/public/locales/fr/profile.json b/public/locales/fr/profile.json
index f6944cc..22d784c 100644
--- a/public/locales/fr/profile.json
+++ b/public/locales/fr/profile.json
@@ -8,21 +8,26 @@
   "myProfile": {
     "groupPanel": {
       "title": "Mes groupes",
+      "description": "Appartenir à un groupe permet d'accéder aux champs \"privés\" de certaines sources lors de vos recherches.",
       "edit": "Modifier mes groupes"
     },
     "rolePanel": {
       "title": "Mon rôle",
+      "description": "Votre rôle permet de gérer vos droits dans l'application, notamment l'accès à certains outils.",
       "edit": "Modifier mon rôle"
     },
     "requestsPanel": {
-      "title": "Mes requêtes en cours"
+      "title": "Mes requêtes en cours",
+      "description": "Vos requêtes sont gérées par les administrateurs du SI in-sylva. Elles permettent l'obtention d'un groupe ou un rôle."
     },
     "fieldsDisplaySettingsPanel": {
       "title": "Mes paramètres de visualisation des résultats",
+      "description": "Modifiez les champs visualisés lors de l'affichage du tableau de résultats après une recherche.",
       "edit": "Modifier mes paramètres de visualisation"
     },
     "fieldsDownloadSettingsPanel": {
       "title": "Mes paramètres d'export des résultats",
+      "description": "Modifiez les champs téléchargés lors de l'export de résultats après une recherche.",
       "edit": "Modifier mes paramètres d'export"
     }
   },
diff --git a/src/pages/profile/MyProfile.js b/src/pages/profile/MyProfile.js
index f2a64b6..57bf89a 100644
--- a/src/pages/profile/MyProfile.js
+++ b/src/pages/profile/MyProfile.js
@@ -8,6 +8,7 @@ import {
   EuiPanel,
   EuiSpacer,
   EuiTitle,
+  EuiIconTip,
 } from '@elastic/eui';
 import { deleteUserRequest } from '../../actions/user';
 import { buildFieldName } from '../../Utils';
@@ -28,6 +29,7 @@ const MyProfile = ({
 
   const MyProfileCustomPanel = ({
     title,
+    description,
     linkedTabNumber,
     linkedTabButton,
     children,
@@ -36,9 +38,21 @@ const MyProfile = ({
       <EuiFlexItem>
         <EuiPanel paddingSize="l" hasShadow={false} hasBorder={true}>
           <EuiFlexGroup justifyContent={'spaceBetween'} alignItems={'center'}>
-            <EuiTitle size="xs">
-              <p>{title}</p>
-            </EuiTitle>
+            <div style={{ display: 'flex', justifyContent: 'flex-start' }}>
+              <EuiTitle size="xs">
+                <p>{title}</p>
+              </EuiTitle>
+              {description && (
+                <div style={{ marginLeft: '10px' }}>
+                  <EuiIconTip
+                    size="l"
+                    color="primary"
+                    position="right"
+                    content={description}
+                  />
+                </div>
+              )}
+            </div>
             {linkedTabNumber && (
               <EuiButtonEmpty
                 size="s"
@@ -133,6 +147,7 @@ const MyProfile = ({
       <EuiFlexGroup>
         <MyProfileCustomPanel
           title={t('profile:myProfile.groupPanel.title')}
+          description={t('profile:myProfile.groupPanel.description')}
           linkedTabButton={t('profile:myProfile.groupPanel.edit')}
           linkedTabNumber={1}
         >
@@ -140,13 +155,18 @@ const MyProfile = ({
         </MyProfileCustomPanel>
         <MyProfileCustomPanel
           title={t('profile:myProfile.rolePanel.title')}
+          description={t('profile:myProfile.rolePanel.description')}
           linkedTabButton={t('profile:myProfile.rolePanel.edit')}
           linkedTabNumber={2}
         >
           <p>{userRole}</p>
         </MyProfileCustomPanel>
       </EuiFlexGroup>
-      <MyProfileCustomPanel title={t('profile:myProfile.requestsPanel.title')}>
+
+      <MyProfileCustomPanel
+        title={t('profile:myProfile.requestsPanel.title')}
+        description={t('profile:myProfile.requestsPanel.description')}
+      >
         {userRequests.length !== 0 ? (
           <EuiBasicTable items={userRequests} columns={requestsColumns} />
         ) : (
@@ -158,6 +178,7 @@ const MyProfile = ({
       <EuiFlexGroup>
         <MyProfileCustomPanel
           title={t('profile:myProfile.fieldsDisplaySettingsPanel.title')}
+          description={t('profile:myProfile.fieldsDisplaySettingsPanel.description')}
           linkedTabButton={t('profile:myProfile.fieldsDisplaySettingsPanel.edit')}
           linkedTabNumber={3}
         >
@@ -165,6 +186,7 @@ const MyProfile = ({
         </MyProfileCustomPanel>
         <MyProfileCustomPanel
           title={t('profile:myProfile.fieldsDownloadSettingsPanel.title')}
+          description={t('profile:myProfile.fieldsDownloadSettingsPanel.description')}
           linkedTabButton={t('profile:myProfile.fieldsDownloadSettingsPanel.edit')}
           linkedTabNumber={3}
         >
diff --git a/src/pages/profile/Profile.js b/src/pages/profile/Profile.js
index 636eda1..313212f 100644
--- a/src/pages/profile/Profile.js
+++ b/src/pages/profile/Profile.js
@@ -1,6 +1,13 @@
 import React, { useEffect, useState } from 'react';
 import { useTranslation } from 'react-i18next';
-import { EuiPanel, EuiTabbedContent, EuiFlexGroup } from '@elastic/eui';
+import {
+  EuiPanel,
+  EuiTabbedContent,
+  EuiFlexGroup,
+  EuiCallOut,
+  EuiSpacer,
+  EuiFlexItem,
+} from '@elastic/eui';
 import UserFieldsDisplaySettings from './UserFieldsDisplaySettings';
 import GroupSettings from './GroupSettings';
 import RoleSettings from './RoleSettings';
@@ -71,7 +78,13 @@ const Profile = () => {
     });
   };
 
-  const Tab = ({ children }) => {
+  const Tab = ({ children, description }) => {
+    const [showCallOut, setShowCallOut] = useState(true);
+
+    const onDismiss = () => {
+      setShowCallOut(false);
+    };
+
     return (
       <EuiPanel
         style={{
@@ -79,7 +92,17 @@ const Profile = () => {
         }}
         paddingSize="l"
       >
-        <EuiFlexGroup direction={'column'}>{children}</EuiFlexGroup>
+        <EuiFlexGroup direction={'column'}>
+          {showCallOut && description && (
+            <EuiCallOut
+              onDismiss={onDismiss}
+              size="s"
+              title={description}
+              iconType="iInCircle"
+            />
+          )}
+          {children}
+        </EuiFlexGroup>
       </EuiPanel>
     );
   };
@@ -106,7 +129,7 @@ const Profile = () => {
       id: 'tab2',
       name: t('profile:tabs.groups'),
       content: (
-        <Tab>
+        <Tab description={t('profile:myProfile.groupPanel.description')}>
           <GroupSettings userGroups={userGroups} />
         </Tab>
       ),
@@ -115,7 +138,7 @@ const Profile = () => {
       id: 'tab3',
       name: t('profile:tabs.role'),
       content: (
-        <Tab>
+        <Tab description={t('profile:myProfile.rolePanel.description')}>
           <RoleSettings userRole={userRole} />
         </Tab>
       ),
@@ -124,7 +147,7 @@ const Profile = () => {
       id: 'tab4',
       name: t('profile:tabs.fieldsDisplaySettings'),
       content: (
-        <Tab>
+        <Tab description={t('profile:myProfile.fieldsDisplaySettingsPanel.description')}>
           <UserFieldsDisplaySettings
             userSettings={fieldsDisplaySettings}
             setUserSettings={setFieldsDisplaySettings}
diff --git a/src/pages/profile/UserFieldsDisplaySettings.js b/src/pages/profile/UserFieldsDisplaySettings.js
index 4ae90c5..575ea84 100644
--- a/src/pages/profile/UserFieldsDisplaySettings.js
+++ b/src/pages/profile/UserFieldsDisplaySettings.js
@@ -76,6 +76,7 @@ const UserFieldsDisplaySettings = ({ userSettings, setUserSettings, publicFields
           checked: isOptionChecked(field.id),
           disabled: field.field_type === 'List',
           toolTipContent: field.definition_and_comment,
+          toolTipProps: { position: 'bottom' },
         });
       }
     });
@@ -153,7 +154,7 @@ const UserFieldsDisplaySettings = ({ userSettings, setUserSettings, publicFields
             onChange={(newOptions) => setSettingsOptions(newOptions)}
             searchable
             listProps={{ bordered: true }}
-            style={{ minHeight: '70vh' }}
+            style={{ minHeight: '65vh' }}
             height={'full'}
           >
             {(list, search) => (
-- 
GitLab


From 63d167b1f2c2ab16921775a2a71bce763e552bbc Mon Sep 17 00:00:00 2001
From: rbisson <remi.bisson@inrae.fr>
Date: Fri, 13 Dec 2024 12:28:55 +0100
Subject: [PATCH 12/17] [SearchMap] Corrected ign map link

---
 src/pages/maps/SearchMap.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/pages/maps/SearchMap.js b/src/pages/maps/SearchMap.js
index 5ee592a..6d47330 100644
--- a/src/pages/maps/SearchMap.js
+++ b/src/pages/maps/SearchMap.js
@@ -181,7 +181,7 @@ const SearchMap = ({ searchResults, selectedPointsIds, setSelectedPointsIds }) =
       name: 'IGN',
       visible: false,
       source: new WMTS({
-        url: 'https://wxs.ign.fr/choisirgeoportail/geoportail/wmts',
+        url: 'https://data.geopf.fr/wmts',
         layer: 'GEOGRAPHICALGRIDSYSTEMS.PLANIGNV2',
         matrixSet: 'PM',
         format: 'image/png',
-- 
GitLab


From 6f3058b2ee94c80ee2bc351b3f3f23959d034349 Mon Sep 17 00:00:00 2001
From: rbisson <remi.bisson@inrae.fr>
Date: Fri, 13 Dec 2024 14:28:46 +0100
Subject: [PATCH 13/17] [ResultsDownload] corrected json data encoding on file
 download

---
 src/pages/results/ResultsDownload.js | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/src/pages/results/ResultsDownload.js b/src/pages/results/ResultsDownload.js
index 9461052..7a6b609 100644
--- a/src/pages/results/ResultsDownload.js
+++ b/src/pages/results/ResultsDownload.js
@@ -10,9 +10,11 @@ const ResultsDownload = ({ query, searchResults, selectedRowsIds }) => {
 
   const downloadJSON = (content) => {
     download(
-      `{"query": ${JSON.stringify(query)}, "metadataRecords": ${JSON.stringify(content, null, '\t')}}`,
+      new Blob([
+        `{"query": ${JSON.stringify(query)}, "metadataRecords": ${JSON.stringify(content, null, '\t')}}`,
+      ]),
       `InSylvaSearchResults.json`,
-      'application/json'
+      'application/json;charset=utf-8'
     );
   };
 
-- 
GitLab


From 366b9dc54084fb025916ac358cf8d99acd865576 Mon Sep 17 00:00:00 2001
From: rbisson <remi.bisson@inrae.fr>
Date: Fri, 13 Dec 2024 15:06:10 +0100
Subject: [PATCH 14/17] [ResultsTableMUI] build columns now using all public
 fields but displaying only those from user settings or default

---
 src/pages/results/ResultsTableMUI.js | 38 ++++++++++------------------
 1 file changed, 14 insertions(+), 24 deletions(-)

diff --git a/src/pages/results/ResultsTableMUI.js b/src/pages/results/ResultsTableMUI.js
index 01e8b5d..01ae83b 100644
--- a/src/pages/results/ResultsTableMUI.js
+++ b/src/pages/results/ResultsTableMUI.js
@@ -54,9 +54,10 @@ const ResultsTableMUI = ({
     if (!searchResults && searchResults.length > 0) {
       return;
     }
-    getColumnFields().then((columnFields) => {
-      setColumns(buildColumns(columnFields));
-      setRows(buildRows(searchResults, columnFields));
+    getStdFieldsIds().then((stdFieldsIds) => {
+      const columns = buildColumns(stdFieldsIds);
+      setColumns(columns);
+      setRows(buildRows(searchResults, columns));
       setIsLoading(false);
     });
   }, [publicFields]);
@@ -74,34 +75,23 @@ const ResultsTableMUI = ({
     return stdFieldsIds;
   };
 
-  // Get fields data from ids
-  const getColumnFields = async () => {
-    const stdFieldsIds = await getStdFieldsIds();
-    return publicFields.filter((stdField) => {
-      return stdFieldsIds.includes(stdField.id);
-    });
-  };
-
   // Returns value from JSON obj associated to key string.
   const getValueByPath = (obj, path) => {
     return path.split('.').reduce((acc, key) => acc && acc[key], obj);
   };
 
   // Build each row in table from search results data
-  const buildRows = (results, columnFields) => {
+  const buildRows = (results, columns) => {
     let dataRows = [];
     if (results.length === 0) {
       return dataRows;
     }
-    results.forEach((result, index) => {
+    results.forEach((result) => {
       let row = {
         id: result.id,
       };
-      columnFields.forEach((columnField) => {
-        const fieldValue = getValueByPath(result, columnField.field_name);
-        if (typeof fieldValue === 'string') {
-          row[columnField.field_name] = fieldValue;
-        }
+      columns.forEach((columnField) => {
+        row[columnField.name] = getValueByPath(result, columnField.name);
       });
       dataRows.push(row);
     });
@@ -109,7 +99,7 @@ const ResultsTableMUI = ({
   };
 
   // Build table columns names (label and name)
-  const buildColumns = (columnFields) => {
+  const buildColumns = (stdFieldsIds) => {
     let dataColumns = [];
     dataColumns.push({
       name: 'id',
@@ -118,12 +108,12 @@ const ResultsTableMUI = ({
         display: 'excluded',
       },
     });
-    columnFields.forEach((columnField) => {
+    publicFields.forEach((publicField) => {
       dataColumns.push({
-        name: columnField.field_name,
-        label: buildFieldName(columnField.field_name),
+        name: publicField.field_name,
+        label: buildFieldName(publicField.field_name),
         options: {
-          display: true,
+          display: stdFieldsIds.includes(publicField.id),
         },
       });
     });
@@ -212,7 +202,7 @@ const ResultsTableMUI = ({
     onChangeRowsPerPage: (newRowsPerPage) => {
       setRowsPerPage(newRowsPerPage);
     },
-    rowsPerPageOptions: [15, 30, 50, 100],
+    rowsPerPageOptions: [15, 30, 50, 100, 250],
     jumpToPage: true,
     searchPlaceholder: t('results:table.search'),
     elevation: 0, // remove the boxShadow style
-- 
GitLab


From 01111b04fda00500e57dad14d33bd2bbdc77e61b Mon Sep 17 00:00:00 2001
From: rbisson <remi.bisson@inrae.fr>
Date: Fri, 13 Dec 2024 16:03:35 +0100
Subject: [PATCH 15/17] [ResultsTableMUI] now displaying value of type number
 in table rows

---
 src/pages/results/ResultsTableMUI.js | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/src/pages/results/ResultsTableMUI.js b/src/pages/results/ResultsTableMUI.js
index 01ae83b..e3e8887 100644
--- a/src/pages/results/ResultsTableMUI.js
+++ b/src/pages/results/ResultsTableMUI.js
@@ -91,7 +91,12 @@ const ResultsTableMUI = ({
         id: result.id,
       };
       columns.forEach((columnField) => {
-        row[columnField.name] = getValueByPath(result, columnField.name);
+        const fieldValue = getValueByPath(result, columnField.name);
+        if (typeof fieldValue === 'string') {
+          row[columnField.name] = fieldValue;
+        } else if (typeof fieldValue === 'number') {
+          row[columnField.name] = fieldValue.toString();
+        }
       });
       dataRows.push(row);
     });
-- 
GitLab


From c9723b29209b9aba485caad4ea4b64d7da756923 Mon Sep 17 00:00:00 2001
From: rbisson <remi.bisson@inrae.fr>
Date: Fri, 13 Dec 2024 17:15:51 +0100
Subject: [PATCH 16/17] [AdvancedSearch] Corrected bug causing save search
 modal to refresh on input ; added toasts on history creation and creation
 error

---
 public/locales/en/search.json                 |  1 +
 public/locales/en/validation.json             |  2 +-
 public/locales/fr/search.json                 |  3 +-
 public/locales/fr/validation.json             |  2 +-
 src/actions/user.js                           |  2 +-
 src/context/InSylvaGatekeeperClient.js        |  2 +-
 .../search/AdvancedSearch/AdvancedSearch.js   | 73 ++++++++++---------
 7 files changed, 44 insertions(+), 41 deletions(-)

diff --git a/public/locales/en/search.json b/public/locales/en/search.json
index ebcd7b4..8453bd3 100644
--- a/public/locales/en/search.json
+++ b/public/locales/en/search.json
@@ -60,6 +60,7 @@
       "addSavedSearchName": "Search name",
       "addSavedSearchDescription": "Description (optional)",
       "addSavedSearchDescriptionPlaceholder": "Search description..."
+      "searchSaved": "Search saved"
     },
     "searchOptions": {
       "title": "Search option",
diff --git a/public/locales/en/validation.json b/public/locales/en/validation.json
index dea09f1..189255c 100644
--- a/public/locales/en/validation.json
+++ b/public/locales/en/validation.json
@@ -1,4 +1,4 @@
 {
   "requestSent": "Your request has been sent to the administrators.",
-  "error": "Error:"
+  "error": "Error"
 }
diff --git a/public/locales/fr/search.json b/public/locales/fr/search.json
index 57080e8..101410a 100644
--- a/public/locales/fr/search.json
+++ b/public/locales/fr/search.json
@@ -59,7 +59,8 @@
       "saveSearch": "Sauvegarder ma recherche",
       "addSavedSearchName": "Nom de la recherche",
       "addSavedSearchDescription": "Description (optionel)",
-      "addSavedSearchDescriptionPlaceholder": "Description de la recherche..."
+      "addSavedSearchDescriptionPlaceholder": "Description de la recherche...",
+      "searchSaved": "Recherche sauvegardée"
     },
     "searchOptions": {
       "title": "Option de recherche",
diff --git a/public/locales/fr/validation.json b/public/locales/fr/validation.json
index f762974..ef37775 100644
--- a/public/locales/fr/validation.json
+++ b/public/locales/fr/validation.json
@@ -1,4 +1,4 @@
 {
   "requestSent": "Votre requête à bien été envoyée.",
-  "error": "Erreur:"
+  "error": "Erreur"
 }
diff --git a/src/actions/user.js b/src/actions/user.js
index 6cfe2cf..d5cc589 100644
--- a/src/actions/user.js
+++ b/src/actions/user.js
@@ -133,7 +133,7 @@ export const addUserHistory = async (kcId, query, name, uiStructure, description
   igClient.token = sessionStorage.getItem('access_token');
   try {
     const jsonUIStructure = JSON.stringify(uiStructure);
-    await igClient.addUserHistory(kcId, query, name, jsonUIStructure, description);
+    return await igClient.addUserHistory(kcId, query, name, jsonUIStructure, description);
   } catch (error) {
     console.error(error);
   }
diff --git a/src/context/InSylvaGatekeeperClient.js b/src/context/InSylvaGatekeeperClient.js
index 7c78df2..26f1078 100644
--- a/src/context/InSylvaGatekeeperClient.js
+++ b/src/context/InSylvaGatekeeperClient.js
@@ -83,7 +83,7 @@ class InSylvaGatekeeperClient {
 
   async addUserHistory(kcId, query, name, uiStructure, description) {
     const path = `/user/add-history`;
-    await this.post('POST', `${path}`, {
+    return await this.post('POST', `${path}`, {
       kcId,
       query,
       name,
diff --git a/src/pages/search/AdvancedSearch/AdvancedSearch.js b/src/pages/search/AdvancedSearch/AdvancedSearch.js
index 44b14ae..53f5ad4 100644
--- a/src/pages/search/AdvancedSearch/AdvancedSearch.js
+++ b/src/pages/search/AdvancedSearch/AdvancedSearch.js
@@ -48,6 +48,7 @@ import styles from './styles.js';
 import moment from 'moment';
 import SearchModeSwitcher from '../SearchModeSwitcher';
 import { toast } from 'react-toastify';
+import ToastMessage from '../../../components/ToastMessage/ToastMessage';
 
 const updateSources = (
   searchFields,
@@ -129,25 +130,6 @@ const fieldValuesToString = (field) => {
   return strValues;
 };
 
-const addHistory = (
-  kcID,
-  search,
-  searchName,
-  searchFields,
-  searchDescription,
-  setUserHistory
-) => {
-  addUserHistory(
-    sessionStorage.getItem('kcId'),
-    search,
-    searchName,
-    searchFields,
-    searchDescription
-  ).then(() => {
-    fetchHistory(setUserHistory);
-  });
-};
-
 const fetchHistory = (setUserHistory) => {
   fetchUserHistory(sessionStorage.getItem('kcId')).then((result) => {
     if (result[0] && result[0].ui_structure) {
@@ -279,8 +261,6 @@ const SearchBar = ({
   const [isLoading, setIsLoading] = useState(false);
   const [userHistory, setUserHistory] = useState({});
   const [isSaveSearchModalOpen, setIsSaveSearchModalOpen] = useState(false);
-  const [searchDescription, setSearchDescription] = useState('');
-  const [searchName, setSearchName] = useState('');
   const [readOnlyQuery, setReadOnlyQuery] = useState(true);
 
   const closeSaveSearchModal = () => {
@@ -322,23 +302,44 @@ const SearchBar = ({
     }
   };
 
-  const onClickSaveSearch = () => {
-    if (!!searchName) {
-      addHistory(
-        sessionStorage.getItem('kcId'),
-        search,
-        searchName,
-        searchFields,
-        searchDescription,
-        setUserHistory
-      );
-      setSearchName('');
-      setSearchDescription('');
-      closeSaveSearchModal();
-    }
+  const addHistory = (
+    search,
+    searchName,
+    searchFields,
+    searchDescription,
+    setUserHistory
+  ) => {
+    addUserHistory(
+      sessionStorage.getItem('kcId'),
+      search,
+      searchName,
+      searchFields,
+      searchDescription
+    ).then((result) => {
+      if (result.error) {
+        toast.error(
+          <ToastMessage title={t('validation:error')} message={result.error} />
+        );
+      } else {
+        toast.success(t('search:advancedSearch.searchHistory.searchSaved'));
+      }
+      fetchHistory(setUserHistory);
+    });
   };
 
   const SaveSearchModal = () => {
+    const [searchName, setSearchName] = useState('');
+    const [searchDescription, setSearchDescription] = useState('');
+
+    const onClickSaveSearch = () => {
+      if (!!searchName) {
+        addHistory(search, searchName, searchFields, searchDescription, setUserHistory);
+        setSearchName('');
+        setSearchDescription('');
+        closeSaveSearchModal();
+      }
+    };
+
     return (
       <EuiOverlayMask>
         <EuiModal onClose={closeSaveSearchModal} initialFocus="[name=searchName]">
@@ -630,7 +631,7 @@ const PopoverValueContent = ({
   availableSources,
   setAvailableSources,
 }) => {
-  const { t } = useTranslation(['search', 'common']);
+  const { t } = useTranslation(['search', 'common', 'validation']);
   const [valueError, setValueError] = useState(undefined);
 
   const onValueSearchChange = (value, hasMatchingOptions) => {
-- 
GitLab


From 18c14fe6dd3e57c8d04abf74b081b5596a6d5f01 Mon Sep 17 00:00:00 2001
From: rbisson <remi.bisson@inrae.fr>
Date: Mon, 16 Dec 2024 10:22:21 +0100
Subject: [PATCH 17/17] [locales/en] corrected forgotten , at end of line

---
 public/locales/en/search.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/public/locales/en/search.json b/public/locales/en/search.json
index 8453bd3..67d35bc 100644
--- a/public/locales/en/search.json
+++ b/public/locales/en/search.json
@@ -59,7 +59,7 @@
       "saveSearch": "Save search",
       "addSavedSearchName": "Search name",
       "addSavedSearchDescription": "Description (optional)",
-      "addSavedSearchDescriptionPlaceholder": "Search description..."
+      "addSavedSearchDescriptionPlaceholder": "Search description...",
       "searchSaved": "Search saved"
     },
     "searchOptions": {
-- 
GitLab