From 499febc6c898c57995b16cfbe4c1137e419fc12e Mon Sep 17 00:00:00 2001 From: rbisson <remi.bisson@inrae.fr> Date: Thu, 4 Jul 2024 11:27:55 +0200 Subject: [PATCH 01/10] [package.json] removed all unused dependencies [resourceFlyout] replaced old json viewer fork by real maintained package --- package.json | 5 +---- src/pages/results/ResourceFlyout.js | 21 +++++++++------------ 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 7c9b4ba..7b82055 100644 --- a/package.json +++ b/package.json @@ -6,12 +6,12 @@ "dependencies": { "@elastic/datemath": "^5.0.3", "@elastic/eui": "^27.4.0", - "@in-sylva/json-view": "git+ssh://git@forgemia.inra.fr:in-sylva-development/json-view.git", "@in-sylva/react-use-storage": "git+ssh://git@forgemia.inra.fr:in-sylva-development/react-use-storage.git", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "^4.0.0-alpha.48", "@material-ui/styles": "^4.10.0", + "@microlink/react-json-view": "^1.23.1", "downloadjs": "^1.4.7", "i18next": "^23.11.2", "i18next-http-backend": "^2.5.1", @@ -21,8 +21,6 @@ "proj4": "^2.11.0", "react": "^16.13.1", "react-dom": "^16.13.1", - "react-hanger": "^2.2.1", - "react-html-parser": "^2.0.2", "react-i18next": "^14.1.1", "react-router-dom": "^5.2.0", "react-scripts": "^3.3.0" @@ -52,7 +50,6 @@ "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", "husky": "8.0.3", - "jscs": "^3.0.7", "lint-staged": "14.0.1", "prettier": "^3.2.5" }, diff --git a/src/pages/results/ResourceFlyout.js b/src/pages/results/ResourceFlyout.js index bd7522b..0c6bc5c 100644 --- a/src/pages/results/ResourceFlyout.js +++ b/src/pages/results/ResourceFlyout.js @@ -1,7 +1,7 @@ -import React, { Fragment } from 'react'; +import React from 'react'; import { EuiFlyout, EuiFlyoutBody, EuiText } from '@elastic/eui'; import { useTranslation } from 'react-i18next'; -import JsonView from '@in-sylva/json-view'; +import ReactJson from '@microlink/react-json-view'; const ResourceFlyout = ({ resourceFlyoutData, @@ -24,16 +24,13 @@ const ResourceFlyout = ({ > <EuiFlyoutBody> <EuiText size="s"> - <Fragment> - <JsonView - src={resourceFlyoutData} - name={t('results:flyout.JSON.title')} - collapsed={true} - iconStyle={'triangle'} - enableClipboard={false} - displayDataTypes={false} - /> - </Fragment> + <ReactJson + src={resourceFlyoutData} + name={t('results:flyout.JSON.title')} + iconStyle={'triangle'} + displayDataTypes={false} + collapsed={true} + /> </EuiText> </EuiFlyoutBody> </EuiFlyout> -- GitLab From d25d5ab4096c829c5f70cad79bca0f3add4f6dd4 Mon Sep 17 00:00:00 2001 From: rbisson <remi.bisson@inrae.fr> Date: Thu, 4 Jul 2024 19:53:21 +0200 Subject: [PATCH 02/10] [package.json] removed forked dependency to replace it by real npm package [store] replace import call to new dependency --- package.json | 4 ++-- src/store/useStore.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 7b82055..9bdaa6f 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,6 @@ "dependencies": { "@elastic/datemath": "^5.0.3", "@elastic/eui": "^27.4.0", - "@in-sylva/react-use-storage": "git+ssh://git@forgemia.inra.fr:in-sylva-development/react-use-storage.git", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "^4.0.0-alpha.48", @@ -23,7 +22,8 @@ "react-dom": "^16.13.1", "react-i18next": "^14.1.1", "react-router-dom": "^5.2.0", - "react-scripts": "^3.3.0" + "react-scripts": "^3.3.0", + "react-use-storage": "^0.5.1" }, "scripts": { "start": "react-scripts start", diff --git a/src/store/useStore.js b/src/store/useStore.js index 778e2fe..96c39a8 100644 --- a/src/store/useStore.js +++ b/src/store/useStore.js @@ -1,4 +1,4 @@ -import { useSessionStorage } from '@in-sylva/react-use-storage'; +import { useSessionStorage } from 'react-use-storage'; function setState(store, newState, afterUpdateCallback) { store.state = { ...store.state, ...newState }; -- GitLab From 231a6bba9c9ffd4656ba5f65a80c3b4e724465d5 Mon Sep 17 00:00:00 2001 From: rbisson <remi.bisson@inrae.fr> Date: Fri, 5 Jul 2024 08:37:22 +0200 Subject: [PATCH 03/10] [App.test.js] removed useless test file --- src/App.test.js | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 src/App.test.js diff --git a/src/App.test.js b/src/App.test.js deleted file mode 100644 index a754b20..0000000 --- a/src/App.test.js +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import App from './App'; - -it('renders without crashing', () => { - const div = document.createElement('div'); - ReactDOM.render(<App />, div); - ReactDOM.unmountComponentAtNode(div); -}); -- GitLab From 4721636ebc923eec9316af15f9dde4188e05a08a Mon Sep 17 00:00:00 2001 From: rbisson <remi.bisson@inrae.fr> Date: Fri, 5 Jul 2024 08:38:32 +0200 Subject: [PATCH 04/10] [package.json] updated to React 18 [index.js] changed REACTDOM.render to createRoot to suit new React --- package.json | 12 +++++++----- src/index.js | 8 ++++---- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 9bdaa6f..f816edd 100644 --- a/package.json +++ b/package.json @@ -18,11 +18,11 @@ "mui-datatables": "^3.4.0", "ol": "^6.3.2-dev.1594217558556", "proj4": "^2.11.0", - "react": "^16.13.1", - "react-dom": "^16.13.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", "react-i18next": "^14.1.1", "react-router-dom": "^5.2.0", - "react-scripts": "^3.3.0", + "react-scripts": "^5.0.1", "react-use-storage": "^0.5.1" }, "scripts": { @@ -59,6 +59,9 @@ } }, "eslintConfig": { + "rules": { + "react/prop-types": "off" + }, "env": { "node": true, "browser": true, @@ -66,8 +69,7 @@ }, "extends": [ "plugin:react/recommended", - "plugin:prettier/recommended", - "airbnb" + "plugin:prettier/recommended" ], "parserOptions": { "ecmaFeatures": { diff --git a/src/index.js b/src/index.js index 285bd62..a1e933a 100644 --- a/src/index.js +++ b/src/index.js @@ -1,11 +1,11 @@ import React, { Suspense } from 'react'; -import ReactDOM from 'react-dom'; import '@elastic/eui/dist/eui_theme_light.css'; import { UserProvider, checkUserLogin } from './context/UserContext'; import App from './App'; import { getLoginUrl, getUrlParam, redirect } from './Utils.js'; import './i18n'; import Loading from './components/Loading'; +import { createRoot } from 'react-dom/client'; const userId = getUrlParam('kcId', ''); const accessToken = getUrlParam('accessToken', ''); @@ -16,13 +16,13 @@ if (refreshToken.includes('#/app/search')) { checkUserLogin(userId, accessToken, refreshToken); if (sessionStorage.getItem('access_token')) { - ReactDOM.render( + const root = createRoot(document.getElementById('root')); + root.render( <UserProvider> <Suspense fallback={<Loading />}> <App userId={userId} accessToken={accessToken} refreshToken={refreshToken} /> </Suspense> - </UserProvider>, - document.getElementById('root') + </UserProvider> ); } else { redirect(getLoginUrl() + '?requestType=search'); -- GitLab From 4f8ede24068117f8c49af25c968ca0b576e5d237 Mon Sep 17 00:00:00 2001 From: rbisson <remi.bisson@inrae.fr> Date: Fri, 5 Jul 2024 08:44:49 +0200 Subject: [PATCH 05/10] [package.json] updated ol package version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f816edd..7c9904d 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "i18next-http-backend": "^2.5.1", "moment": "^2.27.0", "mui-datatables": "^3.4.0", - "ol": "^6.3.2-dev.1594217558556", + "ol": "^9.2.4", "proj4": "^2.11.0", "react": "^18.3.1", "react-dom": "^18.3.1", -- GitLab From 0de79cb57cbfeb8722279057ac760ee92be49eb3 Mon Sep 17 00:00:00 2001 From: rbisson <remi.bisson@inrae.fr> Date: Fri, 5 Jul 2024 11:09:04 +0200 Subject: [PATCH 06/10] [package.json] updated react-router-dom package [App][Layout][index.js] refactored router implementation and layout --- package.json | 2 +- src/App.js | 37 ++++++++++++++--------- src/components/Header/Header.js | 52 +++++++++++++++------------------ src/components/Layout/Layout.js | 27 +++++++---------- src/components/Layout/styles.js | 3 ++ src/index.js | 2 +- 6 files changed, 64 insertions(+), 59 deletions(-) diff --git a/package.json b/package.json index 7c9904d..3bb00fd 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-i18next": "^14.1.1", - "react-router-dom": "^5.2.0", + "react-router-dom": "^6.24.1", "react-scripts": "^5.0.1", "react-use-storage": "^0.5.1" }, diff --git a/src/App.js b/src/App.js index 5d03570..5ae4717 100644 --- a/src/App.js +++ b/src/App.js @@ -1,20 +1,31 @@ import React from 'react'; -import { EuiPage, EuiPageBody } from '@elastic/eui'; -import { HashRouter, Route, Switch, Redirect } from 'react-router-dom'; +import { + RouterProvider, + createHashRouter, + Route, + createRoutesFromElements, + Navigate, +} from 'react-router-dom'; +import Home from './pages/home'; +import Search from './pages/search'; +import Profile from './pages/profile'; import Layout from './components/Layout'; const App = () => { - return ( - <EuiPage style={{ padding: '0px' }} restrictWidth={false}> - <EuiPageBody> - <HashRouter> - <Switch> - <Route exact path="/" render={() => <Redirect to="/app/home" />} /> - <Route component={Layout} /> - </Switch> - </HashRouter> - </EuiPageBody> - </EuiPage> + const router = createHashRouter( + createRoutesFromElements( + <> + <Route path="/" element={<Navigate to="/app/home" />} /> + <Route element={<Layout />}> + <Route index path="/app/home" element={<Home />} /> + <Route path="/app/search" element={<Search />} /> + <Route path="/app/profile" element={<Profile />} /> + </Route> + </> + ) ); + + return <RouterProvider router={router} />; }; + export default App; diff --git a/src/components/Header/Header.js b/src/components/Header/Header.js index 6e571d6..5ea7ce6 100644 --- a/src/components/Header/Header.js +++ b/src/components/Header/Header.js @@ -1,5 +1,5 @@ import React from 'react'; -import { Link } from 'react-router-dom'; +import { NavLink } from 'react-router-dom'; import { EuiHeader, EuiHeaderSection, @@ -13,18 +13,16 @@ import logoInSylva from '../../assets/favicon.svg'; import { useTranslation } from 'react-i18next'; import LanguageSwitcher from '../LanguageSwitcher/LanguageSwitcher'; -const structure = [ +const routes = [ { id: 0, label: 'home', href: '/app/home', - icon: '', }, { id: 1, label: 'search', href: '/app/search', - icon: '', }, ]; @@ -32,30 +30,28 @@ const Header = () => { const { t } = useTranslation(['header', 'common']); return ( - <> - <EuiHeader> - <EuiHeaderSection grow={true}> - <EuiHeaderSectionItem> - <img style={style.logo} src={logoInSylva} alt={t('common:inSylvaLogoAlt')} /> - </EuiHeaderSectionItem> - <EuiHeaderLinks> - {structure.map((link) => ( - <EuiHeaderLink iconType="empty" key={link.id}> - <Link to={link.href}>{t(`tabs.${link.label}`)}</Link> - </EuiHeaderLink> - ))} - </EuiHeaderLinks> - </EuiHeaderSection> - <EuiHeaderSection side="right"> - <EuiHeaderSectionItem style={style.languageSwitcherItem} border={'none'}> - <LanguageSwitcher /> - </EuiHeaderSectionItem> - <EuiHeaderSectionItem style={style.userMenuItem} border={'none'}> - <HeaderUserMenu /> - </EuiHeaderSectionItem> - </EuiHeaderSection> - </EuiHeader> - </> + <EuiHeader> + <EuiHeaderSection grow={true}> + <EuiHeaderSectionItem> + <img style={style.logo} src={logoInSylva} alt={t('common:inSylvaLogoAlt')} /> + </EuiHeaderSectionItem> + <EuiHeaderLinks> + {routes.map((link) => ( + <EuiHeaderLink key={link.id}> + <NavLink to={link.href}>{t(`tabs.${link.label}`)}</NavLink> + </EuiHeaderLink> + ))} + </EuiHeaderLinks> + </EuiHeaderSection> + <EuiHeaderSection side="right"> + <EuiHeaderSectionItem style={style.languageSwitcherItem} border={'none'}> + <LanguageSwitcher /> + </EuiHeaderSectionItem> + <EuiHeaderSectionItem style={style.userMenuItem} border={'none'}> + <HeaderUserMenu /> + </EuiHeaderSectionItem> + </EuiHeaderSection> + </EuiHeader> ); }; diff --git a/src/components/Layout/Layout.js b/src/components/Layout/Layout.js index af5be08..62075f5 100644 --- a/src/components/Layout/Layout.js +++ b/src/components/Layout/Layout.js @@ -1,25 +1,20 @@ import React from 'react'; -import { Route, Switch, withRouter } from 'react-router-dom'; +import { Outlet } from 'react-router-dom'; import Header from '../../components/Header'; -import Search from '../../pages/search'; -import Home from '../../pages/home'; -import Profile from '../../pages/profile'; -import { EuiPageContent } from '@elastic/eui'; +import { EuiPage, EuiPageBody, EuiPageContent } from '@elastic/eui'; import styles from './styles.js'; const Layout = () => { return ( - <> - <Header /> - <EuiPageContent style={styles.pageContent}> - <Switch> - <Route path="/app/home" component={Home} /> - <Route path="/app/search" component={Search} /> - <Route path="/app/profile" component={Profile} /> - </Switch> - </EuiPageContent> - </> + <EuiPage style={styles.page} restrictWidth={false}> + <EuiPageBody> + <Header /> + <EuiPageContent style={styles.pageContent}> + <Outlet /> + </EuiPageContent> + </EuiPageBody> + </EuiPage> ); }; -export default withRouter(Layout); +export default Layout; diff --git a/src/components/Layout/styles.js b/src/components/Layout/styles.js index 787354e..9b841ee 100644 --- a/src/components/Layout/styles.js +++ b/src/components/Layout/styles.js @@ -1,4 +1,7 @@ const styles = { + page: { + padding: '0px', + }, pageContent: { borderRadius: 0, border: 0, diff --git a/src/index.js b/src/index.js index a1e933a..737fbdf 100644 --- a/src/index.js +++ b/src/index.js @@ -20,7 +20,7 @@ if (sessionStorage.getItem('access_token')) { root.render( <UserProvider> <Suspense fallback={<Loading />}> - <App userId={userId} accessToken={accessToken} refreshToken={refreshToken} /> + <App /> </Suspense> </UserProvider> ); -- GitLab From a2355572db0730f82d6d30d82a3110070c8fd43c Mon Sep 17 00:00:00 2001 From: rbisson <remi.bisson@inrae.fr> Date: Fri, 5 Jul 2024 11:18:28 +0200 Subject: [PATCH 07/10] [package.json] updated i18n packages --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 3bb00fd..821fe82 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,8 @@ "@material-ui/styles": "^4.10.0", "@microlink/react-json-view": "^1.23.1", "downloadjs": "^1.4.7", - "i18next": "^23.11.2", - "i18next-http-backend": "^2.5.1", + "i18next": "^23.11.5", + "i18next-http-backend": "^2.5.2", "moment": "^2.27.0", "mui-datatables": "^3.4.0", "ol": "^9.2.4", -- GitLab From 68fcafba7f54a694683eee1f7218695f4c727d48 Mon Sep 17 00:00:00 2001 From: rbisson <remi.bisson@inrae.fr> Date: Fri, 5 Jul 2024 11:30:55 +0200 Subject: [PATCH 08/10] [package.json] removed useless package mui/lab --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 821fe82..ef0e8ee 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,6 @@ "@elastic/eui": "^27.4.0", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", - "@material-ui/lab": "^4.0.0-alpha.48", "@material-ui/styles": "^4.10.0", "@microlink/react-json-view": "^1.23.1", "downloadjs": "^1.4.7", -- GitLab From 57426fb7a8b9bdf580c9301b20b0c885086247a5 Mon Sep 17 00:00:00 2001 From: rbisson <remi.bisson@inrae.fr> Date: Tue, 9 Jul 2024 17:23:53 +0200 Subject: [PATCH 09/10] [package.json][src] updated EUI and MUI packages [AdvancedSearch] replaced MUI datepickers by EUI; improved styling; added missing translations [ErrorBoundary] added an error catcher page --- package.json | 30 +- public/locales/en/common.json | 4 + public/locales/en/search.json | 34 +- public/locales/fr/common.json | 4 + public/locales/fr/search.json | 34 +- src/App.js | 3 +- src/components/Header/HeaderUserMenu.js | 60 +- src/components/Layout/Layout.js | 6 +- src/components/Layout/styles.js | 4 +- src/index.js | 5 +- src/pages/error/ErrorBoundary.js | 25 + src/pages/home/Home.js | 42 +- src/pages/profile/Profile.js | 167 ++-- src/pages/results/ResourceFlyout.js | 1 + src/pages/results/ResultsTableMUI.js | 12 +- .../search/AdvancedSearch/AdvancedSearch.js | 822 +++++++++--------- src/pages/search/AdvancedSearch/styles.js | 14 +- src/pages/search/Data.js | 16 +- src/pages/search/Search.js | 16 +- 19 files changed, 677 insertions(+), 622 deletions(-) create mode 100644 src/pages/error/ErrorBoundary.js diff --git a/package.json b/package.json index ef0e8ee..9e72c1f 100644 --- a/package.json +++ b/package.json @@ -4,25 +4,27 @@ "private": true, "homepage": ".", "dependencies": { - "@elastic/datemath": "^5.0.3", - "@elastic/eui": "^27.4.0", - "@material-ui/core": "^4.11.0", - "@material-ui/icons": "^4.9.1", - "@material-ui/styles": "^4.10.0", - "@microlink/react-json-view": "^1.23.1", - "downloadjs": "^1.4.7", - "i18next": "^23.11.5", - "i18next-http-backend": "^2.5.2", - "moment": "^2.27.0", - "mui-datatables": "^3.4.0", - "ol": "^9.2.4", - "proj4": "^2.11.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-i18next": "^14.1.1", "react-router-dom": "^6.24.1", "react-scripts": "^5.0.1", - "react-use-storage": "^0.5.1" + "react-use-storage": "^0.5.1", + "@elastic/eui": "^95.3.0", + "@elastic/datemath": "^5.0.3", + "@mui/material": "^5.15.21", + "@mui/icons-material": "^5.15.21", + "mui-datatables": "^4.3.0", + "moment": "^2.27.0", + "@emotion/css": "^11.11.2", + "@emotion/react": "^11.11.4", + "@emotion/styled": "^11.11.5", + "i18next": "^23.11.5", + "i18next-http-backend": "^2.5.2", + "@microlink/react-json-view": "^1.23.1", + "downloadjs": "^1.4.7", + "ol": "^9.2.4", + "proj4": "^2.11.0" }, "scripts": { "start": "react-scripts start", diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 506293c..aeeba79 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -9,5 +9,9 @@ "send": "Send", "save": "Save", "validate": "Validate" + }, + "errorPage": { + "title": "An error has occurred.", + "reload": "Reload page to try again." } } diff --git a/public/locales/en/search.json b/public/locales/en/search.json index cebc9eb..3ad7159 100644 --- a/public/locales/en/search.json +++ b/public/locales/en/search.json @@ -1,5 +1,4 @@ { - "pageTitle": "In-Sylva Metadata Search Platform", "tabs": { "composeSearch": "Compose search", "results": "Results", @@ -24,10 +23,9 @@ "removeFieldButton": "Remove field", "clearValues": "Clear values", "addFieldPopover": { - "openPopoverButton": "Add field", - "title": "Select a field", - "button": "Add this field", - "selectSection": "Select a section" + "title": "Add a field", + "selectSection": "Select a field", + "validateButton": "Add this field" }, "fieldContentPopover": { "addFieldValues": "Add field values", @@ -35,9 +33,25 @@ "firstValue": "1st value", "secondValue": "2nd value", "inputTextValue": "Type value", - "betweenDate": "between", - "andDate": "and", - "selectValues": "Select values" + "betweenDate": "Between", + "andDate": "And", + "dateSelection": "Select a date", + "selectValues": "Select values", + "operatorSelection": { + "label": "Select an operator", + "numeric": { + "value": "Equal to", + "under": "Under", + "over": "Over", + "range": "Within a range" + }, + "date": { + "value": "Exact date", + "before": "Before a certain day", + "after": "After a certain day", + "range": "Within a range" + } + } } }, "searchHistory": { @@ -49,8 +63,8 @@ }, "searchOptions": { "title": "Search option", - "matchAll": "Match all criterias", - "matchAtLeastOne": "Match at least one criteria" + "matchAll": "Match all fields criteria", + "matchAtLeastOne": "Match at least one field criteria" }, "partnerSources": { "title": "Partner sources", diff --git a/public/locales/fr/common.json b/public/locales/fr/common.json index 8e9085f..cb15bc4 100644 --- a/public/locales/fr/common.json +++ b/public/locales/fr/common.json @@ -9,5 +9,9 @@ "send": "Envoyer", "save": "Sauvegarder", "validate": "Valider" + }, + "errorPage": { + "title": "Une erreur est survenue.", + "reload": "Rechargez la page pour essayer à nouveau." } } diff --git a/public/locales/fr/search.json b/public/locales/fr/search.json index 5f6ab39..36458a6 100644 --- a/public/locales/fr/search.json +++ b/public/locales/fr/search.json @@ -1,5 +1,4 @@ { - "pageTitle": "Plateforme de recherche de métadonnées In-Sylva", "tabs": { "composeSearch": "Composer une recherche", "results": "Résultats", @@ -24,10 +23,9 @@ "removeFieldButton": "Supprimer le champ", "clearValues": "Vider les valeurs", "addFieldPopover": { - "openPopoverButton": "Selectionnez un champ", - "title": "Ajouter ce champ", - "button": "Selectionnez une section", - "selectSection": "Ajouter un champ" + "title": "Ajouter un champ", + "selectSection": "Sélectionnez un champ", + "validateButton": "Ajouter ce champ" }, "fieldContentPopover": { "addValue": "Ajouter une valeur", @@ -35,9 +33,25 @@ "firstValue": "1ère valeur", "secondValue": "2ème valeur", "inputTextValue": "Entrez une valeur", - "betweenDate": "entre", - "andDate": "et", - "selectValues": "Sélectionnez au moins une valeur" + "betweenDate": "Entre le", + "andDate": "Et le", + "dateSelection": "Choisir une date", + "selectValues": "Sélectionnez au moins une valeur", + "operatorSelection": { + "label": "Sélectionnez un opérateur", + "numeric": { + "value": "Egal à ", + "under": "Inférieur à ", + "over": "Supérieur à ", + "range": "Dans un interval" + }, + "date": { + "value": "Date exacte", + "before": "Avant une date", + "after": "Après une date", + "range": "Dans une période de temps" + } + } } }, "searchHistory": { @@ -49,8 +63,8 @@ }, "searchOptions": { "title": "Option de recherche", - "matchAll": "Répondre à tous les critères", - "matchAtLeastOne": "Répondre à au moins un critère" + "matchAll": "Répondre à tous les critères de champ", + "matchAtLeastOne": "Répondre à minimum un critère de champ" }, "partnerSources": { "title": "Liste des sources de partenaires", diff --git a/src/App.js b/src/App.js index 5ae4717..dbcae62 100644 --- a/src/App.js +++ b/src/App.js @@ -10,13 +10,14 @@ import Home from './pages/home'; import Search from './pages/search'; import Profile from './pages/profile'; import Layout from './components/Layout'; +import ErrorBoundary from './pages/error/ErrorBoundary'; const App = () => { const router = createHashRouter( createRoutesFromElements( <> <Route path="/" element={<Navigate to="/app/home" />} /> - <Route element={<Layout />}> + <Route errorElement={<ErrorBoundary />} element={<Layout />}> <Route index path="/app/home" element={<Home />} /> <Route path="/app/search" element={<Search />} /> <Route path="/app/profile" element={<Profile />} /> diff --git a/src/components/Header/HeaderUserMenu.js b/src/components/Header/HeaderUserMenu.js index 2235573..96ee7d7 100644 --- a/src/components/Header/HeaderUserMenu.js +++ b/src/components/Header/HeaderUserMenu.js @@ -50,41 +50,37 @@ const HeaderUserMenu = () => { return user.username ? ( <EuiPopover - id="headerUserMenu" - ownFocus - button={HeaderUserButton} isOpen={isOpen} - anchorPosition="downRight" closePopover={closeMenu} - panelPaddingSize="none" + button={HeaderUserButton} + anchorPosition="downRight" + ownFocus > - <div> - <EuiFlexGroup gutterSize="m" className="euiHeaderProfile" responsive={false}> - <EuiFlexItem grow={false}> - <EuiAvatar name={user.username} size="xl" /> - </EuiFlexItem> - <EuiFlexItem> - <EuiText>{user.username}</EuiText> - <EuiSpacer size="m" /> - <EuiFlexGroup> - <EuiFlexItem> - <EuiFlexGroup justifyContent="spaceBetween"> - <EuiFlexItem grow={false}> - <EuiLink href="#/app/profile"> - {t('userMenu.editProfileButton')} - </EuiLink> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiLink onClick={() => signOut()}> - {t('userMenu.logOutButton')} - </EuiLink> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - </EuiFlexGroup> - </div> + <EuiFlexGroup gutterSize="m" responsive={false}> + <EuiFlexItem grow={false}> + <EuiAvatar name={user.username} size="xl" /> + </EuiFlexItem> + <EuiFlexItem> + <EuiText>{user.username}</EuiText> + <EuiSpacer size="m" /> + <EuiFlexGroup> + <EuiFlexItem> + <EuiFlexGroup justifyContent="spaceBetween"> + <EuiFlexItem grow={false}> + <EuiLink href="#/app/profile" onClick={closeMenu}> + {t('userMenu.editProfileButton')} + </EuiLink> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiLink onClick={() => signOut()}> + {t('userMenu.logOutButton')} + </EuiLink> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + </EuiFlexGroup> </EuiPopover> ) : ( <></> diff --git a/src/components/Layout/Layout.js b/src/components/Layout/Layout.js index 62075f5..40c6164 100644 --- a/src/components/Layout/Layout.js +++ b/src/components/Layout/Layout.js @@ -1,7 +1,7 @@ import React from 'react'; import { Outlet } from 'react-router-dom'; import Header from '../../components/Header'; -import { EuiPage, EuiPageBody, EuiPageContent } from '@elastic/eui'; +import { EuiPage, EuiPageBody, EuiPageSection } from '@elastic/eui'; import styles from './styles.js'; const Layout = () => { @@ -9,9 +9,9 @@ const Layout = () => { <EuiPage style={styles.page} restrictWidth={false}> <EuiPageBody> <Header /> - <EuiPageContent style={styles.pageContent}> + <EuiPageSection style={styles.pageContent} grow={true}> <Outlet /> - </EuiPageContent> + </EuiPageSection> </EuiPageBody> </EuiPage> ); diff --git a/src/components/Layout/styles.js b/src/components/Layout/styles.js index 9b841ee..fd435cb 100644 --- a/src/components/Layout/styles.js +++ b/src/components/Layout/styles.js @@ -3,9 +3,7 @@ const styles = { padding: '0px', }, pageContent: { - borderRadius: 0, - border: 0, - boxShadow: 'none', + backgroundColor: '#ffffff', }, }; diff --git a/src/index.js b/src/index.js index 737fbdf..44ba1eb 100644 --- a/src/index.js +++ b/src/index.js @@ -6,6 +6,7 @@ import { getLoginUrl, getUrlParam, redirect } from './Utils.js'; import './i18n'; import Loading from './components/Loading'; import { createRoot } from 'react-dom/client'; +import { EuiProvider } from '@elastic/eui'; const userId = getUrlParam('kcId', ''); const accessToken = getUrlParam('accessToken', ''); @@ -20,7 +21,9 @@ if (sessionStorage.getItem('access_token')) { root.render( <UserProvider> <Suspense fallback={<Loading />}> - <App /> + <EuiProvider> + <App /> + </EuiProvider> </Suspense> </UserProvider> ); diff --git a/src/pages/error/ErrorBoundary.js b/src/pages/error/ErrorBoundary.js new file mode 100644 index 0000000..d9174f2 --- /dev/null +++ b/src/pages/error/ErrorBoundary.js @@ -0,0 +1,25 @@ +import React from 'react'; +import { EuiFlexGroup, EuiPage, EuiTitle } from '@elastic/eui'; +import { useTranslation } from 'react-i18next'; +import { useRouteError } from 'react-router-dom'; + +const ErrorBoundary = () => { + const { t } = useTranslation('common'); + const error = useRouteError(); + console.error(error); + + return ( + <EuiPage grow={true} style={{ height: '100vh' }}> + <EuiFlexGroup alignItems={'center'} justifyContent={'center'} direction={'column'}> + <EuiTitle size={'l'}> + <h2>{t('common:errorPage.title')}</h2> + </EuiTitle> + <EuiTitle size={'s'}> + <h2>{t('common:errorPage.reload')}</h2> + </EuiTitle> + </EuiFlexGroup> + </EuiPage> + ); +}; + +export default ErrorBoundary; diff --git a/src/pages/home/Home.js b/src/pages/home/Home.js index 1be5b26..b30af15 100644 --- a/src/pages/home/Home.js +++ b/src/pages/home/Home.js @@ -1,34 +1,28 @@ import React from 'react'; -import { - EuiPageContentHeader, - EuiPageContentHeaderSection, - EuiTitle, -} from '@elastic/eui'; import { useTranslation } from 'react-i18next'; +import { EuiTitle, EuiSpacer } from '@elastic/eui'; const Home = () => { const { t } = useTranslation('home'); return ( - <EuiPageContentHeader> - <EuiPageContentHeaderSection> - <EuiTitle> - <h2>{t('pageTitle')}</h2> - </EuiTitle> - <br /> - <p>{t('searchToolDescription.part1')}</p> - <br /> - <p>{t('searchToolDescription.part2')}</p> - <br /> - <p>{t('searchToolDescription.part3')}</p> - <br /> - <p>{t('searchToolDescription.part4')}</p> - <br /> - <p>{t('searchToolDescription.part5')}</p> - <br /> - <p>{t('searchToolDescription.part6')}</p> - </EuiPageContentHeaderSection> - </EuiPageContentHeader> + <> + <EuiTitle size="m"> + <h4>{t('home:pageTitle')}</h4> + </EuiTitle> + <EuiSpacer size={'l'} /> + <p>{t('home:searchToolDescription.part1')}</p> + <br /> + <p>{t('home:searchToolDescription.part2')}</p> + <br /> + <p>{t('home:searchToolDescription.part3')}</p> + <br /> + <p>{t('home:searchToolDescription.part4')}</p> + <br /> + <p>{t('home:searchToolDescription.part5')}</p> + <br /> + <p>{t('home:searchToolDescription.part6')}</p> + </> ); }; diff --git a/src/pages/profile/Profile.js b/src/pages/profile/Profile.js index 72c2138..936e5ac 100644 --- a/src/pages/profile/Profile.js +++ b/src/pages/profile/Profile.js @@ -159,7 +159,7 @@ const Profile = () => { }; const onSendGroupRequest = () => { - const groupList = []; + let groupList = []; if (userGroups) { userGroups.forEach((group) => { groupList.push(group.label); @@ -174,92 +174,85 @@ const Profile = () => { return ( <> - <EuiPageContentHeader> - <EuiPageContentHeaderSection> - <EuiTitle> - <h2>{t('pageTitle')}</h2> - </EuiTitle> - </EuiPageContentHeaderSection> - </EuiPageContentHeader> - <EuiPageContentBody> - <EuiForm component="form"> - <EuiTitle size="s"> - <h3>{t('groups.groupsList')}</h3> - </EuiTitle> - <EuiFormRow fullWidth label=""> - <EuiBasicTable items={groups} columns={groupColumns} /> - </EuiFormRow> - <EuiSpacer size="l" /> - <EuiTitle size="s"> - <h3>{t('requestsList.requestsList')}</h3> - </EuiTitle> - <EuiFormRow fullWidth label=""> - <EuiBasicTable items={userRequests} columns={requestsColumns} /> - </EuiFormRow> - <EuiSpacer size="l" /> - <EuiTitle size="s"> - <h3>{t('groupRequests.requestGroupAssignment')}</h3> - </EuiTitle> - {getUserGroupLabels() ? ( - <p - style={styles.currentRoleOrGroupText} - >{`${t('groupRequests.currentGroups')} ${getUserGroupLabels()}`}</p> - ) : ( - <p>{t('groupRequests.noGroup')}</p> - )} - <EuiFormRow error={valueError} isInvalid={valueError !== undefined}> - <EuiComboBox - placeholder={'Select groups'} - 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('roleRequests.requestRoleAssignment')}</h3> - </EuiTitle> - {userRole ? ( - <p - style={styles.currentRoleOrGroupText} - >{`${t('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> - </EuiForm> - </EuiPageContentBody> + <EuiTitle> + <h2>{t('pageTitle')}</h2> + </EuiTitle> + <EuiSpacer size={'l'} /> + <EuiTitle size="s"> + <h3>{t('groups.groupsList')}</h3> + </EuiTitle> + <EuiFormRow fullWidth label=""> + <EuiBasicTable items={groups} columns={groupColumns} /> + </EuiFormRow> + <EuiSpacer size="l" /> + <EuiTitle size="s"> + <h3>{t('requestsList.requestsList')}</h3> + </EuiTitle> + <EuiFormRow fullWidth label=""> + <EuiBasicTable items={userRequests} columns={requestsColumns} /> + </EuiFormRow> + <EuiSpacer size="l" /> + <EuiTitle size="s"> + <h3>{t('groupRequests.requestGroupAssignment')}</h3> + </EuiTitle> + {getUserGroupLabels() ? ( + <p + style={styles.currentRoleOrGroupText} + >{`${t('groupRequests.currentGroups')} ${getUserGroupLabels()}`}</p> + ) : ( + <p>{t('groupRequests.noGroup')}</p> + )} + <EuiFormRow error={valueError} isInvalid={valueError !== undefined}> + <EuiComboBox + placeholder={'Select groups'} + 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('roleRequests.requestRoleAssignment')}</h3> + </EuiTitle> + {userRole ? ( + <p + style={styles.currentRoleOrGroupText} + >{`${t('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> </> ); }; diff --git a/src/pages/results/ResourceFlyout.js b/src/pages/results/ResourceFlyout.js index 0c6bc5c..0ef21f3 100644 --- a/src/pages/results/ResourceFlyout.js +++ b/src/pages/results/ResourceFlyout.js @@ -29,6 +29,7 @@ const ResourceFlyout = ({ name={t('results:flyout.JSON.title')} iconStyle={'triangle'} displayDataTypes={false} + enableClipboard={false} collapsed={true} /> </EuiText> diff --git a/src/pages/results/ResultsTableMUI.js b/src/pages/results/ResultsTableMUI.js index b289991..2d30e04 100644 --- a/src/pages/results/ResultsTableMUI.js +++ b/src/pages/results/ResultsTableMUI.js @@ -1,16 +1,18 @@ import React, { useEffect, useMemo, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import MUIDataTable from 'mui-datatables'; -import { createTheme, ThemeProvider } from '@material-ui/core'; import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { createTheme, ThemeProvider } from '@mui/material'; const getMuiTheme = () => createTheme({ - overrides: { + components: { MuiTableRow: { - root: { - '&:hover': { - cursor: 'pointer', + styleOverrides: { + root: { + '&:hover': { + cursor: 'pointer', + }, }, }, }, diff --git a/src/pages/search/AdvancedSearch/AdvancedSearch.js b/src/pages/search/AdvancedSearch/AdvancedSearch.js index ce4b015..132e33b 100644 --- a/src/pages/search/AdvancedSearch/AdvancedSearch.js +++ b/src/pages/search/AdvancedSearch/AdvancedSearch.js @@ -29,8 +29,9 @@ import { EuiTextArea, EuiTextColor, EuiTitle, + EuiDatePicker, } from '@elastic/eui'; -import React, { useEffect, useState } from 'react'; +import React, { Fragment, useEffect, useState } from 'react'; import { changeNameToLabel, createAdvancedQueriesBySource, @@ -43,10 +44,10 @@ import { } from '../../../Utils'; import { getQueryCount, searchQuery } from '../../../actions/source'; import { DateOptions, NumericOptions, Operators } from '../Data'; -import TextField from '@material-ui/core/TextField'; import { addUserHistory, fetchUserHistory } from '../../../actions/user'; import { useTranslation } from 'react-i18next'; -import styles from './styles'; +import styles from './styles.js'; +import moment from 'moment'; const updateSources = ( searchFields, @@ -512,7 +513,7 @@ const PopoverSelect = ({ standardFields, searchFields, setSearchFields }) => { } }; - const selectField = () => { + const SelectField = () => { const renderOption = (option, searchValue, contentClassName) => { const { label, color } = option; return <EuiHealth color={color}>{label}</EuiHealth>; @@ -521,7 +522,7 @@ const PopoverSelect = ({ standardFields, searchFields, setSearchFields }) => { return ( <> <EuiComboBox - placeholder={t('search:advancedSearch.fields.addFieldPopover.title')} + placeholder={t('search:advancedSearch.fields.addFieldPopover.selectSection')} singleSelection={{ asPlainText: true }} options={getFieldsBySection(standardFields, selectedSection[0])} selectedOptions={selectedField} @@ -529,19 +530,6 @@ const PopoverSelect = ({ standardFields, searchFields, setSearchFields }) => { isClearable={true} renderOption={renderOption} /> - <EuiPopoverFooter> - <EuiButton - size="s" - onClick={() => { - handleAddField(); - setIsPopoverSelectOpen(false); - setSelectedSection([]); - setSelectedField([]); - }} - > - {t('search:advancedSearch.fields.addFieldPopover.button')} - </EuiButton> - </EuiPopoverFooter> </> ); } @@ -556,16 +544,16 @@ const PopoverSelect = ({ standardFields, searchFields, setSearchFields }) => { iconSide="left" onClick={() => setIsPopoverSelectOpen(!isPopoverSelectOpen)} > - {t('search:advancedSearch.fields.addFieldPopover.openPopoverButton')} + {t('search:advancedSearch.fields.addFieldPopover.title')} </EuiButton> } isOpen={isPopoverSelectOpen} closePopover={() => setIsPopoverSelectOpen(false)} > - <div style={{ width: 'intrinsic', minWidth: 240 }}> - <EuiPopoverTitle> - {t('search:advancedSearch.fields.addFieldPopover.title')} - </EuiPopoverTitle> + <EuiPopoverTitle style={styles.noBorder} paddingSize={'m'}> + {t('search:advancedSearch.fields.addFieldPopover.title')} + </EuiPopoverTitle> + <EuiFlexGroup direction={'column'} style={{ width: '240px' }}> <EuiComboBox placeholder={t('search:advancedSearch.fields.addFieldPopover.selectSection')} singleSelection={{ asPlainText: true }} @@ -577,8 +565,21 @@ const PopoverSelect = ({ standardFields, searchFields, setSearchFields }) => { }} isClearable={false} /> - </div> - {selectField()} + <SelectField /> + </EuiFlexGroup> + <EuiPopoverFooter style={styles.noBorder} paddingSize={'m'}> + <EuiButton + size="s" + onClick={() => { + handleAddField(); + setIsPopoverSelectOpen(false); + setSelectedSection([]); + setSelectedField([]); + }} + > + {t('search:advancedSearch.fields.addFieldPopover.validateButton')} + </EuiButton> + </EuiPopoverFooter> </EuiPopover> ); }; @@ -592,7 +593,6 @@ const PopoverValueContent = ({ setSearchCount, fieldCount, setFieldCount, - isPopoverValueOpen, setIsPopoverValueOpen, selectedOperatorId, createPolicyToast, @@ -602,7 +602,6 @@ const PopoverValueContent = ({ setAvailableSources, }) => { const { t } = useTranslation(['search', 'common']); - const datePickerStyles = styles(); const [valueError, setValueError] = useState(undefined); const onValueSearchChange = (value, hasMatchingOptions) => { @@ -675,10 +674,10 @@ const PopoverValueContent = ({ updateSearch(setSearch, updatedSearchFields, selectedOperatorId, setSearchCount); }; - const ValuePopoverFooter = (i) => { + const ValuePopoverFooter = ({ i }) => { if (i === searchFields[index].values.length - 1) { return ( - <EuiPopoverFooter> + <> <EuiButton size="s" onClick={() => { @@ -699,17 +698,19 @@ const PopoverValueContent = ({ > {t('search:advancedSearch.fields.fieldContentPopover.addValue')} </EuiButton> - <EuiButton - size="s" - style={{ float: 'right' }} - onClick={() => { - validateFieldValues(); - setIsPopoverValueOpen(updateArrayElement(isPopoverValueOpen, index, false)); - }} - > - {t('common:validationActions.validate')} - </EuiButton> - </EuiPopoverFooter> + <EuiPopoverFooter style={styles.noBorder}> + <EuiButton + size="s" + style={{ float: 'right' }} + onClick={() => { + validateFieldValues(); + setIsPopoverValueOpen(false); + }} + > + {t('common:validationActions.validate')} + </EuiButton> + </EuiPopoverFooter> + </> ); } }; @@ -736,6 +737,180 @@ const PopoverValueContent = ({ return listFieldValues; }; + const SelectDates = ({ i }) => { + if (!!searchFields[index].values[i].option) { + switch (searchFields[index].values[i].option) { + case 'between': + return ( + <EuiFlexItem> + <EuiFormRow + label={t('search:advancedSearch.fields.fieldContentPopover.betweenDate')} + > + <EuiDatePicker + selected={moment(searchFields[index].values[i].startDate)} + onChange={(date) => + setSearchFields( + updateSearchFieldValues( + searchFields, + index, + updateArrayElement(searchFields[index].values, i, { + option: searchFields[index].values[i].option, + startDate: date.format('YYYY-MM-DD'), + endDate: searchFields[index].values[i].endDate, + }) + ) + ) + } + /> + </EuiFormRow> + <EuiFormRow + label={t('search:advancedSearch.fields.fieldContentPopover.andDate')} + > + <EuiDatePicker + placeholder={t( + 'search:advancedSearch.fields.fieldContentPopover.andDate' + )} + selected={moment(searchFields[index].values[i].endDate)} + onChange={(date) => + setSearchFields( + updateSearchFieldValues( + searchFields, + index, + updateArrayElement(searchFields[index].values, i, { + option: searchFields[index].values[i].option, + startDate: searchFields[index].values[i].startDate, + endDate: date.format('YYYY-MM-DD'), + }) + ) + ) + } + /> + </EuiFormRow> + <ValuePopoverFooter i={i} /> + </EuiFlexItem> + ); + + default: + return ( + <EuiFlexItem> + <EuiFormRow + label={t( + 'search:advancedSearch.fields.fieldContentPopover.dateSelection' + )} + > + <EuiDatePicker + selected={moment(searchFields[index].values[i].startDate)} + onChange={(date) => + setSearchFields( + updateSearchFieldValues( + searchFields, + index, + updateArrayElement(searchFields[index].values, i, { + option: searchFields[index].values[i].option, + startDate: date.format('YYYY-MM-DD'), + endDate: moment(), + }) + ) + ) + } + /> + </EuiFormRow> + <ValuePopoverFooter i={i} /> + </EuiFlexItem> + ); + } + } + }; + + const NumericValues = ({ i }) => { + if (!!searchFields[index].values[i].option) { + switch (searchFields[index].values[i].option) { + case 'between': + return ( + <EuiFlexItem> + <EuiFormRow + label={t('search:advancedSearch.fields.fieldContentPopover.firstValue')} + > + <EuiFieldText + placeholder={t( + 'search:advancedSearch.fields.fieldContentPopover.inputTextValue' + )} + value={searchFields[index].values[i].value1} + onChange={(e) => { + setSearchFields( + updateSearchFieldValues( + searchFields, + index, + updateArrayElement(searchFields[index].values, i, { + option: searchFields[index].values[i].option, + value1: e.target.value, + value2: searchFields[index].values[i].value2, + }) + ) + ); + }} + /> + </EuiFormRow> + <EuiFormRow + label={t('search:advancedSearch.fields.fieldContentPopover.secondValue')} + > + <EuiFieldText + placeholder={t( + 'search:advancedSearch.fields.fieldContentPopover.inputTextValue' + )} + value={searchFields[index].values[i].value2} + onChange={(e) => + setSearchFields( + updateSearchFieldValues( + searchFields, + index, + updateArrayElement(searchFields[index].values, i, { + option: searchFields[index].values[i].option, + value1: searchFields[index].values[i].value1, + value2: e.target.value, + }) + ) + ) + } + /> + </EuiFormRow> + <ValuePopoverFooter i={i} /> + </EuiFlexItem> + ); + + default: + return ( + <EuiFlexItem> + <EuiFormRow + label={t('search:advancedSearch.fields.fieldContentPopover.addValue')} + > + <EuiFieldText + placeholder={t( + 'search:advancedSearch.fields.fieldContentPopover.inputTextValue' + )} + value={searchFields[index].values[i].value1} + onChange={(e) => { + setSearchFields( + updateSearchFieldValues( + searchFields, + index, + updateArrayElement(searchFields[index].values, i, { + option: searchFields[index].values[i].option, + value1: e.target.value, + value2: searchFields[index].values[i].value2, + }) + ) + ); + }} + /> + </EuiFormRow> + <ValuePopoverFooter i={i} /> + </EuiFlexItem> + ); + } + } + }; + switch (searchFields[index].type) { case 'Text': return ( @@ -753,15 +928,13 @@ const PopoverValueContent = ({ } /> </EuiFlexItem> - <EuiPopoverFooter> + <EuiPopoverFooter style={styles.noBorder}> <EuiButton size="s" style={{ float: 'right' }} onClick={() => { validateFieldValues(); - setIsPopoverValueOpen( - updateArrayElement(isPopoverValueOpen, index, false) - ); + setIsPopoverValueOpen(false); }} > {t('common:validationActions.validate')} @@ -772,31 +945,31 @@ const PopoverValueContent = ({ case 'List': return ( <> - <EuiFormRow error={valueError} isInvalid={valueError !== undefined}> - <EuiComboBox - placeholder={t( - 'search:advancedSearch.fields.fieldContentPopover.selectValues' - )} - options={getListFieldValues()} - selectedOptions={searchFields[index].values} - onChange={(selectedOptions) => { - setValueError(undefined); - setSearchFields( - updateSearchFieldValues(searchFields, index, selectedOptions) - ); - }} - onSearchChange={onValueSearchChange} - /> - </EuiFormRow> - <EuiPopoverFooter> + <EuiFlexItem> + <EuiFormRow error={valueError} isInvalid={valueError !== undefined}> + <EuiComboBox + placeholder={t( + 'search:advancedSearch.fields.fieldContentPopover.selectValues' + )} + options={getListFieldValues()} + selectedOptions={searchFields[index].values} + onChange={(selectedOptions) => { + setValueError(undefined); + setSearchFields( + updateSearchFieldValues(searchFields, index, selectedOptions) + ); + }} + onSearchChange={onValueSearchChange} + /> + </EuiFormRow> + </EuiFlexItem> + <EuiPopoverFooter style={styles.noBorder}> <EuiButton size="s" style={{ float: 'right' }} onClick={() => { validateFieldValues(); - setIsPopoverValueOpen( - updateArrayElement(isPopoverValueOpen, index, false) - ); + setIsPopoverValueOpen(false); }} > {t('common:validationActions.validate')} @@ -805,236 +978,61 @@ const PopoverValueContent = ({ </> ); case 'Numeric': - const NumericValues = (i) => { - if (!!searchFields[index].values[i].option) { - switch (searchFields[index].values[i].option) { - case 'between': - return ( - <> - <EuiFlexItem> - <EuiFieldText - placeholder={t( - 'search:advancedSearch.fields.fieldContentPopover.firstValue' - )} - value={searchFields[index].values[i].value1} - onChange={(e) => { - setSearchFields( - updateSearchFieldValues( - searchFields, - index, - updateArrayElement(searchFields[index].values, i, { - option: searchFields[index].values[i].option, - value1: e.target.value, - value2: searchFields[index].values[i].value2, - }) - ) - ); - }} - /> - </EuiFlexItem> - <EuiFlexItem> - <EuiFieldText - placeholder={t( - 'search:advancedSearch.fields.fieldContentPopover.secondValue' - )} - value={searchFields[index].values[i].value2} - onChange={(e) => - setSearchFields( - updateSearchFieldValues( - searchFields, - index, - updateArrayElement(searchFields[index].values, i, { - option: searchFields[index].values[i].option, - value1: searchFields[index].values[i].value1, - value2: e.target.value, - }) - ) - ) - } - /> - </EuiFlexItem> - {ValuePopoverFooter(i)} - </> - ); - - default: - return ( - <> - <EuiFlexItem> - <EuiFieldText - placeholder={t( - 'search:advancedSearch.fields.fieldContentPopover.inputTextValue' - )} - value={searchFields[index].values[i].value1} - onChange={(e) => { - setSearchFields( - updateSearchFieldValues( - searchFields, - index, - updateArrayElement(searchFields[index].values, i, { - option: searchFields[index].values[i].option, - value1: e.target.value, - value2: searchFields[index].values[i].value2, - }) - ) - ); - }} - /> - </EuiFlexItem> - {ValuePopoverFooter(i)} - </> - ); - } - } - }; - return ( <> {searchFields[index].values.map((value, i) => ( - <div key={i}> - <EuiSelect - hasNoInitialSelection - id="Select an option" - options={NumericOptions} - value={searchFields[index].values[i].option} - onChange={(e) => { - addFieldValue(i, e.target.value); - invalidateFieldValues(); - }} - /> - {NumericValues(i)} - </div> + <Fragment key={i}> + <EuiFormRow + label={t( + 'search:advancedSearch.fields.fieldContentPopover.operatorSelection.label' + )} + > + <EuiSelect + value={searchFields[index].values[i].option} + options={NumericOptions.map((option) => { + return { ...option, text: t(option.text) }; + })} + onChange={(e) => { + addFieldValue(i, e.target.value); + invalidateFieldValues(); + }} + hasNoInitialSelection + /> + </EuiFormRow> + <NumericValues i={i} /> + </Fragment> ))} </> ); case 'Date': - const SelectDates = (i) => { - if (!!searchFields[index].values[i].option) { - switch (searchFields[index].values[i].option) { - case 'between': - return ( - <> - <form className={datePickerStyles.container} noValidate> - <TextField - label={t( - 'search:advancedSearch.fields.fieldContentPopover.betweenDate' - )} - type="date" - defaultValue={ - !!searchFields[index].values[i].startDate - ? searchFields[index].values[i].startDate - : Date.now() - } - className={datePickerStyles.textField} - InputLabelProps={{ - shrink: true, - }} - onChange={(e) => - setSearchFields( - updateSearchFieldValues( - searchFields, - index, - updateArrayElement(searchFields[index].values, i, { - option: searchFields[index].values[i].option, - startDate: e.target.value, - endDate: searchFields[index].values[i].endDate, - }) - ) - ) - } - /> - </form> - <form className={datePickerStyles.container} noValidate> - <TextField - label={t( - 'search:advancedSearch.fields.fieldContentPopover.andDate' - )} - type="date" - defaultValue={ - !!searchFields[index].values[i].endDate - ? searchFields[index].values[i].endDate - : Date.now() - } - className={datePickerStyles.textField} - InputLabelProps={{ - shrink: true, - }} - onChange={(e) => - setSearchFields( - updateSearchFieldValues( - searchFields, - index, - updateArrayElement(searchFields[index].values, i, { - option: searchFields[index].values[i].option, - startDate: searchFields[index].values[i].startDate, - endDate: e.target.value, - }) - ) - ) - } - /> - </form> - {ValuePopoverFooter(i)} - </> - ); - - default: - return ( - <> - <form className={datePickerStyles.container} noValidate> - <TextField - type="date" - defaultValue={ - !!searchFields[index].values[i].startDate - ? searchFields[index].values[i].startDate - : Date.now() - } - className={datePickerStyles.textField} - InputLabelProps={{ - shrink: true, - }} - onChange={(e) => - setSearchFields( - updateSearchFieldValues( - searchFields, - index, - updateArrayElement(searchFields[index].values, i, { - option: searchFields[index].values[i].option, - startDate: e.target.value, - endDate: Date.now(), - }) - ) - ) - } - /> - </form> - {ValuePopoverFooter(i)} - </> - ); - } - } - }; - return ( <> {searchFields[index].values.map((value, i) => ( - <div key={i}> - <EuiSelect - hasNoInitialSelection - id="Select an option" - options={DateOptions} - value={searchFields[index].values[i].option} - onChange={(e) => { - addFieldValue(i, e.target.value); - invalidateFieldValues(); - }} - /> - {SelectDates(i)} - </div> + <Fragment key={i}> + <EuiFormRow + label={t( + 'search:advancedSearch.fields.fieldContentPopover.operatorSelection.label' + )} + > + <EuiSelect + value={searchFields[index].values[i].option} + options={DateOptions.map((option) => { + return { ...option, text: t(option.text) }; + })} + onChange={(e) => { + addFieldValue(i, e.target.value); + invalidateFieldValues(); + }} + hasNoInitialSelection + /> + </EuiFormRow> + <SelectDates i={i} /> + </Fragment> ))} </> ); default: + return <></>; } }; @@ -1055,7 +1053,7 @@ const PopoverValueButton = ({ setAvailableSources, }) => { const { t } = useTranslation('search'); - const [isPopoverValueOpen, setIsPopoverValueOpen] = useState([false]); + const [isPopoverValueOpen, setIsPopoverValueOpen] = useState(false); return ( <EuiPopover @@ -1063,12 +1061,7 @@ const PopoverValueButton = ({ button={ <EuiButtonIcon size="s" - color="primary" - onClick={() => - setIsPopoverValueOpen( - updateArrayElement(isPopoverValueOpen, index, !isPopoverValueOpen[index]) - ) - } + onClick={() => setIsPopoverValueOpen(!isPopoverValueOpen)} iconType="documentEdit" title={t('search:advancedSearch.fields.fieldContentPopover.addFieldValues')} aria-label={t( @@ -1076,12 +1069,10 @@ const PopoverValueButton = ({ )} /> } - isOpen={isPopoverValueOpen[index]} - closePopover={() => - setIsPopoverValueOpen(updateArrayElement(isPopoverValueOpen, index, false)) - } + isOpen={isPopoverValueOpen} + closePopover={() => setIsPopoverValueOpen(false)} > - <div style={{ width: 240 }}> + <EuiFlexGroup direction={'column'} style={{ width: 240 }}> <PopoverValueContent index={index} standardFields={standardFields} @@ -1100,7 +1091,7 @@ const PopoverValueButton = ({ availableSources={availableSources} setAvailableSources={setAvailableSources} /> - </div> + </EuiFlexGroup> </EuiPopover> ); }; @@ -1112,7 +1103,6 @@ const FieldsPanel = ({ setSearch, setSearchCount, selectedOperatorId, - setSelectedOperatorId, fieldCount, setFieldCount, availableSources, @@ -1202,130 +1192,145 @@ const FieldsPanel = ({ <EuiTitle size="xs"> <h2>{t('search:advancedSearch.fields.title')}</h2> </EuiTitle> - <EuiPanel paddingSize="m"> + <EuiPanel hasShadow={false} paddingSize="m"> <EuiFlexGroup direction="column"> {searchFields.map((field, index) => ( - <EuiPanel key={'field' + index} paddingSize="s"> - <EuiFlexItem grow={false}> - <EuiFlexGroup direction="row" alignItems="center"> + <EuiPanel + hasShadow={false} + hasBorder={true} + key={'field' + index} + paddingSize="s" + > + <EuiFlexGroup direction="row" alignItems="center"> + <EuiFlexItem grow={false}> + <EuiButtonIcon + size="s" + color="danger" + onClick={() => handleRemoveField(index)} + iconType="indexClose" + title={t('search:advancedSearch.fields.removeFieldButton')} + aria-label={t('search:advancedSearch.fields.removeFieldButton')} + /> + </EuiFlexItem> + <EuiFlexItem> + {field.isValidated ? ( + <> + {field.sources.length ? ( + <EuiHealth color="danger"> + {fieldValuesToString(field).replace(/_|\./g, ' ')} + </EuiHealth> + ) : ( + <EuiHealth color="primary"> + {fieldValuesToString(field).replace(/_|\./g, ' ')} + </EuiHealth> + )} + </> + ) : ( + <> + {field.sources.length ? ( + <EuiHealth color="danger"> + {field.name.replace(/_|\./g, ' ')} + </EuiHealth> + ) : ( + <EuiHealth color="primary"> + {field.name.replace(/_|\./g, ' ')} + </EuiHealth> + )} + </> + )} + </EuiFlexItem> + {!isNaN(fieldCount[index]) && ( + <EuiFlexItem grow={false}> + <EuiTextColor color="secondary"> + {t('search:advancedSearch.resultsCount', { + count: fieldCount[index], + })} + </EuiTextColor> + </EuiFlexItem> + )} + {field.isValidated && ( <EuiFlexItem grow={false}> <EuiButtonIcon size="s" - color="danger" - onClick={() => handleRemoveField(index)} - iconType="indexClose" - title={t('search:advancedSearch.fields.removeFieldButton')} - aria-label={t('search:advancedSearch.fields.removeFieldButton')} + onClick={() => countFieldValues(field, index)} + iconType="number" + title={t('search:advancedSearch.countResultsButton')} + aria-label={t('search:advancedSearch.countResultsButton')} /> </EuiFlexItem> - <EuiFlexItem> - {field.isValidated ? ( - <> - {field.sources.length ? ( - <EuiHealth color="danger"> - {fieldValuesToString(field).replace(/_|\./g, ' ')} - </EuiHealth> - ) : ( - <EuiHealth color="primary"> - {fieldValuesToString(field).replace(/_|\./g, ' ')} - </EuiHealth> - )} - </> - ) : ( - <> - {field.sources.length ? ( - <EuiHealth color="danger"> - {field.name.replace(/_|\./g, ' ')} - </EuiHealth> - ) : ( - <EuiHealth color="primary"> - {field.name.replace(/_|\./g, ' ')} - </EuiHealth> - )} - </> - )} - </EuiFlexItem> - <EuiFlexItem grow={false}> - {!isNaN(fieldCount[index]) && ( - <EuiTextColor color="secondary"> - {t('search:advancedSearch.resultsCount', { - count: fieldCount[index], - })} - </EuiTextColor> - )} - </EuiFlexItem> - <EuiFlexItem grow={false}> - {field.isValidated && ( - <EuiButtonIcon - size="s" - onClick={() => countFieldValues(field, index)} - iconType="number" - title={t('search:advancedSearch.countResultsButton')} - aria-label={t('search:advancedSearch.countResultsButton')} - /> - )} - </EuiFlexItem> - <EuiFlexItem grow={false}> - {field.isValidated && ( - <EuiButtonIcon - size="s" - color="danger" - onClick={() => handleClearValues(index)} - iconType="trash" - title={t('search:advancedSearch.fields.clearValues')} - aria-label={t('search:advancedSearch.fields.clearValues')} - /> - )} - </EuiFlexItem> + )} + {field.isValidated && ( <EuiFlexItem grow={false}> - <PopoverValueButton - index={index} - standardFields={standardFields} - searchFields={searchFields} - setSearchFields={setSearchFields} - setSearch={setSearch} - setSearchCount={setSearchCount} - fieldCount={fieldCount} - setFieldCount={setFieldCount} - selectedOperatorId={selectedOperatorId} - createPolicyToast={createPolicyToast} - selectedSources={selectedSources} - setSelectedSources={setSelectedSources} - availableSources={availableSources} - setAvailableSources={setAvailableSources} + <EuiButtonIcon + size="s" + color="danger" + onClick={() => handleClearValues(index)} + iconType="trash" + title={t('search:advancedSearch.fields.clearValues')} + aria-label={t('search:advancedSearch.fields.clearValues')} /> </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> + )} + <EuiFlexItem grow={false}> + <PopoverValueButton + index={index} + standardFields={standardFields} + searchFields={searchFields} + setSearchFields={setSearchFields} + setSearch={setSearch} + setSearchCount={setSearchCount} + fieldCount={fieldCount} + setFieldCount={setFieldCount} + selectedOperatorId={selectedOperatorId} + createPolicyToast={createPolicyToast} + selectedSources={selectedSources} + setSelectedSources={setSelectedSources} + availableSources={availableSources} + setAvailableSources={setAvailableSources} + /> + </EuiFlexItem> + </EuiFlexGroup> </EuiPanel> ))} </EuiFlexGroup> - <EuiSpacer size="l" /> + {searchFields.length > 0 && <EuiSpacer size="l" />} <PopoverSelect standardFields={standardFields} searchFields={searchFields} setSearchFields={setSearchFields} /> </EuiPanel> - <EuiSpacer size="s" /> - <EuiRadioGroup - options={Operators.map((operator) => { - return { ...operator, label: t(operator.label) }; - })} - idSelected={selectedOperatorId} - onChange={(id) => { - setSelectedOperatorId(id); - updateSearch(setSearch, searchFields, id, setSearchCount); - }} - name="operators group" - legend={{ - children: <span>{t('search:advancedSearch.searchOptions.title')}</span>, - }} - /> </> ); }; +const SearchOperatorSelection = ({ + setSearch, + searchFields, + setSearchCount, + selectedOperatorId, + setSelectedOperatorId, +}) => { + const { t } = useTranslation('search'); + + return ( + <EuiRadioGroup + options={Operators.map((operator) => { + return { ...operator, label: t(operator.label) }; + })} + idSelected={selectedOperatorId} + onChange={(id) => { + setSelectedOperatorId(id); + updateSearch(setSearch, searchFields, id, setSearchCount); + }} + name="operators group" + legend={{ + children: <span>{t('search:advancedSearch.searchOptions.title')}</span>, + }} + /> + ); +}; + const SourceSelect = ({ availableSources, selectedSources, setSelectedSources }) => { const { t } = useTranslation('search'); const [sourceSelectError, setSourceSelectError] = useState(undefined); @@ -1364,17 +1369,15 @@ const SourceSelect = ({ availableSources, selectedSources, setSelectedSources }) <h2>{t('search:advancedSearch.partnerSources.title')}</h2> </EuiTitle> <EuiSpacer size="s" /> - <EuiFlexItem> - <EuiFormRow error={sourceSelectError} isInvalid={sourceSelectError !== undefined}> - <EuiComboBox - placeholder={t('search:advancedSearch.partnerSources.allSourcesSelected')} - options={availableSources} - selectedOptions={selectedSources} - onChange={onSourceChange} - onSearchChange={onSourceSearchChange} - /> - </EuiFormRow> - </EuiFlexItem> + <EuiFormRow error={sourceSelectError} isInvalid={sourceSelectError !== undefined}> + <EuiComboBox + placeholder={t('search:advancedSearch.partnerSources.allSourcesSelected')} + options={availableSources} + selectedOptions={selectedSources} + onChange={onSourceChange} + onSearchChange={onSourceSearchChange} + /> + </EuiFormRow> </> ); }; @@ -1502,6 +1505,13 @@ const AdvancedSearch = ({ sources={sources} createPolicyToast={createPolicyToast} /> + <SearchOperatorSelection + setSearch={setSearch} + searchFields={searchFields} + setSearchCount={setSearchCount} + selectedOperatorId={selectedOperatorId} + setSelectedOperatorId={setSelectedOperatorId} + /> <EuiSpacer size="s" /> <SourceSelect availableSources={availableSources} diff --git a/src/pages/search/AdvancedSearch/styles.js b/src/pages/search/AdvancedSearch/styles.js index 66022dc..b192102 100644 --- a/src/pages/search/AdvancedSearch/styles.js +++ b/src/pages/search/AdvancedSearch/styles.js @@ -1,15 +1,11 @@ -import { makeStyles } from '@material-ui/core/styles'; - -const style = makeStyles((theme) => ({ +const styles = { container: { display: 'flex', flexWrap: 'wrap', }, - textField: { - marginLeft: theme.spacing(1), - marginRight: theme.spacing(1), - width: 240, + noBorder: { + border: 'none', }, -})); +}; -export default style; +export default styles; diff --git a/src/pages/search/Data.js b/src/pages/search/Data.js index d0a399a..b4a39bb 100644 --- a/src/pages/search/Data.js +++ b/src/pages/search/Data.js @@ -13,38 +13,38 @@ export const Operators = [ export const DateOptions = [ { - text: 'Date', + text: 'search:advancedSearch.fields.fieldContentPopover.operatorSelection.date.value', value: '=', }, { - text: 'Before', + text: 'search:advancedSearch.fields.fieldContentPopover.operatorSelection.date.before', value: '<=', }, { - text: 'After', + text: 'search:advancedSearch.fields.fieldContentPopover.operatorSelection.date.after', value: '>=', }, { - text: 'Period', + text: 'search:advancedSearch.fields.fieldContentPopover.operatorSelection.date.range', value: 'between', }, ]; export const NumericOptions = [ { - text: 'Equal to', + text: 'search:advancedSearch.fields.fieldContentPopover.operatorSelection.numeric.value', value: '=', }, { - text: 'Under', + text: 'search:advancedSearch.fields.fieldContentPopover.operatorSelection.numeric.under', value: '<=', }, { - text: 'Over', + text: 'search:advancedSearch.fields.fieldContentPopover.operatorSelection.numeric.over', value: '>=', }, { - text: 'Between', + text: 'search:advancedSearch.fields.fieldContentPopover.operatorSelection.numeric.range', value: 'between', }, ]; diff --git a/src/pages/search/Search.js b/src/pages/search/Search.js index 5f92324..a28a6af 100644 --- a/src/pages/search/Search.js +++ b/src/pages/search/Search.js @@ -144,15 +144,13 @@ const Search = () => { ]; return ( - <EuiPageContentBody> - <EuiTabbedContent - tabs={tabsContent} - selectedTab={tabsContent[selectedTabNumber]} - onTabClick={(tab) => { - setSelectedTabNumber(tabsContent.indexOf(tab)); - }} - /> - </EuiPageContentBody> + <EuiTabbedContent + tabs={tabsContent} + selectedTab={tabsContent[selectedTabNumber]} + onTabClick={(tab) => { + setSelectedTabNumber(tabsContent.indexOf(tab)); + }} + /> ); }; -- GitLab From a45ed1cb6e79d090a58e8015777085d720961739 Mon Sep 17 00:00:00 2001 From: rbisson <remi.bisson@inrae.fr> Date: Tue, 9 Jul 2024 18:34:39 +0200 Subject: [PATCH 10/10] [AdvancedSearch] corrected crash from deleting user search history selection --- .../search/AdvancedSearch/AdvancedSearch.js | 17 ++++++++++++++++- src/pages/search/Search.js | 8 +------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/pages/search/AdvancedSearch/AdvancedSearch.js b/src/pages/search/AdvancedSearch/AdvancedSearch.js index 132e33b..d977c81 100644 --- a/src/pages/search/AdvancedSearch/AdvancedSearch.js +++ b/src/pages/search/AdvancedSearch/AdvancedSearch.js @@ -71,7 +71,9 @@ const updateSources = ( field.sources.forEach((sourceId) => { noPrivateField = false; const source = availableSources.find((src) => src.id === sourceId); - if (source && !updatedSources.includes(source)) updatedSources.push(source); + if (source && !updatedSources.includes(source)) { + updatedSources.push(source); + } }); } }); @@ -197,6 +199,19 @@ const HistorySelect = ({ const onHistoryChange = (selectedSavedSearch) => { setHistorySelectError(undefined); + if (!selectedSavedSearch) { + return; + } + // if no search is selected + if (selectedSavedSearch.length <= 0) { + setSelectedSavedSearch(undefined); + setSearch(''); + setSearchCount(); + setFieldCount([]); + setSelectedSources([]); + setSearchFields([]); + return; + } if (!!selectedSavedSearch[0].query) { setSelectedSavedSearch(selectedSavedSearch); setSearch(selectedSavedSearch[0].query); diff --git a/src/pages/search/Search.js b/src/pages/search/Search.js index a28a6af..80a364e 100644 --- a/src/pages/search/Search.js +++ b/src/pages/search/Search.js @@ -1,11 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { - EuiTabbedContent, - EuiPageContentBody, - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, -} from '@elastic/eui'; +import { EuiTabbedContent, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import Results from '../results/Results'; import SearchMap from '../maps/SearchMap'; import { removeNullFields } from '../../Utils.js'; -- GitLab