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